移动端架构体系(一):组件化

一、背景与目标

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 测试抽样。

十一、落地检查清单(评审用)

  1. 是否两条管道:远程 URL ≠ 本地 perform?
  2. 本地是否可传非常规参数而不污染全链路 URL?
  3. 跨模块是否禁止直接依赖对方 Business / Model?
  4. 对外是否只有 Target-Action(或等价),业务类不为「被 Protocol 绑定」而改结构?
  5. 敏感能力是否禁止被远程路径直达?
  6. 动态调度是否落在合适的切点,避免全量拦截性能问题?
  7. 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。
相关推荐
陈酒尽余欢2 小时前
告别 Vibe Coding:用 SDD 让 AI 编程提效 50%,三工具实战对比
后端·架构
wggmrlee2 小时前
AI技术架构全局视角
人工智能·架构
大迪deblog2 小时前
系统架构设计-系统架构评估
架构·系统架构·软件构建
Memory_荒年3 小时前
Nacos双面超人:注册中心 + 配置中心,一个都不能少!
java·后端·架构
CoovallyAIHub3 小时前
传感器数据相互矛盾时,无人机蜂群如何做出可靠的管道泄漏检测决策?
算法·架构·无人机
CoovallyAIHub3 小时前
Claude Code Review:多 Agent 自动审查 PR,代码产出翻倍后谁来把关?
算法·架构·github
恋猫de小郭3 小时前
Android 17 新适配要求,各大权限进一步收紧,适配难度提升
android·前端·flutter
cyforkk3 小时前
前端架构实战:当服务器关闭时,如何优雅提示 502 错误?
服务器·前端·架构