Alamofire 5 的使用

Alamofire 5 的使用 - 基本用法

此文章是對(duì) Alamofire Usage 的翻譯,有需要的可以去看原文。

另外此文章的內(nèi)容也保存到了我的 GitHub 倉庫撵渡。如果覺得對(duì)你有用的椒丧,可以順手給個(gè) Star。謝謝闷祥!

特性

  • 可鏈接的請(qǐng)求/響應(yīng)函數(shù)
  • URL / JSON 參數(shù)編碼
  • 上傳文件 / Data / 流 / 多表單數(shù)據(jù)
  • 使用請(qǐng)求或者恢復(fù)數(shù)據(jù)下載文件
  • 使用 URLCredential 進(jìn)行身份驗(yàn)證
  • HTTP 響應(yīng)驗(yàn)證
  • 帶有進(jìn)度的上傳和下載閉包
  • cURL 命令的輸出
  • 動(dòng)態(tài)調(diào)整和重試請(qǐng)求
  • TLS 證書和公鑰固定
  • 網(wǎng)絡(luò)可達(dá)性
  • 全面的單元和集成測試覆蓋率

組件庫

為了讓 Alamofire 專注于核心網(wǎng)絡(luò)實(shí)現(xiàn),Alamofire 軟件基金會(huì)創(chuàng)建了額外的組件庫,為 Alamofire 生態(tài)系統(tǒng)帶來額外的功能砸泛。

  • AlamofireImage:一個(gè)圖片庫,包括圖像響應(yīng)序列化器蛆封、UIImageUIImageView 的擴(kuò)展唇礁、自定義圖像濾鏡、自動(dòng)清除內(nèi)存緩存和基于優(yōu)先級(jí)的圖像下載系統(tǒng)惨篱。
  • AlamofireNetworkActivityIndicator: 使用 Alamofire 控制 iOS 上網(wǎng)絡(luò)活動(dòng)指示器的可見性盏筐。它包含可配置的延遲計(jì)時(shí)器,有助于減少閃爍砸讳,并且可以支持不由 Alamofire 管理的 URLSession 實(shí)例琢融。

要求的使用環(huán)境

  • iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+
  • Xcode 10.2+
  • Swift 5+

安裝方法

CocoaPods

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!

target '項(xiàng)目名稱' do
    pod 'Alamofire', '~> 5.0'
end
復(fù)制代碼

iOS 版本和 Alamofire 版本可以自己根據(jù)實(shí)際情況自行更改。CocoaPods 是比較常用的第三方庫管理工具簿寂,其他方法就不詳細(xì)說了漾抬。其他集成方法可以查看原文檔

基本用法

介紹

Alamofire 為 HTTP 網(wǎng)絡(luò)請(qǐng)求提供了一個(gè)優(yōu)雅且可組合的接口常遂。它沒有實(shí)現(xiàn)自己的 HTTP 網(wǎng)絡(luò)功能纳令。取而代之的是,它建立在由 Foundation 框架提供的 URL 加載系統(tǒng)之上。系統(tǒng)的核心是 URLSessionURLSessionTask 子類平绩。Alamofire 將這些 APIs 和許多其他 APIs 封裝在一個(gè)更易于使用的接口中圈匆,并提供使用 HTTP 網(wǎng)絡(luò)進(jìn)行現(xiàn)代應(yīng)用程序開發(fā)所必需的各種功能。但是捏雌,了解 Alamofire 的許多核心行為來自何處很重要跃赚,因此熟悉 URL 加載系統(tǒng)非常重要。歸根結(jié)底腹忽,Alamofire 的網(wǎng)絡(luò)特性受到該系統(tǒng)功能的限制来累,應(yīng)該始終記住并遵守其行為和最佳實(shí)踐。

此外窘奏,Alamofire(以及 URL 加載系統(tǒng))中的聯(lián)網(wǎng)是異步完成的嘹锁。異步編程可能會(huì)讓不熟悉這個(gè)概念的程序員感到沮喪,但是有很好的理由這樣做着裹。

另外:AF 命名空間和引用

以前的 Alamofire 文檔使用了類似 Alamofire.request() 的示例领猾。這個(gè) API 雖然看起來需要 Alamofire前綴,但實(shí)際上在沒有它的情況下也可以骇扇。request 方法和其他函數(shù)在任何帶有 import Alamofire 的文件中都是全局可用的摔竿。從 Alamofire 5 開始,此功能已被刪除少孝,被更改為 AF 继低,它是對(duì) Session.default 的引用。這允許 Alamofire 提供同樣的便利功能稍走,同時(shí)不必每次使用 Alamofire 時(shí)都污染全局命名空間袁翁,也不必全局復(fù)制 Session API。類似地婿脸,由 Alamofire 擴(kuò)展的類型將使用 af 屬性擴(kuò)展來將 Alamofire 添加的功能與其他擴(kuò)展分開粱胜。

發(fā)起請(qǐng)求

Alamofire 為發(fā)出 HTTP 請(qǐng)求提供了多種方便的方法。最簡單的是狐树,只需提供一個(gè)可以轉(zhuǎn)換為 URL 的 String

AF.request("https://httpbin.org/get").response { response in
    debugPrint(response)
}
復(fù)制代碼

所有示例都需要在源文件中的某個(gè)位置 import Alamofire焙压。

這實(shí)際上是 Alamofire Session 類型上用于發(fā)出請(qǐng)求的兩個(gè)頂層 APIs 的一種形式。它的完整定義如下:

open func request<Parameters: Encodable>(
    _ convertible: URLConvertible,
    method: HTTPMethod = .get,
    parameters: Parameters? = nil,
    encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default,
    headers: HTTPHeaders? = nil,
    interceptor: RequestInterceptor? = nil
) -> DataRequest
復(fù)制代碼

此方法創(chuàng)建一個(gè) DataRequest抑钟,同時(shí)允許組合來自各個(gè)組件(如 methodheaders )的請(qǐng)求涯曲,同時(shí)還允許每個(gè)傳入 RequestInterceptorsEncodable 參數(shù)。

還有其他方法允許您使用 Parameters 字典和 ParameterEncoding 類型來發(fā)出請(qǐng)求在塔。不再推薦此 API幻件,最終將被棄用并從 Alamofire 中刪除。

這個(gè) API 的第二個(gè)版本要簡單得多:

open func request(
    _ urlRequest: URLRequestConvertible,
    interceptor: RequestInterceptor? = nil
) -> DataRequest
復(fù)制代碼

此方法為遵循 AlamofireURLRequestConvertible 協(xié)議的任何類型創(chuàng)建 DataRequest 心俗。所有不同于前一版本的參數(shù)都封裝在該值中,這會(huì)產(chǎn)生非常強(qiáng)大的抽象。這將在我們的高級(jí)用法中討論城榛。

HTTP Methods

HTTPMethod 類型列出了 RFC 7231 §4.3 中定義的 HTTP 方法:

public struct HTTPMethod: RawRepresentable, Equatable, Hashable {
    public static let connect = HTTPMethod(rawValue: "CONNECT")
    public static let delete = HTTPMethod(rawValue: "DELETE")
    public static let get = HTTPMethod(rawValue: "GET")
    public static let head = HTTPMethod(rawValue: "HEAD")
    public static let options = HTTPMethod(rawValue: "OPTIONS")
    public static let patch = HTTPMethod(rawValue: "PATCH")
    public static let post = HTTPMethod(rawValue: "POST")
    public static let put = HTTPMethod(rawValue: "PUT")
    public static let trace = HTTPMethod(rawValue: "TRACE")

    public let rawValue: String

    public init(rawValue: String) {
        self.rawValue = rawValue
    }
}
復(fù)制代碼

這些值可以作為 method 參數(shù)傳遞給 AF.request API:

AF.request("https://httpbin.org/get")
AF.request("https://httpbin.org/post", method: .post)
AF.request("https://httpbin.org/put", method: .put)
AF.request("https://httpbin.org/delete", method: .delete)
復(fù)制代碼

重要的是要記住揪利,不同的 HTTP 方法可能有不同的語義,需要不同的參數(shù)編碼狠持,這取決于服務(wù)器的期望疟位。例如,URLSession 或 Alamofire 不支持在 GET 請(qǐng)求中傳遞 body 數(shù)據(jù)喘垂,并將返回錯(cuò)誤甜刻。

Alamofire 還提供了對(duì) URLRequest 的擴(kuò)展,以橋接將字符串返回到 HTTPMethod 值的 httpMethod 屬性:

public extension URLRequest {
    /// Returns the `httpMethod` as Alamofire's `HTTPMethod` type.
    var method: HTTPMethod? {
        get { return httpMethod.flatMap(HTTPMethod.init) }
        set { httpMethod = newValue?.rawValue }
    }
}
復(fù)制代碼

如果需要使用 Alamofire 的 HTTPMethod 類型不支持的 HTTP 方法正勒,可以擴(kuò)展該類型以添加自定義值:

extension HTTPMethod {
    static let custom = HTTPMethod(rawValue: "CUSTOM")
}
復(fù)制代碼

請(qǐng)求參數(shù)和參數(shù)編碼器

Alamofire 支持將任何 Encodable 類型作為請(qǐng)求的參數(shù)得院。然后,這些參數(shù)通過遵循 ParameterEncoder協(xié)議的類型傳遞章贞,并添加到 URLRequest 中祥绞,然后通過網(wǎng)絡(luò)發(fā)送。Alamofire 包含兩種遵循 ParameterEncoder 的類型:JSONParameterEncoderURLEncodedFormParameterEncoder 鸭限。這些類型涵蓋了現(xiàn)代服務(wù)使用的最常見的編碼蜕径。

struct Login: Encodable {
    let email: String
    let password: String
}

let login = Login(email: "test@test.test", password: "testPassword")

AF.request("https://httpbin.org/post",
           method: .post,
           parameters: login,
           encoder: JSONParameterEncoder.default).response { response in
    debugPrint(response)
}
復(fù)制代碼

URLEncodedFormParameterEncoder

URLEncodedFormParameterEncoder 將值編碼為 URL 編碼字符串,以將其設(shè)置為或附加到任何現(xiàn)有 URL 查詢字符串败京,或設(shè)置為請(qǐng)求的 HTTP body兜喻。通過設(shè)置編碼的目的地,可以控制編碼字符串的設(shè)置位置赡麦。URLEncodedFormParameterEncoder.Destination 枚舉有三種情況:

  • .methodDependent - 對(duì)于 .get朴皆、.head.delete 請(qǐng)求隧甚,它會(huì)將已編碼查詢字符串應(yīng)用到現(xiàn)有的查詢字符串中车荔;對(duì)于其他類型的請(qǐng)求,會(huì)將其設(shè)置為 HTTP body戚扳。
  • .queryString - 將編碼字符串設(shè)置或追加到請(qǐng)求的 URL 中忧便。
  • .httpBody - 將編碼字符串設(shè)置為 URLRequest 的 HTTP body。

如果尚未設(shè)置 Content-Type帽借,那么會(huì)把具有 HTTP body 的已編碼請(qǐng)求的 HTTP header 設(shè)置為 application/x-www-form-urlencoded; charset=utf-8珠增。

在內(nèi)部,URLEncodedFormParameterEncoder 使用 URLEncodedFormEncoderEncodable 類型編碼為 URL 編碼形式的 String砍艾。此編碼器可用于自定義各種類型的編碼蒂教,包括使用 ArrayEncodingArray、使用 BoolEncodingBool脆荷、使用 DataEncodingData凝垛、使用 DateEncodingDate懊悯、使用 KeyEncoding 的 keys 以及使用 SpaceEncoding 的空格。

使用 URL 編碼參數(shù)的 GET 請(qǐng)求
let parameters = ["foo": "bar"]

// 下面三種方法都是等價(jià)的
AF.request("https://httpbin.org/get", parameters: parameters) // encoding defaults to `URLEncoding.default`
AF.request("https://httpbin.org/get", parameters: parameters, encoder: URLEncodedFormParameterEncoder.default)
AF.request("https://httpbin.org/get", parameters: parameters, encoder: URLEncodedFormParameterEncoder(destination: .methodDependent))

// https://httpbin.org/get?foo=bar
復(fù)制代碼
使用 URL 編碼參數(shù)的 POST 請(qǐng)求
let parameters: [String: [String]] = [
    "foo": ["bar"],
    "baz": ["a", "b"],
    "qux": ["x", "y", "z"]
]

// 下面三種方法都是等價(jià)的
AF.request("https://httpbin.org/post", method: .post, parameters: parameters)
AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: URLEncodedFormParameterEncoder.default)
AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: URLEncodedFormParameterEncoder(destination: .httpBody))

// HTTP body: "qux[]=x&qux[]=y&qux[]=z&baz[]=a&baz[]=b&foo[]=bar"
復(fù)制代碼

配置已編碼參數(shù)的排序

從 Swift 4.2 開始梦皮,Swift 的 Dictionary 類型使用的隨機(jī)算法在運(yùn)行時(shí)產(chǎn)生一個(gè)隨機(jī)的內(nèi)部順序炭分,并且在應(yīng)用程序的每次啟動(dòng)都是不同的。這可能會(huì)導(dǎo)致已編碼參數(shù)更改順序剑肯,這可能會(huì)影響緩存和其他行為捧毛。默認(rèn)情況下,URLEncodedFormEncoder 將對(duì)其編碼的鍵值對(duì)進(jìn)行排序让网。雖然這會(huì)為所有 Encodable 類型生成常量輸出呀忧,但它可能與該類型實(shí)現(xiàn)的實(shí)際編碼順序不匹配。您可以將 alphabetizeKeyValuePairs設(shè)置為 false 以返回到實(shí)現(xiàn)的順序溃睹,因此這將變成隨機(jī) Dictionary 順序而账。

您可以創(chuàng)建自己的 URLEncodedFormParameterEncoder,在初始化時(shí)丸凭,可以在 URLEncodedFormEncoder參數(shù)中設(shè)置 alphabetizeKeyValuePairs 的值:

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(alphabetizeKeyValuePairs: false))
復(fù)制代碼
配置 Array 參數(shù)的編碼

由于沒有關(guān)于如何對(duì)集合類型進(jìn)行編碼的規(guī)范福扬,默認(rèn)情況下,Alamofire 遵循以下約定:將 [] 附加到數(shù)組值的鍵(foo[]=1&foo[]=2)惜犀,并附加由中括號(hào)包圍的嵌套字典值的鍵(foo[bar]=baz)铛碑。

URLEncodedFormEncoder.ArrayEncoding 枚舉提供了以下對(duì)數(shù)組參數(shù)進(jìn)行編碼的方法:

  • .brackets - 為每個(gè)值在鍵后附加一組空的中括號(hào)。這是默認(rèn)情況虽界。
  • .noBrackets - 不附加括號(hào)汽烦。key 按原樣編碼。

默認(rèn)情況下莉御,Alamofire 使用 .brackets 編碼撇吞,其中 foo = [1, 2] 編碼為 foo[]=1&foo[]=2

使用 .noBrackets 編碼礁叔,foo = [1, 2] 編碼為 foo=1&foo=2牍颈。

您可以創(chuàng)建自己的 URLEncodedFormParameterEncoder,在初始化時(shí)琅关,可以在 URLEncodedFormEncoder參數(shù)中設(shè)置 arrayEncoding 的值:

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(arrayEncoding: .noBrackets))
復(fù)制代碼
配置 Bool 參數(shù)的編碼

URLEncodedFormEncoder.BoolEncoding 枚舉提供了以下用于編碼 Bool 參數(shù)的方法:

  • .numeric - 把 true 編碼為 1煮岁, false 編碼為 0。這是默認(rèn)情況涣易。
  • .literal - 把 truefalse 編碼為字符串文本画机。

默認(rèn)情況下,Alamofire 使用 .numeric新症。

您可以創(chuàng)建自己的 URLEncodedFormParameterEncoder步氏,在初始化時(shí),可以在 URLEncodedFormEncoder參數(shù)中設(shè)置 boolEncoding 的值:

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(boolEncoding: .numeric))
復(fù)制代碼
配置 Data 參數(shù)的編碼

DataEncoding 包括以下用于編碼 Data 參數(shù)的方法:

  • .deferredToData - 使用 Data 的自帶 Encodable 支持徒爹。
  • .base64 - 將 Data 編碼為 base64 編碼的字符串荚醒。這是默認(rèn)情況芋类。
  • .custom((Data) -> throws -> String) - 使用給定的閉包對(duì) Data 進(jìn)行編碼。

您可以創(chuàng)建自己的 URLEncodedFormParameterEncoder界阁,在初始化時(shí)梗肝,可以在 URLEncodedFormEncoder參數(shù)中設(shè)置 dataEncoding 的值:

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(dataEncoding: .base64))
復(fù)制代碼
配置 Date 參數(shù)的編碼

鑒于將 Date 編碼為 String 的方法非常多,DateEncoding 包括以下用于編碼 Date 參數(shù)的方法:

  • .deferredToDate - 使用 Date 的自帶 Encodable 支持铺董。這是默認(rèn)情況。
  • .secondsSince1970 - 將 Date 編碼為 1970 年 1 月 1 日 UTC 零點(diǎn)的秒數(shù)禀晓。
  • .millisecondsSince1970 - 將 Date 編碼為 1970 年 1 月 1 日 UTC 零點(diǎn)的毫秒數(shù)精续。
  • .iso8601 - 根據(jù) ISO 8601 和 RFC3339 標(biāo)準(zhǔn)對(duì) Date 進(jìn)行編碼。
  • .formatted(DateFormatter) - 使用給定的 DateFormatter 對(duì) Date 進(jìn)行編碼粹懒。
  • .custom((Date) throws -> String) - 使用給定的閉包對(duì) Date 進(jìn)行編碼重付。

您可以創(chuàng)建自己的 URLEncodedFormParameterEncoder,在初始化時(shí)凫乖,可以在 URLEncodedFormEncoder參數(shù)中設(shè)置 dateEncoding 的值:

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(dateEncoding: .iso8601))
復(fù)制代碼
配置 Coding Keys 的編碼

由于 key 參數(shù)樣式的多樣性确垫,KeyEncoding 提供了以下方法來從 lowerCamelCase 中自定義 key 編碼:

  • .useDefaultKeys - 使用每種類型指定的 key。這是默認(rèn)情況帽芽。
  • .convertToSnakeCase - 將 key 轉(zhuǎn)換為 snake case:oneTwoThree 變成 one_two_three删掀。
  • .convertToKebabCase - 將 key 轉(zhuǎn)換為 kebab case:oneTwoThree 變成 one-two-three
  • .capitalized - 將第一個(gè)字母大寫导街,例如 oneTwoThree 變?yōu)?OneTwoThree披泪。
  • .uppercased - 所有字母大寫:oneTwoThree 變成 ONETWOTHREE
  • .lowercased - 所有字母小寫:oneTwoThree 變成 onetwothree搬瑰。
  • .custom((String) -> String) - 使用給定的閉包對(duì) key 進(jìn)行編碼款票。

您可以創(chuàng)建自己的 URLEncodedFormParameterEncoder,在初始化時(shí)泽论,可以在 URLEncodedFormEncoder參數(shù)中設(shè)置 keyEncoding 的值:

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(keyEncoding: .convertToSnakeCase))
復(fù)制代碼
配置空格的編碼

舊的表單編碼器使用 + 來對(duì)空格進(jìn)行編碼艾少,而一些服務(wù)器仍然希望使用這種編碼,而不是現(xiàn)代的百分比編碼翼悴,因此 Alamofire 包含以下對(duì)空格進(jìn)行編碼的方法:

.percentEscaped - 通過應(yīng)用標(biāo)準(zhǔn)百分比轉(zhuǎn)義對(duì)空格字符進(jìn)行編碼缚够。" " 編碼為"%20"。這是默認(rèn)情況抄瓦。 .plusReplaced - 將空格字符替換為 + 潮瓶," " 編碼為"+"

您可以創(chuàng)建自己的 URLEncodedFormParameterEncoder钙姊,在初始化時(shí)毯辅,可以在 URLEncodedFormEncoder參數(shù)中設(shè)置 spaceEncoding 的值:

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(spaceEncoding: .plusReplaced))
復(fù)制代碼

JSONParameterEncoder

JSONParameterEncoder 使用 Swift 的 JSONEncoder 對(duì) Encodable 值進(jìn)行編碼,并將結(jié)果設(shè)置為 URLRequesthttpBody煞额。如果 Content-Type 尚未設(shè)置思恐,則將其設(shè)置為 application/json沾谜。

JSON 編碼參數(shù)的 POST 請(qǐng)求
let parameters: [String: [String]] = [
    "foo": ["bar"],
    "baz": ["a", "b"],
    "qux": ["x", "y", "z"]
]

AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default)
AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: JSONParameterEncoder.prettyPrinted)
AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: JSONParameterEncoder.sortedKeys)

// HTTP body: {"baz":["a","b"],"foo":["bar"],"qux":["x","y","z"]}
復(fù)制代碼
自定義 JSONEncoder

您可以自定義 JSONParameterEncoder 的行為,方法是將自定義的 JSONEncoder 實(shí)例傳遞給它:

let encoder = JSONEncoder()
encoder.dateEncoding = .iso8601
encoder.keyEncodingStrategy = .convertToSnakeCase
let parameterEncoder = JSONParameterEncoder(encoder: encoder)
復(fù)制代碼
手動(dòng)對(duì) URLRequest 進(jìn)行參數(shù)編碼

ParameterEncoder APIs 也可以在 Alamofire 之外使用胀莹,方法是直接在URLRequest 中編碼參數(shù)基跑。

let url = URL(string: "https://httpbin.org/get")!
var urlRequest = URLRequest(url: url)

let parameters = ["foo": "bar"]
let encodedURLRequest = try URLEncodedFormParameterEncoder.default.encode(parameters, into: urlRequest)
復(fù)制代碼

HTTP Headers

Alamofire 包含自己的 HTTPHeaders 類型,這是一種順序保持且不區(qū)分大小寫的 HTTP header name/value 對(duì)的表示描焰。HTTPHeader 類型封裝單個(gè) name/value 對(duì)媳否,并為常用的 headers 提供各種靜態(tài)值。

Request 添加自定義 HTTPHeaders 就像向 request 方法傳遞值一樣簡單:

let headers: HTTPHeaders = [
    "Authorization": "Basic VXNlcm5hbWU6UGFzc3dvcmQ=",
    "Accept": "application/json"
]

AF.request("https://httpbin.org/headers", headers: headers).responseJSON { response in
    debugPrint(response)
}
復(fù)制代碼

HTTPHeaders 也可以由 HTTPHeader 數(shù)組構(gòu)造:

let headers: HTTPHeaders = [
    .authorization(username: "Username", password: "Password"),
    .accept("application/json")
]

AF.request("https://httpbin.org/headers", headers: headers).responseJSON { response in
    debugPrint(response)
}
復(fù)制代碼

對(duì)于不會(huì)變的 HTTP headers荆秦,建議在 URLSessionConfiguration 上設(shè)置它們篱竭,以便讓它們自動(dòng)應(yīng)用于底層 URLSession 創(chuàng)建的任何 URLSessionTask

默認(rèn)的 Alamofire Session 為每個(gè) Request 提供一組默認(rèn)的 headers步绸。其中包括:

  • Accept-Encoding掺逼,默認(rèn)為 br;q=1.0, gzip;q=0.8, deflate;q=0.6,根據(jù) RFC 7230 §4.2.3瓤介。
  • Accept-Language吕喘,默認(rèn)為系統(tǒng)中最多 6 種首選語言,格式為 en;q=1.0刑桑,根據(jù) RFC 7231 §5.3.5氯质。
  • User-Agent,其中包含有關(guān)當(dāng)前應(yīng)用程序的版本信息祠斧。例如:iOS Example/1.0 (com.alamofire.iOS-Example; build:1; iOS 13.0.0) Alamofire/5.0.0病梢,根據(jù) RFC 7231 §5.5.3

如果需要自定義這些 headers梁肿,則應(yīng)創(chuàng)建自定義 URLSessionConfiguration蜓陌,更新 defaultHTTPHeaders 屬性钮热,并將配置應(yīng)用于新 Session 實(shí)例。使用URLSessionConfiguration.af.default 來自定義配置,會(huì)保留 Alamofire 的默認(rèn) headers。

響應(yīng)驗(yàn)證

默認(rèn)情況下鹏浅,無論響應(yīng)的內(nèi)容如何,Alamofire 都會(huì)將任何已完成的請(qǐng)求視為成功。如果響應(yīng)具有不可接受的狀態(tài)代碼或 MIME 類型武通,則在響應(yīng)處理程序之前調(diào)用 validate() 將導(dǎo)致生成錯(cuò)誤。

自動(dòng)驗(yàn)證

validate() API 自動(dòng)驗(yàn)證狀態(tài)代碼是否在 200..<300 范圍內(nèi)境析,以及響應(yīng)的 Content-Type header 是否與請(qǐng)求的 Accept 匹配(如果有提供)。

AF.request("https://httpbin.org/get").validate().responseJSON { response in
    debugPrint(response)
}
復(fù)制代碼

手動(dòng)驗(yàn)證

AF.request("https://httpbin.org/get")
    .validate(statusCode: 200..<300)
    .validate(contentType: ["application/json"])
    .responseData { response in
        switch response.result {
        case .success:
            print("Validation Successful")
        case let .failure(error):
            print(error)
        }
    }
復(fù)制代碼

響應(yīng)處理

Alamofire 的 DataRequestDownloadRequest 都有相應(yīng)的響應(yīng)類型:DataResponse<Success, Failure: Error>DownloadResponse<Success, Failure: Error>。這兩個(gè)類型都由兩個(gè)泛型組成:序列化類型和錯(cuò)誤類型。默認(rèn)情況下,所有響應(yīng)值都將生成 AFError 錯(cuò)誤類型(DataResponse<Success, AFError>)。Alamofire 在其公共 API 中使用了更簡單的 AFDataResponse<Success>AFDownloadResponse<Success>,它們總是有 AFError 錯(cuò)誤類型。UploadRequestDataRequest 的一個(gè)子類了袁,使用相同的 DataResponse 類型载绿。

處理在 Alamofire 中發(fā)出的 DataRequestUploadRequestDataResponse 涉及到鏈接 response handler,例如 responseJSON 鏈接到 DataRequest:

AF.request("https://httpbin.org/get").responseJSON { response in
    debugPrint(response)
}
復(fù)制代碼

在上面的示例中崭庸,responseJSON handler 被添加到 DataRequest 中,以便在 DataRequest 完成后執(zhí)行执赡。傳遞給 handler 閉包的參數(shù)是從響應(yīng)屬性來的 JSONResponseSerializer 生成的 AFDataResponse<Any> 值。

此閉包并不阻塞執(zhí)行以等待服務(wù)器的響應(yīng)函筋,而是作為回調(diào)添加,以便在收到響應(yīng)后處理該響應(yīng)。請(qǐng)求的結(jié)果僅在響應(yīng)閉包的范圍內(nèi)可用。任何依賴于從服務(wù)器接收到的響應(yīng)或數(shù)據(jù)的執(zhí)行都必須在響應(yīng)閉包中完成。

Alamofire 的網(wǎng)絡(luò)請(qǐng)求是異步完成的炊甲。異步編程可能會(huì)讓不熟悉這個(gè)概念的程序員感到沮喪全景,但是有很好的理由這樣做。

默認(rèn)情況下牵囤,Alamofire 包含六個(gè)不同的數(shù)據(jù)響應(yīng) handlers爸黄,包括:


// Response Handler - 未序列化的 Response
func response(
    queue: DispatchQueue = .main,
    completionHandler: @escaping (AFDataResponse<Data?>) -> Void
) -> Self

// Response Serializer Handler - Serialize using the passed Serializer
func response<Serializer: DataResponseSerializerProtocol>(
    queue: DispatchQueue = .main,
    responseSerializer: Serializer,
    completionHandler: @escaping (AFDataResponse<Serializer.SerializedObject>) -> Void
) -> Self

// Response Data Handler - Serialized into Data
func responseData(
    queue: DispatchQueue = .main,
    completionHandler: @escaping (AFDataResponse<Data>) -> Void
) -> Self

// Response String Handler - Serialized into String
func responseString(
    queue: DispatchQueue = .main,
    encoding: String.Encoding? = nil,
    completionHandler: @escaping (AFDataResponse<String>) -> Void
) -> Self

// Response JSON Handler - Serialized into Any Using JSONSerialization
func responseJSON(
    queue: DispatchQueue = .main,
    options: JSONSerialization.ReadingOptions = .allowFragments,
    completionHandler: @escaping (AFDataResponse<Any>) -> Void
) -> Self

// Response Decodable Handler - Serialized into Decodable Type
func responseDecodable<T: Decodable>(
    of type: T.Type = T.self,
    queue: DispatchQueue = .main,
    decoder: DataDecoder = JSONDecoder(),
    completionHandler: @escaping (AFDataResponse<T>) -> Void
) -> Self
復(fù)制代碼

沒有一個(gè)響應(yīng) handlers 對(duì)從服務(wù)器返回的 HTTPURLResponse 執(zhí)行任何驗(yàn)證。

例如揭鳞,400..<500500..<600 范圍內(nèi)的響應(yīng)狀態(tài)代碼不會(huì)自動(dòng)觸發(fā)錯(cuò)誤炕贵。Alamofire 使用響應(yīng)驗(yàn)證鏈接來實(shí)現(xiàn)這一點(diǎn)。

響應(yīng) Handler

響應(yīng) handler 不計(jì)算任何響應(yīng)數(shù)據(jù)野崇。它只是直接從 URLSessionDelegate 轉(zhuǎn)發(fā)所有信息称开。它相當(dāng)于使用 cURL 執(zhí)行請(qǐng)求。

AF.request("https://httpbin.org/get").response { response in
    debugPrint("Response: \(response)")
}
復(fù)制代碼

我們強(qiáng)烈建議您利用 ResponseResult 類型來利用其他響應(yīng)序列化器。

響應(yīng) Data Handler

responseData handler 使用 DataResponseSerializer 提取并驗(yàn)證服務(wù)器返回的數(shù)據(jù)鳖轰。如果沒有發(fā)生錯(cuò)誤并且返回?cái)?shù)據(jù)清酥,則響應(yīng)結(jié)果將為 .successvalue 將為從服務(wù)器返回的 Data蕴侣。

AF.request("https://httpbin.org/get").responseData { response in
    debugPrint("Response: \(response)")
}
復(fù)制代碼

響應(yīng) String Handler

responseString handler 使用 StringResponseSerializer 將服務(wù)器返回的數(shù)據(jù)轉(zhuǎn)換為具有指定編碼的 String焰轻。如果沒有發(fā)生錯(cuò)誤,并且服務(wù)器數(shù)據(jù)成功序列化為 String昆雀,則響應(yīng)結(jié)果將為 .success辱志,并且值的類型為 String

AF.request("https://httpbin.org/get").responseString { response in
    debugPrint("Response: \(response)")
}
復(fù)制代碼

如果未指定編碼狞膘,Alamofire 將使用服務(wù)器 HTTPURLResponse 中指定的文本編碼揩懒。如果服務(wù)器響應(yīng)無法確定文本編碼,則默認(rèn)為 .isoLatin1挽封。

響應(yīng) JSON Handler

responseJSON handler 使用 JSONResponseSerializer 使用指定的 JSONSerialization.ReadingOptions 將服務(wù)器返回的數(shù)據(jù)轉(zhuǎn)換為 Any 類型已球。如果沒有出現(xiàn)錯(cuò)誤,并且服務(wù)器數(shù)據(jù)成功序列化為 JSON 對(duì)象辅愿,則響應(yīng) AFResult 將為 .success智亮,值將為 Any 類型。

AF.request("https://httpbin.org/get").responseJSON { response in
    debugPrint("Response: \(response)")
}
復(fù)制代碼

響應(yīng) Decodable Handler

responseDecodable handler 使用 DecodableResponseSerializer 和 指定的 DataDecoderDecoder 的協(xié)議抽象渠缕,可以從 Data 解碼)將服務(wù)器返回的數(shù)據(jù)轉(zhuǎn)換為傳遞進(jìn)來的 Decodable 類型。如果沒有發(fā)生錯(cuò)誤褒繁,并且服務(wù)器數(shù)據(jù)已成功解碼為 Decodable 類型亦鳞,則響應(yīng) Result 將為 .success,并且 value 將為傳遞進(jìn)來的類型棒坏。

struct HTTPBinResponse: Decodable {
    let url: String
}

AF.request("https://httpbin.org/get").responseDecodable(of: HTTPBinResponse.self) { response in
    debugPrint("Response: \(response)")
}
復(fù)制代碼

鏈?zhǔn)巾憫?yīng) handlers

響應(yīng) handlers 還可以連接起來:

Alamofire.request("https://httpbin.org/get")
    .responseString { response in
        print("Response String: \(response.value)")
    }
    .responseJSON { response in
        print("Response JSON: \(response.value)")
    }
復(fù)制代碼

需要注意的是燕差,對(duì)同一請(qǐng)求使用多個(gè)響應(yīng) handlers 需要多次序列化服務(wù)器數(shù)據(jù),每個(gè)響應(yīng) handlers 處理一次坝冕。作為最佳實(shí)踐徒探,通常應(yīng)避免對(duì)同一請(qǐng)求使用多個(gè)響應(yīng) handlers,特別是在生產(chǎn)環(huán)境中喂窟。它們只能用于調(diào)試或在沒有更好選擇的情況下使用测暗。

響應(yīng) Handler 隊(duì)列

默認(rèn)情況下,傳遞給響應(yīng) handler 的閉包在 .main 隊(duì)列上執(zhí)行磨澡,但可以傳遞一個(gè)指定的 DispatchQueue 來執(zhí)行閉包碗啄。實(shí)際的序列化工作(將 Data 轉(zhuǎn)換為其他類型)總是在后臺(tái)隊(duì)列上執(zhí)行。

let utilityQueue = DispatchQueue.global(qos: .utility)

AF.request("https://httpbin.org/get").responseJSON(queue: utilityQueue) { response in
    print("Executed on utility queue.")
    debugPrint(response)
}
復(fù)制代碼

響應(yīng)緩存

響應(yīng)緩存使用系統(tǒng)自帶的 URLCache 處理稳摄。它提供了內(nèi)存和磁盤上的復(fù)合緩存稚字,并允許您管理用于緩存的內(nèi)存和磁盤的大小。

默認(rèn)情況下,Alamofire 利用 URLCache.shared 實(shí)例胆描。要自定義使用的URLCache 實(shí)例瘫想,請(qǐng)查看高級(jí)用法

身份驗(yàn)證

身份驗(yàn)證使用系統(tǒng)自帶的 URLCredentialURLAuthenticationChallenge 處理昌讲。

這些身份驗(yàn)證 APIs 用于提示授權(quán)的服務(wù)器国夜,而不是一般用于需要身份驗(yàn)證或等效的 header APIs。

支持的身份驗(yàn)證方案

HTTP Basic 身份驗(yàn)證

Requestauthenticate 方法將在使用 URLAuthenticationChallenge 進(jìn)行質(zhì)詢時(shí)自動(dòng)提供 URLCredential(如果適用):

let user = "user"
let password = "password"

AF.request("https://httpbin.org/basic-auth/\(user)/\(password)")
    .authenticate(username: user, password: password)
    .responseJSON { response in
        debugPrint(response)
    }
復(fù)制代碼

使用 URLCredential 進(jìn)行驗(yàn)證

let user = "user"
let password = "password"

let credential = URLCredential(user: user, password: password, persistence: .forSession)

AF.request("https://httpbin.org/basic-auth/\(user)/\(password)")
    .authenticate(with: credential)
    .responseJSON { response in
        debugPrint(response)
    }
復(fù)制代碼

需要注意的是剧蚣,當(dāng)使用 URLCredential 進(jìn)行身份驗(yàn)證時(shí)支竹,如果服務(wù)器發(fā)出質(zhì)詢,底層 URLSession 實(shí)際上將發(fā)出兩個(gè)請(qǐng)求鸠按。第一個(gè)請(qǐng)求將不包括“可能”觸發(fā)服務(wù)器質(zhì)詢的 credential礼搁。然后,Alamofire 接收質(zhì)詢目尖,追加 credential馒吴,并由底層 URLSession 重試請(qǐng)求。

手動(dòng)驗(yàn)證

如果您正在與始終需要 Authenticate 或類似 header 而不提示的 API 通信瑟曲,則可以手動(dòng)添加:

let user = "user"
let password = "password"

let headers: HTTPHeaders = [.authorization(username: user, password: password)]

AF.request("https://httpbin.org/basic-auth/user/password", headers: headers)
    .responseJSON { response in
        debugPrint(response)
    }
復(fù)制代碼

但是饮戳,必須是所有請(qǐng)求的一部分的 headers,通常作為自定義 URLSessionConfiguration 的一部分或通過使用 RequestAdapter 來更好地處理洞拨。

下載數(shù)據(jù)到文件中

除了將數(shù)據(jù)提取到內(nèi)存中之外扯罐,Alamofire 還提供了 Session.downloadDownloadRequestDownloadResponse<Success烦衣,F(xiàn)ailure:Error> 以方便下載數(shù)據(jù)到磁盤歹河。雖然下載到內(nèi)存中對(duì)小負(fù)載(如大多數(shù) JSON API 響應(yīng))非常有用,但獲取更大的資源(如圖像和視頻)應(yīng)下載到磁盤花吟,以避免應(yīng)用程序出現(xiàn)內(nèi)存問題秸歧。

AF.download("https://httpbin.org/image/png").responseData { response in
    if let data = response.value {
        let image = UIImage(data: data)
    }
}
復(fù)制代碼

DownloadRequest 具有與 DataRequest 相同的大多數(shù)響應(yīng) handlers。但是衅澈,由于它將數(shù)據(jù)下載到磁盤键菱,因此序列化響應(yīng)涉及從磁盤讀取,還可能涉及將大量數(shù)據(jù)讀入內(nèi)存今布。在設(shè)計(jì)下載處理時(shí)经备,記住這些事實(shí)是很重要的。

下載文件的存放位置

所有下載的數(shù)據(jù)最初都存儲(chǔ)在系統(tǒng)臨時(shí)目錄中部默。它最終會(huì)在將來的某個(gè)時(shí)候被系統(tǒng)刪除弄喘,所以如果它需要更長的壽命,將文件移到其他地方是很重要的甩牺。

您可以提供 Destination 閉包蘑志,將文件從臨時(shí)目錄移動(dòng)到最終的存放位置。在臨時(shí)文件實(shí)際移動(dòng)到 destinationURL 之前,將執(zhí)行閉包中指定的 Options急但。當(dāng)前支持的兩個(gè) Options 是:

  • .createIntermediateDirectories - 如果指定澎媒,則為目標(biāo) URL 創(chuàng)建中間目錄。
  • .removePreviousFile - 如果指定波桩,則從目標(biāo) URL 中刪除以前的文件戒努。
let destination: DownloadRequest.Destination = { _, _ in
    let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    let fileURL = documentsURL.appendingPathComponent("image.png")

    return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}

AF.download("https://httpbin.org/image/png", to: destination).response { response in
    debugPrint(response)

    if response.error == nil, let imagePath = response.fileURL?.path {
        let image = UIImage(contentsOfFile: imagePath)
    }
}
復(fù)制代碼

您還可以使用建議的 destination API:

let destination = DownloadRequest.suggestedDownloadDestination(for: .documentDirectory)

AF.download("https://httpbin.org/image/png", to: destination)
復(fù)制代碼

下載進(jìn)度

很多時(shí)候向用戶報(bào)告下載進(jìn)度是有幫助的。任何 DownloadRequest 都可以使用 downloadProgress 報(bào)告下載進(jìn)度镐躲。

AF.download("https://httpbin.org/image/png")
    .downloadProgress { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseData { response in
        if let data = response.value {
            let image = UIImage(data: data)
        }
    }
復(fù)制代碼

URLSession 的進(jìn)度報(bào)告 APIs(也是 Alamofire 的)只有在服務(wù)器正確返回可用于計(jì)算進(jìn)度的 Content-Length header 時(shí)才能工作储玫。如果沒有這個(gè) header,進(jìn)度將保持在 0.0萤皂,直到下載完成撒穷,此時(shí)進(jìn)度將跳到 1.0

downloadProgress API 還可以接收一個(gè) queue 參數(shù)裆熙,該參數(shù)定義應(yīng)該對(duì)哪個(gè) DispatchQueue 調(diào)用下載進(jìn)度閉包端礼。

let utilityQueue = DispatchQueue.global(qos: .utility)

AF.download("https://httpbin.org/image/png")
    .downloadProgress(queue: utilityQueue) { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseData { response in
        if let data = response.value {
            let image = UIImage(data: data)
        }
    }
復(fù)制代碼

取消和恢復(fù)下載

除了所有請(qǐng)求類都有 cancel() API 外,DownloadRequest 還可以生成恢復(fù)數(shù)據(jù)入录,這些數(shù)據(jù)可以用于以后恢復(fù)下載蛤奥。此 API 有兩種形式:1)cancel(producingResumeData: Bool),它允許控制是否生成恢復(fù)數(shù)據(jù)僚稿,但僅在 DownloadResponse 可用凡桥;2)cancel(byProducingResumeData: (_ resumeData: Data?) -> Void),它執(zhí)行相同的操作蚀同,但恢復(fù)數(shù)據(jù)在 completion handler 中可用缅刽。

如果 DownloadRequest 被取消或中斷,則底層的 URLSessionDownloadTask 可能會(huì)生成恢復(fù)數(shù)據(jù)唤崭。如果發(fā)生這種情況拷恨,可以重新使用恢復(fù)數(shù)據(jù)來重新啟動(dòng)停止的 DownloadRequest脖律。

重要提示:在所有 Apple 平臺(tái)的某些版本(iOS 10 - 10.2谢肾、macOS 10.12 - 10.12.2、tvOS 10 - 10.1小泉、watchOS 3 - 3.1.1)上芦疏,resumeData 在后臺(tái) URLSessionConfiguration 上被破壞。resumeData 生成邏輯中存在一個(gè)潛在的錯(cuò)誤微姊,即數(shù)據(jù)寫入錯(cuò)誤酸茴,并且總是無法恢復(fù)下載。有關(guān)此錯(cuò)誤和可能的解決方法的詳細(xì)信息兢交,請(qǐng)參閱此 Stack Overflow 的帖子薪捍。

var resumeData: Data!

let download = AF.download("https://httpbin.org/image/png").responseData { response in
    if let data = response.value {
        let image = UIImage(data: data)
    }
}

// download.cancel(producingResumeData: true) // Makes resumeData available in response only.
download.cancel { data in
    resumeData = data
}

AF.download(resumingWith: resumeData).responseData { response in
    if let data = response.value {
        let image = UIImage(data: data)
    }
}
復(fù)制代碼

上傳數(shù)據(jù)到服務(wù)器

當(dāng)使用 JSON 或 URL 編碼的參數(shù)向服務(wù)器發(fā)送相對(duì)少量的數(shù)據(jù)時(shí),request() 通常就足夠了。如果需要從內(nèi)存酪穿、文件 URL 或 InputStream 中的 Data 發(fā)送大量數(shù)據(jù)凳干,那么 upload() 就是您想要使用的。

上傳 Data

let data = Data("data".utf8)

AF.upload(data, to: "https://httpbin.org/post").responseJSON { response in
    debugPrint(response)
}
復(fù)制代碼

上傳文件

let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

AF.upload(fileURL, to: "https://httpbin.org/post").responseJSON { response in
    debugPrint(response)
}
復(fù)制代碼

上傳多表單數(shù)據(jù)

AF.upload(multipartFormData: { multipartFormData in
    multipartFormData.append(Data("one".utf8), withName: "one")
    multipartFormData.append(Data("two".utf8), withName: "two")
}, to: "https://httpbin.org/post")
    .responseJSON { response in
        debugPrint(response)
    }
復(fù)制代碼

上傳進(jìn)度

當(dāng)用戶等待上傳完成時(shí)被济,有時(shí)向用戶顯示上傳的進(jìn)度會(huì)很方便救赐。任何 UploadRequest 都可以使用 uploadProgressdownloadProgress 報(bào)告響應(yīng)數(shù)據(jù)下載的上傳進(jìn)度和下載進(jìn)度。

let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

AF.upload(fileURL, to: "https://httpbin.org/post")
    .uploadProgress { progress in
        print("Upload Progress: \(progress.fractionCompleted)")
    }
    .downloadProgress { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseJSON { response in
        debugPrint(response)
    }
復(fù)制代碼

統(tǒng)計(jì)指標(biāo)

URLSessionTaskMetrics

Alamofire 為每個(gè) Request 收集了 URLSessionTaskMetrics只磷。URLSessionTaskMetrics 封裝了一些關(guān)于底層網(wǎng)絡(luò)連接经磅、請(qǐng)求和響應(yīng)時(shí)間的奇妙統(tǒng)計(jì)信息。

AF.request("https://httpbin.org/get").responseJSON { response in
    print(response.metrics)
}
復(fù)制代碼

cURL 的命令輸出

調(diào)試平臺(tái)問題可能令人沮喪钮追。幸運(yùn)的是预厌,Alamofire 的 Request 類型可以生成等效的 cURL 命令,以便調(diào)試畏陕。由于 Alamofire Request 創(chuàng)建的異步性配乓,這個(gè) API 有同步和異步兩個(gè)版本。要盡快獲取 cURL 命令惠毁,可以將 cURLDescription 鏈接到請(qǐng)求上:

AF.request("https://httpbin.org/get")
    .cURLDescription { description in
        print(description)
    }
    .responseJSON { response in
        debugPrint(response.metrics)
    }
復(fù)制代碼

這將會(huì)生成:

$ curl -v \
-X GET \
-H "Accept-Language: en;q=1.0" \
-H "Accept-Encoding: br;q=1.0, gzip;q=0.9, deflate;q=0.8" \
-H "User-Agent: Demo/1.0 (com.demo.Demo; build:1; iOS 13.0.0) Alamofire/1.0" \
"https://httpbin.org/get"

文章來源: https://juejin.cn/post/6875140053635432462#heading-37

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末犹芹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子鞠绰,更是在濱河造成了極大的恐慌腰埂,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜈膨,死亡現(xiàn)場離奇詭異屿笼,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)翁巍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門驴一,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人灶壶,你說我怎么就攤上這事肝断。” “怎么了驰凛?”我有些...
    開封第一講書人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵胸懈,是天一觀的道長。 經(jīng)常有香客問我恰响,道長趣钱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任胚宦,我火速辦了婚禮首有,結(jié)果婚禮上燕垃,老公的妹妹穿的比我還像新娘。我一直安慰自己井联,他們只是感情好利术,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著低矮,像睡著了一般印叁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上军掂,一...
    開封第一講書人閱讀 52,337評(píng)論 1 310
  • 那天轮蜕,我揣著相機(jī)與錄音,去河邊找鬼蝗锥。 笑死跃洛,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的终议。 我是一名探鬼主播汇竭,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼穴张!你這毒婦竟也來了细燎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤皂甘,失蹤者是張志新(化名)和其女友劉穎玻驻,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體偿枕,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡璧瞬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了渐夸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嗤锉。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖墓塌,靈堂內(nèi)的尸體忽然破棺而出瘟忱,到底是詐尸還是另有隱情,我是刑警寧澤桃纯,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布懈玻,位于F島的核電站批狱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏埂淮。R本人自食惡果不足惜棒拂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一伞梯、第九天 我趴在偏房一處隱蔽的房頂上張望玫氢。 院中可真熱鬧,春花似錦谜诫、人聲如沸漾峡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽生逸。三九已至,卻和暖如春且预,著一層夾襖步出監(jiān)牢的瞬間槽袄,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來泰國打工锋谐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留遍尺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓涮拗,卻偏偏與公主長得像乾戏,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子三热,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容