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

iOS/Swift: Good architecture approach for connecting REST APIs

我开发iOS应用程序已经很长时间了。但最终我对我的网络层的体系结构设计并不满意。尤其是在连接API时。

这里可能有一个副本,但我认为我的问题更具体,如您所见。

构建iOS网络应用程序的最佳体系结构方法(REST客户端)

我并不是在寻找"使用网络/Alamoire"之类的答案。这个问题与使用哪种第三方框架无关。

我的意思是,我们经常会遇到这样的情况:

"Develop an app X that uses API Y"

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

  • 实施登录/注册
  • 您得到一个身份验证令牌,必须将其保存在keychain中,并在每个API调用中附加它。
  • 您必须重新验证并重新发送由于401失败的API请求。
  • 您有要处理的错误代码(如何集中处理它们?)
  • 您可以实现不同的API调用。
  • 一个问题3)

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

    关于4)

    如何处理HTTP状态代码?每一个错误都使用许多不同的类吗?您是否将一般错误处理集中在一个类中?您是在同一级别处理它们,还是较早地捕获服务器错误?(可能在任何第三方库的API包装中)

    开发人员是如何解决这个问题的?你想出了一个"最佳匹配"的设计方案吗?如何测试API?尤其是你如何迅速做到这一点(没有真正的嘲弄可能性?).

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

    期待有趣的答案!

    干杯奥兰多??


    But that limited us to use a Singleton (which I don’t like much) and we also had to limit the concurrent requests to 1. I like more the second approach - but is there a better solution?

    我正在使用一些层对API进行身份验证。

    身份验证管理器

    此管理器负责所有与身份验证相关的功能。您可以考虑验证、重置密码、重新发送验证代码等功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct AuthenticationManager
    {
        static func authenticate(username:String!, password:String!) -> Promise<Void>
        {
            let request = TokenRequest(username: username, password: password)

            return TokenManager.requestToken(request: request)
        }
    }

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

    令牌管理器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    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)
        }
    }

    为了请求一个令牌,我们需要一个称为令牌服务的第三层来处理所有HTTP调用。我对API调用使用evreflection和promises。

    令牌服务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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(或您想要截取的任何代码),并在将响应返回给用户之前修复这些错误。使用这种方法,所有东西都由这个层处理,您不再需要担心一个无效的令牌了。

    In Obj-C I used NSProxy for intercepting every API Call before it was send, re-authenticated the user if the token expired and and fired the actual request. In Swift we had some NSOperationQueue where we queued an auth call if we got a 401 and queued the actual request after successful refresh. But that limited us to use a Singleton (which I don’t like much) and we also had to limit the concurrent requests to 1. I like more the second approach - but is there a better solution?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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
                }
            }
        }
    }

    网络服务

    The last part will be the network-service. In this service layer we will do all interactor-like code. All business logic will end up here, anything related to networking. If you briefly review this service you'll note that there is no UI-logic in here, and that's for a reason.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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请求。我将向您展示如何使用上面描述的体系结构来完成这项工作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    AuthenticationManager.authenticate(username: username, password: password).then { (result) -> Void in

    // your logic

    }.catch { (error) in

      // Handle errors

    }

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

    总之…

    我希望我能帮助你回到正轨。如果有关于这个架构的任何问题,我将非常乐意帮助您解决这个问题。在我看来,没有完美的体系结构,也没有可以应用于所有项目的体系结构。

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

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