首頁 > 軟體

Swift中的HTTP請求體Request Bodies使用範例詳解

2023-02-05 14:01:56

正文

在進行HTTPRequest請求傳送前,我們稍稍改進一下我們的結構體,最後,我們將會以下面的資訊輸出:

public struct HTTPRequest {
    private var urlComponents = URLComponents()
    public var method: HTTPMethod = .get
    public var headers: [String: String] = [:]
    public var body: Data?
}

在本節中,我們將著重討論一下body屬性,並對其進行改造。

通用化body

在HTTP簡介那一節,我們瞭解到,一個請求體是原始二進位制資料,但是,在與 Web API 通訊時,這些資料有多種標準格式,例如 JSON 和表單提交。

我們可以將其概括為一種“給我們資料的東西”的形式,而不是要求此程式碼的客戶手動構造其提交資料的二進位制表示。

由於我們不打算對用於構造資料的演演算法施加任何限制,因此通過協定而不是具體型別來定義此功能是有意義的:

public protocol HTTPBody { }

接下來,我們需要一種方法從其中一個值中獲取Data,並在出現問題時選擇性地報告錯誤:

public protocol HTTPBody { 
    func encode() throws -> Data 
}

我們可以在這一點上停下來,但還有另外兩條資訊值得擁有:

public protocol HTTPBody { 
    var isEmpty: Bool { get }
    var additionalHeaders: [String: String] { get } 
    func encode() throws -> Data 
}

如果我們能快速知道一個body是空的,那麼我們就可以省去嘗試檢索任何編碼資料和處理錯誤或空資料值的麻煩。

此外,某些型別的正文與請求中的header結合使用。 例如,當我們將值編碼為 JSON 時,我們希望有一種方法可以自動指定 Content-Type: application/json 的header,而無需在請求中手動指定它。 為此,我們將允許這些型別宣告額外的header,這些檔頭將作為最終請求的一部分結束。 為了進一步簡化採用,我們可以為這些提供預設實現:

extension HTTPBody {
    public var isEmpty: Bool { return false }
    public var additionalHeaders: [String: String] { return [:] }
}

最後,我們可以將我們的型別更新到這個新的協定中

public struct HTTPRequest {
    private var urlComponents = URLComponents()
    public var method: HTTPMethod = .get
    public var headers: [String: String] = [:]
    public var body: HTTPBody?
}

空請求體 EmptyBody

最簡單的HTTPBody是”無體“。有了這個協定,定義一個空請求體也是很方便的。

public struct EmptyBody: HTTPBody {
    public let isEmpty = true
    public init() { }
    public func encode() throws -> Data { Data() }
}

我們甚至可以將其設定為預設的主體值,從而完全消除對該屬性的可選性的需要:

public struct HTTPRequest {
    private var urlComponents = URLComponents()
    public var method: HTTPMethod = .get
    public var headers: [String: String] = [:]
    public var body: HTTPBody = EmptyBody()
}

資料體 DataBody

下一個明顯要實現的主體型別是返回給定的任何Data值的主體。 這將用於我們不一定有 HTTPBody 實現但也許我們已經有Data值本身要傳送的情況。

具體實現如下:

public struct DataBody: HTTPBody {    
    private let data: Data
    public var isEmpty: Bool { data.isEmpty }
    public var additionalHeaders: [String: String]
    public init(_ data: Data, additionalHeaders: [String: String] = [:]) {
        self.data = data
        self.additionalHeaders = additionalHeaders
    }
    public func encode() throws -> Data { data }    
}

有了這個,我們可以很輕鬆的將一個Data值封裝進HTTPBody裡:

let otherData: Data = ...
var request = HTTPRequest()
request.body = DataBody(otherData)

JSON體 JSONBody

在傳送網路請求時,將值編碼為 JSON 是一項非常常見的任務。 製作一個 HTTPBody 來為我們處理這個現在很容易:

public struct JSONBody: HTTPBody {
    public let isEmpty: Bool = false
    public var additionalHeaders = [
        "Content-Type": "application/json; charset=utf-8"
    ]
    private let encode: () throws -> Data
    public init<T: Encodable>(_ value: T, encoder: JSONEncoder = JSONEncoder()) {
        self.encode = { try encoder.encode(value) }
    }
    public func encode() throws -> Data { return try encode() }
}

首先,我們假設我們得到的任何值都會至少產生一些結果,因為即使是空字串也會編碼為非空 JSON 值。 因此,isEmpty = false

接下來,大多數伺服器在接收 JSON 正文時需要 application/jsonContent-Type,因此我們假設這是常見情況,並在 additionalHeaders 中預設該值。 但是,我們會將該屬性保留為 var,以防萬一出現客戶不希望這樣的情況。

對於編碼,我們需要接受一些通用值(要編碼的東西),但最好不要讓整個結構對編碼型別通用。 我們可以通過將型別的泛型引數限制為初始化器來避免型別的泛型引數,然後在閉包中捕獲泛型值。

我們還需要一種方法來提供自定義 JSONEncoder,以便客戶有機會擺弄諸如 .keyEncodingStrategy 之類的東西。 但是,我們將提供一個預設編碼器來簡化使用。

最後,encode() 方法本身只是呼叫我們建立的閉包,它捕獲通用值並通過 JSONEncoder 執行它。

其中一個的使用方法如下:

struct PagingParameters: Encodable {
    let page: Int
    let number: Int
}
let parameters = PagingParameters(page: 0, number: 10)
var request = HTTPRequest()
request.body = JSONBody(parameters)

這樣,正文將自動編碼為 {"page":0,"number":10},我們的最終請求將具有正確的 Content-Type 檔頭。

表單 FormBody

我們將在本文中看到的最後一種主體是表示基本表單提交的body。 當我們專門討論多部分表單上傳時,我們將儲存檔案上傳以備將來使用。

表單提交正文最終為粗略的 URL 編碼鍵值對,例如 name=Arthur&age=42

我們將從與我們的 HTTPBody 實現相同的基本結構開始:

public struct FormBody: HTTPBody {
    public var isEmpty: Bool { values.isEmpty }
    public let additionalHeaders = [
        "Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
    ]
    private let values: [URLQueryItem]
    public init(_ values: [URLQueryItem]) {
        self.values = values
    }
    public init(_ values: [String: String]) {
        let queryItems = values.map { URLQueryItem(name: $0.key, value: $0.value) }
        self.init(queryItems)
    }
    public func encode() throws -> Data {
        let pieces = values.map { /* TODO */ }
        let bodyString = pieces.joined(separator: "&")
        return Data(bodyString.utf8)
    }
}

和以前一樣,我們有一個自定義的 Content-Type 檔頭來應用於請求。 我們還公開了幾個初始化器,以便使用者端可以以對他們有意義的方式描述這些值。 我們還刪除了大部分 encode() 方法,省略了 URLQueryItem 值的實際編碼。

不幸的是,對名稱和值進行編碼有點模稜兩可。 如果你仔細閱讀關於表單提交的古老規範,你會看到提到“換行規範化”和將空格編碼為 + 的內容。 我們可以努力挖掘並找出這些東西的含義,但在實踐中,Web 伺服器往往可以很好地處理任何百分比編碼的內容,甚至是空格。 我們將走捷徑並假設這是真的。 我們還將全面假設字母數位字元在名稱和值中是可以的,並且其他所有內容都應該被編碼:

private func urlEncode(_ string: String) -> String {
    let allowedCharacters = CharacterSet.alphanumerics
    return string.addingPercentEncoding(withAllowedCharacters: allowedCharacters) ?? ""
}

使用 = 字元組合名稱和值:

private func urlEncode(_ queryItem: URLQueryItem) -> String {
    let name = urlEncode(queryItem.name)
    let value = urlEncode(queryItem.value ?? "")
    return "(name)=(value)"
}

有了這個,我們可以解決 /* TODO */ 評論:

public struct FormBody: HTTPBody {
    public var isEmpty: Bool { values.isEmpty }
    public let additionalHeaders = [
        "Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
    ]
    private let values: [URLQueryItem]
    public init(_ values: [URLQueryItem]) {
        self.values = values
    }
    public init(_ values: [String: String]) {
        let queryItems = values.map { URLQueryItem(name: $0.key, value: $0.value) }
        self.init(queryItems)
    }
    public func encode() throws -> Data {
        let pieces = values.map(self.urlEncode)
        let bodyString = pieces.joined(separator: "&")
        return Data(bodyString.utf8)
    }
    private func urlEncode(_ queryItem: URLQueryItem) -> String {
        let name = urlEncode(queryItem.name)
        let value = urlEncode(queryItem.value ?? "")
        return "(name)=(value)"
    }
    private func urlEncode(_ string: String) -> String {
        let allowedCharacters = CharacterSet.alphanumerics
        return string.addingPercentEncoding(withAllowedCharacters: allowedCharacters) ?? ""
    }
}

和以前一樣,使用它變得很簡單:

var request = HTTPRequest()
request.body = FormBody(["greeting": "Hello, ", "target": "

IT145.com E-mail:sddin#qq.com