前言:本文分享将两个依赖Git分支维护的网站迁移为Monorepo的完整实战,核心原则是先物理后逻辑:先物理平移代码,再抽公共代码与组件。覆盖pnpm workspace搭建、Vue项目共享包下沉、公共组件改造与CI/CD流水线改造,并总结依赖地狱、版本锁定等踩坑经验,帮助团队在业务不中断的前提下完成代码仓库重构。
给正在高速行驶的车,换了一次引擎
先给你看个画面。
一家公司,两个网站,一个叫主站,一个叫县域站。长得几乎一模一样,同一个 Logo,同一套配色,连导航栏的菜单都大差不差,就差几块业务页面,差几列表格数据。
按理说,这种项目,就该是一套代码,对吧。
不是。
它们是两个分支,同一个仓库里,一个叫 master,一个叫 county,靠着 Git 分支,各过各的日子。
我跟你说,这日子是真的难熬。主站这边改了一个公共的表格组件,县域那边就得把改动手动搬一遍,搬的时候还得提心吊胆,别把县域自己改过的东西给覆盖了。改着改着,两个分支的差异越来越大,偶尔想合并一次,冲突文件几十个,谁都不敢碰,最后只能靠先到先得这种野蛮方式解决问题。
真的就是一声叹息。。。

你可能会说,两个站就这么将就着维护,也不是活不下去。确实,能活,很多团队就这么活了五六年。但每一次改公共代码,都是在给未来埋雷,这个成本是复利增长的,总有一天,会把你击穿。
所以当团队拍板,把这套靠分支续命的东西,重构成一个 Monorepo 的时候,我脑子里蹦出来的第一句话是,这等于给一辆正在高速行驶的车,换引擎。
车不能停,业务一天都不能断。
引擎,还得换。
写这种文章,说实话我是有心理负担的。我又不是什么架构大师,顶多算一个在代码堆里摸爬滚打了几年的普通人,这套方案也谈不上什么灵丹妙药,中间踩过的坑,比我记住的步骤多得多。但我觉得里面有一个原则,是真的值钱,值得掏出来跟各位聊聊。
六个字,「先物理,后逻辑」。
意思就是,先把代码原样搬过去,让它在新的目录结构下能跑起来,再去想怎么抽公共代码、怎么拆包。顺序一反过来,必死。
你可能觉得,重构嘛,重头戏肯定是抽公共代码啊,怎么上来就说搬家?你听我说完,就明白了。
第一步,物理平移,先让车跑起来。
这一步的目标只有一个,让主站代码在 Monorepo 的骨架下,能正常打包。做法不复杂,在根目录建一个 pnpm 工程,写上 pnpm-workspace.yaml,把 apps 和 packages 两个目录划出来,有条件的话再上一个 Turborepo,为后面多包并行构建做准备。然后在根目录把 ESLint、Prettier、Husky、Commitlint 这些规范统一沉下去,一套配置,全员通用。
接着就是把主站分支的代码,整体剪切进 apps/main-site,改一改脚本路径。
然后验证,pnpm i 能装上,启动能跑起来,build 能出包。
就这些。不抽代码,不拆组件,什么都不动。
怎么说呢,这一步最大的难点,不是技术,是忍住。代码只是搬了个家,看起来什么都没变,很容易让人怀疑自己在干嘛,是不是在做无用功。但这是整个工程的地基,地基没打牢,后面全是空中楼阁。
先物理,后逻辑,顺序不能乱。
第二步,县域入驻,划定边界。
把县域分支的代码也整体拉进来,放进 apps/county-site,同样验证一遍独立启动、独立打包。
然后重头戏来了,做一次目录级的 Diff,把两套代码按差异度分类。
这一分,就分出了三种东西。第一种,完全一致的,比如工具函数、网络拦截器、常量枚举,这是后面要下沉的核心目标。第二种,逻辑一致但 UI 有别的,比如通用的表格组件、表单组件,这是要改造的对象。第三种,完全定制的,县域特有的业务页面,主站独有的门户展示页,这种保持原样,别碰。
你可能会问,Diff 一下有什么难的?难的不是 Diff 这个动作,是分类的判断。边界划错了,后面 packages 的边界就全乱了,到时候想改都改不回来。
这一步做完,你手里就有了一张地图,哪里能动,哪里不能动,一目了然。
第三步,剥离公共基建。这块是整个迁移信息密度最高的地方,我尽量讲人话。
先在 packages 下面建两个包,shared-utils 和 shared-api,把两边完全一致的代码挪进去。然后,因为这两个站都是 Vue 2 加 Webpack 的生态,vue.config.js 里必须做三件事,缺一件都会出事。
第一件,transpileDependencies,强制 Babel 编译软链接过来的包。为啥?因为 pnpm 装包走的是软链接,Webpack 默认不编译 node_modules 里的代码,Babel 看都不看它一眼,报错报到你怀疑人生。第二件,config.resolve.symlinks(false),关掉软链接解析,不然 ESLint 找文件路径的时候会迷路。第三件,也是最骚的一件,用 alias 把 Vue 实例锁死。不锁的话,主站引一份 Vue,共享包再引一份 Vue,两个实例打架,什么 Unknown custom element 这种玄学报错全来了。锁到一个文件上,保证全天下只有一个 Vue。
三件套配齐,再把业务侧的 import 路径全局替换一遍,从 @/utils/xxx 换成 @my-org/shared-utils。
到这一步,两个站的公共基建,算是真正合流了。

第四步,UI 组件下沉。这块是整个迁移的深水区。
把基于 Element-UI 封装的业务组件,下沉到 packages/vue2-ui-components。下沉本身不难,难的是那 20% 的定制化差异怎么处理。处理不好,组件库就会膨胀成一个没人敢动的怪兽。
常见的打法,无非三种,一种比一种高级。
第一种,Props 开关。如果只是县域比主站多显示两列数据,那就传一个配置数组进去,完事。第二种,插槽分发。如果组件骨架一样,但某一块区域长得完全不同,那就留一个 slot,把差异部分的渲染权,交还给各个站自己。第三种,策略模式,压轴的。如果连点击按钮之后的行为都不一样,那组件内部就别写死任何业务逻辑,把行为通过 props 或者 provide/inject 传进来,组件只负责触发,emit 一下,剩下的,谁传进来的谁说了算。
你看这个递进,从加个开关,到留个口子,再到把脑子还给你。越往后,组件越轻,业务越自由。
我自己的感受是,这块是整个迁移最磨人的阶段。一个组件为了兼容两个站,props 可能会越写越多,花的时间比原来两套代码分别维护还长。一开始可能会有点笨拙,这很正常,等策略定型了,就顺了。
第五步,CI/CD 流水线改造,然后全量回归。
Jenkins 或者 GitLab CI 的脚本,从原来的直接构建,升级成基于依赖图的精准构建,比如 pnpm –filter main-site build,构建主站的时候,公共包也会被一并构建,但不会动县域一根汗毛。
然后全量 QA 回归,重点盯三类地方,路由跳转、文件上传、复杂表单提交。为啥盯这三类?因为它们最容易涉及深层对象引用,公共包一换,引用一断,就是线上事故。
整个迁移走下来,快的一两个月,慢的能拖半年,全看两个分支这些年攒下的差异有多大。
五步走完,你以为就完了?
没有。真正的坑,都在后面等着呢。我跟你说几个这个项目里真实踩过的坑,你提前有个心理准备。
坑一,依赖地狱。Monorepo 里有依赖提升机制,有时候 main-site 没装 lodash 也能跑,因为它用的是别的包提上来的版本。本地好好的,一到 CI,环境干净了,直接崩。你敢信???原则只有一个,每个 package.json 必须显式声明自己直接用的所有依赖,一个都不能省。
坑二,版本薛定谔。主站用 vue-router@3.0,县域用 vue-router@3.5,两边的 API 都对不上,共享包一出来就打架。这种版本问题,你不去测,永远不知道它是死是活。解法也简单,Vue、Vuex、Vue-Router、Element-UI 这些核心生态,在根目录用 overrides 或者 resolutions 全局锁死,谁都不许私自升。
坑三,CSS 样式污染。组件下沉之后,如果不写 scoped,县域的样式就能顺着公共组件,流到主站的全局样式池里,主站页面直接花掉。血泪教训,scope 记得写。
坑四,也是最难的一个,心智转换。改 packages 里的代码,等于同时改了两个站,但很多同学没有这个意识,改完在主站验证一下就提交了,县域第二天直接炸。这个问题的解法不在技术,在流程。迁移初期,packages 目录必须设置更严格的 Code Review 权限,提交公共组件的代码之前,双端都得预览确认。
技术上的坑都能填,人的认知,最难填。
当然,单仓库也不是什么银弹,它也有自己的烦恼。构建时间会变长,packages 的权限要单独管理,git 历史乱成一锅粥,这些都是实打实的代价。但比起两个分支的暗无天日,这点代价,值。

写到这,我突然想起了两千多年前的一件大事。
秦始皇统一六国之后,干的第一批大事里,有一件叫车同轨,书同文。为什么要干这个?因为之前六国各写各的字,各用各的度量衡,许慎在《说文解字》里说得很清楚,言语异声,文字异形。同一个字,在各国能写出好几种花样,文书往来要翻译,车轨宽度不统一,路上压出来的车辙都不一样。
你品品,那不就是一套靠分支管理的国家吗。每个诸侯国就是一个分支,各自 fork,各自演进,演进了几百年,最后要统一的时候,谁都合不动谁。
秦始皇干的事,就是把这些分支,并成一个 Monorepo。统一度量衡,是统一配置;书同文,是统一代码规范;车同轨,是统一基础设施。而且他也不是一夜之间推倒重来,是一步一步来的,先修路,再通文。
跟我们的迁移,一模一样。
回到开头那个画面。那两个长得一模一样的网站,现在还跑得好好的,业务一天没断过。看起来什么都没变,但底下的东西,已经彻底变了。
这就是换引擎的最高境界,乘客全程无感,只有开车的知道,发动机已经换了一台新的。
车同轨,书同文,这套两千多年前就被验证过的智慧,放在今天的技术栈里,依然好使。
如果你也正被两个分支折磨得死去活来,别急着上大方案,先从第一步开始,把代码物理搬过去,让车先跑起来。
先物理,后逻辑。
这个顺序,值得刻在每一个重构项目的墙上。
能做的,还是那句话,磨平一点点信息差。
哪怕,只是很小很小的一点。
/ 注意,本项目仅在 SyncMein 的500人小群分享。
/ 谢谢你看完了我的文章,我们下次再见吧。
/ 作者:明察
/ 投稿或爆料,请联系邮箱:mingcha@gqmg.com














暂无评论内容