打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
用Swift编写网络层:面向协议方式


在这篇文章中我们会看到怎样实现用纯swift编写网络层,而不依靠任何第三方库。让我们快去看看吧。相信看完之后我们的代码能够做到:


  • 面向协议

  • 易用

  • 容易实现

  • 类型安全

  • 用枚举(enums)来配置终端(endPoints)


下面是一个最终我们网络层的示例


这个项目的最终目标


通过输入router.request(. 借助枚举的力量,我们可以看到所有有效的终端和我们请求的参数)


首先,一些结构


创建任何东西之前,有个结构都是很重要的,这样后面我们就容易找到需要的东西。我坚定相信文件夹结构对软件架构至关重要。为了让我们的文件组织有序,让我们提前建立好所有的组,我会标记好每一个文件该放的位置。这是一个项目结构总览。(请注意这里的名字仅仅是建议,你可以按你喜好给你的类和组命名)


项目文件夹结构


终端类型(EndPointType)协议


我们要做的第一件事情就是定义我们的终端类型协议。这个协议要包含用于配置终端的所有信息。什么是终端?本质上来讲它是一个包含各种组件比如头文件(headers),查询参数(query parameters),体参数(body parameters)的URL请求(URLRequest)。终端类型协议是我们网络层实现的基石。我们建一个文件,并命名EndPointType,把它放到服务组中(不是终端组,后面我们分清楚的)。


终端类型协议


HTTP协议


为了创建一个完整的终端,我的终端类型协议里有很多HTTP协议。让我们看看这些协议需要什么。


HTTP方法


创建一个名为HTTPMethod的文件并把它放在服务组中。这个枚举会用于设置我们请求用的HTTP方法。


HTTPMethod枚举


HTTP任务


创建一个名为HTTPTask的文件并把它放在服务组中。HTTPTask用于为一个特定的终端配置参数,你可以添加适当数量的案例(cases)到你的网络层请求中。我会按下图建立我的请求,它只包含3个案例


HTTPTask 枚举


在下一章我们会讨论参数和如何处理参数的编码。


HTTP头文件


HTTPHeaders是一个字典的别名(typealias)。你可以在你HTTPTask文件的开头创建它。


public typealias HTTPHeaders = [String:String]


参数与编码


创建一个名为ParameterEncoding的文件并把它放在编码组中。我们首先要定义一个参数的别名,通过它我们可以让代码更干净简洁。


public typealias Parameters = [String:Any]


之后用一个静态函数编码定义一个协议参数编码器(ParameterEncoder)。这种编码方式含有2个参数,一个inout URLRequest和Parameters。(为了防止混淆,后面我会把函数参数称为参量)。INOUT是一个swift关键词,用于把一个参量定义为引用参量。通常变量作为值类型传送给函数。通过在参量的开头加上inout,我们把它定义为引用类型。要学更多关于双向参量,你可以点击这里。参数编码器协议会通过JSONParameterEncoder和URLPameterEncoder实现。


public protocol ParameterEncoder {
static func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws
}


参数编码器执行编码参数的函数,这个方法会失败,返回一个错误,因而我们需要处理它。


能够返回一个自定的错误提示比标准错误提示会更有价值。我总是花很多时间去分析Xcode给的一些错误提示。有了自定的错误提示你就可以定义属于自己的错误信息,就能清楚知道错误到底来自哪里。为了做到这些,我创建了一个继承自Error的枚举。


NetworkError枚举


URL参数编码器(URLParameterEncoder)


创建一个名为URLParameterEncoder的文件并把它放在编码组中。


URL参数编码器代码


上面的代码含有一些参数,它可以将他们变成URL参数来安全传递。你要知道一些字符在URL中一些字符是禁用的。参数也被‘&’标记分开,我们需要考虑到所有这些。如果之前没有设置,我们还要为请求添加合适的头文件。


这个示例代码是使用单元测试时应该考虑到的。如果URL没有正确建立,我们就会有很多不必要的错误。如果你在使用一个开放API,你一定不希望自己的请求配额被一堆错误测试用完。


JSON参数编码器(JSONParameterEncoder)


创建一个名为JSONParameterEncoder的文件,也把它放在编码组中。


JSON参数编码器代码


类似URL参数编码器,不过这里是为JSON编码参数,同样要添加合适的头文件。


网络路由器(NetworkRouter)


创建一个名为NetworkRouter的文件并把它放在服务组中。我们从为一个完成部分(completion)定义别名开始。


public typealias NetworkRouterCompletion = (_ data: Data?,_ response: URLResponse?,_ error: Error?)->()


之后我们定义一个协议网络路由器


NetworkRouter代码


一个网络路由器有一个用于产生请求的终端,一旦请求产生,它会传递对完成部分的应答。我加入了一个取消函数,有它当然好,但不是一定要用到。这个函数可以在一个请求存在周期的任意时刻调用并取消它。如果你的应用有上传或下载任务,这会很有用。为了让我们的路由器能处理任何终端类型,我们这里使用了关联类型。如果不用关联类型,路由器就不得不有一个具体的终端类型。


路由器


创建一个名为Router的文件并把它放在服务组中。我们声明一个URLSessionTask类型的私有变量任务。这个任务本质上是整个工作要做的。我们让这个变量私有化,因为我们不想任何这个类之外的任何东西会调整我们的任务。


Router方法存根


请求


这里我们使用共享的会话管理(session)创建URLSession,这是创建URLSession最简单的办法,但请记住这不是唯一的方法。要实现对URLSession更复杂的配置,则要用能够改变会话管理表现的配置。想了解更多,我推荐读一读这篇文章。


这里我们通过调用buildRequest生成我们的请求,并给它一个终端作为路径。这个buildRequest的调用被限制在一个do-try-catch区块,因为我们的编码器可能会报出错误。我们仅仅把所有应答,数据和错误传送给完成部分。


Request方法代码


建立请求


在Router中创建一个名为buildRequest的私有函数,这个函数负责我们网络层中一切重要工作。本质上就是把EndPointType转化为URLRequest。一旦我们的终端生成请求,我们可以把它传递给会话管理。这里有很多工作要做,所以我们将会分别看看每个方法。让我们分解buildRequest方法:


  1. 我们举了一个URLRequest类型的变量请求的例子。把我们的基础URL给它,并附上我们要用到的路径。

  2. 我们设定这请求的httpMethod和我们终端的一致。

  3. 考虑到我们的编码器会报告错误,我们创建一个do-try-catch区块。只要创建一个大的do-try-catch区块,我们就不需要为每次尝试分别建一个。

  4. 开启route.task

  5. 根据任务,调用合适的编码器。


buildRequest方法代码.


配置参数


在Router中创建一个名为configureParameters的函数


configureParameters方法的实现


这个函数负责为我们的参数编码。因为我们的API要求所有的bodyParameters都是JSON,并且URLParameters是URL编码的,我们把合适的参数传递给设计好的编码器。如果你正在用一个有多种编码方式的API,我建议修改HTTPTask来使用编码器枚举。这个枚举需要包含所有你需要的不同类型编码器。之后在configureParameters添加一个关于你编码枚举的附加参量。开启这个枚举,合适地为参数编码。


添加附加头文件


在Router中创建一个名为addAdditionalHeaders的函数


addAdditionalHeaders方法的实现


添加所有附加头文件,让它们成为请求头文件的一部分。


取消


取消函数的实现是这样的:


Cancel方法的实现


实践


现在让我们用一个实际例子看看我们建立的网络层。我们将从TheMovieDB获取一些电影数据到我们的应用。


电影终端(MovieEndPoint)


电影终端与我们在Getting Started with Moya中提到的目标类型很相似。与实现Moya中目标类型不同的是这里我们实现我们自己的终端类型。把这个文件放在终端组中。


import Foundation

enum NetworkEnvironment {
   case qa
   case production
   case staging
}

public enum MovieApi {
   case recommended(id:Int)
   case popular(page:Int)
   case newMovies(page:Int)
   case video(id:Int)
}

extension MovieApi: EndPointType
{
   
   var environmentBaseURL : String {
       switch NetworkManager.environment {
       case .production: return 'https://api.themoviedb.org/3/movie/'
       case .qa: return 'https://qa.themoviedb.org/3/movie/'
       case .staging: return 'https://staging.themoviedb.org/3/movie/'
       }
   }
  
   var baseURL: URL {
       guard let url = URL(string: environmentBaseURL) else { fatalError('baseURL could not be configured.')}
       return url
   }
   
   var path: String {
       switch self {
       case .recommended(let id):
           return '\(id)/recommendations'
       case .popular:
           return 'popular'
       case .newMovies:
           return 'now_playing'
       case .video(let id):
           return '\(id)/videos'
       }
   }
 
   var httpMethod: HTTPMethod {
       return .get
   }
  
   var task: HTTPTask {
       switch self {
       case .newMovies(let page):
           return .requestParameters(bodyParameters: nil,
                                     urlParameters: ['page':page,
                                                     'api_key':NetworkManager.MovieAPIKey])
       default:
           return .request
       }
   }
   
   var headers: HTTPHeaders? {
       return nil
   }
}

终端类型


电影模式(MovieModel)


因为对TheMovieDB的回应同样是JSON,我们的电影模式也不会改变。我们用可解码协议来把JSON转化为我们的模式。把这个文件放在模式组中。


import Foundation

struct MovieApiResponse {
   let page: Int
   let numberOfResults: Int
   let numberOfPages: Int
   let movies: [Movie]
}

extension MovieApiResponse: Decodable {
   
   private enum MovieApiResponseCodingKeys: String, CodingKey {
       case page
       case numberOfResults = 'total_results'
       case numberOfPages = 'total_pages'
       case movies = 'results'
   }
 
   init(from decoder: Decoder) throws {
       let container = try decoder.container(keyedBy: MovieApiResponseCodingKeys.self)
       page = try container.decode(Int.self, forKey: .page)
       numberOfResults = try container.decode(Int.self, forKey: .numberOfResults)
       numberOfPages = try container.decode(Int.self, forKey: .numberOfPages)
       movies = try container.decode([Movie].self, forKey: .movies)
   }
}

struct Movie {
   let id: Int
   let posterPath: String
   let backdrop: String
   let title: String
   let releaseDate: String
   let rating: Double
   let overview: String
}

extension Movie: Decodable {
   
   enum MovieCodingKeys: String, CodingKey {
       case id
       case posterPath = 'poster_path'
       case backdrop = 'backdrop_path'
       case title
       case releaseDate = 'release_date'
       case rating = 'vote_average'
       case overview
   }
 
   init(from decoder: Decoder) throws {
       let movieContainer = try decoder.container(keyedBy: MovieCodingKeys.self)
       id = try movieContainer.decode(Int.self, forKey: .id)
       posterPath = try movieContainer.decode(String.self, forKey: .posterPath)
       backdrop = try movieContainer.decode(String.self, forKey: .backdrop)
       title = try movieContainer.decode(String.self, forKey: .title)
       releaseDate = try movieContainer.decode(String.self, forKey: .releaseDate)
       rating = try movieContainer.decode(Double.self, forKey: .rating)
       overview = try movieContainer.decode(String.self, forKey: .overview)
   }
}

电影模式


网络管理员


创建一个名为NetworkManager的文件并把它放在管理员组中。

现在开始我们的网络管理员将仅有2个静态属性:你的API密码和网络环境(引用MovieEndPoint)。网络管理员也有一个类型为MovieApi的Router。


网络管理员代码


网络响应


在NetworkManager中创建一个名为NetworkResponse的枚举。

NetworkResponse枚举


我们将用这个枚举处理来自API的响应,并显示相应的信息。


结果


在NetworkManager中创建一个枚举Result。


Result枚举


一个结果枚举可以用在很多不同事情上,非常有用。我们根据结果确定我们对API的调用是成功还是失败。如果失败了,我们会返回一个错误信息并说明原因。


处理网络响应

创建一个名为handleNetworkResponse的函数,这个函数有一个参量,即HTTPResponse,并返回一个Result.




这里我们开启HTTPResponse的状态码,状态码是一个能告诉我们响应状态的HTTP协议。基本上200-299之间都是成功。更多关于状态码点击这里。


产生调用


现在我们已经为我们的网络层打下雄厚的基础。是时候开始调用了。


我们将会从API获取一个新电影列表。创建一个名为getNewMovies的函数。


getNewMovies方法的实现


让我们分解这个方法的每一步


  1. 我们定义getNewMovies方法含有2个参量:一个页码和一个能返回电影数组或错误信息的完成部分(completion)。

  2. 我们调用我们的路由器,输入页码并在一个闭包(closure)内处理这个完成部分。

  3. 如果没有网络或者出于一些原因无法调用API,URLSession会返回错误。请注意这并不是API的失败。这种失败多是客服端的,很可能是因为网络连接不好。

  4. 我们需要把我们的响应转变为一个HTTPURLResponse,因为我们需要访问状态码属性。

  5. 我们声明一个从handleNetworkResponse方法得到的结果,之后在switch-case区块检查这个结果。

  6. 成功意味着我们成功地和API联系,并得到一个适当的响应。之后我们检查这个响应是否携带数据。如果没有数据我们就用返回语句退出这个方法。

  7. 如果携带有数据,我们需要把数据编码成我们的模式,之后我们把编码好的电影传递给完成部分。

  8. 如果结果是失败,我们就把错误传递给完成部分。


这就完成了,这就是我们不依赖Cocoapods和第三方库的纯Swift网络层。想要测试api请求能否获取电影,就创建一个带有Network Manager 的viewController之后在管理员调用getNewMovies。


class MainViewController: UIViewController {

   var networkManager: NetworkManager!
 
   init(networkManager: NetworkManager) {
       super.init(nibName: nil, bundle: nil)
       self.networkManager = networkManager
   }
 
   required init?(coder aDecoder: NSCoder) {
       fatalError('init(coder:) has not been implemented')
   }
  
   override func viewDidLoad() {
       super.viewDidLoad()
       view.backgroundColor = .green
       networkManager.getNewMovies(page: 1) { movies, error in
           if let error = error {
               print(error)
           }
           if let movies = movies {
               print(movies)
           }
       }
   }
}

MainViewControoler的示例


迂回网络(DETOUR- NETWORK)记录器


我最喜欢的Moya特性之一就是网络记录器。它使得调试变得更容易,并且通过记录所有网络通信可以看到关于请求和响应发生了什么。我决定实现这个网络层时候就想要有这个特性了。创建一个名为NetworkLogger的文件并把它放在服务组中。我已经实现了一个记录对控制台请求的代码。我不会展示我们应该把代码放到代码层中的哪里。这是对你的一个挑战,创建一个记录控制台响应的函数,并在我们的架构中找到合适的位置放置它们。


提示:静态函数记录(响应:URLResponse)


小技巧


你在Xcode中遇到过不理解的占位符吗?比如让我们看看刚刚为了实现Router写的代码



NetworkRouterCompletion是我们实现的。即使我们实现了它,有时候也很难记清它是哪种类型,我们该怎么用它。我们喜欢的Xcode有解决办法。只要在占位符上双击,Xcode就会告诉你。



结论


我们有了一个简单好用,面向协议,还可以自己定制的网络层。我们能完全控制它的功能,完全理解它的机制。通过进行这个练习,我可以说我本人学到不少新事情。所以比起那些只需要装一个库就能完成的工作,我对这项工作更感到自豪。希望这篇文章能说明,用Swift创建你自己的网络层并没那么难。只要不做这样的事情就行了:



你可以在我的GitHub上找到源代码,感谢阅读。


相关链接:


本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
PHP JSON | 菜鸟教程
深入学习PHP中的JSON相关函数
Python函数方法实例详解全集(更新中...)
用case when替代decode
CodeSmith自动生成代码
printf 源代码 实现
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服