请求链自定义
了解如何通过自定义拦截器来自定义 Apollo iOS 请求链
在Apollo iOS中,ApolloClient
使用NetworkTransport
对象从远程GraphQL服务器中抓取GraphQL查询。
默认的 NetworkTransport
是 RequestChainNetworkTransport
。因此,此网络传输使用一个名为 请求链 的结构来逐步处理每个 操作。
要了解如何配置客户端支持通过 WebSocket 发送 订阅 操作,请参阅 启用订阅支持。
请求链
一个 请求链 定义了一串 拦截器,这些拦截器处理特定 GraphQL 操作 执行的生命周期。一个拦截器可能会向请求添加自定义 HTTP 头部,而另一个则负责实际上 发送 请求到 GraphQL 服务器。第三个拦截器随后会将操作的结果写入 Apollo iOS 缓存。
执行操作时,一个名为 InterceptorProvider
的对象为该操作生成一个 RequestChain
。然后,调用请求链上的 kickoff
,它运行链中的第一个拦截器:
拦截器可以在任何线程上执行任意的异步逻辑。当拦截器运行完成后,它在其 RequestChain
上调用 proceedAsync
,从而转到下一个拦截器。
默认情况下,当链中的最后一个拦截器完成时,如果解析的操作结果可用,则将该结果返回给操作的原调用者。否则,调用错误处理逻辑。
每个请求都有自己的短生命周期的 RequestChain
。 这意味着每个操作的拦截器序列可能不同。
拦截器提供者
为了为每个 GraphQL 操作生成一个 请求链,Apollo iOS 将操作传递到一个名为 拦截器提供者 的对象。该对象遵循 InterceptorProvider
协议。
默认提供者
DefaultInterceptorProvider
是拦截器提供者的默认实现。该默认拦截器提供者支持:
- 读取/写入响应数据到标准化缓存。
- 使用
URLSession
发送网络请求。 - 解析 JSON 格式的 GraphQL 响应数据
- 自动持久化查询。
默认拦截器提供器DefaultInterceptorProvider
使用一个URLSessionClient
和一个ApolloStore
来初始化,这些将被传递到它创建的拦截器中。通过配置这两个对象,DefaultInterceptorProvider
可以支持大多数常见用法。
如果您需要进一步自定义请求管道,您可以创建一个自定义拦截器提供器。
默认拦截器
对于每次操作DefaultInterceptorProvider
都会创建以下拦截器的请求链。
以下是对内置拦截器的描述。。
自定义拦截器提供器
如果您的用例需要,您可以创建一个符合InterceptorProvider
协议的定制struct或class。
如果您定义了自定义的InterceptorProvider
,它通常应该创建一个类似于默认的RequestChain
结构,但需要根据特定操作添加或修改。
提示:如果您只需向默认请求链的开始或结束处添加拦截器,}可以子类化 DefaultInterceptorProvider
而不是从头开始创建一个新的类。
在创建自定义拦截器提供器中的请求链时请注意以下事项
- 拦截器设计为是短暂的。您的拦截器提供器应针对每次请求提供一组全新的拦截器,以免同时使用相同的拦截器实例。
- 通常不建议保留个别拦截器的引用(测试验证除外)。相反,您可以创建一个持有关联较长的对象的拦截器,提供器可以将此对象传递到每个新的拦截器集合中。这样,每个拦截器都是可丢弃的,但您不需要重创建执行更多重工作的底层对象。
如果您创建了您自己的InterceptorProvider
,您可以使用任何在Apollo iOS中包含的内置拦截器:
内置拦截器
Apollo iOS提供一系列内建的拦截器,您可以在自定义拦截器提供者中创建。自定义拦截器提供者中定义。您还可以通过定义一个符合ApolloInterceptor接口的类来创建一个自定义拦截器。
名称 | 描述 |
---|---|
预网络 | |
| 强制对初始失败的GraphQL操作执行最大次数的重试(默认三次重试)。 |
| 在服务器上执行操作之前,从Apollo iOS缓存中读取数据,根据该操作cachePolicy。 如果找到缓存数据可以完全解决该操作,则将返回该数据。然后根据操作cachePolicy继续或终止请求链。 |
网络 | |
| 接收一个 如果您通过网络发送操作,则您的RequestChain需要此拦截器(或处理网络通信的自定义拦截器)。 |
后网络 | |
| 对于未成功执行的 操作,检查GraphQL服务器的HTTP响应状态码并将它传递给 请注意,大多数在GraphQL层的错误都伴随着状态码 有关更多信息,请参阅这篇关于GraphQL错误处理的文章。 |
| 检查GraphQL服务器在执行后的响应,以确定提供的APQ哈希是否成功由服务器找到。如果没有找到,拦截器将重新启动链并使用完整的查询字符串重新尝试操作。 |
| 使用增量交付HTTP规范解析多部分响应,并将其传递给下一个拦截器。 注意,下一个拦截器必须是JSONResponseParsingInterceptor,以便将单个消息解析为 |
| 将 GraphQL 服务器's JSON 响应解析为 |
| 在服务器上执行操作后,根据该操作的 缓存策略 将响应数据写入 Apollo iOS 缓存 |
additionalErrorInterceptor
InterceptorProvider 可以选择提供 additionalErrorInterceptor,在将错误返回给调用者之前调用。这主要用于日志记录和错误跟踪。此拦截器必须遵守 ApolloErrorInterceptor 协议。ApolloErrorInterceptor
.
additionalErrorInterceptor 不是请求链的一部分。相反,任何其他拦截器都可以通过调用 chain.handleErrorAsync 来调用此拦截器。
注意:对于有明确解决方法的预期错误(比如更新过期的认证令牌),你应该在你的请求链中定义一个可以解决该问题并重试操作的中继器。
拦截器流程
大多数拦截器执行其逻辑然后调用 chain.proceedAsync 以继续到请求链中的下一个拦截器。然而,拦截器可以调用其他方法来覆盖此默认流程。
重试操作
任何拦截器都可以调用 chain.retry 来立即从开始重播当前请求链。如果在拦截器需要刷新访问令牌或修改操作以成功执行其他配置的情况下,这很有用。
重要:不要无限制地调用重试。如果你的服务器返回 500 或者用户没有互联网连接,反复重试可以创建请求无限循环(特别是在你没有使用 MaxRetryInterceptor 来限制重试次数时)。
无限制的重试会耗尽用户的电池电量,并可能增加他们的数据使用量。确保只有在代码可以处理原始失败的情况下才进行重试!
返回值
拦截器可以直接向操作的原调用人返回值,而不是等待请求链完成。为此,拦截器可以调用 chain.returnValueAsync。
这不会阻止请求链中其余部分的执行。拦截器仍然可以在调用 chain.returnValueAsync
后按常规调用 chain.proceedAsync
。但在调用 chain.returnValueAsync
后,拦截器仍然可以按常规继续调用 chain.proceedAsync
。但是,如果遇到的错误会导致 操作 失败,则可以跳过调用 chain.proceedAsync
来结束请求链。
你甚至可以在请求链中多次调用 chain.returnValueAsync
!这在最初返回本地缓存值,然后返回由 GraphQL 服务器返回的值时很有用。
返回错误
如果拦截器遇到错误,它可以调用 chain.handleErrorAsync
来返回该错误的详细信息。
这不会阻止请求链中其余部分的执行。拦截器仍然可以按常规在调用 chain.handleErrorAsync
后调用 chain.proceedAsync
。然而,如果遇到的错误会导致操作失败,您可以跳过调用 chain.proceedAsync
以结束请求链。
示例
以下示例代码片段演示了如何使用带有自定义拦截的请求管道。此代码假定您有以下 假设 的类在您的代码中(这些类不是 Apollo iOS 的部分):
UserManager
: 检查活动用户是否已登录,在错误和响应上进行相关检查,看是否有必要续订令牌,并在必要时执行续订。Logger
: 根据日志级别处理打印日志。支持.debug
、.error
和.always
日志级别。
示例拦截器
UserManagementInterceptor
此示例拦截器检查活动用户是否已登录。如果是,并且令牌已过有效期,则异步续订该用户的访问令牌。最后,在继续到请求链中的下一个拦截器之前,将其添加到 Authorization
标头中。
import Foundationimport Apolloclass UserManagementInterceptor: ApolloInterceptor {enum UserError: Error {case noUserLoggedIn}public var id: String = UUID().uuidString/// Helper function to add the token then move on to the next stepprivate func addTokenAndProceed<Operation: GraphQLOperation>(_ token: Token,to request: HTTPRequest<Operation>,chain: RequestChain,response: HTTPResponse<Operation>?,completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {request.addHeader(name: "Authorization", value: "Bearer \(token.value)")chain.proceedAsync(request: request,response: response,interceptor: self,completion: completion)}func interceptAsync<Operation: GraphQLOperation>(chain: RequestChain,request: HTTPRequest<Operation>,response: HTTPResponse<Operation>?,completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {guard let token = UserManager.shared.token else {// In this instance, no user is logged in, so we want to call// the error handler, then return to prevent further workchain.handleErrorAsync(UserError.noUserLoggedIn,request: request,response: response,completion: completion)return}// If we've gotten here, there is a token!if token.isExpired {// Call an async method to renew the tokenUserManager.shared.renewToken { [weak self] tokenRenewResult inguard let self = self else {return}switch tokenRenewResult {case .failure(let error):// Pass the token renewal error up the chain, and do// not proceed further. Note that you could also wrap this in a// `UserError` if you want.chain.handleErrorAsync(error,request: request,response: response,completion: completion)case .success(let token):// Renewing worked! Add the token and move onself.addTokenAndProceed(token,to: request,chain: chain,response: response,completion: completion)}}} else {// We don't need to wait for renewal, add token and move onself.addTokenAndProceed(token,to: request,chain: chain,response: response,completion: completion)}}}
RequestLoggingInterceptor
此示例拦截器使用假设的 Logger
类记录出站请求,然后继续到请求链中的下一个拦截器:
import Foundationimport Apolloclass RequestLoggingInterceptor: ApolloInterceptor {public var id: String = UUID().uuidStringfunc interceptAsync<Operation: GraphQLOperation>(chain: RequestChain,request: HTTPRequest<Operation>,response: HTTPResponse<Operation>?,completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {Logger.log(.debug, "Outgoing request: \(request)")chain.proceedAsync(request: request,response: response,interceptor: self,completion: completion)}}
ResponseLoggingInterceptor
此示例拦截器使用假设的 Logger
类记录请求的响应(如果存在),然后继续到请求链中的下一个拦截器:
这是一个可以继续执行并抛出错误的拦截器的示例。我们不一定想在拦截器被错误地放置时停止处理,但我们确实希望知道错误信息。并且想要知道该错误。
import Foundationimport Apolloclass ResponseLoggingInterceptor: ApolloInterceptor {enum ResponseLoggingError: Error {case notYetReceived}public var id: String = UUID().uuidStringfunc interceptAsync<Operation: GraphQLOperation>(chain: RequestChain,request: HTTPRequest<Operation>,response: HTTPResponse<Operation>?,completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {defer {// Even if we can't log, we still want to keep going.chain.proceedAsync(request: request,response: response,interceptor: self,completion: completion)}guard let receivedResponse = response else {chain.handleErrorAsync(ResponseLoggingError.notYetReceived,request: request,response: response,completion: completion)return}Logger.log(.debug, "HTTP Response: \(receivedResponse.httpResponse)")if let stringData = String(bytes: receivedResponse.rawData, encoding: .utf8) {Logger.log(.debug, "Data: \(stringData)")} else {Logger.log(.error, "Could not convert data to string!")}}}
LoggingErrorInterceptor
此示例错误拦截器演示了在请求过程中抛出错误时如何进行自定义日志记录。
import Foundationimport Apolloclass LoggingErrorInterceptor: ApolloErrorInterceptor {weak var errorLogger: MyErrorLogger?init(errorLogger: MyErrorLogger) {self.errorLogger = errorLogger}func handleErrorAsync<Operation: GraphQLOperation>(error: Error,chain: RequestChain,request: Apollo.HTTPRequest<Operation>,response: Apollo.HTTPResponse<Operation>?,completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {errorLogger?.requestFailed(response?.httpResponse, withError: error)completion(.failure(error))}}
示例拦截器提供者
此InterceptorProvider
创建请求链,使用默认拦截器按照常规顺序,并将上面定义的所有示例拦截器添加到请求管道中的适当位置:
import Foundationimport Apollostruct NetworkInterceptorProvider: InterceptorProvider {// These properties will remain the same throughout the life of the `InterceptorProvider`, even though they// will be handed to different interceptors.private let store: ApolloStoreprivate let client: URLSessionClientinit(store: ApolloStore, client: URLSessionClient) {self.store = storeself.client = client}func interceptors<Operation: GraphQLOperation>(for operation: Operation) -> [ApolloInterceptor] {return [MaxRetryInterceptor(),CacheReadInterceptor(store: self.store),UserManagementInterceptor(),RequestLoggingInterceptor(),NetworkFetchInterceptor(client: self.client),ResponseLoggingInterceptor(),ResponseCodeInterceptor(),JSONResponseParsingInterceptor(),AutomaticPersistedQueryInterceptor(),CacheWriteInterceptor(store: self.store)]}}
示例 ApolloClient
配置
以下是如何使用我们的示例NetworkInterceptorProvider
设置ApolloClient
。
import Foundationimport Apollolet client: ApolloClient = {// The cache is necessary to set up the store, which we're going// to hand to the providerlet cache = InMemoryNormalizedCache()let store = ApolloStore(cache: cache)let client = URLSessionClient()let provider = NetworkInterceptorProvider(store: store, client: client)let url = URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/graphql")!let requestChainTransport = RequestChainNetworkTransport(interceptorProvider: provider,endpointURL: url)// Remember to give the store you already created to the client so it// doesn't create one on its ownreturn ApolloClient(networkTransport: requestChainTransport, store: store)}()
说明如何设置一个可以处理WebSocket和订阅的客户端的示例包括在订阅文档中。
URLSessionClient
类
由于URLSession
只支持通过基于代理的API在后台使用,Apollo iOS提供了一个URLSessionClient
类,帮助管理URLSessionDelegate
。
请注意,由于只能在URLSession
的初始化器中设置代理,因此只能将URLSessionClient
的初始化器传递给URLSessionConfiguration
,而不是现有的URLSession
。
默认情况下,URLSessionClient
实例使用URLSessionConfiguration.default
设置会话,DefaultInterceptorProvider
实例使用URLSessionClient
的默认初始化器。
URLSessionClient
类及其大多数方法都是公开的
,所以如果您需要覆盖任何URLSession
代理的代理方法,或者需要处理其他代理场景,您可以对它进行子类化。