一、背景与目标
1.1 为什么要组件化
- 业务规模:模块多、页面多、链路长,单仓巨石难以并行开发与发布。
- 团队规模:多人/多团队需要边界清晰的职责与依赖约束。
- 演进需求:独立编译、按需集成、灰度、动态调度、安全隔离。
1.2 组件化要达成的目标
| 维度 | 目标 |
|---|---|
| 依赖 | 业务模块之间不直接依赖彼此实现,仅通过稳定、窄化的"对外能力"交互 |
| 调用 | 支持页面级与非页面能力(服务、任务、弹窗等)的统一调度 |
| 参数 | 支持复杂结构与非常规类型(如图片、回调、原生对象) |
| 入口 | 远程唤起(URL/Universal Link)与进程内组件调用在架构上显式分离 |
| 演进 | Target-Action 层可承载动态调度、安全策略、监控而不污染业务内核 |
二、设计原则
2.1 "本地调用"为"远程调用"提供能力,而不是反过来
- 远程场景(外 App、推送、短信链、Universal Link):只能通过 URL(及有限 payload)表达意图,无法传递任意原生对象;本质是本地 perform(Target, Action, params) 能力的子集。
- 正确关系:openURL → 解析路由 → 映射到 Target-Action → 与本地调用走同一套执行内核。
- 错误关系:把进程内所有跨模块通信都做成 "类 openURL",会导致非常规参数无法表达或被迫用丑陋的 encode/旁路 API,维护成本陡增。
2.2 显式拆分两类入口
- 远程入口:openURL / App Links → 解析 → 校验 → 映射 → 执行。
- 本地入口:mediator.perform(...)(或等价封装),不经 URL 表达业务参数。
原因简述:
- 远程多一步 URL 解析与安全上下文,错误处理(如 404、降级页)常与产品策略绑定。
- 本地无响应时,可能是开发期版本不齐,也可能是线上数据版本不一致(如收藏数据结构升级),产品期望的降级/补救与远程 404 不一定相同。
- 若共用一个入口,后期需求会倒逼重构。
2.3 组件对外接口落在"调度层",而不是直接暴露 Business
- 跨模块只暴露 Target + Action(或等价抽象),业务实体仍在模块内部。
- 避免「为了被调用」把业务类改成符合某公共 Protocol、或把调用方拖进多步拼装对象的反模式;调用方应发意图,响应方在自己的上下文内完成逻辑(黑盒、与服务端「请求-响应」模型一致)。
2.4 参数"去 Model"与调用体验可兼得
- Mediator 与 params:跨边界传递宜用 字典 / 通用序列化结构(iOS 为 NSDictionary),避免调用方、响应方、Mediator 三角依赖具体 Model 类型。
- Hardcode 的 key 集中在 Mediator 的薄封装层(用 Category 按 Target 分文件):
- 调用方看到的是 语义化方法,不必记 URL、不必猜参数名;
- 可做 参数校验、默认值、转发;
- key 的作用域被限制在"声明式 API 层",可接受。
三、 总体架构
3.1 逻辑分层(推荐)

3.2 模块内结构(每个业务组件)
Business:领域逻辑、UI、数据;尽量不感知"被谁调"。
Target_xxx:仅负责"接 Action → 调 Business",可视为适配器/防腐层。
对外:不暴露具体类给其它模块,仅通过 Mediator 约定好的 Target/Action 名称与参数契约。
四、调用契约:复杂参数与非常规参数
4.1 术语对齐
普通类型:可被 JSON 等表达的标量、字符串、数组、字典等(及团队约定的"可序列化"扩展)。
复杂参数:仅由普通类型构成的多层结构。
非常规类型:无法直接 JSON 化的类型(UIImage、Bitmap、FileDescriptor、闭包/block、大对象句柄等)。
边界:结构中任一处含非常规类型,则整体按非常规调用处理。
4.2 传递策略
远程:仅能通过 URL + 编码后的字符串/少量标准字段;到达 App 后反序列化为字典,再走 Target-Action。
本地:params 中可直接放非常规对象;必要时对 block/回调用 wrapper(需注意循环引用与线程)。
五、安全设计
5.1 远程与本地能力分级
对仅允许本地的敏感 Action(如资产页、支付确认),在协议层标记(native 前缀),URL 解析阶段若命中则拒绝或降级。
远程入口必须:签名校验 / 白名单 host-path / 版本 gate / 登录态 等按产品要求叠加。
5.2 最小架构支撑
只要远程与本地入口分离,安全策略、埋点、熔断都可以在各自管道上独立演进,而不会把 URL 语义强加到所有本地调用上。
六、动态调度(运营、活动、AB)
6.1 调度切点选型
| 切点 | 优点 | 缺点 |
|---|---|---|
| URL 解析 | 实现简单 | 通常管不到纯本地跳转 |
| 实例化每个 Target | 全覆盖 | 多数静态页也被审查,浪费 |
| Category 方法 | 只影响本地 | 无法统一覆盖远程+本地 |
| 具体 Action 内(推荐) | 只审查「可能动态」的入口,命中率高 | 需在规范里标明哪些 Action 可配置 |
若产品要求任意页面都可动态替换,再考虑在Target 实例化或统一拦截层做全量审查。
6.2 审查数据来源
1.启动或定时拉取映射表:(Target, Action) → (Target', Action')。
2.每次进入 Action 前请求服务端:即时性强,适合强运营。
无需维护庞大的「URL 注册表」才能做动态化;维护 target-action 映射往往更贴近实际业务语义。
7. 工程与依赖治理
7.1 依赖方向(强制)
- 业务模块 → 不依赖其它业务模块实现。
- 业务模块 → 可依赖:基础库、UI 组件库、Mediator 接口(若拆接口模块)。
- Mediator 实现 → 运行时依赖各 Target(或通过反射/注册表延迟绑定,视语言而定)。
- Category/API 封装 → 与 Mediator 同仓或独立「API 聚合」模块,供各调用方依赖;响应方模块不依赖 Category。
7.2 与"URL 注册表"方案对比(理念层)
- 服务发现:在 iOS 上可用 runtime 按命名约定发现 Target-Action,不必启动时全量注册 URL(减少常驻内存与漏删注册项)。
- Android:可用反射 + 约定包名,或编译期注解生成注册表(等价于「无手写维护的注册」),原则仍是:注册若是实现细节,应由工具生成,而非业务人肉维护长列表。
7.3 协作规范
- 新增跨模块能力:只加 Target 方法 + Mediator Category 方法;主工程尽量不改动。
- 废弃调用:Category 标 @deprecated,监控无调用后删 Action。
- 契约:每个 Target 的 Action 维护参数表(key、类型、必填、示例),可放在文档或代码注释,与网络 API 文档同级对待。
八、何时做组件化
- 不宜过早:业务链路仍在剧烈变化、MVP 未稳时,组件边界频繁变动会带来全局调度与重构成本。
- 较佳时机:业务形态相对稳定、团队/代码量明显上升、并行交付压力大时。
- 组件化不仅是拆仓库和跳页面:还要考虑非页面调度、复杂/非常规参数、安全、动态化、接口演进。
九、平台映射建议
9.1 iOS
- Mediator + performTarget:action:params: + Runtime。
- CTMediator 式 Category:按业务域拆分,封装参数与校验。
- Swift 项目:可用 @objc Target 类,或 Swift 侧再包一层类型安全 facade(内部仍字典边界)。
9.2 Android
- 等价物:AppRouter / ServiceLoader / 注解生成路由表 + 显式区分 handleDeepLink() 与 inAppNavigate()。
- Params:跨模块用 Bundle / Map<String, Any?>,Parcelable 对象类比**"非常规参数"**;同样用 Router API 模块 收敛 key,
- 免业务互相引用 Model。
9.3 跨端与 KMP
共享层只定义 intent 与可序列化 payload;非常规对象留在各端 native 管道处理,避免强行在 KMP 里传 UI 图像句柄。
十、已知权衡与治理
- Mediator Category 中的 key 字面量:为换取响应方零依赖 Mediator 与强解耦,通常可接受;通过 lint、单测、契约文档控制质量。
- 调试:统一入口便于断点与日志;建议在 Mediator 层做 traceId、调用方模块名(可选)。
- 测试:对 Category 做参数单测;对 Target-Action 做集成测试或 UI 测试抽样。
十一、落地检查清单(评审用)
- 是否两条管道:远程 URL ≠ 本地 perform?
- 本地是否可传非常规参数而不污染全链路 URL?
- 跨模块是否禁止直接依赖对方 Business / Model?
- 对外是否只有 Target-Action(或等价),业务类不为「被 Protocol 绑定」而改结构?
- 敏感能力是否禁止被远程路径直达?
- 动态调度是否落在合适的切点,避免全量拦截性能问题?
- Category/API 层是否承担参数校验与文档化,调用方是否无需手写 URL?
十二、附:目录结构示例 + 命名约定 + 一则端到端调用示例
12.1 双端目录结构示例
约定:业务模块相互不依赖;跨模块只依赖 Mediator 接口 + 各模块对外 API 壳(iOS Category / Android api 模块)。
1. iOS(Xcode 工程 / SPM / CocoaPods 均可)
swift
MobileApp/
├── App/ # 宿主:仅组装、Deep Link、启动
│ ├── AppDelegate.swift
│ └── SceneDelegate.swift
├── Core/
│ ├── Mediator/ # 调度内核(可 SPM 独立包)
│ │ ├── Sources/
│ │ │ ├── Mediator.swift # perform(target:action:params:)
│ │ │ └── Mediator+Remote.swift # open(url:) → 解析 → perform
│ │ └── Tests/
│ └── FoundationKit/ # 与组件化无关的基础能力
├── Features/
│ ├── Product/ # 业务模块(独立 Target / Pod)
│ │ ├── Sources/
│ │ │ ├── Business/ # 页面、VM、领域逻辑
│ │ │ └── Targets/
│ │ │ └── Target_Product.swift # 仅承接 Action,转调 Business
│ │ └── ProductAPI/ # 可选:仅声明供 Mediator 调用的 @objc 入口
│ └── Home/
│ └── Sources/
├── MediatorAPI/ # 给「调用方」依赖:Category 封装 + 参数契约
│ └── Sources/
│ ├── Mediator+Product.swift
│ └── Mediator+Home.swift
└── Workspace.xcworkspace
要点:
- Mediator:运行时 Target_* + Action_*,params 用字典。
- MediatorAPI:按业务域分文件的 extension,对外暴露「像方法一样」的 API;业务响应方(Product)不依赖 MediatorAPI。
- Target_Product:放在 Product 模块内,类名固定为 Target_Product(与 Casa 约定一致),便于反射。
2. Android(Gradle 多模块)
kotlin
mobile/
├── app/ # 宿主:Application、Manifest、Deep Link
├── core/
│ ├── mediator/ # :core:mediator
│ │ └── src/main/java/.../mediator/
│ │ ├── Mediator.kt
│ │ ├── MediatorRemote.kt # handle(uri) → Route → perform
│ │ └── RouteParser.kt
│ └── foundation/
├── feature/
│ ├── product/ # :feature:product
│ │ └── src/main/java/.../product/
│ │ ├── business/ # Activity/Fragment/ViewModel
│ │ └── target/
│ │ └── TargetProduct.kt
│ └── home/ # :feature:home
├── mediator-api/ # :mediator-api 仅封装调用入口,供各 feature 依赖
│ └── src/main/java/.../mediator/api/
│ ├── ProductMediatorApi.kt
│ └── HomeMediatorApi.kt
└── settings.gradle.kts
settings.gradle.kts 中典型依赖关系:
- app → feature-*、mediator、mediator-api
- feature: home → mediator-api(不依赖 feature:product)
- feature: product → mediator(实现 TargetProduct)
- mediator → 各 feature(或编译期生成表,避免手写注册;此处从结构上保留 Target 在业务模块)
12.2 命名约定(双端对齐)
| 概念 | iOS | Android |
|---|---|---|
| 调度中心类 | Mediator(或 CTMediator 风格) | Mediator |
| 业务 Target 类 | Target_,如 Target_Product | TargetProduct 或 Target_Product(团队二选一,建议 Target + 领域名) |
| Action 方法 | Action_...,本地敏感能力加前缀 native_,如 Action_native_openWallet | 同左:actionNativeOpenWallet 或 Action_native_openWallet(与反射/映射表一致即可) |
| 远程 URL 路径 | 与路由表一致,如 scheme://product/detail 或 https://host/product/detail | App Links / Custom Scheme 与 RouteParser 规则一致 |
| 调用方 API 壳 | Mediator+Product.swift 内方法:product_detail(...) | ProductMediatorApi.kt |
| params 键 | snake_case 或 camelCase 全工程统一,如 product_id、image | 与 iOS 对齐,避免双端文档两套 key |
硬规则建议:
- native_ 前缀:仅允许 Mediator.perform / 本地 API 调用;MediatorRemote 解析 URL 后若指向 native_ Action → 拒绝或降级(防外链直达敏感页)。
- Target 只做转发:不写复杂业务;复杂逻辑在 Business。
- 跨模块不传己方 Model:只传 Dictionary / Bundle / Map + 非常规对象(如 UIImage / Bitmap)按平台放置。
12.3. 端到端示例:打开商品详情(远程 + 本地)
12.3.1 业务能力定义(两端一致)
- Target:Product
- Action(公共,可被远程映射):open_detail(无 native_,允许经合法 Deep Link 打开详情)
- Action(仅本地):native_present_share_sheet(分享当前页截图等,禁止出现在对外 URL 映射里)
参数(字典):
- product_id:String
- cover_image:可选,仅本地传递(非常规对象)
12.3.2 远程链路(外部分享 / 推送打开 App)
URL 示例:myapp://product/detail?product_id=123
iOS:AppDelegate → Remote → perform
swift
// App/AppDelegate.swift
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
return Mediator.shared.openRemote(url: url, options: options)
}
swift
// Core/Mediator/Mediator+Remote.swift
extension Mediator {
func openRemote(url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
guard let route = RouteParser.parse(url: url) else { return false }
// 安全:禁止 URL 直接映射到 native_ Action
guard !route.action.hasPrefix("native_") else {
showSecurityBlocked()
return false
}
var params: [String: Any] = route.query
params["_source"] = "remote"
return perform(target: route.target, action: route.action, params: params) != nil
}
}
swift
// 解析结果示例:target = "Product", action = "open_detail"
// Core/Mediator/RouteParser.swift --- 极简:myapp://product/detail?product_id=123
struct ParsedRoute { let target: String; let action: String; var query: [String: Any] }
swift
// Features/Product/Sources/Targets/Target_Product.swift
import UIKit
@objc(Target_Product)
public class Target_Product: NSObject {
@objc func Action_open_detail(_ params: [String: Any]?) -> UIViewController? {
let id = params?["product_id"] as? String ?? ""
// 转调 Business 组装 VC
return ProductDetailViewController(productId: id)
}
/// 仅本地:带 UIImage,禁止出现在 URL 路由表
@objc func Action_native_present_share_sheet(_ params: [String: Any]?) -> NSNumber? {
guard let image = params?["cover_image"] as? UIImage else { return false }
// present UIActivityViewController ...
return true
}
}
Android:Activity / NavDeepLink → MediatorRemote
kotlin
// app/src/main/AndroidManifest.xml 中注册 scheme / app link,由单一 Activity 接收后转发(示意)
// core/mediator/MediatorRemote.kt
object MediatorRemote {
fun handle(uri: Uri, mediator: Mediator): Boolean {
val route = RouteParser.parse(uri) ?: return false
require(!route.action.startsWith("native_")) { "blocked" }
val params = route.query.toMutableMap()
params["_source"] = "remote"
return mediator.perform(route.target, route.action, params)
}
}
kotlin
// feature/product/target/TargetProduct.kt
class TargetProduct {
fun actionOpenDetail(params: Map<String, Any?>): Boolean {
val id = params["product_id"] as? String ?: return false
// startActivity(ProductDetailActivity.intent(context, id))
return true
}
fun actionNativePresentShareSheet(params: Map<String, Any?>): Boolean {
val bitmap = params["cover_image"] as? android.graphics.Bitmap ?: return false
// 系统分享
return true
}
}
远程端到端路径(字符串):
外部 URL → 宿主 → Mediator.openRemote / MediatorRemote.handle → 解析出 (Product, open_detail) + product_id → Mediator.perform → Target_Product.Action_open_detail → 打开详情页。
12.3.3 本地链路(Home 模块调 Product,且带图片)
iOS:只依赖 MediatorAPI,不写 URL
swift
// MediatorAPI/Mediator+Product.swift
extension Mediator {
func product_openDetail(productId: String) -> UIViewController? {
perform(target: "Product", action: "open_detail", params: ["product_id": productId])
}
func product_presentShareSheet(cover: UIImage) {
perform(target: "Product", action: "native_present_share_sheet", params: ["cover_image": cover])
}
}
swift
// Features/Home --- 某处点击
if let vc = Mediator.shared.product_openDetail(productId: "123") {
navigationController?.pushViewController(vc, animated: true)
}
// 另一处:分享
Mediator.shared.product_presentShareSheet(cover: snapshotImage)
Android:ProductMediatorApi
kotlin
// mediator-api/ProductMediatorApi.kt
class ProductMediatorApi(private val mediator: Mediator) {
fun openDetail(productId: String): Boolean =
mediator.perform("Product", "open_detail", mapOf("product_id" to productId))
fun presentShareSheet(cover: Bitmap): Boolean =
mediator.perform("Product", "native_present_share_sheet", mapOf("cover_image" to cover))
}
kotlin
// feature/home/SomeFragment.kt
productMediatorApi.openDetail("123")
productMediatorApi.presentShareSheet(bitmap)
本地端到端路径:
Home → ProductMediatorApi / Mediator+Product → Mediator.perform(Product, ..., params) → Target_Product 对应 Action → 业务执行。
12.4 小结表
| 维度 | 远程 | 本地 |
|---|---|---|
| 入口 | openRemote / MediatorRemote.handle(Uri) | Mediator+Product / ProductMediatorApi |
| 参数 | 仅 query / 可编码字段 → 字典 | 字典 + Bitmap/UIImage 等 |
| 安全 | 禁止映射到 native_* | 可调用 native_* |
| 落点 | 与本地共用 perform → Target-Action | 同上 |
十三、附:Mediator.perform最小实现骨架资源
13.1 资源见附件
13.2 调用说明
1. iOS · Swift + Target_*(Runtime)
| 文件 | 作用 |
|---|---|
| ios-swift/Mediator.swift | perform(target:action:params:),NSClassFromString 解析 Target_,带 主可执行名作为模块前缀 的回退 |
| ios-swift/Target_Product.swift | @objc(Target_Product) + Action_open_detail: / Action_native_present_share_sheet: |
调用示例:
swift
let vc = Mediator.shared.perform(target: "Product", action: "open_detail", params: ["product_id": "123"]) as? UIViewController
Mediator.shared.perform(target: "Product", action: "native_present_share_sheet", params: ["cover_image": image])
2. iOS · ObjC + Target_*(objc_msgSend)
| 文件 | 作用 |
|---|---|
| ios-objc/Mediator.h / Mediator.m | performTarget:action:params:,与 Casa 思路一致 |
| ios-objc/Target_Product.h / .m | Action_open_detail: / Action_native_present_share_sheet: |
调用示例:
objectivec
UIViewController *vc = [[Mediator sharedInstance] performTarget:@"Product"
action:@"open_detail"
params:@{@"product_id": @"123"}];
3. Android · 显式 when(target, action)
| 文件 | 作用 |
|---|---|
| android-when/.../Mediator.kt | when (target to action) 分发 |
| android-when/.../TargetProduct.kt | 与 action 对应的方法,由 when 手动绑定 |
调用示例:
kotlin
Mediator().perform("Product", "open_detail", mapOf("product_id" to "123"))
4. Android · @MediatorRoute + 模拟生成的路由表
为符合真实依赖方向(调度内核不依赖 feature),注解版放在同一源码树里:GeneratedMediatorRouteTable 扮演 KSP/APT 生成物;多模块时把该文件挪到 :app 或 :mediator-registry(依赖各 feature),注解定义可留在 mediator-api。文件如下:
| 文件 | 作用 |
|---|---|
| android-annotation/.../annotation/MediatorRoute.kt | 路由注解 |
| android-annotation/.../RouteKey.kt | RouteKey + RouteHandler |
| android-annotation/.../Mediator.kt | 查表执行 |
| android-annotation/.../GeneratedMediatorRouteTable.kt | 手写等价于 codegen 的 handlers |
| android-annotation/.../target/TargetProduct.kt | 带 @MediatorRoute 的 Target |
调用示例:
kotlin
Mediator().perform("Product", "open_detail", mapOf("product_id" to "123"))
13.3 说明
- Swift 模块名:Mediator.swift 里用 CFBundleExecutable 尝试 Module.Target_Product;若仍找不到类,在 Xcode 里确认 Product Module Name 与可执行名一致,或改为你们统一的 硬编码模块前缀。
- ObjC objc_msgSend:要求所有 Action 签名为 - (id)Action_xxx:(NSDictionary )params(返回 id/UIViewController/NSNumber* 等),与当前 Mediator.m 一致。
- Android 注解版:当前无 Gradle/KSP,仅结构示范;接入 KSP 后应用生成逻辑替换 GeneratedMediatorRouteTable.kt 内容,不要在 core:mediator 里引用各 feature。