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

ors*_*aef 12 architecture rest networking ios swift

我正在开发iOS应用程序已有很长一段时间了.但最后我对网络层的架构设计从不满意.特别是当它连接API时.


这里可能有重复,但我认为我的问题更具体,你会看到.

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


我不是在寻找像"使用AFNetworking/Alamofire"这样的答案.无论使用哪个第三方框架,此问题都是如此.

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

"开发一个使用API​​ Y的应用X"

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

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

3)的一个问题

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

关于4)

你如何处理http状态代码?每个错误都使用许多不同的类吗?您是否将一般错误处理集中在一个类中?您是在同一级别处理它们还是更早地捕获服务器错误?(也许在你的任何第三方lib的API Wrapper中)


你是如何开发人员试图解决这个问题的?你有没有找到"最佳匹配"的设计?你如何测试你的API?特别是你如何在Swift中做到这一点(没有真正的嘲弄可能性?).

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

期待有趣的答案!

干杯
奥兰多

Kev*_*gts 5

但这限制了我们使用单例(我不太喜欢),我们还必须将并发请求限制为 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

代币管理器


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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

令牌服务


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)
    }
}
Run Code Online (Sandbox Code Playgroud)

授权服务


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

在 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
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

网络服务


最后一部分将是。在这个服务层中,我们将执行所有类似交互器的代码。所有业务逻辑都将在这里结束,任何与网络相关的事情。如果您简要回顾一下这项服务,您会注意到这里没有 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()
    }
 }
Run Code Online (Sandbox Code Playgroud)

小型认证演示


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

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

// your logic

}.catch { (error) in

  // Handle errors

}
Run Code Online (Sandbox Code Playgroud)

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

反正...

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

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

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