正确使用 Alamofire 的 URLRequestConvertible

Proper usage of the Alamofire's URLRequestConvertible

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

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

  2. 第二个问题很可能是由于缺乏 Swift 语言经验造成的。我不明白为什么 enum 用于构建路由器?为什么我们不对静态方法使用 class? 这是一个例子(来自 Alamofire 的自述文件)

    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. 参数传递有2种方式:

    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 在示例中使用第一个。但这将导致 "hardcoding" 参数名称在路由器之外(例如在 UIViewControllers 中)。 参数名称中的错字可能会导致错误。
    其他人正在使用第二个选项,但在那种情况下,每个参数代表什么并不明显。
    正确的方法是什么?

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

What is the proper usage of URLRequestConvertible in real world API?

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

Should I create one Router per endpoint?

绝对不是。这将破坏使用 Enum 的全部目的。 Swift 枚举对象非常强大,允许您共享大量公共状态,并打开实际不同的部分。能够用像下面这样简单的东西创建一个 NSURLRequest 真的很强大!

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

I can't figure out why enum is used for building router? Why we don't use class with static methods?

正在使用枚举,因为它是在公共接口下表达多个相关对象的更简洁的方式。所有方法在所有案例之间共享。如果您使用静态方法,则必须为每种方法的每种情况都有一个静态方法。或者您必须在对象内使用 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)"
        }
    }
}

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

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
}

NOTE: If you don't have many properties or functions that switch on the cases, then an enum doesn't present many advantages over a struct. It is simply an alternative approach with different syntactic sugar.

枚举可以最大化状态和代码重用。关联的值还允许您做一些非常强大的事情,例如对有些相似但具有极其不同要求的对象进行分组......例如 NSURLRequest 创建。

What is the right way to construct parameters for enum cases to improve readability? (had to mash this one together)

这是一个了不起的问题。您已经提出了两种可能的选择。让我添加第三个可能更适合您的需求。

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.

中详细记录了这个新系统

我找到了一种使用它的方法,我创建了一个 Class 并在其中放置了 Router: 从请求

继承 classes

文件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
            }
        }
    }
}

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

采用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
  }
}

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

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

这是 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
    }
}

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

下面是带有 sweet router 的路由器示例:

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())))

而不是 case UpdateUser(username: String, firstName: String, lastName: String, email: String)

你会

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

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

case UpdateUser(参数:UserAttributes)