【问题标题】:Proper usage of the Alamofire's URLRequestConvertible正确使用 Alamofire 的 URLRequestConvertible
【发布时间】:2015-04-04 16:12:17
【问题描述】:

我已经阅读了几个教程,来自@mattt 的自述文件,但无法弄清楚一些事情。

  1. URLRequestConvertible 在实际 API 中的正确用法是什么?看起来如果我将通过为所有 API 实现URLRequestConvertible 协议来创建一个路由器 - 它几乎不可读。我应该为每个端点创建一个路由器吗?

  2. 第二个问题很可能是由于缺乏 Swift 语言经验引起的。我不明白为什么enum 用于构建路由器?为什么我们不使用带有静态方法的类? 这是一个示例(来自 Alamofire 的 README)

    enum Router: URLRequestConvertible {
        static let baseURLString = "http://example.com"
        static let perPage = 50
    
        case Search(query: String, page: Int)
    
        // MARK: URLRequestConvertible
    
        var URLRequest: NSURLRequest {
            let (path: String, parameters: [String: AnyObject]?) = {
                switch self {
                case .Search(let query, let page) where page > 1:
                    return ("/search", ["q": query, "offset": Router.perPage * page])
                case .Search(let query, _):
                    return ("/search", ["q": query])
                }
            }()
    
            let URL = NSURL(string: Router.baseURLString)!
            let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(path))
            let encoding = Alamofire.ParameterEncoding.URL
    
            return encoding.encode(URLRequest, parameters: parameters).0
        }
    }
    
  3. 传参有两种方式:

    case CreateUser([String: AnyObject])
    case ReadUser(String)
    case UpdateUser(String, [String: AnyObject])
    case DestroyUser(String)
    

    并且(假设用户有 4 个参数)

    case CreateUser(String, String, String, String)
    case ReadUser(String)
    case UpdateUser(String, String, String, String, String)
    case DestroyUser(String)
    

    @mattt 在示例中使用第一个。但这将导致在路由器之外“硬编码”参数名称(例如在 UIViewControllers 中)。 参数名称中的拼写错误会导致错误。
    其他人正在使用第二个选项,但在这种情况下,每个参数代表什么并不明显。
    正确的方法是什么?

【问题讨论】:

    标签: ios swift alamofire


    【解决方案1】:

    好问题。让我们逐一分解。

    URLRequestConvertible 在实际 API 中的正确用法是什么?

    URLRequestConvertible 协议是确保给定对象可以创建有效NSURLRequest 的轻量级方法。实际上并没有一套严格的规则或指南来强制您以任何特定方式使用此协议。它只是一个方便的协议,允许其他对象存储正确创建NSURLRequest 所需的状态。更多关于 Alamofire 的信息可以在here找到。

    我应该为每个端点创建一个路由器吗?

    绝对不是。这将破坏使用Enum 的全部目的。 Swift Enum 对象非常强大,可以让你共享大量的公共状态,并打开真正不同的部分。能够使用如下简单的东西创建NSURLRequest 真的很强大!

    let URLRequest: NSURLRequest = Router.ReadUser("cnoon")
    

    我不明白为什么使用枚举来构建路由器?为什么我们不使用带有静态方法的类?

    之所以使用枚举,是因为它是一种在通用接口下表达多个相关对象的更简洁的方式。所有方法在所有案例之间共享。如果您使用静态方法,则每种方法的每种情况都必须有一个静态方法。或者您必须在对象内使用 Obj-C 样式的枚举。这是我的意思的一个简单示例。

    enum Router: URLRequestConvertible {
        static let baseURLString = "http://example.com"
    
        case CreateUser([String: AnyObject])
        case ReadUser(String)
        case UpdateUser(String, [String: AnyObject])
        case DestroyUser(String)
    
        var method: Alamofire.HTTPMethod {
            switch self {
            case .CreateUser:
                return .post
            case .ReadUser:
                return .get
            case .UpdateUser:
                return .put
            case .DestroyUser:
                return .delete
            }
        }
    
        var path: String {
            switch self {
            case .CreateUser:
                return "/users"
            case .ReadUser(let username):
                return "/users/\(username)"
            case .UpdateUser(let username, _):
                return "/users/\(username)"
            case .DestroyUser(let username):
                return "/users/\(username)"
            }
        }
    }
    

    要获取任何不同端点的方法,您可以调用相同的方法,而无需传入任何参数来定义您要查找的端点类型,它已经由您选择的案例处理。

    let createUserMethod = Router.CreateUser.method
    let updateUserMethod = Router.UpdateUser.method
    

    或者如果你想获取路径,相同类型的调用。

    let updateUserPath = Router.UpdateUser.path
    let destroyUserPath = Router.DestroyUser.path
    

    现在让我们使用静态方法尝试相同的方法。

    struct Router: URLRequestConvertible {
        static let baseURLString = "http://example.com"
    
        static var method: Method {
            // how do I pick which endpoint?
        }
    
        static func methodForEndpoint(endpoint: String) -> Method {
            // but then I have to pass in the endpoint each time
            // what if I use the wrong key?
            // possible solution...use an Obj-C style enum without functions?
            // best solution, merge both concepts and bingo, Swift enums emerge
        }
    
        static var path: String {
            // bummer...I have the same problem in this method too.
        }
    
        static func pathForEndpoint(endpoint: String) -> String {
            // I guess I could pass the endpoint key again?
        }
    
        static var pathForCreateUser: String {
            // I've got it, let's just create individual properties for each type
            return "/create/user/path"
        }
    
        static var pathForUpdateUser: String {
            // this is going to get really repetitive for each case for each method
            return "/update/user/path"
        }
    
        // This approach gets sloppy pretty quickly
    }
    

    注意:如果你没有很多属性或函数来切换 case,那么 enum 不会比结构体有很多优势。它只是一种具有不同语法糖的替代方法。

    枚举可以最大化状态和代码重用。关联的值还允许您执行一些非常强大的操作,例如对有些相似但具有令人难以置信的不同要求的对象进行分组……例如 NSURLRequest 创建。

    为枚举案例构造参数以提高可读性的正确方法是什么? (不得不把这个混在一起)

    这是一个了不起的问题。您已经列出了两种可能的选择。让我添加第三个可能会更好地满足您的需求。

    case CreateUser(username: String, firstName: String, lastName: String, email: String)
    case ReadUser(username: String)
    case UpdateUser(username: String, firstName: String, lastName: String, email: String)
    case DestroyUser(username: String)
    

    如果您有关联的值,我认为为元组中的所有值添加显式名称会很有帮助。这确实有助于构建上下文。缺点是您必须像这样在 switch 语句中重新声明这些值。

    static var method: String {
        switch self {
        case let CreateUser(username: username, firstName: firstName, lastName: lastName, email: email):
            return "POST"
        default:
            return "GET"
        }
    }
    

    虽然这为您提供了一个很好、一致的上下文,但它变得非常冗长。这些是您目前在 Swift 中的三个选项,哪个是正确的使用取决于您的用例。


    更新

    随着 ?? Alamofire 4.0 ?? 的发布,URLRequestConvertible 现在可以变得更加智能并且还可以投掷。我们在 Alamofire 中添加了对处理无效请求和通过响应处理程序生成合理错误的全面支持。这个新系统在我们的README中有详细记录。

    【讨论】:

    • 谢谢。关于您关于一个路由器与为每个端点构建路由器的答案只有一个问题(例如,来自 Alamofire 页面的 CRUD 示例)。你不认为如果我说 5 个端点,每个端点都有 3-4 个方法,那就是 15-20 个case 语句。这对我来说似乎是一个巨大的方法。我不确定这是否会导致可读的代码......
    • 关于第二个答案(枚举与静态方法) - 我这里的漏洞是在枚举/类中隐藏一个实现。我不需要知道它之外的方法或路径。我想打电话给Router.createUser("test@mail.com", "....") 并有一个块用于解释服务器的结果。所有详细信息(方法、路径、API 根等)都可以为路由器私有 - 这很好。
    • 对于您的最后一条评论,如果您还有一堆函数,我认为您不会想将 20 个不同的端点填充到一个枚举中。你的 switch 语句会很长,它不会很可读。那时绝对是代码气味。对我来说,一旦你的 switch 超过 5 或 6 个 case,你就真的开始失去可读性了。
    • 对于您的最后一条评论@cnoon,(我阅读了以前的 cmets)您是说(使用您的 CRUD 用户路由器示例),如果我有一些属于不同上下文的请求,例如 Request来自 twitter 和 User CRUD 的帖子,它们将是两个独立的路由器?
    • 是的,这是正确的@RenanKosicki。当路由器枚举中有太多案例时,您肯定会遇到不归路。将它们分成逻辑组当然是更理想的设计。
    【解决方案2】:

    这是 Swift 3 中最新的 enum Router,推荐在 Alamofire's Github 上使用。我希望您发现它对如何使用URLRequestConvertible 正确实现路由器很有用。

    import Alamofire
    
    enum Router: URLRequestConvertible
    {
        case createUser(parameters: Parameters)
        case readUser(username: String)
        case updateUser(username: String, parameters: Parameters)
        case destroyUser(username: String)
    
        static let baseURLString = "https://example.com"
    
        var method: HTTPMethod
        {
            switch self {
            case .createUser:
                return .post
            case .readUser:
                return .get
            case .updateUser:
                return .put
            case .destroyUser:
                return .delete
            }
         }
    
        var path: String
        {
            switch self {
            case .createUser:
                return "/users"
            case .readUser(let username):
                return "/users/\(username)"
            case .updateUser(let username, _):
                return "/users/\(username)"
            case .destroyUser(let username):
                return "/users/\(username)"
            }
        }
    
        // MARK: URLRequestConvertible
    
        func asURLRequest() throws -> URLRequest
        {
            let url = try Router.baseURLString.asURL()
    
            var urlRequest = URLRequest(url: url.appendingPathComponent(path))
            urlRequest.httpMethod = method.rawValue
    
            switch self {
            case .createUser(let parameters):
                urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
            case .updateUser(_, let parameters):
                urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
            default:
                break
            }
    
            return urlRequest
        }
    }
    

    【讨论】:

      【解决方案3】:

      您为什么不尝试使用SweetRouter。它将帮助您删除声明路由器时的所有样板文件,它还支持多种环境等内容,您的代码将真正可读。

      这是一个带有甜蜜路由器的路由器示例:

      struct Api: EndpointType {
          enum Environment: EnvironmentType {
              case localhost
              case test
              case production
      
              var value: URL.Environment {
                  switch self {
                  case .localhost: return .localhost(8080)
                  case .test: return .init(IP(126, 251, 20, 32))
                  case .production: return .init(.https, "myproductionserver.com", 3000)
                  }
              }
          }
      
          enum Route: RouteType {
              case auth, me
              case posts(for: Date)
      
              var route: URL.Route {
                  switch self {
                  case .me: return .init(at: "me")
                  case .auth: return .init(at: "auth")
                  case let .posts(for: date):
                      return URL.Route(at: "posts").query(("date", date), ("userId", "someId"))
                  }
              }
          }
      
          static let current: Environment = .localhost
      }
      

      以下是你将如何使用它:

      Alamofire.request(Router<Api>(at: .me))
      Alamofire.request(Router<Api>(.test, at: .auth))
      Alamofire.request(Router<Api>(.production, at: .posts(for: Date())))
      

      【讨论】:

        【解决方案4】:

        我找到了一种使用它的方法,我创建了一个带有路由器的类: 从请求继承类

        文件 request.swift

        class request{
        
            func login(user: String, password: String){
                /*use Router.login(params)*/
            }
            /*...*/
            enum Router: URLRequestConvertible {
                static let baseURLString = "http://example.com"
                static let OAuthToken: String?
        
                case Login([String: AnyObject])
                /*...*/
        
                var method: Alamofire.Method {
                    switch self {
                    case .Login:
                        return .POST
                    /*...*/
                }
        
                var path: String {
                    switch self {
                    case .Login:
                        return "/login"
                    /*...*/
                    }
                }
        
                var URLRequest: NSURLRequest {
                    switch self {
                        case .Login(let parameters):
                            return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
                        /*...*/
                        default:
                            return mutableURLRequest
                    }
                }
            }
        }
        

        文件 requestContacts.swift

        class requestContacts: api{
        
            func getUser(id: String){
                /*use Router.getUser(id)*/
            }
            /*...*/
        
            enum Router: URLRequestConvertible {
        
                case getUser(id: String)
                case setUser([String: AnyObject])
        
                var method: Alamofire.Method {
                    switch self {
                        case .getUser:
                            return .GET
                        case .setUser:
                            return .POST
                        /*...*/
                    }
                }
        
                var path: String {
                    switch self {
                    case .getUser(id: String):
                        return "/user\(id)/"
                    case .setUser(id: String):
                        return "/user/"
                    /*...*/
                    }
                }
                // MARK: URLRequestConvertible
        
                var URLRequest: NSURLRequest {
                    //use same baseURLString seted before
                    let URL = NSURL(string: Router.baseURLString)!
                        let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
                        mutableURLRequest.HTTPMethod = method.rawValue
        
                    if let token = Router.OAuthToken {
                        mutableURLRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
                    }
                    switch self {
                        /*...*/
                        case .setUser(let parameters):
                            return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
                        default: //for GET methods, that doesent need more
                            return mutableURLRequest
                    }
                }
            }
        }
        

        所以子类将从父类获取Router的参数,你甚至可以在任何子类中使用Route.login。 不过,不知道有没有办法得到一个短的URLRequest,所以我不需要一次又一次地设置参数

        【讨论】:

        • 嗨,我正在尝试按照您在回答中所说的那样做,但是当我尝试使用 POST 方法时,我仍然得到 GET 方法响应。例如:当我访问我的 url“/users”而不是使用 POST 方法创建用户时,我得到了所有用户的列表,即 GET 方法响应。知道为什么会这样吗?似乎甚至使用mutableURLRequest.HTTPMethod = method.rawValue 设置方法没有任何变化。
        • 你访问了什么枚举??您必须为 POST 选择枚举,并为该枚举值设置一个 POST,这里的帖子是 Router.setUser(...)
        • 你能在这里检查我的问题吗?我在那里提供所有细节。这是链接:Question
        【解决方案5】:

        采用 URLRequestConvertible 协议的类型可用于构造 URL 请求。

        这是取自www.raywenderlich.com的示例

        public enum ImaggaRouter : URLRequestConvertible{
        
          static let baseURL = "http://api.imagga.com/v1"
          static let authenticationToken = "XAFDSADGDFSG DAFGDSFGL"
        
          case Content, Tags(String), Colors(String)
        
          public var URLRequest: NSMutableURLRequest {
            let result: (path: String, method: Alamofire.Method, parameters: [String: AnyObject]) = {
              switch self {
              case .Content:
                return ("/content", .POST, [String: AnyObject]())
              case .Tags(let contentID):
                let params = [ "content" : contentID ]
                return ("/tagging", .GET, params)
              case .Colors(let contentID):
                let params = [ "content" : contentID, "extract_object_colors" : NSNumber(int: 0) ]
                return ("/colors", .GET, params)
              }
            }()
        
            let URL = NSURL(string: ImaggaRouter.baseURL)!
            let URLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(result.path))
            URLRequest.HTTPMethod = result.method.rawValue
            URLRequest.setValue(ImaggaRouter.authenticationToken, forHTTPHeaderField: "Authorization")
            URLRequest.timeoutInterval = NSTimeInterval(10 * 1000)
        
            let encoding = Alamofire.ParameterEncoding.URL
            return encoding.encode(URLRequest, parameters: result.parameters).0
          }
        }
        

        我们可以像下面这样使用这个 ImageRouter:

        Alamofire.request(ImaggaRouter.Tags(contentID))
              .responseJSON{ response in
        

        【讨论】:

          【解决方案6】:

          而不是大小写UpdateUser(用户名:字符串,名字:字符串,姓氏:字符串,电子邮件:字符串)

          你会的

          struct UserAttributes
          {
              let username: String
               ....
          }
          

          并将该模型对象作为参数而不是一组未命名的不可读字符串提供

          案例UpdateUser(参数:UserAttributes)

          【讨论】:

            猜你喜欢
            • 2015-09-26
            • 1970-01-01
            • 1970-01-01
            • 2023-04-05
            • 1970-01-01
            • 1970-01-01
            • 2017-07-10
            • 2020-11-03
            • 2015-04-24
            相关资源
            最近更新 更多