回顾iOS - 使用Moya封装网络框架

来历

在2020年初,尝试Swift开发,于是有了项目中第一个Swift版本的网络框架。

它是采用Moya + RxSwift + HandyJSON的设计,这是最初版本的使用例子:

swift 复制代码
func getDrug() -> Observable<Result<KKMPPlanSearchDrugModel?, LCError>> {
    return Observable.create { [weak self] ob in
        guard let self = self else { return Disposables.create() }
        try? self.request.rx.request(.init(action: .drug))
        .asObservable()
        .mapJSON()
        .verify()
        .mapToObject(KKMPPlanSearchDrugModel.self)
        .subscribe(onNext: { obj in
            ob.onNext(.success(obj))
        }, onError: { error in
            guard let error = error as? LCError else { return }
            ob.onNext(.failure(error))
        }, onCompleted: nil, onDisposed:nil).disposed(by: self.dispose)
    return Disposables.create()
}

在之前OC时代,我一直在使用ReactCocoa,本着对响应式开发的迷恋加上我的蜜汁自信,我尝试去封装出了自己认为还不错,可以满足需求,但四不像的网络框架。

它的使用有许多弊端,我接下来会列出来:

  • 方法使用繁琐
  • 不支持网关处理单端登录(随着版本迭代暴露出的问题)
  • 全局事件抛出机制繁琐
  • 对RxSwift依赖严重

随着Flutter的流行,我们新启动的项目全部All in Flutter。老项目处于一个日常维护迭代,发版频率大概一年一次。

随着这次Swift5.9的更新,HandyJSON毫无征兆的崩溃了。方案一是采用Codable替换HandyJSON框架,但真正在实际执行中,发现自己4年前作为一个Swift小白写的那些垃圾代码,改动起来是真要命。于是采取了方案二,通过修改cocoapods配置,暂时解决了这一崩溃问题。

ruby 复制代码
if target.name == 'HandyJSON'
    target.build_configurations.each do |config|
    config.build_settings['SWIFT_COMPILATION_MODE'] = 'incremental'
    end
end

现在回头看之前自己写的代码,就像接手别人的代码一样,那种感觉,就像一坨翔。虽然三年多没写Swift,但还是想尝试把网络框架替换掉。

这就是LZNetwork的由来,虽然它目前看来很简单,但使用它可以进行更深层次的二次开发,它对未来业务的扩展也更加灵活,使用方式也更加简单。

Cocoapods

ruby 复制代码
pod 'LZNetwork'

Package

swift 复制代码
dependencies: [
    .package(url: "https://github.com/coder-cjl/LZNetwork.git", .upToNextMajor(from: "0.0.1"))
]

优势

  • 通过对Moya的二次封装,调用更加方便。
  • MoyaAlamofireError整合,错误处理标准化
  • 保持了PluginType的支持
  • 规定了Env网络配置管理,配置网络化,后台化,定制化。
  • await async支持

Example

网络配置

json

首先要先从自己的服务端加载网络配置,网络配置接口参数返回规则如下

json 复制代码
{
    "env":{dev}{test}{uat}{prod},
    "dev": {
        "api_url": "xxx",
        "xxx": "xxx",
    },
    "test": {
        "api_url": "xxx",
        "xxx": "xxx",
    },
    "uat": {
        "api_url": "xxx",
        "xxx": "xxx",
    },
    "prod": {
        "api_url": "xxx",
        "xxx": "xxx",
    },
}

字段env为当前选择的网络环境,默认为四套环境,其中,dev test uat prod为默认字段,不可以随意更改。

但是env变量中的json字段,可以根据自身的业务需求合理配置。

加载配置

在程序启动的合适时机加载网络配置,支持缓存,默认不开启。

swift 复制代码
LZEnv.default.loadConfig("url", loadCache: true)

当开启缓存配置后,默认会去加载缓存,不会进行网络请求。只有当缓存为nil时,才会主动去进行网络请求。

swift 复制代码
public func loadConfig( _ url: String, loadCache: Bool = false) -> Void {
    /// 如果有缓存策略
    if loadCache,
        let cacheString = UserDefaults.standard.string(forKey: url),
        let cachedJSON = cacheString.lz.asJSONObject() {
        setGlobalURLConfig(value: cachedJSON)
        return
    }

    let semaphore = DispatchSemaphore(value: 0)

    AF.request(url).response(queue: .global(qos: .userInteractive), completionHandler: { response in
        if let data = response.data, let value = try? data.lz.asJSONObject() as? [String: Any] {
            LZEnv.default.setGlobalURLConfig(value: value)
            if (loadCache) {
                if let valueString = value.lz.asString() {
                    UserDefaults.standard.set(valueString, forKey: url)
                }
            }
        }
        semaphore.signal()
    }).resume()

    semaphore.wait()
}

当开启缓存配置后,想定期更新网络配置,可以试着在url后拼接参数

JSON 复制代码
url?r=xxxx

其中r=xxx是你的缓存策略,可以以天为单位,也可以以周或者月为单位。

切换网络环境

swift 复制代码
/// 切换当前环境
public func changeCurrentEnv(env: LZEnvEnum) {
    if let value = globalURLConfig?[env.rawValue] as? [String: Any] {
        currentURLConfig = value
    }
}

为了应对开发和测试环节不同环境的切换,提供了切换环境的func,方便开发与调试。

数据转模型

采用的是Codable系统方案,随着Swift的更新迭代以及我的使用体验,Codable已经可以胜任日常的开发工作。

网络请求

配置 LZTargetType

swift 复制代码
public protocol LZTargetType: TargetType { }

extension LZTargetType {
    var baseURL: URL {
        if let urlStr = LZEnv.default.currentURLConfig?["api_url"] as? String,
           let url = URL(string: urlStr) {
            return url
        }
        return URL.init(string: "")!
    }

    var task: Moya.Task {
        .requestPlain
    }
    
    var headers: [String : String]? {
        var h = [String: String]()
        h["version"] = "0.0.1"
        h["platform"] = "ios"
        h["Content-Type"] = "application/json"
//        if let accessToken = UserDefaults.standard.string(forKey: "accessToken") {
//            h["Authorization"] = "Bearer \(accessToken)"
//        }
        return h
    }
}

可以在extension中配置项目的全局参数,如accessToken等字段,也可以在PluginType中设置。

Api

swift 复制代码
enum TestTargetApi {
    case login(String, String)
    case sms(String)
    case list
}

extension TestTargetApi: LZTargetType {

    var path: String {
        switch self {
        case .login(_, _):
            return "v1/login"
        case .sms(_):
            return "v1/sms"
        case .list:
            return "v1/list"
        }
    }

    var method: Moya.Method {
        switch self {
        case .login, .sms:
            return .post
        case .list:
            return .get
        }
    }

    var task: Task {
        switch self {
        case .login(let phone, let password):
            let params = ["phoneNo": phone, "password": password]
            return .requestParameters(parameters: params, encoding: JSONEncoding.default)
        case .sms(let phone):
            let params = ["phoneNo": phone]
            return .requestParameters(parameters: params, encoding: JSONEncoding.default)
        case .list:
            return .requestPlain

        }

    }

}

支持enumstructclass三种类型使用,只需要遵守LZTargetType协议

支持 await async

swift 复制代码
_Concurrency.Task {
    let result = await LZRequest<User>().request(TestTargetApi.login("123", "123"))
    switch result {
    case .success(let t):
        print(t?.name ?? "")
    case .failure(let error):
        print(error.errorDescription ?? "")
    }
}

支持 block

swift 复制代码
LZRequest().request(target: TestTargetApi.sms("123"), type: User.self) { result in
    switch result {
    case .success(let user):
        print(user?.name ?? "")
    case .failure(let error):
        print(error.errorDescription ?? "")
    }
}

List Model

swift 复制代码
_Concurrency.Task {
    let result = await LZRequest<[List]>().request(TestTargetApi.list)
    switch result {
    case .success(let list):
        print(list?.count ?? "0")
    case .failure(let error):
        print(error.errorDescription ?? "")
    }
}

支持 get 缓存

swift 复制代码
_Concurrency.Task {
    let result = await LZRequest<[List]>(plugins:[LZGetwayPlugin(), LZCachePlugin()]).request(TestTargetApi.list)
    switch result {
    case .success(let list):
        print(list?.count ?? "0")
    case .failure(let error):
        print(error.errorDescription ?? "")
    }
}

更多

更多使用场景,请查看Example

如果您有更好的建议或者方案,欢迎issues反馈🤝。

Author

coder-cjl, cjlsire@126.com

License

LZNetwork is available under the MIT license. See the LICENSE file for more info.

相关推荐
恋猫de小郭6 小时前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
网安墨雨10 小时前
iOS应用网络安全之HTTPS
web安全·ios·https
福大大架构师每日一题11 小时前
37.1 prometheus管理接口源码讲解
ios·iphone·prometheus
BangRaJun1 天前
LNCollectionView-替换幂率流体
算法·ios·设计
刘小哈哈哈1 天前
iOS 多个输入框弹出键盘处理
macos·ios·cocoa
靴子学长1 天前
iOS + watchOS Tourism App(含源码可简单复现)
mysql·ios·swiftui
一如初夏丿2 天前
xcode15 报错 does not contain ‘libarclite‘
ios·xcode
杨武博2 天前
ios 混合开发应用白屏问题
ios
BangRaJun2 天前
LNCollectionView
android·ios·objective-c
二流小码农3 天前
鸿蒙元服务项目实战:终结篇之备忘录搜索功能实现
android·ios·harmonyos