Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RxSwift学习与第三方网络库(RxAlamofire与Moya)选型———(三) #102

Open
soapgu opened this issue Jan 21, 2022 · 0 comments
Labels

Comments

@soapgu
Copy link
Owner

soapgu commented Jan 21, 2022

  • 铺垫

上一篇博客已经初步试水了RxAlamofire的基本用法和体验。
主要体验上

  • 和RxSwift结合方面没啥黑点,还算合格
  • 对http StateCode验证不足
  • 代码简洁度和抽象潜力,毕竟没有对基础库Alamofire做太多封装,可能有问题

另外有个高Star 组件Moya是不是能不能帮助到我们的项目那,Network abstraction layer written in Swift. 至少介绍还是很诱人的,是骡子是马还要遛一遛才行

  • 总体选型计划及架构

毕竟好坏都是比较出来的,如果让同样的逻辑让两个组件都实现一下,再来体会下好坏。
现在ViewController太大了,啥东西都能装,这不好。东西多了逻辑就看不清

图片

首先抽象出一个Restful 的API接口层。
ViewController耦合接口层
RestfulRxAlamofireService和RestfulMoyaService作为网络实现层各自实现。
最后我们再来比较下

  • 公共部分

  • RestfulAPI
import Foundation
import RxSwift

protocol RestfulAPI {
    func login(deviceCode: String) -> Single<String>
    func getDevice(token: String) -> Single<Device>
}

定义了两个http的api接口
1.登陆获取token
2.根据token获取设备信息

  • ViewController
import Cocoa
import RxSwift

class ViewController: NSViewController {
    var disposeBag = DisposeBag()
    var api: RestfulAPI = RestfulRxAlamofireService()
    @IBOutlet var msgLabel: NSTextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        api.login(deviceCode: "D9C3F5FF-664B-4DCA-968B-37228FA1C460")
            .flatMap{
                [unowned self] token in
                self.api.getDevice(token: token)
            }
            .subscribe(onSuccess:{
                [weak msgLabel] device in
                msgLabel?.stringValue = "\(device.name) in \(device.room.name)"
            })
            .disposed(by: disposeBag)
        // Do any additional setup after loading the view.
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }
}

调用接口并显示设备信息在msgLabel上显示
这里以后要想办法IoC,这样就可以实现解耦

  • RxAlamofire实现部分

import Foundation
import RxAlamofire
import RxSwift
import Alamofire

class RestfulRxAlamofireService : RestfulAPI {
    
    let baseUrl = "http://XXX.XXX.XXX.XX/api"

    func login(deviceCode: String) -> Single<String> {
    
        let requestObject = ["device_code": deviceCode ,
                             "parameter1": "AAAAAAA",
                             "parameter2": "BBBBBBB"]
        
        let auth: Observable<[String: String]> = RxAlamofire.decodable(.post, "\(baseUrl)/XX/XXXXX/authorization",parameters: requestObject,encoding: JSONEncoding.default)//.debug()
        return auth.map{
            item in
            if let token = item["token"]{
                return token
            }
            throw MyError.errorjson
        }
        .asSingle()
    }

    func getDevice(token: String) -> Single<Device> {
        return RxAlamofire.decodable(.get, "\(baseUrl)/device", headers: ["Authorization":"Bearer \(token)"]).asSingle()
    }
    
}

总体上上一篇没太大区别,讲下重点

  • 增加了post body的处理,需要在parameters和encoding里面转入。
    parameters:body可序列化对象
    encoding:这里传的JSONEncoding.default

  • 增加了headers对象的处理,这里加了Authorization头

  • Single比Observable更准确,但是RxAlamofire接口都是Observable,转换也比较轻松,调用asSingle()

  • 吐槽下对泛型方法,Swfit有点点不一样,调用端并不能直接使用<>显示声明
    比如这个方法

public func decodable<T: Decodable>(_ method: HTTPMethod,
                                    _ url: URLConvertible,
                                    parameters: Parameters? = nil,
                                    encoding: ParameterEncoding = URLEncoding.default,
                                    headers: HTTPHeaders? = nil,
                                    interceptor: RequestInterceptor? = nil)
  -> Observable<T>

我调用RxAlamofire.decodable<[String: String]>(x,x,x,x)是不行的
一定要let auth: Observable<[String: String]> = RxAlamofire.decodable(x,x,x,x)
直接破坏了我链式语法,一句话硬生生被拆分了两句代码
相关链接 Cannot explicitly specialize a generic function

  • Moya部分实现

好了“新人胜旧人”
图片

Moya登场
Moya相对RxAlamofire做了很多http层面的封装好抽象,底层的实现还是Alamofire干的,但是为我们的业务层做了一定层面的抽象。

  1. 定义enum来表达api接口和参数声明
    对于Moya来说利用enum的语法来来生命接口和参数,其实上手的时候感觉有一点小奇葩,至少是没遇到过的
enum MoyaInnerService {
    case login(deviceCode: String)
    case getDevice(token: String)
}

这里Swift很特殊,枚举是可以带参数的。这当然也是Moya实现的语言支持基础。
现在看看基本上和一般的函数声明没啥差别。
唯一缺失的是返回类型声明

  1. 继承TargetType
    光声明肯定是不够的,还需要干活。
    Moya的框架是Target声明的枚举必须继承TargetType来实现相关协议约定
  • baseURL
    就是提取的url的root,因为基本上api的url都是同一个baseURL,这里就不需要在声明接口的url使用全路径了。减少代码同时也减少出错概率。

  • path
    api的路由url的相对path和baseURL merge

  • method
    api method声明

  • task
    api的参数发送方式声明
    有一些关键的方法
    .requestPlain 没有任何东西发送
    .requestJSONEncodable(_:) 发送可序列化json的对象
    还有download,upload等一批api覆盖你几乎可能用到的需求

  • headers
    api的header参数声明,定义类似Content-type还比较合适。
    但是定义变量话参数,如accessToken显然不太合适,需要其他方案我下面会提到

  1. AccessTokenAuthorizable接口
    上面我们提到了验证的问题,这里Moya贴心的给我们设计了AccessTokenAuthorizable接口
    只要继承了就需要实现authorizationType
    这里声明了各个路由的验证方式
    默认返回是nil 无验证
    也有basic和bearer

好了AccessTokenAuthorizable接口只做这些,至于提供token的操作不是在Target里面做。我后面说

代码实现如下

import Moya

enum MoyaInnerService {
    case login(deviceCode: String)
    case getDevice(token: String)
}

extension MoyaInnerService : TargetType,AccessTokenAuthorizable {
    var authorizationType: AuthorizationType? {
        switch self {
        case .login(_):
            return nil
        case .getDevice(_):
            return .bearer
        }
    }
    
    var baseURL: URL {
        URL(string: "http://XXX.XXX.X.XXX/api")!
    }

    var path: String {
        switch self {
        case .login:
            return "authorization"
        case .getDevice:
            return "device"
        }
    }

    var method: Method {
        switch self {
        case .login:
            return .post
        case .getDevice:
            return .get
        }
    }

    var task: Task {
        switch self {
        case .login(let deviceCode):
            let requestObject = ["device_code": deviceCode ,
                                 "id": "XXXX",
                                 "secret": "XXXXXXX"]
            return .requestJSONEncodable(requestObject)
        case .getDevice:
            return .requestPlain
        }
    }

    var headers: [String : String]? {
        return ["Content-type": "application/json"]
    }
}

主要感受

  • 基本实现了的网络层封装
  • 通过4块代码 block逻辑来实现,相比其他api,感觉接口逻辑被打混在一起了而且如果api多的话会出现几个case一起合并返回的情况,牺牲一点独立性,这是我主要不喜欢Moya的点

相关参考资料

  • Targets

  • 实现RestfulAPI接口并使用Providers

接下来就是调用部分了

  1. 首先需要使用到MoyaProvider来关联api请求
    let provider = MoyaProvider<MyService>()

  2. 通过指定enum调用相关api

provider.request(.zen) { result in
    // `result` is either .success(response) or .failure(error)
}
  1. response及RxSwift适配
    如果需要和RxSwift集成,则Podfile需要下面的声明
pod 'Moya/RxSwift'

调用provider.rx.request()以后直接就是Rx对象了
另外Moya还提供了一堆很实用的扩展方法比较方便

  • filter 系列方法
    帮助我们验证stateCode

  • map 系列方法
    帮助我们做一系列返回的转换

相关参考资料

  1. 插件声明
    前面的AccessTokenAuthorizable只是做了一半,接口层的处理。
    调用层还需要声明AccessTokenPlugin,定义获取token的闭包

最后实现代码

import Foundation
import RxSwift
import Moya

var accessToken: String = ""

class RestfulMoyaService : RestfulAPI {
    let provider: MoyaProvider<MoyaInnerService>
    
    init(){
        let authPlugin = AccessTokenPlugin {  _ in accessToken }
        provider = MoyaProvider<MoyaInnerService>(plugins: [authPlugin])
    }
    
    func login(deviceCode: String) -> Single<String> {
         provider.rx.request(.login(deviceCode: deviceCode))
            .filterSuccessfulStatusCodes()
            .map(Dictionary<String,String>.self)
            .map{
                 $0["access_token"]!
            }
    }
    
    
    func getDevice(token: String) -> Single<Device> {
        accessToken = token
        return provider.rx.request(.getDevice(token: token))
            .filterSuccessfulStatusCodes()
            .map(Device.self)
    }
    
}

说明token的方式有点怪,为了保持接口统一才这么写。接口的返回参数必须自己来声明的,没有办法在api声明的时候指定这一点Moya弱了一定,不过总体代码还是流畅的
图片

@soapgu soapgu added Demo Demo ReactiveX ReactiveX Swift labels Jan 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant