iOS/Swift:连接 REST API 的良好架构方法

Posted

技术标签:

【中文标题】iOS/Swift:连接 REST API 的良好架构方法【英文标题】:iOS/Swift: Good architecture approach for connecting REST APIs 【发布时间】:2016-10-18 05:34:04 【问题描述】:

我已经开发 ios 应用程序很长时间了。但最终我对网络层的架构设计从未感到满意。尤其是在连接 API 时。


这里可能存在重复,但我认为我的问题更具体,如您所见。

Best architectural approaches for building iOS networking applications (REST clients)


我不是在寻找“使用 AFNetworking/Alamofire”之类的答案。这个问题与使用哪个 3rd 方框架无关。

我的意思是,我们经常有这样的场景:

“开发使用 API Y 的应用 X”

这主要包括相同的步骤 - 每次。

    实现登录/注册 您获得了一个身份验证令牌,必须将其保存在钥匙串中并附加到每个 API 调用中 您必须重新验证并重新发送失败并返回 401 的 API 请求 您有错误代码需要处理(如何集中处理?) 您实现不同的 API 调用。

3)的一个问题

在 Obj-C 中,我使用NSProxy 在发送之前拦截每个 API 调用,如果令牌过期,则重新验证用户并触发实际请求。 在 Swift 中,我们有一些 NSOperationQueue,如果我们收到 401,我们会在其中排队 auth 调用,并在成功刷新后将实际请求排队。但这限制了我们使用单例(我不太喜欢),我们还必须将并发请求限制为 1。 我更喜欢第二种方法 - 但有更好的解决方案吗?

关于4)

您如何处理 http 状态码?您是否为每个错误使用许多不同的类?您是否将一般错误处理集中在一个类中?您是在同一级别处理它们还是更早地发现服务器错误? (可能在任何第 3 方库的 API 包装器中)


你们的开发人员是如何尝试解决这个问题的?您是否找到了“最佳匹配”设计? 您如何测试您的 API?尤其是你如何在 Swift 中做到这一点(没有真正的模拟可能性?)。

当然:每个用例、每个应用程序、每个场景都是不同的 - 没有“一个解决方案适合所有人”。但我认为这些一般性问题经常再次出现,所以我很想说“是的,对于这些情况 - 可能有一个或多个解决方案 - 您可以每次都重复使用这些解决方案”。

期待有趣的答案!

干杯 奥兰多????

【问题讨论】:

为了在你的测试中存根网络请求,你可以使用 Mockingjay lib github.com/kylef/Mockingjay 它工作得很好 【参考方案1】:

但这限制了我们使用单例(我不太喜欢),我们还必须将并发请求限制为 1。我更喜欢第二种方法 - 但有更好的解决方案吗?

我使用几个层来通过 API 进行身份验证。

身份验证管理器


此经理负责所有与身份验证相关的功能。可以考虑认证、重置密码、重发验证码等功能。

struct AuthenticationManager

    static func authenticate(username:String!, password:String!) -> Promise<Void>
    
        let request = TokenRequest(username: username, password: password)

        return TokenManager.requestToken(request: request)
    

为了请求令牌,我们需要一个名为 TokenManager 的新层,它管理与 token 相关的所有事情。

令牌管理器


struct TokenManager

    private static var userDefaults = UserDefaults.standard
    private static var tokenKey = CONSTANTS.userDefaults.tokenKey
    static var date = Date()

    static var token:Token?
    
        guard let tokenDict = userDefaults.dictionary(forKey: tokenKey) else  return nil 

        let token = Token.instance(dictionary: tokenDict as NSDictionary)

        return token
    

    static var tokenExist: Bool  return token != nil 

    static var tokenIsValid: Bool
    
        if let expiringDate = userDefaults.value(forKey: "EXPIRING_DATE") as? Date
        
            if date >= expiringDate
            
                return false
            else
                return true
            
        
        return true
    

    static func requestToken(request: TokenRequest) -> Promise<Void>
    
        return Promise  fulFill, reject in

            TokenService.requestToken(request: request).then  (token: Token) -> Void in
                setToken(token: token)

                let today = Date()
                let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today)
                userDefaults.setValue(tomorrow, forKey: "EXPIRING_DATE")

                fulFill()
            .catch  error in
                reject(error)
            
        
    

    static func refreshToken() -> Promise<Void>
    
        return Promise  fulFill, reject in

            guard let token = token else  return 

            let  request = TokenRefresh(refreshToken: token.refreshToken)

            TokenService.refreshToken(request: request).then  (token: Token) -> Void in
                setToken(token: token)
                fulFill()
            .catch  error in
                reject(error)
            
        
    

    private static func setToken (token:Token!)
    
        userDefaults.setValue(token.toDictionary(), forKey: tokenKey)
    

    static func deleteToken()
    
        userDefaults.removeObject(forKey: tokenKey)
    

为了请求令牌,我们需要一个名为 TokenService 的第三层来处理所有 HTTP 调用。我使用 EVReflection 和 Promises 进行 API 调用。

令牌服务


struct TokenService: NetworkService

    static func requestToken (request: TokenRequest) -> Promise<Token>  return POST(request: request) 

    static func refreshToken (request: TokenRefresh) -> Promise<Token>  return POST(request: request) 

    // MARK: - POST

    private static func POST<T:EVReflectable>(request: T) -> Promise<Token>
    
        let headers = ["Content-Type": "application/x-www-form-urlencoded"]

        let parameters = request.toDictionary(.DefaultDeserialize) as! [String : AnyObject]

        return POST(URL: URLS.auth.token, parameters: parameters, headers: headers, encoding: URLEncoding.default)
    

授权服务


我正在使用授权服务来解决您在此处描述的问题。该层负责拦截服务器错误,例如401(或您想要拦截的任何代码)并在将响应返回给用户之前修复它们。使用这种方法,所有事情都由这一层处理,您不必再担心令牌无效。

在 Obj-C 中,我使用 NSProxy 在发送之前拦截每个 API 调用,如果令牌过期,则重新验证用户并触发实际请求。在 Swift 中,我们有一些 NSOperationQueue,如果我们得到 401,我们会在其中排队 auth 调用,并在成功刷新后将实际请求排队。但这限制了我们使用单例(我不太喜欢),我们还必须将并发请求限制为 1。我更喜欢第二种方法 - 但有更好的解决方案吗?

struct AuthorizationService: NetworkService

    private static var authorizedHeader:[String: String]
    
        guard let accessToken = TokenManager.token?.accessToken else
        
            return ["Authorization": ""]
        
        return ["Authorization": "Bearer \(accessToken)"]
    

    // MARK: - POST

    static func POST<T:EVObject> (URL: String, parameters: [String: AnyObject], encoding: ParameterEncoding) -> Promise<T>
    
        return firstly
        
            return POST(URL: URL, parameters: parameters, headers: authorizedHeader, encoding: encoding)

        .catch  error in

            switch ((error as NSError).code)
            
            case 401:
                _ = TokenManager.refreshToken().then  return POST(URL: URL, parameters: parameters, encoding: encoding) 
            default: break
            
        
    

网络服务


最后一部分是network-service。在这个服务层中,我们将编写所有类似交互器的代码。所有与网络相关的业务逻辑都将在这里结束。如果您简要回顾一下这个服务,您会注意到这里没有 UI 逻辑,这是有原因的。

protocol NetworkService

    static func POST<T:EVObject>(URL: String, parameters: [String: AnyObject]?, headers: [String: String]?, encoding: ParameterEncoding) -> Promise<T>



extension NetworkService

    // MARK: - POST

    static func POST<T:EVObject>(URL: String,
                                 parameters: [String: AnyObject]? = nil,
                                 headers: [String: String]? = nil, encoding: ParameterEncoding) -> Promise<T>
    
        return Alamofire.request(URL,
                                 method: .post,
                                 parameters: parameters,
                                 encoding: encoding,
                                 headers: headers).responseObject()
    
 

小型认证演示


此架构的一个示例实现是用于登录用户的经过身份验证的 HTTP 请求。我将向您展示如何使用上述架构完成此操作。

AuthenticationManager.authenticate(username: username, password: password).then  (result) -> Void in

// your logic

.catch  (error) in

  // Handle errors


处理错误始终是一项繁琐的任务。每个开发人员都有自己的方式来做到这一点。在网络上有大量关于错误处理的文章,例如 swift。显示我的错误处理不会有太大帮助,因为这只是我个人的做法,在这个答案中发布的代码也很多,所以我宁愿跳过它。

无论如何...

我希望我已经通过这种方法帮助您重回正轨。如果对此架构有任何疑问,我将非常乐意为您提供帮助。在我看来,没有完美的架构,也没有可以应用于所有项目的架构。

这是您团队内部的偏好、项目要求和专业知识的问题。

祝你好运,如果有任何问题,请随时与我联系!

【讨论】:

嘿,谁能帮我把它转换成最新的 Alamofire 版本。我认为最新的 Alamofire 不支持 responseObject() @BhaveshBansal 你想把它转换成什么? 它在 responseObject 中向我抛出错误。 Alamofire 需要完成处理程序。我需要知道如何使用 Alamofire 完成处理程序实现网络服务层 我可以知道你的代币模型吗?我没有找到你声明的任何地方。

以上是关于iOS/Swift:连接 REST API 的良好架构方法的主要内容,如果未能解决你的问题,请参考以下文章

用于 iOS 的 REST API 的良好序列化格式?

iOS8/Swift 奇怪的空行为与来自 REST 服务的响应

在 Django Rest Framework + Django Social Auth 中连接 Facebook

使用 LXD REST API,ClientWebSocket 在 .Net Core 上抛出“AuthenticationException”,但在 .Net Framework 上,它运行良好

如何使用 CKReference cloudkit IOS Swift 为数据库创建良好的结构?

在 react-redux 应用程序中获取详细 api 数据的良好做法