一、一个编译时间超过 8 分钟的 Monorepo
2024 年初,我们的企业 IM 项目已经运营了两年,代码仓库膨胀到了一个危险的状态:
- 单仓库包含 300+ 页面、60+ 业务模块
flutter pub get耗时 2 分钟以上- 增量编译时间 3-5 分钟,全量编译超过 8 分钟
- 任何一行代码改动都需要等待数分钟才能验证效果
- 合并冲突成为日常——多人同时修改不同模块的代码,但都在同一个仓库里
更致命的是,编译缓存经常失效。Flutter 的热重载在 300+ 页面的仓库里已经形同虚设——改动一行代码后热重载需要 15-20 秒,比冷启动好不了多少。
这不是技术能力的问题,而是项目规模超过了单体仓库的承载上限。经过评估,我们启动了组件化拆分,将单体仓库拆解为 12 个独立组件。本文将复盘整个拆分过程,包括组件拆分策略、通信机制、集成调试和生产环境效果。
二、组件化的目标与原则
在动手之前,我们先对齐了组件化的核心目标:
- 编译速度:单组件独立编译,修改一个组件不影响其他组件的缓存。
- 团队独立开发:不同业务组可以在各自的组件仓库里独立开发,减少 merge conflict。
- 复用性:通用组件(如选人组件、转发组件)可以被多个上层模块引用。
- 稳定性:组件升级只影响自身,不会导致整个 App 崩溃。
基于这四个目标,我们制定了拆分原则:
- 按业务域拆分:不是按技术层级(UI/数据/网络),而是按业务功能(消息/通讯录/会议/工作台)。
- 组件即产品:每个组件有独立的入口页面、独立的路由、独立的数据层。它可以独立运行——不依赖宿主 App 的任何代码。
- 稳定接口,不稳定实现:组件对外暴露的接口越少越好,内部实现可以频繁变更。
三、拆分策略:从三层到十二个组件
3.1 三层架构图
1 | ┌───────────────────────────────────────────────┐ |
3.2 基础组件层(4 个)
core_im:消息核心层,封装 IM SDK 的消息收发、会话管理、消息同步、离线消息处理。这是整个 App 的「心脏」,所有业务组件都依赖它。
core_ui:统一 UI 组件库。消息气泡、头像、Loading、Empty 状态、通用弹窗等。在 300+ 页面中,有大量 UI 重复——比如头像组件在 40+ 个页面中被重复实现。统一提取为核心 UI 组件后,不仅减少了代码量,还保证了一致的交互体验。
core_net:网络层封装。基于 dio 做统一拦截器链(Token 刷新、请求重试、日志记录、加解密)。所有业务组件共享同一套网络策略,避免每个组件各自封装一遍。
core_storage:本地存储层。统一数据库操作、Key-Value 缓存、文件存储接口。这是为后续可能的数据库迁移(如 sqflite → Drift)做的隔离。
3.3 业务组件层(4 个)
chat:聊天页面核心功能——消息列表渲染、消息发送/撤回/删除/转发、输入框、附件发送(图片/文件/位置/语音)、消息搜索。
contact:通讯录模块——组织架构树、联系人列表、部门列表、外部联系人。
calendar:日程与待办——日历视图、日程创建与提醒、待办任务管理。
meeting:会议系统——会议创建、加入、屏幕共享、会议管理。
3.4 功能组件层(4 个)
selector:选人组件。聊天页面、会议邀请、转发都需要选人,这是一个典型的高复用组件。
forward:消息转发组件。从聊天页面、收藏列表、搜索页面都可以触发转发,需要独立的转发流程。
webview:轻应用容器。工作台中的 OA 审批、报表、公告等轻应用都在 WebView 中承载。
profile:个人资料卡。所有人员头像和姓名的点击都会唤起资料卡,显示详细信息。
四、组件间通信:路由与数据流
4.1 路由解耦
组件化的核心挑战之一:A 组件如何跳转到 B 组件的页面,而不直接引用 B 组件的代码?
我们的方案是「路由协议 + 注册」:
1 | // core_router 中定义路由协议 |
所有页面间的跳转都通过 AppRouter 接口进行,组件不直接引用其他组件的页面类。当聊天页面从 Flutter 原生实现改为混合栈时,只需修改 toChat 的实现,所有调用方无需改动。
4.2 数据共享
组件间的数据共享通过「发布订阅」模式实现:
1 | // 消息数据总线 |
比如:新消息到达时,core_im 通过事件总线发布 NewMessageEvent。会话列表组件、聊天组件、未读计数组件各自监听这个事件,独立更新自己的状态。这种模式下,数据生产者不关心消费者是谁,组件之间零耦合。
五、组件集成与调试
5.1 开发模式:组件独立运行
每个组件都有自己的 main.dart 入口和示例数据,可以脱离主工程独立运行:
1 | chat_component/ |
组件开发者只需运行 cd chat_component/example && flutter run,就能启动一个只包含聊天页面的 mini App。编译时间从 5 分钟降到 20 秒,开发体验大幅提升。
5.2 集成模式:组件依赖管理
主工程的 pubspec.yaml 通过 Git 依赖引用各组件:
1 | dependencies: |
通过指定 ref 锁定版本,避免「改了基础组件导致所有业务组件挂掉」的连锁反应。基础组件升级时,先在各自的 example 工程中验证,再逐步推送到业务组件。
5.3 版本管理与 CI/CD
每个组件独立发版,有独立的 CHANGELOG。CI 流水线验证:
- 组件自身的单元测试和 Widget 测试
- 组件 example 工程的编译检查
- 组件 API 的向后兼容检查(禁止删除已有公开接口)
只有三个检查全部通过,才允许合入主分支并发版。
六、实战效果
6.1 量化指标
| 指标 | 拆分前 | 拆分后 |
|---|---|---|
| 全量编译时间 | 8 分钟 | 3 分钟(主工程) |
| 组件独立编译时间 | N/A | 15-25 秒 |
| flutter pub get | 2 分钟 | 30 秒 |
| 热重载延迟 | 15-20 秒 | 2-3 秒 |
| 单模块开发人员 | 全局锁 | 独立仓库,零冲突 |
| 代码复用率 | 未知 | 选人组件被 6 个业务复用 |
6.2 遇到的问题
问题一:组件边界模糊。拆分初期,有些开发者习惯性地在 chat 组件中直接 import contact 组件的代码——因为聊天页面需要展示联系人详情。这破坏了组件独立性。解决方案是在 CI 中增加依赖方向检查:禁止业务组件之间互相直接依赖。
问题二:资源文件冗余。拆分后,每个组件都带了一份自己的 pubspec.yaml,部分通用资源(如字体、图标)被多个组件重复声明。后期将这些通用资源统一放入 core_ui 组件,业务组件不再各自携带。
问题三:组件版本漂移。半年后,不同业务组使用的 core_im 版本从 v2.3 到 v3.1 不等,出现了接口不兼容。解决方案是引入「版本宪章」:基础组件的旧版本只维护 3 个月,超期后强制升级。
七、总结
组件化不是把代码分散到多个文件夹那么简单,它本质上是建立组织协作的边界。在 50+ 开发者的团队中,组件化最大的收益不是编译速度提升了多少,而是——你的改动不太可能搞崩我的模块。
拆分的核心原则:
- 按业务域拆分,不按技术层拆分。技术层的变化频率不同,业务域的变化独立性更强。
- 组件独立可运行。如果一个组件没有自己的 example 工程,那它不是真正的组件——只是换了个文件夹存放代码。
- 接口优先,实现隐藏。组件对外暴露的 Dart API 就是它的契约,契约必须稳定。内部实现的重构不应影响调用方。
12 个组件拆完之后回头看,最大的感受是:组件化不是架构优化,是协作模式的变革。从「所有人改一棵树」变成「各自维护各自的模块」,这才是组件化真正的价值。
扫描二维码,分享此文章