多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Moya 进阶:使用 `MultiTarget` 让单个 `MoyaProvider` 承载多个 API Target

Moya 进阶:使用 `MultiTarget` 让单个 `MoyaProvider` 承载多个 API Target 开发工具【免费下载链接】MoyaNetwork abstraction layer written in Swift.项目地址https://gitcode.com/gh_mirrors/mo/Moya点击查看免费下载导读当项目的接口数量增长到一定程度时一个TargetType枚举里会塞满成百上千个 case对应的switch分支也越来越难以维护。本指南以 Moya 内置的MultiTarget类型为核心讲解如何用同一个MoyaProvider优雅地承载多个相互独立的 API 目标并进一步介绍在引入associatedtype扩展TargetType后如何通过MultiMoyaProvider实现一次请求直接返回反序列化模型的高级用法。读完本文你将掌握多目标 Provider 的完整实战方案并理解其底层转发机制。问题背景一个 Provider 只认一种 TargetMoya 的请求入口是MoyaProviderTarget其中泛型Target约束为TargetType见 MoyaProvider.swift。这意味着当你为GitHub定义了一个MoyaProviderGitHub之后它就只能处理GitHub枚举里的 case。当接口数量庞大时常见痛点有三单个 Target 枚举过长所有 case 堆在一起path、method、task、sampleData里的switch动辄几百行拆分成多个 Target 就得拆分多个 Provider虽然逻辑上清晰了但应用层需要同时维护MoyaProviderGitHub、MoyaProviderGiphy、MoyaProviderGitHubUserContent等多个实例插件与闭包配置重复日志插件、网络活动指示器、endpointClosure、stubClosure等配置本应是全局统一的拆分后每建一个 Provider 都要重复配置一遍维护成本很高。MultiTarget正是为化解这一矛盾而生的内置类型。基本用法三步完成迁移原文档给出的迁移过程非常简洁只需三步。第一步把 Provider 的泛型参数改为MultiTargetlet provider MoyaProviderMultiTarget()第二步把原来的请求调用provider.request(.zen) { result in // do something with result }第三步改为用MultiTarget(...)包装目标provider.request(MultiTarget(GitHub.zen)) { result in // do something with result }就这么简单——原有的result回调签名、插件逻辑、stub行为全部保持不变改动只发生在调用处。更进一步你甚至可以在创建 Provider 时配置插件让日志、网络活动指示等能力对所有目标统一生效。仓库自带的 Multi-Target 示例工程正是这么做的见 Examples/Multi-Target/ViewController.swiftlet provider MoyaProviderMultiTarget(plugins: [NetworkLoggerPlugin(configuration: .init(logOptions: .verbose))])这个示例工程用同一个 Provider 同时发起了三种完全不同类型的请求源码见 ViewController.swift// GitHub 仓库列表GET 查询参数 provider.request(MultiTarget(GitHub.userRepositories(username))) { result in ... } // GitHub 禅语GET 纯文本 provider.request(MultiTarget(GitHub.zen)) { result in ... } // Giphy 上传POST multipart 表单 provider.request(MultiTarget(Giphy.upload(gif: Giphy.animatedBirdData)), callbackQueue: DispatchQueue.main, progress: progressClosure, completion: progressCompletionClosure) // GitHub raw 内容下载下载任务 provider.request(MultiTarget(GitHubUserContent.downloadMoyaWebContent(logo_github.png)), callbackQueue: DispatchQueue.main, progress: progressClosure, completion: progressCompletionClosure)GitHub、Giphy、GitHubUserContent是三个完全独立的TargetType分别定义在 GitHubAPI.swift、GiphyAPI.swift 和 GitHubUserContentAPI.swift它们各自的baseURL都不一样api.github.com、upload.giphy.com、raw.githubusercontent.com但通过MultiTarget全部塞进了同一个 Provider还顺带复用了统一的日志插件。这就是多目标单 Provider最直观的收益。源码剖析MultiTarget的透明转发机制MultiTarget的整个实现只有 47 行见 Sources/Moya/MultiTarget.swift核心思想是组合composition而非继承public enum MultiTarget: TargetType { /// The embedded TargetType. case target(TargetType) /// Initializes a MultiTarget. public init(_ target: TargetType) { self MultiTarget.target(target) } /// The embedded targets base URL. public var path: String { target.path } /// The baseURL of the embedded target. public var baseURL: URL { target.baseURL } /// The HTTP method of the embedded target. public var method: Moya.Method { target.method } /// The sampleData of the embedded target. public var sampleData: Data { target.sampleData } /// The Task of the embedded target. public var task: Task { target.task } /// The ValidationType of the embedded target. public var validationType: ValidationType { target.validationType } /// The headers of the embedded target. public var headers: [String: String]? { target.headers } /// The embedded TargetType. public var target: TargetType { switch self { case .target(let target): return target } } }理解这份源码能帮你解答几个关键疑问它自身是TargetType吗是的MultiTarget声明为enum MultiTarget: TargetType因此MoyaProviderMultiTarget完全合法。它是如何伪装成任意目标的它把TargetType协议要求的全部 7 个属性path、baseURL、method、sampleData、task、validationType、headers全部直接转发给内部持有的target。也就是说请求真正发出时使用的 URL、方法、任务等全部来自被包装的真实目标MultiTarget本身只是一个透明的分发壳。init(_ target:)与case .target是什么关系init只是语法糖内部执行self MultiTarget.target(target)。你可以通过MultiTarget(GitHub.zen)或MultiTarget.target(GitHub.zen)两种方式构造效果等价。sampleData也转发了stub 测试怎么办转发意味着每个被包装目标的sampleData依然各自生效所以单元测试和 UI 测试里按目标分别 stub 的行为完全不受影响。另外值得注意MultiTarget还扩展实现了AccessTokenAuthorizable见 MultiTarget.swiftextension MultiTarget: AccessTokenAuthorizable { public var authorizationType: AuthorizationType? { guard let authorizableTarget target as? AccessTokenAuthorizable else { return nil } return authorizableTarget.authorizationType } }这样在使用 AccessTokenPlugin 做鉴权时只要被包装的目标遵循AccessTokenAuthorizableMultiTarget就能把它的authorizationType透传给插件多目标场景下的 Token 注入同样开箱即用。测试验证所有属性按预期透传仓库的单元测试对MultiTarget的转发行为做了全面覆盖见 Tests/MoyaTests/MultiTargetSpec.swift。测试构造了一个同时遵循TargetType与AccessTokenAuthorizable的结构体StructAPIstruct StructAPI: TargetType, AccessTokenAuthorizable { let baseURL URL(string: http://example.com)! let path /endpoint let method Moya.Method.get let task Task.requestParameters(parameters: [key: value], encoding: JSONEncoding.default) let sampleData sample data.data(using: .utf8)! let validationType: ValidationType .successCodes let headers: [String: String]? [headerKey: headerValue] let authorizationType: AuthorizationType? .basic }随后逐项断言MultiTarget.target(StructAPI())的baseURL、path、method、task的参数与编码方式、sampleData、validationType、headers乃至authorizationType均与被包装目标完全一致。测试结果印证了源码中全属性转发的设计——MultiTarget不会对任何请求细节做二次加工。进阶associatedtype场景下的MultiMoyaProvider为什么会失败Moya 允许你扩展TargetType通过添加associatedtype让请求返回经过反序列化的具体模型。原文档给出的示例是把request的结果从MoyaResponse提升为随请求变化的模型类型protocol DecodableTargetType: Moya.TargetType { associatedType ResultType: SomeJSONDecodableProtocolConformance } enum UserApi: DecodableTargetType { case get(id: Int) case update(id: Int, name: String) ... var baseURL: URL { ... } var path: String { switch self ... } var method: Moya.Method { ... } typealias ResultType UserModel }问题在于associatedtype的存在使DecodableTargetType无法再作为具体的泛型参数使用。而MultiTarget内部持有的是TargetType不带关联类型MoyaProviderMultiTarget在编译期也就无法知道你最终想要的ResultType是什么。因此MultiTarget与DecodableTargetType无法直接组合。解决方案MultiMoyaProvider原文档给出的解法是定义一个不要求泛型参数的子类把所有初始化参数原样透传给父类final class MultiMoyaProvider: MoyaProviderMultiTarget { typealias Target MultiTarget override init(endpointClosure: escaping MoyaProviderTarget.EndpointClosure, requestClosure: escaping MoyaProviderTarget.RequestClosure, stubClosure: escaping MoyaProviderTarget.StubClosure, callbackQueue: DispatchQueue?, manager: Manager, plugins: [PluginType], trackInflights: Bool) { super.init(endpointClosure: endpointClosure, requestClosure: requestClosure, stubClosure: stubClosure, manager: manager, plugins: plugins, trackInflights: trackInflights) } }注意上例中的manager: Manager对应的是以 Alamofire 4 为依赖的 Moya 旧版本 API。当前仓库的MoyaProvider.init已将其替换为session: Session MoyaProviderTarget.defaultAlamofireSession()并支持全部参数默认值见 MoyaProvider.swift。若你的项目基于当前版本可按同样思路把manager换成session或直接不覆写 init、利用默认参数。这样得到的MultiMoyaProvider可以发起任意遵循TargetType的请求为在请求层封装associatedtype能力提供了空间。封装requestDecoded回调直接拿到模型借助MultiMoyaProvider可以给请求包装一个返回ResultType的方法。原文档的示例实现如下extension MultiMoyaProvider { func requestDecodedT: DecodableTargetType(_ target: T, completion: escaping (_ result: Result[T.ResultType], Moya.Error) - ()) - Cancellable { request(target) { result in switch result { case .success(let response): if let parsed T.ResultType.parse(try! response.mapJSON()) { completion(.success(parsed)) } else { completion(.failure(.jsonMapping(response))) } case .failure(let error): completion(.failure(error)) } } } }这个方法的关键点泛型T约束为DecodableTargetType因此T.ResultType在编译期就是确定的模型类型request(target)中的target直接以T传入——正因为MultiMoyaProvider不绑定单一目标这里才能传入任意遵循协议的实例错误路径被完整保留网络层失败直接透传error数据解析失败mapJSON抛错或parse返回 nil则映射为.jsonMapping(response)。调用的体验是回调中的类型由传入的 target 隐式推断let provider MultiMoyaProvider() provider.requestDecoded(UserApi.get(id: 1)) { result in switch result { case .success(let user): // type of user is implicitly UserModel. Using any other type results // in compile error print(user.name) } }一个 Provider 处理多种模型associatedtype的代价是每种ResultType都要对应一个独立的DecodableTargetType目标。但多个目标可以共享同一个MultiMoyaProvider实例。例如再定义一个返回SessionModel的目标struct SessionApi: DecodableTargetType { typealias ResultType SessionModel }同一个provider直接复用provider.requestDecoded(SessionApi.get) { result in switch result { case .success(let session): // type of session is implicitly SessionModel here } }在同一处代码里user是UserModel、session是SessionModel——类型完全由你传入的目标决定任何类型不匹配都会在编译期报错而不是运行时崩溃。这正是静态校验参数这一 Moya 设计哲学在多目标场景下的延续。两种方案如何选择场景推荐方案理由接口数量多、想拆分枚举但回调仍以MoyaResponse为主MoyaProviderMultiTarget内置、零额外代码插件与闭包统一配置通过associatedtype扩展了TargetType要求请求直接返回模型MultiMoyaProvider不受泛型约束限制可为不同ResultType的目标提供统一请求包装多目标 需要 Token 鉴权MoyaProviderMultiTarget或MultiMoyaProviderMultiTarget已透传AccessTokenAuthorizable配合 AccessTokenPlugin 即可小结MultiTarget是 Moya 为解决大枚举 多 Provider维护难题给出的内置答案一个 47 行的枚举靠全属性转发让单个MoyaProvider透明地承载任意数量的TargetType插件、stub、鉴权能力全部复用。而当你在TargetType上引入associatedtype追求请求即模型时MultiMoyaProvider则补上了泛型约束的缺口让同一 Provider 服务于多种返回类型的目标。若想直接运行体验仓库的 Multi-Target 示例工程Examples/Multi-Target/ViewController.swift一次请求就同时覆盖了 GET 查询、POST 上传、文件下载三种形态是理解本主题的最佳配套代码。赞分享开发工具【免费下载链接】MoyaNetwork abstraction layer written in Swift.项目地址https://gitcode.com/gh_mirrors/mo/Moya点击查看免费下载相关推荐gRPC-Go Multiplex 复用实战单个 ClientConn 共享多 Stub、单个 Server 承载多服务gRPC Go Multiplex 复用实战单个 ClientConn 共享多 Stub、单个 Server 承载多服务 导读 在 gRPC Go 项目中一后端RPC框架clap Multicall 实战像 BusyBox 一样用一个二进制承载多个命令Appletclap Multicall 实战像 BusyBox 一样用一个二进制承载多个命令Applet 导读 本篇文章以 clap 官方示例 examples/mCLI开发工具Modular Monolith 架构实战MyMeetings 项目如何用单个进程承载四个 DDD 业务模块Modular Monolith 架构实战MyMeetings 项目如何用单个进程承载四个 DDD 业务模块 导读 本文以 docs/architecture后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表