- 原文地址: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、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。