Alamofire 基本用法文档翻译

本文的目的是单纯的翻译一下Alamofire的基本用法的文档,原文是在github上:https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md 你看到这篇文章的时候,可能官方文档会有更新(本文5.1.0版本)。个人也是按照自己的理解进行翻译,能力有限,翻译的不好的地方,希望看到的朋友们给我指正出来,感激不尽~

使用 Alamofire


介绍


Alamofire 提供了一个优雅的和可组合的接口去进行 HTTP 网络请求。它是在 Apple 官方的 Foundation 框架所提供的 URL Loading System 之上建立起来的,并不是自己去实现了这些HTTP的请求相关的功能。 URL Loading System 的核心是 URLSessionURLSessionTask 的子类们。Alamofire 把包括他们在内的许多 API 封装成了更加易于使用的API(interface)并且提供了开发 App(需要使用 HTTP 网络请求的) 所需要的各种网络方面的功能。但是,很重要的一点是你要知道 Alamofire 的核心功能是来自哪里的(URL Loading System ),因此,熟悉 Apple 的 URL Loading System是很重要的。归根结底,Alamofire 的网络功能是受限制于 URL Loading System 的能力的,因此遵守 URL Loading System 的行为和准则是必须的。

另外,在 Alamofire (和 URL Loading System 的一般情况)下的网络请求都是异步的。异步编程可能使不熟悉这个概念的程序员感到沮丧,但是异步编程的 好处 是非常多的。

旁注:AF命名空间和参考

以前版本的 Alamofire 的文档都使用了 Alamofire.request() 这样的示例来进行网络请求。这个 API 使用了Alamofire 的前缀,但是事实上没有这个前缀,也是可以的。在任何文件中 import Alamofire 都可以在文件中全局的使用 request 以及其他的方法(functions)。从 Alamofire 5 版本开始,这种使用方法被移除了,取而代之的是 AF 这个全局的对 Session.default的引用(AF = Session.default)。这样做的好处是使得在每次使用 Alamofire 提供的便捷的功能的时候不会去污染 Alamofire 的全局命名空间,也不用每次使用 Alamofire 的时候都写一遍全局的 Session API。同样的原因,Alamofire 拓展出来的类型们会使用 af 这种拓展名来把 Alamofire 提供的功能和其他拓展所提供的功能进行区分。

发一个网络请求(Making Requests)


Alamofire 提供了多个便捷的方法去发送网络请求。最简单的例子,只需要提供一个可以转换成(converted into)URLString 就可以了 (第一个版本):

AF.request("https://httpbin.org/get").response { response in
    debugPrint(response)
}

注:所有的例子都需要在文件中 import Alamofire

这个方法实际上是 Alamofire 的 Session 发送网络请求的两个顶层(top-level) API 中的一个,他的完整定义是这样的:

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

这个方法创建了一个 DataRequest 。在创建每个请求(request)的时候允许他们有自己不同的组成元素(components),他们可以有不同的 method 或者 headers。 还允许每个请求都可以有自己的拦截器(RequestInterceptor)和 参数编码(Encodable)。

除上面之外还有一些额外的方法可以让你使用 Parameters 类型的字典做为参数 ParameterEncoding作为参数类型进行网络请求。不建议再这些 API 了,它们会在将来被 Alamofire 移除掉。

这个 API 的 第二个版本 要简单的多:

open func request(_ urlRequest: URLRequestConvertible, 
                  interceptor: RequestInterceptor? = nil) -> DataRequest

这个方法可以为任何一个符合 Alamofire 的 URLRequestConvertible 协议的类型创建一个 DataRequest 。所有之前版本的参数(对比上一个完整的函数中的参数)都被封装进了这个 URLRequestConvertible 的参数里面,这样就产生了一个非常强大的抽象。这一点我们在 高级用法 的文档中进行了讨论。

HTTP 请求方式 (HTTP Methods)

HTTPMethod 这个类型列举出了 RFC 7231 §4.3 中定义的 HTTP 请求方式们(HTTP Methods):

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
    }
}

它里面的这些值可以被作为方法参数传递给 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)

需要记住的很重要的一点是,不同的网络请求方式(HTTP methods)会有不同的语义,还会根据服务器的不同要求进行相应的参数编码。比如,URLSessionAlamofire 都不支持往 GET 的请求体里插入数据(passing body data)(那样 是 POST 的做法),那样做的话会报错。

Alamofire 还提供了一个 URLRequest 的拓展。里面的 httpMethod 属性起到了桥接的作用,这个属性把 URLRequest 中的 String 类型的 HTTP 请求方式(HTTP Methods) 转变为 Alamofire的 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 }
    }
}

如果你想使用一个 Alamofire 的 HTTPMethod 不支持的请求方式,你可以拓展 HTTPMethod 来添加你自己的请求方式:

extension HTTPMethod {
    static let custom = HTTPMethod(rawValue: "CUSTOM")
}

设置其他的 URLRequest 的属性(Setting Other URLRequest Properties)

Alamofire 创建网络请求的时候提供了最通用的自定义请求参数,但是,有时候这些默认的参数并不能满足需求。通过传入各种参数的方式创建请求的时候,可以使用 RequestModifier 闭包来修改这个正在创建的请求。举个例子,可以设置这个请求的超时时间(URLRequest 'S timeoutInterval)为5s,我们利用这个闭包(closure)来这样修改:

AF.request("https://httpbin.org/get", requestModifier: { $0.timeoutInterval = 5 }).response(...)

RequestModifier 也可以使用尾随闭包的语法形式:

AF.request("https://httpbin.org/get") { urlRequest in
    urlRequest.timeoutInterval = 5
    urlRequest.allowsConstrainedNetworkAccess = false
}
.response(...)

RequestModifier仅仅适用于通过 URL 和自定义参数创建请求的时候(译者:上面第一个版本的请求),并不适用于直接从 URLRequestConvertible 里面的值们来提供参数值的那种创建请求的方式(译者:上面第二个版本的请求),因为第二个版本封装起来的那些值应该提供所有的请求所需的参数值。另外,一旦但多数的网络请求需要在创建的时候进行修改的话,那么还是推荐使用 URLRequestConvertible(译者:第二种高级抽象出来的类型提供所有参数,在创建的时候就可以修改,第一种采用闭包,是创建完了之后对请求进行的配置)。你可以更多的在这里进行了解: Advanced Usage documentation

请求参数和参数编码(Request Parameters and Parameter Encoders)

Alamofire 支持传递任何 Encodable 类型的数据作为网络请求的参数。这些参数会被一个符合 ParmeterEncoder 协议(protocol)的类型处理之后进行传递,添加到网络请求之中,然后通过网络发送出去。Alamofier 里面有两个符合 ParameterEncoder 的类型,他们分别是:JSONParameterEncoderURLEncodedFormParameterEncoder。这俩类型涵盖了几乎所有的现代网络请求服务所需的参数编码方式(XML的编码方式就作为练习交给使用我们的老铁们了)。

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)
}

第一种参数编码方式(URLEncodedFormParameterEncoder)

URLEncodedFormParameterEncoder 类型的编码器将值编码成url编码(url-encoded)用以设置或者添加到现有的URL查询字符串中(译者:GET),或者设置为HTTP请求的请求体(HTTP body)(译者:POST)。通过设置编码器的 destination 来控制被编码后字符串设置到哪里(译者:URL中或者HTTP body中)。URLEncodedFormParameterEncoder.Destination 这个枚举有以下几个枚举值:

  • .methodDependent - 把编码过后的结果设置到现有的.get.head.delete 类型的请求的URL查询字符串中,以及其他的HTTP请求类型的请求体之中(HTTP body)。
  • .queryString - 把编码过后的字符串设置或者添加到网络请求的这个URL查询字符串中。
  • .httpBody - 把编码过后的字符串设置到 HTTP 请求体中去(HTTP body)。

译者:第一种是根据情况自动选择,第二种是与 GET 相同类别的请求,第三种是与 POST 相同类型的请求

对于使用HTTP请求体(HTTP body)的网络请求,如果HTTP头(HTTP header)中的 Content-Type 没有被设置过,那么就会被设置成 application/x-www-form-urlencoded; charset=utf-8

在 Alamofire 内部的实现中,URLEncodedFormParameterEncoder 使用了 URLEncodedFormEncoder 去真正的执行把一个 Encodable 类型的数据编码成URL编码的字符串。这个编码器可以用于各种类型的自定义的类型们,包括有 Array 使用 ArrayEncodingBool 使用 BoolEncodingData 使用 DataEncodingDate使用 DateEncoding,编码键(keys)使用 KeyEncoding, 空格(spaces)使用 SpaceEncoding

URL编码参数的GET请求(GET Request With URL-Encoded Parameters):
let parameters = ["foo": "bar"]

// All three of these calls are equivalent
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
URL编码参数的POST请求(POST Request With URL-Encoded Parameters):
let parameters: [String: [String]] = [
    "foo": ["bar"],
    "baz": ["a", "b"],
    "qux": ["x", "y", "z"]
]

// All three of these calls are equivalent
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"
配置编码过后的值的排序

从 Swift 4.2 开始,Swift 的 Dictionary 类型所使用的哈希算法会在运行时会产生随机的内部排序,这就使得每次启动应用程序可能会有所不同。这样会导致编码过后的参数顺序有所改变,从而影响缓存和其他行为。URLEncodedFormEncoder将会对编码过后的键值对(key-value pairs)进行排序。这将会为所有 Encodable 类型提供一个恒定的顺序的输出,他可能会与该类型实际的编码顺序不符(译者:自己写的参数的顺序,当然这个顺序在 Dictionary是随机的,和编码过后被排序过得参数顺序可能会不匹配)。你可以通过设置 alphabetizeKeyValuePairsfalse 来返回实际的参数顺序,尽管这样做的话可能会拥有随机的 Dictionary 顺序。

你可以创建一个你自己的 URLEncodedFormParameterEncoder 在传递进来 URLEncodedFormEncoder 的初始化方法中指定你所期望的 alphabetizeKeyValuePairs 的值:

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(alphabetizeKeyValuePairs: false))
配置 Array 参数的编码(Configuring the Encoding of Array Parameters)

由于没有发布过任何有关如何编码集合类型的规范,Alamofire 依照惯例,将 []添加到数组名字的后面(foo[]=1&foo[]=2),对于嵌套字典, 则将被方括号包围的key添加到的后面(foo[bar]=baz)。

URLEncodedFormEncoder.ArrayEncoding这个枚举提供了以下的编码 Array 参数的方式:

  • .brackets - 为每一个数组里面的值添加一个空的方括号的集合,这个也是默认的选项。
  • .noBrackets - 没有添加方括号,按照原样进行编码。

Alamofire 默认的使用 .brackets 去编码 Array, 这样的话 foo = [1, 2] 就会被编码成 foo[]=1&foo[]=2

使用 .noBrackets 编码的话就会把 foo=[1, 2] 编码成 foo=1&foo=2

你可以创建你自己的 URLEncodedFormParameterEncoder,然后在需要传递 URLEncodedFormEncoder 的初始化方法中指定 ArrayEncoding

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(arrayEncoding: .noBrackets))
配置 Bool 类型参数的编码方式(Configuring the Encoding of Bool Parameters)

URLEncodedFormEncoder.BoolEncoding 枚举提供了以下几种用于编码 Bool 类型参数的方式:

  • .numeric - 把 true 编码为 1,把 false 编码为 0。这也是默认的方式。
  • .literal - 把 truefalse 编码为文字。

Alamofire 默认的使用 .numeric 的这种方式。

你可以创建你自己的 URLEncodedFormParameterEncoder,然后在需要传递 URLEncodedFormEncoder 的初始化方法中指定 BoolEncoding

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(boolEncoding: .numeric))
配置 Data 类型的参数编码

DataEncoding 包括以下几种编码 Data 参数类型的方式:

  • .deferredToData - 使用 Data 的本地的 Encodable 支持。
  • .base64 - 把 Data 编码为 Base 64 编码格式的字符串。这是默认的方式。
  • .custom((Data) -> throws -> String) - 使用提供的闭包(closure)进行编码。

你可以创建你自己的 URLEncodedFormParameterEncoder,然后在需要传递 URLEncodedFormEncoder 的初始化方法中指定 DataEncoding

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(dataEncoding: .base64))
配置 Date类型的参数编码:

鉴于有大量的方法可以吧 Date 编码称为 StringDateEncoding 包括了以下这些方式来编码 Date 参数:

  • .deferredToDate - 使用 Date 的本地的 Encodable 支持,这是默认的方式。
  • .secondsSince1970 - 把 Date 编码为从1970年1月1日的午夜开始到现在的秒数。
  • .millisecondsSince1970 - 把 Date 编码为从1970年1月1日的午夜开始到现在的毫秒数。
  • .iso8610 - 根据 ISO 8610 和 RFC3339 标准对 Date 进行编码。
  • formatted(DateFormatter) - 使用给定的 DateFormatterDate 进行编码。
  • custom((Date) -> throws -> String) - 使用给定的闭包(closure)对 Date 进行编码。

你可以创建你自己的 URLEncodedFormParameterEncoder,然后在需要传递 URLEncodedFormEncoder 的初始化方法中指定 DateEncoding

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(dateEncoding: .iso8601))
配置键的编码方式(Configuring the Encoding of Coding Keys)

由于参数键的样式多钟多样,KeyEncoding 提供以下方法去编码使用驼峰命名法(lowerCamelCase)的键:

  • .useDefaultKeys - 使用每种类型指定的键。这个是默认的方式。
  • .convertToSnakeCase - 把键编码为下划线(译者:snake是蛇形)样式:oneTwoThree 变成 one_two_three
  • .convertToKebabCase - 把键编码为横线(译者:kebab是烤串)样式:oneTwoThree 变成 one-two-three
  • .capitalized - 仅仅大写第一个字母,又叫做 UpperCamelCaseoneTwoThree 变成 OneTwoThree
  • .uppercased - 大写所有字母: oneTwoThree 变成 ONETWOTHREE
  • lowercased - 小写所有字母: oneTwoThree 变成 onetwothree
  • .custom((String) -> String) - 使用给定的闭包(closure)进行编码。

你可以创建你自己的 URLEncodedFormParameterEncoder,然后在需要传递 URLEncodedFormEncoder 的初始化方法中指定 KeyEncoding

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(keyEncoding: .convertToSnakeCase))
配置空格编码(Configuring the Encoding of Spaces)

比较早的编码器们使用 + 去编码空格,许多服务器至今仍然希望使用这种编码而不是使用现代的百分号编码,因此 Alamofire 包涵了以下的方式对空格进行编码:

  • .percentEscaped - 通过应用标砖的百分比转码对空格字符进行编码。" " 被编码成 "%20"。这个是默认的方式。
  • .plusReplaced - 把 " " 替换 +" " 被编码成 +

你可以创建你自己的 URLEncodedFormParameterEncoder,然后在需要传递 URLEncodedFormEncoder 的初始化方法中指定 SpaceEncoding

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(spaceEncoding: .plusReplaced))

第二种编码方式(JSONParameterEncoder)

JSONParameterEncoder 利用Swift的 JSONEncoderEncodable 类型的值(values)进行编码,然后把编码后的结果设置到 URLRequesthttpBody 中。如果 HTTP header中的 Content-Type 没有设置过的话,会被设置成 aplication/json

发送一个JSON编码(JSON-Encoded)参数的网络请求
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"]}
配置一个自定义的 JSONEncoder

你可以通过传递一个你配置好的 JSONEncoder 实例来自定义 JSONParameterEncoder 的行为:

let encoder = JSONEncoder()
encoder.dateEncoding = .iso8601
encoder.keyEncodingStrategy = .convertToSnakeCase
let parameterEncoder = JSONParameterEncoder(encoder: encoder)
手动为 URLRequest 参数进行编码

ParameterEncoder 的API们也可以用于Alamofire之外直接对 URLRequest 进行参数编码。

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)

HTTP头(HTTP Headers)

Alamofire 拥有自己的的 HTTPHeaders 类型,他是一个保持原有顺序并且不区分大小写的 HTTP头的键值对表示。 HTTPHeader 类型封装了一个单独的键值对并且为通用的HTTP头(headers)提供了许多种静态的值。

为一个 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)
}

HTTPHeaders 也可以使用一个包含有 HTTPHeader 类型值的数组来构建:

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

AF.request("https://httpbin.org/headers", headers: headers).responseJSON { response in
    debugPrint(response)
}

对于 HTTP 头来说,没有变的是,推荐把他们设置到 URLSessionConfiguration,这样他们就可以提供给任何通过基础的 URLSession 所创建的 URLSessionTask 之中了。更多的信息请看会话设置(Session Configurations)章节。

默认的Alamofire Session 为每个 Request 提供了默认的头部信息(headers)集合,他们是:

  • Accept-Encoding,根据RFC 7230 §4.2.3,默认的值是 br;q=1.0, gzip;q=0.8, deflate;q=0.6
  • Accept-Language,根据RFC 7231 §5.3.5,默认的是系统上首选的6种语言,格式就像是:en;q=1.0
  • User-Agent,根据RFC 7231 §5.5.3,包含有当前应用的版本信息,举个例子:iOS Example/1.0 (com.alamofire.iOS-Example; build:1; iOS 13.0.0) Alamofire/5.0.0

如果你想自定义这些头部信息(headers),你应该去创建一个自定义的 URLSessionConfiguration ,更新 defaultHTTPHeaders 属性并将配置应用于新的 Session 实例。使用 URLSessionConfiguration.af.default 去自定义你的配置并且保留 Alamofire 默认的头部信息(headers)。

响应验证(Response Validation)

Alamofire 默认的会把所有已经完成的网络请求都视为是成功的,忽略服务端响应的内容。如果响应的状态码(status code)或者 MIME类型不能被接受(unacceptable)(译者:异常状态码或类型错误啥的)的话可以在响应处理(response handler)生成错误之前调用 validate() 函数。

自动校验(Automatic Validation)

validate() 这个API自动验证响应状态码的区间是 200..<300,如果提供了请求的 Accept头的话,请求的 Accept头的信息要与响应的 Content-Type 相匹配。

AF.request("https://httpbin.org/get").validate().responseJSON { response in
    debugPrint(response)
}
手动校验(Manual Validatin)
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)
        }
    }

响应处理方式(Response Handling)

Alamofire 的 DataRequestDownloadRequest 都有相应的响应类型:DataResponse<Success, Failure: Error>DownloadResponse<Success, Failure: Error>。他们俩都有着相同的组成:一个序列化类型和一个错误类型。默认情况下,所有的响应值都会产生错误类型(即:DataResponse<Success, AFError>)。Alamofire 在公开的 API 中使用更简单的 AFDataResponse<Success>AFDownloadResponse<Success>,他们都包涵有 AFError 类型。 UploadRequestDataRequest 的子类,使用同样的 DataResponse 类型。

处理 Alamofire 的 DataRequest 或者 UploadRequest 所产生的 DataResponse 涉及到将诸如 responseJSON 这样的响应处理 链接DataRequest

AF.request("https://httpbin.org/get").responseJSON { response in
    debugPrint(response)
}

在上面的例子中,responseJSON 处理程序被添加到了 DataRequest 上,在 DataRequest 完成的时候执行一次处理程序。传递给处理程序的是一个闭包(closure),这个闭包接收一个 AFDataResponse<Any> 类型的值,这个值是由响应属性中的 JSONRespnseSerializer 所制造出来的。

这个闭包(closure)作为回调(callback)被添加上去以便在接收到响应的时候去执行处理,这样做比阻塞执行来等待服务器的响应要好的多。请求的结果仅仅在响应闭包(closure)的范围内有效。任何取决于响应的或者从服务器接收的数据的处理都要在响应闭包(closure)中完成。

在 Alamofire 下的网络请求都是异步的。异步编程可能使不熟悉这个概念的程序员感到沮丧,但是异步编程的 好处 是非常多的。

Alamofire 中包涵了6种不同的默认的数据响应处理程序,包括:

// Response Handler - Unserialized 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

没有任何一个响应处理程序对从服务器回来的 HTTPURLResponse 执行校验。

举个例子:响应的状态码在 400..<500500..<600 的范围内并不会自动触发任何 Error。Alamofire 采用响应校验(Response Validation)(上文提到的) 方式来实现校验。(译者:所以处理函数中并不做任何响应的校验)

响应处理程序(Response Handler)

response 处理程序并不校验任何响应数据。他仅仅直接从 URLSessionDelegate 转发所有的信息(infomation)。这就是 Alamofire 等效于使用 cURL 去执行 Request

AF.request("https://httpbin.org/get").response { response in
    debugPrint("Response: \(response)")
}

我们强烈建议您去 ResponseResult 类型的其他的响应序列化工具(译者:我的感觉是不建议直接用response,因为提供了6个方法,建议使用其他更具体好用的方法吧)。

响应 Data 数据的处理程序(Response Data Handler)

responseData 处理程序使用 DataResponseSerializer 去提取和校验服务器返回的 Data 数据。如果没有错误发生并且返回了 Data 数据。那么响应中的 Result 将会是 .success 的并且它里面的 value 会变成服务器返回的 Data 数据。

AF.request("https://httpbin.org/get").responseData { response in
    debugPrint("Response: \(response)")
}
响应字符数据的处理程序(Response String Handler)

responseString 处理程序使用了 StringResponseSerializer 去把服务器返回的 Data 数据转换成指定编码的 String 。如果没有错误产生并且服务端返回的 数据成功的被序列化为 String ,相应的结果将会是 .success 并且结果的 value值将会是 String 类型。

AF.request("https://httpbin.org/get").responseString { response in
    debugPrint("Response: \(response)")
}

如果没有指定任何编码方式,Alamofire 将会使用服务端返回的 HTTPURLResponse 中指定的文本编码。如果无法根据服务器响应确定文本编码,默认的是采用 .isoLatin1

响应JSON数据的处理程序(Response JSON Handler)

responseJSON 处理程序利用 JSONResponseSerializer 去将服务端返回的 Data 数据转换成使用指定 JSONSerialization.ReadingOptionsAny 类型的数据。如果没有错误发生并且成功的将服务端的数据序列化成了JSON对象(JSON object),那么响应 AFResult 将会是 .sucess 并且他的 value 将会是 Any 类型。

AF.request("https://httpbin.org/get").responseJSON { response in
    debugPrint("Response: \(response)")
}

respnseJSON 中使用的 JSON 序列化是使用的 Foundation 框架中的 JSONSerialization来完成处理的。

响应 Decodable 数据的处理程序(Response Decodable Handler)

responseDecodable 数据处理程序使用 DecodableResponseSerializer 去将服务端返回的 Data 数据转换成传参进来的使用指定 DataDecoder(一个可以从数据(Data)解码的解码器(Decoder)的协议) 的 Decodable类型数据。如果没有错误产生并且成功的将服务端返回的数据解码成了一个 Decodable 类型,那么响应的 Result 将会是 .success 的并且 他的 value 将会是传参进来的类型。

struct HTTPBinResponse: Decodable { let url: String }

AF.request("https://httpbin.org/get").responseDecodable(of: HTTPBinResponse.self) { response in
    debugPrint("Response: \(response)")
}
链式的响应处理程序(Chained Response Handlers)

响应处理程序也可以被串联起来:

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

很重要的一点是,如果在同一个 Request使用多重响应处理程序的话,需要服务器对返回数据多次序列化,每个响应处理程序处理一次。最好不要在同一个 Request 上使用多重响应处理程序,特别是在生产环境中。这种做做仅仅应该在开发环境或者没有其他的更好的选择的情况下去做。

相应处理程序队列(Response Handler Queue)

给响应处理程序传递的闭包(closures)默认是在 .main 队列上执行的,但是也可以指定一个 DispatchQueue 给需要执行的闭包(closure)。事实上,序列化的工作(把 Data 数据转换成其他类型的数据)总是在一个后台队列(background queue)中执行的。

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

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

响应的缓存(Response Caching)

响应缓存是由 URLCache 在系统级别进行处理的。他提供了一个内存(in-memory)和磁盘(on-disk)缓存的结合体,使您可以操控内存(in-memory)和磁盘(on-disk)部分的大小。

Alamofire默认的使用 URLCache.shared 这个实例进行自定义的 URLCache 实例的使用,请看会话配置(Session Configuration)章节。

认证方式(Authentication)

认证方式(Authentication)是由 URLCredentialURLAuthenticationChallenge 在系统框架级别进行操作的。

这些认证方式的 API 适用于提示授权的服务器,并非一般用于需要 Authenticate(译者:身份验证) 和 等效的请求头的服务器。

支持的验证方式(Supported Authentication Schemes)
HTTP Basic 授权方式(HTTP Basic Authentication)

当使用 URLAuthenticationChallenge 质询的时候,在适当的时候Request上的 authenticate方法会自动提供 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)
    }
URLCredential 授权方式(Authentication with URLCredential
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)
    }

需要注意的很重要的一点是,当你使用 URLCredential 来进行认证的时候,如果服务器发出了质询,那么底层的 URLSession 实际上最终会发出两个请求(requests)。第一个请求将会不包括可能触发服务器质询的凭据(credential)。Alamofire来接收这个服务器的质询,并把凭证(credential)加上去,再由底层的的 URLSession 重新发送请求(request)。

手动授权(Manual Authentication)

如果你要与一个始终需要 Authenticate 或者类似的请求头的 API 通信而没有提示的时候,你可以手动的加上:

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)
    }

但是,必须作为所有请求的一部分的请求头(headers)通常更好的被处理为自定义的 URLSessionConfiguration 的一部分,或者使用一个 RequestAdapter。(译者注:这些在高级用法中有提到)

下载数据到文件(Downloading Data to a File)

除了把获取到的数据放入内存,Alamofire 还提供了 Session.downloadDownloadRequest,和 DownloadResponse<Success, Failure: Error> 这些 API 去方便的下载数据到磁盘。下载到内存的行为非常适合像大多数 JSON API 的很小的响应信息,而获取大的资源像是图片和视频,就应该下载到磁盘以缓解应用的内存空间。

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

DownloadRequest 具有与 DataRequest 相同的大多数的响应处理程序(response handlers)。但是,由于他把数据下载到磁盘,序列化响应会涉及到从磁盘读取数据,这样也可能导致往内存中读取了大量的数据。在涉设计你自己的数据下载处理的时候要记住这些,这很重要。

文件下载的位置(Download File Destination)

所有下载的数据最初都存放在系统的临时目录里(temporary directory)。这些数据最终将会被系统在某个时间点删除掉,因此如果有些情况需要这些文件存在的时间更长一些的话,把他们移动到某个其他的地方是很重要的。

你可以提供一个 Destination 闭包(closure)去把文件从临时文件目录移动到最终的位置上去。在临时文件被真正的移动到 destinationURL 之前,闭包(closure)中指定的 Options 会被执行,目前支持的两种 Options 是:

  • . createIntermediateDirectories - 如果指定目录,则为目标目录创建中间目录。
  • .removePreviousFile - 如果指定,则从目标目录中删除之前的文件。
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)
    }
}

你也可以使用建议的下载位置的 API:

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

AF.download("https://httpbin.org/image/png", to: destination)
下载进度(download Progress)

很多时候,给用户报告下载进度是很有用的。任何的 DownloadRequest 都可以利用 downloadProgress API 来报告下载进度。

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)
        }
    }

仅当服务器返回 Content-Length 头的时候, URLSession 的进度报告相关的 API 才起作用,对于 ALamofire 也是这样的。没有这个头(header)的话,进度将停留在 0.0,下载完成,进度会跳到 1.0

downloadProgress API 也可以传入一个 queue 参数来定义哪一个 DispatchQueue 是下载进度闭包(closure)所调用的的所在。

let progressQueue = DispatchQueue(label: "com.alamofire.progressQueue", qos: .utility)

AF.download("https://httpbin.org/image/png")
    .downloadProgress(queue: progressQueue) { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseData { response in
        if let data = response.value {
            let image = UIImage(data: data)
        }
    }
取消和重新下载(Canceling and Resuming a Download)

除了所有的 Request 类都有 cancel() API 之外,DownloadRequest 还可以生成恢复数据(resume data),这些恢复数据可以被用来在稍后的时候重新下载数据。这个 API 有两种形式:cancel(producingResumeData: Bool) 可以控制是否生成恢复数据,但是他仅仅对 DownloadResponse 有用;cancel(byProducingResumeData:(_ resumeData: Data?) -> Void) 有着相同的作用,但是他可以让恢复数据在完成处理程序(completion handler)中可用。

如果一个 DownloadRequest 被取消了或者被中断了,底层的 URLSessinDownloadTask 可能会生成回复数据。如果生成了恢复数据,这个恢复数据可以再次被用来在中断的地方重新开始 DownloadRequest

极其重要: 在苹果平台的某些版本(iOS10 - 10.2,macOS10.12-10.12.2, tvOS10-10.1,watchOS3-3.1.1 )中,resumeData 在后台的 URLSessionConfiguration 中是损坏的。在底层的 resumeData 生成逻辑中存在错误,错误的数据被写入,导致了始终无法通过 resumeData 重新进行下载。关于更多的这个bug的信息和可能的解决办法,请看 Stack Overflow post

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)
    }
}

上传数据到服务器(Uploading Data to a Server)

当发送相对少量的使用 JSON 或者 URL 编码的参数到服务器的时候,request() APIs 通常就够用了。如果你需要发送从内存中、通过文件 URL、或者 InputStream来的大量的 Data 的时候,upload() APIs 可能是你想要使用的。

上传数据(Uploading Data)
let data = Data("data".utf8)

AF.upload(data, to: "https://httpbin.org/post").responseDecodable(of: HTTPBinResponse.self) { response in
    debugPrint(response)
}
上传文件(Uploading a File)
let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

AF.upload(fileURL, to: "https://httpbin.org/post").responseDecodable(of: HTTPBinResponse.self) { response in
    debugPrint(response)
}
上传多部分表格数据(Uploading Multipart Form Data)
AF.upload(multipartFormData: { multipartFormData in
    multipartFormData.append(Data("one".utf8), withName: "one")
    multipartFormData.append(Data("two".utf8), withName: "two")
}, to: "https://httpbin.org/post")
    .responseDecodable(of: HTTPBinResponse.self) { response in
        debugPrint(response)
    }
上传进度(Upload Progress)

当你的用户正在等待他们上传数据完成的时候,有时候展示给用户上传进度对于他们是很便利的。任何的 UploadRequest 使用 uploadProgressdownloadProgress的时候都可以报告上传数据的进度和响应数据下载的进度。

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)")
    }
    .responseDecodable(of: HTTPBinResponse.self) { response in
        debugPrint(response)
    }

从服务器来的流式传输数据(Streaming Data from a Server)

使用流传输而不是积累不断接收到的数据,可以更好的服务于长时间下载大型的文件或者连接服务器。Alamofire 提供了 DataStreamRequest 这个类型以及相关的 API 去处理这种用法。尽管这样就提供了好多像其他的 Request 一样的 API,但是它仍然有几个主要的区别。最明显的, DataStreamRequest 不在内存中积累数据或者将数据存入磁盘(disk)。相反的是,它在数据到达的时候反复调用 responseStream 闭包(closures)。在连接完成或者收到错误的时候会再次调用相同的闭包。

每个 Handler 闭包(closure)都捕获一个 Stream 值,其中包括一个 Event 以及可以取消请求的 CancellationToken

public struct Stream<Success, Failure: Error> {
    /// Latest `Event` from the stream.
    public let event: Event<Success, Failure>
    /// Token used to cancel the stream.
    public let token: CancellationToken
    /// Cancel the ongoing stream by canceling the underlying `DataStreamRequest`.
    public func cancel() {
        token.cancel()
    }
}

一个 Event`` 就是一个代表两种流状态的enum```。

public enum Event<Success, Failure: Error> {
    /// Output produced every time the instance receives additional `Data`. The associated value contains the
    /// `Result` of processing the incoming `Data`.
    case stream(Result<Success, Failure>)
    /// Output produced when the instance has completed, whether due to stream end, cancellation, or an error.
    /// Associated `Completion` value contains the final state.
    case complete(Completion)
}

完成的时候,Completion 值将会包含流结束时候的 DataStreamRequest 的状态。

public struct Completion {
    /// Last `URLRequest` issued by the instance.
    public let request: URLRequest?
    /// Last `HTTPURLResponse` received by the instance.
    public let response: HTTPURLResponse?
    /// Last `URLSessionTaskMetrics` produced for the instance.
    public let metrics: URLSessionTaskMetrics?
    /// `AFError` produced for the instance, if any.
    public let error: AFError?
}
流数据(Streaming Data

可以想其他的 Alamofire 请求一样去完成从服务器请求流数据,但是多了一个 Handler 闭包(closure)。

func responseStream(on queue: DispatchQueue = .main, stream: @escaping Handler<Data, Never>) -> Self

提供的 queue 将会是 handler 闭包所在的位置。

AF.streamRequest(...).responseStream { stream in
    switch stream.event {
    case let .stream(result):
        switch result {
        case let .success(data):
            print(data)
        }
    case let .complete(completion):
        print(completion)
    }
}

在上面的例子中处理 .failure 这种 Result 的情况是不必要的,接收 Data 不可能是失败的。

流式的字符串(Streaming String s)

像流式 Data 一样,String 可以添加 Handler 来处理流式字符串数据。

func responseStreamString(on queue: DispatchQueue = .main,
                          stream: @escaping StreamHandler<String, Never>) -> Self

String 值被解码为 UTF8,这种解码不会失败。

AF.streamRequest(...).responseStreamString { stream in
    switch stream.event {
    case let .stream(result):
        switch result {
        case let .success(string):
            print(string)
        }
    case let .complete(completion):
        print(completion)
    }
}
流式 Decodable 值(Streaming Decodable 值)

传入的流式 Data 值可以使用 responseStreamDecodable 转换成任意的 Decodable

func responseStreamDecodable<T: Decodable>(of type: T.Type = T.self,
                                           on queue: DispatchQueue = .main,
                                           using decoder: DataDecoder = JSONDecoder(),
                                           preprocessor: DataPreprocessor = PassthroughPreprocessor(),
                                           stream: @escaping Handler<T, AFError>) -> Self

解码(Decoding)失败并不会终止流,而是在 ResultOutput 中生成一个 AFError

AF.streamRequest(...).responseStreamDecodable(of: SomeType.self) { stream in
    switch stream.event {
    case let .stream(result):
        switch result {
        case let .success(value):
            print(value)
        case let .failure(error):
            print(error)
        }
    case let .complete(completion):
        print(completion)
    }
}
生成一个输入流(Producing an InputStream)

除了使用 StreamHandler 闭包处理传入的 Data 之外, DataStreamRequest 可以生成一个可以读取到达的字节的 InputStream

func asInputStream(bufferSize: Int = 1024) -> InputStream

通过这种方式创建的 InputStream 必须在读取开始之前调用 open(),或者将其传递给自动打开流的 API。一旦从这个方法返回了 InputStream,这个方法的调用者有责任去保持 InputStream 存活,并且在读取完之后调用 close()

let inputStream = AF.streamRequest(...)
    .responseStream { output in
        ...
    }
    .asInputStream()
取消(Cancellation)

DataStreamRequest 们可以用4种方式来取消。第一种,像所有其他的 Alamofire 的 Request 一样,DataStreamRequest 可以调用 cancel() 来取消底层(underlying)的任务(task)并且完成流。

let request = AF.streamRequest(...).responseStream(...)
...
request.cancel()

第二种,DataStreamRequest 们可以在 DataStreamSerializer 发生错误的时候被自动的取消。这种行为默认是被禁止的但是可以通过 automaticallyCancelOnStreamError 这个参数在创建请求的时候允许这种行为。

AF.streamRequest(..., automaticallyCancelOnStreamError: true).responseStream(...)

第三种,DataStreamRequest 们将会在 Handler 闭包(closure)抛出错误的时候被取消。之后,这个错误会被存入请求之中并且在 Completion 值中可用。

AF.streamRequest(...).responseStream { stream in
    // Process stream.
    throw SomeError() // Cancels request.
}

最后一种,DataStreamRequest 们可以使用 Stream 值的 cancel() 方法来取消。

AF.streamRequest(...).responseStream { stream in 
    // Decide to cancel request.
    stream.cancel()
}

统计指标(Statistical Metrics)

URLSessionTaskMetrics

Akanifure 为每一个 Request 收集 URLSessionTaskMetricsURLSessionTaskMetrics 封装了一些关于基础的网络连接和请求响应事件的奇妙的统计信息。

AF.request("https://httpbin.org/get").responseJSON { response in
    print(response.metrics)
}

由于 FB7624529,目前收集 watchOS 的 URLSessionTaskMetrics 是不可用的。

cURL 命令输出(cURL Command Output)

调试平台的错误可能让人蛋疼,但是谢天谢地,Alamofire 的 Request 类型可以产生等效的 cURL 命令来方便调试。由于 Alamofire 的 Request 创建是异步的这个特性,这个API有同步(synchronous)的和一步的(asynchronous)两个版本。为了尽快的获取 cURL 命令,你可以链接一个 CURLDescription 在request上:

AF.request("https://httpbin.org/get")
    .cURLDescription { description in
        print(description)
    }
    .responseJSON { response in
        debugPrint(response.metrics)
    }

这个就制造出来了:

$ 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"
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容