- 原文地址:Writing a Network Layer in Swift: Protocol-Oriented Approach
- 原文做者:Malcolm Kumwenda
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:talisk
- 校对者:ALVINYEH,rydensun
在本指南中,咱们将介绍如何在没有任何第三方库的状况下以纯 Swift 实现网络层。让咱们快开始吧!阅读了本指南后,咱们的代码应该是:html
如下是咱们最终经过网络层实现的一个例子:前端
该项目的最终目标。android
借助枚举输入 router.request(.,咱们能够看到全部可用的端点以及该请求所需的参数。ios
在建立任何东西时,结构老是很是重要的,好的结构便于之后找到所需。我坚信文件夹结构是软件架构的一个关键贡献者。为了让咱们的文件保持良好的组织性,咱们事先就建立好全部组,而后记下每一个文件应该放在哪里。这是一个对项目结构的概述。(请注意如下名称都只是建议,你能够根据本身的喜爱命名你的类和分组。)git
项目目录结构。github
咱们须要的第一件事是定义咱们的 EndPointType 协议。该协议将包含配置 EndPoint 的全部信息。什么是 EndPoint?本质上它是一个 URLRequest,它包含全部包含的组件,如标题,query 参数和 body 参数。EndPointType 协议是咱们网络层实现的基石。接下来,建立一个文件并将其命名为 EndPointType。将此文件放在 Service 组中。(请注意不是 EndPoint 组,这会随着咱们的继续变得更清晰)。编程
EndPointType 协议。swift
咱们的 EndPointType 具备构建整个 endPoint 所需的大量HTTP协议。让咱们来探索这些协议的含义。后端
建立一个名为 HTTPMethod 的文件,并把它放到 Service 组里。这个枚举将被用于为咱们的请求设置 HTTP 方法。api
HTTPMethod 枚举。
建立一个名为 HTTPTask 的文件,并把它放到 Service 组里。HTTPTask 负责为特定的 endPoint 配置参数。你能够添加尽量多的适用于你的网络层要求的状况。 我将要发一个请求,因此我只有三种状况。
HTTPTask 枚举。
咱们将在下一节讨论参数以及参数的编解码。
HTTPHeaders 仅仅是字典的 typealias(别名)。你能够在 HTTPTask 文件的开头写下这个 typealias。
public typealias HTTPHeaders = [String:String]
复制代码
建立一个名为 ParameterEncoding 的文件,并把它放到 Encoding 组里。而后首要之事即是定义 Parameters 的 typealias。咱们利用 typealias 使咱们的代码更简洁、清晰。
public typealias Parameters = [String:Any]
复制代码
接下来,用一个静态函数 encode 定义一个协议 ParameterEncoder。encode 方法包含 inout URLRequest 和 Parameters 这两个参数。inout 是一个 Swift 的关键字,它将参数定义为引用参数。一般来讲,变量以值类型传递给函数。经过在参数前面添加 inout,咱们将其定义为引用类型。要了解更多关于 inout 参数的信息,你能够参考这里。ParameterEncoder协议将由咱们的 JSONParameterEncoder 和 URLPameterEncoder 实现。
public protocol ParameterEncoder {
static func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws
}
复制代码
ParameterEncoder 执行一个函数来编码参数。此方法可能失败而抛出错误,须要咱们处理。
能够证实抛出自定义错误而不是标准错误是颇有价值的。我老是发现本身很难破译 Xcode 给出的一些错误。经过自定义错误,您能够定义本身的错误消息,并确切知道错误来自何处。为此,我只需建立一个从 Error 继承的枚举。
NetworkError 枚举。
建立一个名为 URLParameterEncoder 的文件,并把它放到 Encoding 组里。
URLParameterEncoder 的代码。
上面的代码传递了参数,并将参数安全地做为 URL 类型的参数传递。正如你应该知道,有一些字符在 URL 中是被禁止的。参数须要用「&」符号分开,因此咱们应该注意遵循这些规范。若是没有设置 header,咱们也要为请求添加适合的 header。
这个代码示例是咱们应该考虑使用单元测试进行测试的。正确构建 URL 是相当重要的,否则咱们可能会遇到许多没必要要的错误。若是你使用的是开放 API,你确定不但愿配额被大量失败的测试耗尽。若是你想了解更多有关单元测试方面的知识,能够阅读 S.T.Huang 写的这篇文章。
建立一个名为 JSONParameterEncoder 的文件,并把它放到 Encoding 组里。
JSONParameterEncoder 的代码。
与 URLParameter 解码器相似,但在此,咱们把参数编码成 JSON,再次添加适当的 header。
建立一个名为 NetworkRouter 的文件,并把它放到 Service 组里。咱们来定义一个 block 的 typealias。
public typealias NetworkRouterCompletion = (_ data: Data?,_ response: URLResponse?,_ error: Error?)->()
复制代码
接下来咱们定义一个名为 NetworkRouter 的协议。
NetworkRouter 的代码。
一个 NetworkRouter 具备用于发出请求的 EndPoint,一旦发出请求,就会将响应传递给完成的 block。我已经添加了一个很是好的取消请求的功能,但不要深刻探究它。这个功能能够在请求生命周期的任什么时候候调用,而后取消请求。若是您的应用程序有上传或下载的功能,取消请求可能会是很是有用的。咱们在这里使用 associatedtype,由于咱们但愿咱们的 Router 可以处理任何 EndPointType。若是不使用 associatedtype,则 router 必须具备具体的 EndPointType。更多有关 associatedtypes 的内容,我建议能够看下 NatashaTheRobot 写的这篇文章。
建立一个名为 Router 的文件,并把它放到 Service 组里。咱们声明一个类型为 URLSessionTask 的私有变量 task。这个 task 变量本质上是要完成全部的工做。咱们让变量声明为私有,由于咱们不但愿在这个类以外还能修改这个 task 变量。
Router 方法的代码。
这里咱们使用 sharedSession 建立一个 URLSession。这是建立 URLSession 最简单的方法。但请记住,这不是惟一的方法。更复杂的 URLSession 配置可用能够改变 session 行为的 configuration 来实现。要了解更多信息,我建议花点时间阅读下这篇文章。
这里咱们经过调用 buildRequest 方法来建立请求,并传入名为 route 的一个 EndPoint 类型参数。因为咱们的解码器可能会抛出一个错误,这段调用用一个 do-try-catch 块包起来。咱们只是单纯地把全部请求、数据和错误传给 completion 回调。
Request 方法的代码.
在 Router 里面建立一个名为 buildRequest 的私有方法,这个方法会在咱们的网络层中负责相当重要的工做,从本质上把 EndPointType 转化为 URLRequest。一旦咱们的 EndPoint 发出了一个请求,咱们就把他传递给 session。这里作了不少工做,咱们来逐一看看每一个方法。让咱们分解 buildRequest 方法:
buildRequest 方法的代码。
建立一个名为 configureParameters 的方法,并把它放到 Router 里面。
configureParameters 方法的实现。
这个函数负责编码咱们的参数。因为咱们的API指望全部 bodyParameters 是 JSON 格式的,以及 URLParameters 是 URL 编码的,咱们将相应的参数传递给其指定的编码器便可。若是您正在处理具备不一样编码风格的 API,我会建议修改 HTTPTask 以获取编码器枚举。这个枚举应该有你须要的全部不一样风格的编码器。而后在 configureParameters 里面添加编码器枚举的附加参数。适当地调用枚举并编码参数。
建立一个名为 addAdditionalHeaders 的方法,并把它放到 Router 里面。
addAdditionalHeaders 方法的实现。
cancel 方法的实现就像下面这样:
cancel 方法的实现。
如今让咱们把封装好的网络层在实际样例项目中进行实践。咱们将用 TheMovieDB🍿 获取一些数据,并展现在咱们的应用中。
MovieEndPoint 与咱们在 Getting Started with Moya(若是没看过的话就看看)中的 Target 类型很是相近。Moya 中的 TargetType,在咱们今天的例子中是 EndPointType。把这个文件放到 EndPoint 分组当中。
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
}
}
复制代码
EndPointType
咱们的 MovieModel 也不会改变,由于 TheMovieDB 的响应是相同的 JSON 格式。咱们利用 Decodable 协议将咱们的 JSON 转换为咱们的模型。将此文件放在 Model 组中。
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)
}
}
复制代码
Movie Model
建立一个名为 NetworkManager 的文件,并将它放在 Manager 分组中。如今咱们的 NetworkManager 将有两个静态属性:你的 API key 和 网络环境(参考 MovieEndPoint)。NetworkManager 也有一个 MovieApi 类型的 Router。
Network Manager 的代码。
在 NetworkManager 里建立一个名为 NetworkResponse 的枚举。
Network Response 枚举。
咱们将用这些枚举去处理 API 返回的结果,并显示合适的信息。
在 NetworkManager 中建立一个名为 Result 的枚举。
Result 枚举。
Result 这个枚举很是强大,能够用来作许多不一样的事情。咱们将使用 Result 来肯定咱们对 API 的调用是成功仍是失败。若是失败,咱们会返回一条错误消息,并说明缘由。想了解更多关于 Result 对象编程的信息,你能够 观看或阅读本篇。
建立一个名为 handleNetworkResponse 的方法。这个方法有一个 HTTPResponse 类型的参数,并返回 Result 类型的值。
这里咱们运用 HTTPResponse 状态码。状态码是一个告诉咱们响应值状态的 HTTP 协议。一般状况下,200 至 299 的状态码都表示成功。须要了解更多关于 statusCodes 的信息能够阅读 这篇文章.
所以,如今咱们为咱们的网络层奠基了坚实的基础。如今该去调用了!
咱们将要从 API 拉取一个新电影的列表。建立一个名为 getNewMovies 的方法。
getNewMovies 方法实现。
咱们来分解这个方法的每一步:
完成了!这是咱们用纯 Swift 写的,没有用到 Cocoapods 和第三方库的网络层。为了测试得到电影列表的 API,使用 Network Manager 建立一个 ViewController,而后在 mamager 上调用 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 的例子。
我最喜欢的 Moya 功能之一就是网络日志。它经过记录全部网络流量,来使调试和查看请求和响应更容易。当我决定实现这个网络层时,这是我很是想要的功能。建立一个名为 NetworkLogger 的文件,并将其放入 Service 组中。我已经实现了将请求记录到控制台的代码。我不会显示应该把这个代码放在咱们的网络层的什么位置。做为你的挑战,请继续建立一个将响应记录到控制台的方法,并在咱们的项目结构中找到放置这些函数调用的合适位置。[放置 Gist 文件]
提示:static func log(response: URLResponse) {}
有没有发现本身在 Xcode 中有一个你不太了解的占位符?例如,让咱们看看咱们为 Router 实现的代码。
NetworkRouterCompletion 是须要用户实现的。尽管咱们已经实现了它,但有时很难准确地记住它是什么类型以及咱们应该如何使用它。这让咱们亲爱的 Xcode 来拯救吧!只需双击占位符,Xcode 就会完成剩下的工做。
如今咱们有一个彻底能够自定义的、易于使用的、面向协议的网络层。咱们能够彻底控制其功能并完全理解其机制。经过这个练习,我能够真正地说我本身学到了一些新的东西。因此我对这部分工做感到自豪,而不是仅仅安装了一个库。但愿这篇文章证实了在 Swift 中建立本身的网络层并不难。😜就像这样:
你能够到个人 GitHub 上找到源码,感谢你的阅读!
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。