1. 从单体项目到组件化为什么我劝你别再硬撑了如果你手头有一个Android项目已经写了两三年app模块里的代码量到了几十万行每次编译都要等三分钟以上改一个Button样式都要连带跑完所有功能回归——那这篇文章就是写给你看的。我在前两年接手的一个项目就是典型状态app模块下包名杂乱无章登录、支付、推送、分享、IM、商城全部挤在一起。业务方提需求越来越快代码冲突越来越频繁每次git merge光解决冲突就要花掉半天。更让人头疼的是整个团队十几个人都在改同一个模块Git提交记录乱成一锅粥谁改了哪儿根本说不清楚。真正让我下决心做组件化改造的导火索是一次线上崩溃——起因只是某位同事在改商城页面的SharedPreferences时字段名跟登录模块的工具类撞了结果把所有人的登录态都搞丢了。那一次事故让我彻底意识到单体应用的耦合已经不只是技术债问题而是直接威胁线上稳定性的定时炸弹。组件化并不是什么高深的技术它的核心思想只有一句话把单体的app模块按业务或功能边界拆成多个独立的组件模块每个模块可以独立编译、独立测试、独立维护最终通过一种约定的方式组装成完整的App。这篇文章我会完整记录一次从单体应用到组件化的改造全过程。内容不是教科书式的理论而是我自己踩坑踩出来的实操经验——包括模块怎么划分、路由怎么设计、资源冲突怎么规避、独立调试和集成模式怎么切换、还有改造过程中遇到的各种诡异问题。如果你正在考虑组件化改造或者已经在改造路上被各种问题卡住这篇文章应该能帮你省下不少弯路。2. 动手之前先想明白组件化到底拆的是什么2.1 拆的不是代码是职责边界很多人一听到组件化第一反应就是把代码拆到不同模块里去。这句话对了一半。拆代码只是表象真正拆的是职责边界和依赖方向。在单体架构里代码之间天然没有边界任何类都可以被任意地方引用。你一个工具类放在util包里理论上谁都能调。这听起来很方便但代价是——没有任何人知道这个工具类被谁依赖改起来慌不择路。组件化拆分的本质是把谁依赖谁这个隐性问题变成显性约束。我习惯把模块分成三个层级基础层base / common网络库封装、图片加载、基础工具类、自定义View、通用Dialog等。这一层不依赖任何业务模块只被上层依赖。业务组件层按业务线划分比如登录组件、商城组件、支付组件、IM组件。每个组件内部是完整的业务闭环组件之间禁止相互直接引用。宿主壳层app只负责组装。它依赖需要用到的业务组件统一配置路由表、管理Application的初始化顺序本身不写业务代码。这个分层规则必须要用依赖检查机制落地而不是靠自觉。我在改造时就定了一条铁律基础层禁止依赖任何业务组件业务组件只允许依赖基础层和其他暴露接口的组件宿主壳层是唯一允许依赖所有组件的地方。谁违反规则代码评审直接打回。2.2 组件粒度怎么定一个页面一个组件是误区组件拆到多细直接影响改造工程量。拆得太粗模块之间依然高度耦合没解决实际问题拆得太细光维护组件间的通信就够你喝一壶的增量编译的优势也被频繁跨模块调用抵消了。我自己的经验是按业务闭环而非页面来划分。所谓业务闭环就是用户完成一个完整操作链路所涉及的所有页面和逻辑。比如用户在商品列表页点进详情页、加购物车、下单、支付这条路是一个闭环适合放在一个组件里。而登录算一个独立闭环因为几乎每个App都需要登录但它不依赖商品链路。实际操作中我一般会把一个组件控制在10到30个页面左右。太小了没必要太大了又会失去组件化的意义。如果你发现一个组件内部页面之间互相跳转的逻辑特别多那说明它本身就应该是一个组件而不是分散到多个组件里这点在划分时务必留意。2.3 改造路径的选择自顶向下还是自下而上组件化改造有两种推进路径效果差别很大。自顶向下是先把app壳层拆出来再把业务页面一个一个抽到组件里。这种方式前期见效快app壳层独立出来后构建速度会有明显提升团队能立刻感受到收益。但缺点是改造过程中业务组件还是散落在app里组件之间的耦合问题解决得不彻底。自下而上是先把基础能力抽成底层模块再把业务组件往上搭。这种方式工程量大前期看不到明显效果团队容易失去信心。我实际验证下来比较推荐的做法是双轨并行优先抽基础层中耦合度最低的部分比如网络、图片、工具类同时选一个业务边界最清晰的模块比如登录做第一批组件化试点。基础层先落底给业务组件提供干净的底座试点组件跑通整条链路验证路由、资源隔离、独立调试这些关键机制可行。试点跑通后再按业务优先级逐个迁移剩余模块。这样做的最大好处是风险可控。试点模块出了问题影响面就那么大能快速回滚机制验证成熟之后大批量迁移的速度反而快。一口气想把所有模块全拆完的团队几乎都会在中期暴露出各种历史遗留耦合问题搞得进退两难。3. 模块拆分落地从一个登录组件开始完整走一遍3.1 模拟项目背景与目标结构为了把整个流程讲具体这里用我参与过的某模拟项目来演示。项目原本是单体结构app模块下有登录注册、商品浏览、下单支付、消息中心、个人中心5个业务域。改造目标是最终形成如下结构项目根目录 ├── app宿主壳层只做组装 ├── base_lib基础库网络、图片、工具、UI组件 ├── component_login登录注册 ├── component_mall商品浏览与下单 ├── component_pay支付流程 ├── component_message消息中心 ├── component_profile个人中心 ├── third_libs第三方SDK适配层按需集成 └── buildSrc 或 gradle版本目录统一依赖管理首批试点选登录组件原因是它的业务闭环最清晰登录、注册、找回密码、以及登录后状态同步依赖的外部系统只有登录态存储和用户信息接口边界相对干净。3.2 Gradle模块创建的完整步骤第一步在项目根目录新建component_login模块。使用Android Studio向导创建时类型选Phone Tablet Module包名按公司的域名规范来。component_login的build.gradle初始配置与常规模块没有太大区别重点是为了支持后续独立调试我通常给每个业务组件加一个单独开关。实际调试时我会把isRunAlone切到true这时组件模块会变成独立的application入口有自己的AndroidManifest.xml可以单独跑起来调内部逻辑。关键代码如下android { // 资源前缀约束防止资源名冲突 resourcePrefix login_ } if (isRunAlone.toBoolean()) { apply plugin: com.android.application // applicationId 使用带后缀的独立包名便于测试包共存 } else { apply plugin: com.android.library }这里有个很关键的细节resourcePrefix一定要在组件模块创建时就配上。它的作用是强制组件内所有资源必须以login_开头从源头避免资源名冲突。这个配置我后面会专门讲因为很多人都是彻底改完才想起来到时候满屏的资源命名冲突会改到你怀疑人生。新建模块后记得把模块里的AndroidManifest.xml拆成两份一份给集成模式用不声明Application和启动Activity一份给独立调试用声明Application和入口Activity。具体放在src/main/AndroidManifest.xml和src/debug/AndroidManifest.xml这在后面独立调试章节再详细展开。3.3 代码迁移顺序由底向上的依赖梳理模块建好后最难的是把原有代码搬过去。这里我有几条可复用的经验第一先搬不带业务逻辑的纯工具类和UI组件。这些类依赖最少搬过去风险最低而且能立刻让app模块瘦身。登录组件里的密码加密工具、验证码倒计时控件这些都是第一批迁移对象。第二再搬网络请求和数据层。登录组件的网络请求是独立的LoginApiService把它连带着请求/响应模型一起搬到组件里。这一步要注意如果多个组件共用同一套数据模型应该把模型下沉到base_lib不要各自复制一份。否则同一份用户信息在两个模块里维护两份字段不同步的时候你根本不知道哪边是对的。第三最后搬页面和交互逻辑。页面类是耦合最重的部分通常涉及自定义Application初始化、全局导航、跨页面的状态同步。搬的过程中要同步调整这些依赖关系这也是跨组件通信方案要提前设计好的原因。整个登录组件迁移我花了大约两个工作日。过程不复杂但需要细致。每个类搬过去之后都要同步检查它的依赖是否都迁移完成不然编译时一堆红色的报错会让你非常崩溃。3.4 搬完代码之后的第一轮编译修复把代码搬到新模块后第一轮编译几乎必然报错。常见的错误类型有几类没有导入错误是最容易处理的多数是import缺失根据IDE的提示AltEnter导入即可。找不到符号通常是依赖缺失。某个方法在base_lib里但component_login没有声明依赖base_lib编译就报错。处理方式是检查build.gradle中声明了implementation project(:base_lib)。资源访问权限错误相对隐蔽。在单体项目中app里的代码可以直接引用R.string.app_name这样的资源。拆成组件后如果这个资源在别的模块里默认私有访问会失败。处理方式有两种一是用transitive把资源暴露出去二是把公共资源下沉到base_lib。我强烈建议后者因为资源跨模块随意引用会让资源管理变得一团乱。// base_lib 中放公共资源 dependencies { // 组件模块依赖基础库即可访问其资源 implementation project(:base_lib) }提示当Android Studio对R类导入报红色错误时先检查依赖声明再检查资源是否存在不要盲目执行Build Clean Project很多情况下清理后依旧报错。4. 组件间通信接口下沉与事件总线组件拆好后最核心的问题就浮出水面组件之间怎么通信登录组件登录成功后消息组件和个人中心组件怎么知道用户登录状态变了商城组件怎么通知支付组件去发起支付4.1 基础通信方案接口下沉到基础库组件化刚起步时不必一上来就引入复杂的路由框架。最简单的方案是接口下沉把通信的接口定义放在base_lib里组件A实现这个接口组件B通过查找得到接口实例然后调用。以登录状态为例我在base_lib里定义一个UserAction接口public interface LoginAction { void login(LoginRequest request, LoginCallback callback); void logout(); boolean isLoggedIn(); }登录组件实现这个接口然后在初始化时把实例注册到一个服务管理类里。其他组件通过ServiceManager.get(LoginAction.class)就能拿到实现从而调用登录方法。这样组件之间完全不直接引用对方的类只依赖基础库里定义的接口。这个方案的优点是实现简单不需要引入额外的路由框架改动量小。缺点是随着业务变多base_lib里的接口数量会膨胀而且每个新接口都要手动注册和查找代码写起来比较啰嗦。如果项目规模不大或者你想以最小成本完成组件化改造这个方案足够用了。4.2 组件间跳转路由框架的核心机制如果两个组件之间只是传递数据、调用方法接口下沉就能解决。但如果你在组件A的一个页面里要跳转到组件B的某个页面怎么办总不能直接startActivity然后import页面的类——那样就破坏了组件间的隔离。这时候就要引入路由框架。主流的方案有阿里ARouter之类但我一般建议自己封装一个轻量级的因为第三方路由框架绑定较深出了问题排查难度高而且很多功能你根本用不到。自己封装的路由核心机制非常简单每张需要被跨组件访问的页面在Route注解里注册一个路径。Route(path /login/LoginActivity, description 登录页) public class LoginActivity extends AppCompatActivity { // ... }在初始化时框架通过注解处理器扫描所有带Route的类建立路径到Class的映射表。跳转时调用方只需要传路径字符串Router.getInstance() .build(/login/LoginActivity) .withString(source, mall_checkout) .navigation(context);路由最核心的价值不只是能跳转而是解耦。页面A不关心目标页面是哪个类只关心路径。哪怕目标页面完全重写只要路径不变调用方完全无感。自己封装路由框架时要注意几个高级能力否则落地时会发现不够用参数传递与类型转换支持Integer、String、Parcelable等类型。我的实现是构建一个Bundle在navigation时自动解析withXxx方法存入的参数。拦截器机制登录拦截是路由框架最典型的应用场景。用户跳转到需要登录的页面时路由先执行登录校验未登录则跳转登录页登录成功后再自动跳回原目标。这比在每个页面手动判断登录态要干净得多。降级策略目标页面不存在时给出友好提示而不是让应用崩溃。调试期可以在降级逻辑里打印所有已注册的路由表方便排查。跨组件调用除了页面跳转跨组件方法调用也可以用路由实现和前面的ServiceManager思路相似只是路径化管理起来更统一。4.3 模块间状态同步从观察者模式到事件总线登录成功需要通知消息组件更新未读数量下单成功需要通知个人中心刷新订单角标——这类跨模块状态同步光靠接口查询不够因为你要的是变化通知而不是主动获取。最简单的方案是用一个极简的事件总线管理器。核心逻辑就是观察者模式维护一个事件类型到订阅者的映射发布者发出事件所有订阅者收到通知。public class EventBus { private final MapClass?, ListObject subscribers new ConcurrentHashMap(); public void register(Object subscriber) { ... } public void unregister(Object subscriber) { ... } public void post(Object event) { // 通过反射调用订阅者中带有 Subscribe 注解的方法 } }我用的是自己封装的极简事件总线核心逻辑两百行左右没有用第三方框架。好处是完全可控出了问题半小时就能读完源码。注意事件通知只是传递发生了什么不要传递大对象。事件里只放必要的信息比如事件类型、少量业务ID。具体的业务数据订阅者收到事件后自行查询。这样可以避免多个模块同时维护同一份数据副本导致数据不一致的隐患。4.4 跨组件通信选型什么时候用接口什么时候用路由什么时候用事件很多新手问这三个通信方式到底怎么选我用的判断标准很简单有明确的调用方和目标类用接口下沉。比如支付组件调起支付支付结果需要返回给调用方这种是一对一的强关联接口最合适。页面跳转不看目标是谁只关心路径用路由。因为跳转天然是面向路径的不需要关心目标实现。一对多的状态广播发布方不关心谁在听用事件总线。登录状态变化、购物车数量变化这种天然就是广播语义。三种方式配合使用路由承载页面跳转接口承载强关联的方法调用事件总线承载弱关联的状态同步。三者的关系理清了组件的通信边界就能保持清晰。5. 组件化工程的整体构建策略与依赖管理5.1 统一依赖版本管理版本目录方案怎么落地组件模块多起来之后每个模块的build.gradle都要声明依赖如果每个模块里写着不同的依赖版本后果就是编译时不定期爆出依赖冲突。组件化改造做依赖管理我推荐直接用Gradle的版本目录Version Catalog。它是Gradle官方推荐的依赖管理方式能解决资源冲突、版本统一、依赖声明规范化等问题。在项目根目录的gradle/libs.versions.toml文件里集中定义[versions] androidx-core-ktx 1.12.0 retrofit 2.9.0 [libraries] androidx-core { module androidx.core:core-ktx, version.ref androidx-core-ktx } retrofit { module com.squareup.retrofit2:retrofit, version.ref retrofit }各模块声明依赖时dependencies { implementation libs.androidx.core implementation libs.retrofit }用版本目录的收益不只是统一版本更重要的是依赖变化时只改一处所有模块自动同步。组件化改造后模块数通常会增加好几个如果没有集中管理依赖冲突会让你疲于奔命。5.2 组件调试开关与构建变体前面提到每个业务组件都有一个isRunAlone开关。这个开关放在根目录的gradle.properties里isRunAlonefalse组件模块在build.gradle里读取这个属性决定自己应用application插件还是library插件。同时独立调试模式下要有单独的AndroidManifest.xml里面声明独立的Activity入口。这个开关的常见问题在于切换之后Android Studio的Sync Project没有重新执行或者没切Build Variant导致模块类型没生效。我的经验是每次改完gradle.properties之后一定要执行一次File Sync Project with Gradle Files然后检查Build Variants窗口确认模块的类型已经正确切换。5.3 公共工具类会不会变成垃圾场base_lib这个模块很容易变成代码垃圾场——什么工具类都往里塞功能越加越多最后跟原来的app模块一样臃肿。我做了两条硬性约束第一新工具类必须至少被两个不同组件使用才能进入base_lib。如果只是某个组件内部用就放到组件自己的util包里。第二base_lib里不允许写业务逻辑。哪怕只涉及一点点登录相关的判断也不准放在基础库。基础库的原则是绝对中立的工具箱不给任何业务方向。这两条约束要靠代码评审来硬卡。有一次某同事往base_lib里加了带登录逻辑的弹出框被我发现后打回重新挪到登录组件。这类随手放错位置的代码如果不及时纠正几个月后base_lib就会腐化得跟原来的app一样。6. 组件独立调试与集成联调的双模式切换6.1 为什么组件必须能独立运行组件化改造的一个关键目标是团队并行开发和独立编译。如果一个组件不能独立运行那调试时每改一个按钮颜色都要把整个App编译一次增量编译的优势荡然无存。组件独立运行的机制上面已经提到了核心是isRunAlone开关加独立的debug/AndroidManifest.xml。在独立调试模式下组件作为application运行有自己的Application类可以在里面初始化必要的依赖比如网络框架、图片加载库。src/debug/AndroidManifest.xml的内容大致是manifest xmlns:androidhttp://schemas.android.com/apk/res/android application android:name.debug.DebugApplication android:themestyle/Theme.Debug activity android:name.DebugMainActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity /application /manifest这样集成模式下使用main/AndroidManifest.xml无启动入口独立调试模式下使用debug/AndroidManifest.xml带启动入口互不干扰。6.2 独立调试的完整流程我把独立调试模式下的完整操作步骤整理一下方便直接照着做在根目录gradle.properties中把isRunAlone改为true调试哪个组件就改哪个。打开Build Variants窗口确认目标组件模块的module类型是application。给组件配置运行任务。Android Studio默认会把所有application模块都作为可运行目标直接选择带DebugApplication的配置运行即可。调试DebugApplication里的初始化逻辑确保网络层、图片加载等基础能力可用。在DebugMainActivity里写一个简单的业务入口例如直接跳转到登录页。运行组件验证业务逻辑。调试完成后把isRunAlone改回falseSync Project确认模块回到library类型。6.3 集成模式下的资源与依赖问题排查集成模式下最常见的问题是resourcePrefix规则没遵守导致的资源冲突。举个例子某个组件在模块中定义了一个ic_launcher另一个组件也定义了同名资源打包时aapt会直接报资源冲突或者更隐蔽地指向错误的资源。遇到资源冲突报错时完整排查链路是这样的先看报错信息中冲突资源的名称。通常aapt会直接告诉你resource entry ic_launcher is already defined。用CtrlShiftF全局搜索看哪些模块定义了这个资源。如果是之前的模块按新组件的resourcePrefix规则重命名。如果这个资源被多个模块共用下沉到base_lib并去掉前缀。改完重命名之后同步修改代码中的引用全局搜索旧资源名逐处替换。最后执行Build Clean Project再重新编译验证。这里我特别强调不要在集成模式下花大量时间排查资源冲突应该先从源头卡住。每个组件创建时就设置resourcePrefix评审时检查资源命名。规则前置的成本远低于事后补救。6.4 集成运行中发现的边界问题Application初始化顺序集成模式下所有组件共用一个Application组件初始化顺序就必须明确定义。我把初始化分为三个阶段基础库初始化网络、图片、日志等最先执行。业务组件初始化按依赖关系排序。比如支付组件依赖登录状态那登录组件就要先于支付组件初始化。业务注册路由表加载、事件总线订阅、ServiceManager注册等最后执行。为了保证顺序我在壳层的Application里写了一个ComponentInitializer工具用一个有序链表管理初始化任务public class ComponentInitializer { static { register(new BaseLibInitializer(), 0); register(new LoginComponentInitializer(), 1); register(new PayComponentInitializer(), 2); register(new RouterInitializer(), 99); } }分级注册之后再遇到某个组件初始化顺序不对导致的崩溃直接调整序号就行。这个方案看起来简单实际运行中非常稳。7. 全套编码规范从命名到依赖的硬性约束7.1 资源前缀、类命名与包结构规范组件化项目里规范不是建议而是必须。我列一下我实际项目中强制执行的那几条资源前缀登录组件资源一律以login_开头商城组件以mall_开头。resourcePrefix配置强制工具检查审计时人工抽查。包结构每个组件内统一分包结构。以登录组件为例component_login下的包名规范是com.公司域名.login内部继续按ui、data、api、util、bean分。路由路径统一格式是/模块名/页面名比如/login/LoginActivity/mall/GoodsDetailActivity。路径单词一律用驼峰避免大小写不一致导致的路由找不到。禁止跨组件的类引用组件A的代码里绝对不允许直接import组件B的类。这条我是通过Checkstyle自定义规则拦截的扫描import语句发现跨组件引用直接报错。7.2 依赖方向的限制与检查为了约束基础层禁止依赖业务组件、业务组件禁止互相依赖除了Review我建议用gradle任务做自动检查。在根项目的build.gradle里加一个简单的checkDependencies任务task checkDependencies { def forbidden [ component_mall: [component_pay, component_message], component_login: [component_mall], ] // 读取每个组件的 build.gradle 的依赖声明发现违规依赖则报错 }我在实际项目中就是靠这套自动检查加人工Review双重保险才把依赖混乱的苗头在萌芽期就掐掉。改造成熟之后的项目新增的跨组件依赖几乎为零。7.3 路由优先级与冲突检查路由路径冲突也是组件化改造后期很容易出现的问题。两个组件各自注册了/login/LoginActivity路由表构建时就会冲突覆盖。我在自己的路由框架里加了路径冲突检测初始化时遍历所有路由映射如果同一个路径被两个不同的类注册直接打印错误日志并且杀进程防止带着错误路由表上线。这个机制在调试期帮了大忙几乎每次出现路径冲突都能立刻暴露。8. 真实踩坑记录与排查过程复盘组件化改造过程中我踩了不少坑。这里记录几个印象最深的每个都附上完整的排查链路希望能帮你缩短排查时间。8.1 资源文件冲突的完整排查链路某次集成模式下编译报错aapt提示login_bg_loading资源已被定义。我全局搜索后发现登录组件和商城组件各定义了一个同名资源但内容不同。代码里登录页实际引用的是商城组件的资源表现就是登录页的加载背景图在某些机型上显示异常。排查链路如下先定位到报错资源名确认是哪两个模块冲突。全局搜索这个资源的引用位置发现登录页的布局中引用的是无前缀的旧资源名。检查登录组件的resourcePrefix配置发现该组件创建时没有配置前缀规则导致迁移代码时大量旧资源名没有重命名。处理方法把登录组件所有资源统一加上login_前缀同时修改代码中的引用。验证重新编译确认资源冲突消失再检查登录页背景图显示正常。这个坑的根因就是组件在迁移初期没有立即配置resourcePrefix。如果提前配置就不会有后续问题。8.2 路由找不到页面的诡异故障改造成型后有一段时间用户反馈从商城页跳转支付页偶尔失败点击没反应也不崩溃。第一次排查时查了路由表路径正确页面类存在没有任何报错。后来查了路由初始化时才发现路由表构建是异步的在极端情况下业务页面跳转发生在路由表构建完成之前路径还没有注册进去所以找不到目标页面。修复方案有两个层面一是在路由组件的Application初始化时改为同步构建路由表确保路由注册完成前不会响应跳转请求。二是给路由框架的降级逻辑加上日志万一找不到目标页面时打详细日志而不是静默失败。修复后这类故障再也没有出现过。8.3 登录态丢失的诡异问题改造后的某个版本用户反馈登录状态经常会掉线。起初以为是token过期逻辑有误排查了很久。后来发现根因是登录组件和消息组件都维护了一份用户信息消息组件在某次数据刷新时把未登录状态写回了SharedPreferences覆盖了登录组件刚写的登录态。这个问题的根因是同一份数据在两个组件里各自维护了一份副本。修复方案是把登录态存储下沉到base_lib提供统一的读写接口两个组件都通过接口访问不再各自操作SharedPreferences。这一个看似简单的修复不仅解决了登录态丢失还顺带消灭了其他地方的数据不一致隐患。8.4 代码分支冲突与合并策略组件化改造过程中同时有多个功能分支在并行开发而改造分支涉及大量代码移动几乎每个分支的冲突都是大片大片的。这让merge成了团队最头疼的事。我后来调整了合并策略每周固定一次主干合并冲突由改动方各自负责解决改动方在回归测试时除了验证自己的功能还必须验证与组件化的兼容性。坚持两三个迭代后团队适应了新结构冲突量明显下降。基础库的变更也要小心。凡是涉及base_lib的改动都要在合并前通知所有组件负责人确保影响面可控。9. 拆完不是终点组件化完成后的持续治理9.1 防止组件间悄悄重新耦合组件化改造完成并不代表一劳永逸反而是一场持续战。团队在忙于新功能时为了赶进度非常容易图省事直接在组件A里import组件B的类或者把本该放在基础层的工具函数复制一份塞到组件里。我一直采取两个措施防微杜渐一是Checkstyle加规则凡是import语句中包含其他组件的包名直接报错。这比Review彻底得多——人总有漏看的时候机器不会。二是规范要求新增公共代码必须评审后放入base_lib并且代码库中一旦发现重复工具方法立即合并。9.2 依赖升级策略与回归风险组件多了以后依赖升级不再是改个版本号那么简单。比如升级网络库版本影响的组件可能有七八个每个组件都要回归验证。我自己的升级流程是先在base_lib升级并验证再逐个组件的build.gradle里改版本每改一个组件的版本就编译一次同时跑该组件的关键用例全部通过后再提交。这套流程稍慢但胜在稳。遇到紧急安全补丁时可以并行验证多个组件的依赖性但至少也要保证核心链路不受影响。9.3 团队协作方式、代码评审关注点与组件数量收敛组件化改造对团队协作方式的影响比很多人预想的要大。Review时除了常规代码质量还要重点确认有没有跨组件耦合、有没有放错层级的依赖、资源命名是否规范、路由注册是否完整。组件数量本身也需要持续关注。模块过多会让编译整体耗时上升维护成本增加。我一般认为常见规模的项目业务组件加基础组件控制在10个左右是合理的。如果业务线扩张导致组件超过二十个就要考虑是否需要合并相近的组件或者引入微前端式的小程序容器来收敛。9.4 改造完成后如何量化收益组件化做完总得向团队和老板讲清楚收益否则大家只会觉得改了个架构功能没变化。我梳理了几个可以量化的指标编译时间单体编译从三分钟减到增量编译三十秒左右独立模块的单独编译更短。并行开发效率两个团队同时改不同组件互不阻塞同一时间段内的合并冲突大幅下降。代码耦合度用自动化工具统计业务组件间的直接引用数量从改造前的上百条降到了个位数。稳定性线上崩溃率在改造完成后的两个季度内明显下降资源冲突和数据不一致类的问题没再出现。这些数据记得在改造过程中就持续记录等结束时拿出来跟改造前的基线对比最有说服力。10. 最后分享两点组件化改造的私人体会改造过程中我跟团队反复讲的一句话是组件化拆的不是代码是人心的边界。每个人都要学会从这是公共的思维转变成这是我职责范围内的。代码层面的规则哪怕做得再完善如果大家从根本上没有认同这个边界腐化只是时间问题。所以我在每次周会上只花五分钟讲规则剩下全是讲实际案例——这个组件因为耦合出了什么bug那个模块因为规范省了多少时间。讲多了团队成员自然会形成共同的价值观。另外还有一个经常被忽略的小技巧保留一份组件化改造前的完整代码分支。这个东西在改造过程中帮了我们大忙——遇到任何以前是这样但现在怎么不行了的问题直接切回旧分支对比行为定位速度能快好几倍。直到改造彻底稳定之后我才删掉了这份备份分支。组件化这条路走一次就知道值不值。它不会让你的功能瞬间变多但会让你的团队在功能越加越多的时候依然保持快速和平稳。如果你也在犹豫要不要动手我的建议是选一个边界最清晰的模块先试点跑通它。试点的收益会比任何理论分析都有说服力。