Skip to content

十年 Angular 项目,团队分开后如何真正拆开代码仓库

Published: at 18:3010 min read
目录

最近遇到一个很典型、也很难靠某条 Git 命令解决的问题。

这是一个运行了十年的 Angular 项目。过去由 A、B 两个团队共同维护,两个主要业务的代码已经分别放在独立目录中,但仍然共享一部分业务代码和 common 组件。B 团队还把部分代码发布成了 npm 包,不过这个包不只是 UI 组件,里面也包含更多属于 B 领域的业务逻辑。

现在团队已经分开,却还在维护同一个仓库。一个团队要修改代码,经常需要先通知另一个团队,确认影响、等待回复、协调发布时间。代码仓库逐渐变成了一把人工锁。

这次的约束也很明确:不考虑 monorepo,最终要形成两个可以独立开发和发布的项目。

真正的问题不是 Git,而是所有权

从 Git 的角度看,拆分并不困难。

只要 Repo 2 从 Repo 1 的完整历史创建,两个仓库就拥有共同祖先。Repo 2 可以把 Repo 1 配置成 upstream,然后执行 fetchmergecherry-pick。两个不同的远程仓库并不会阻止 Git 合并提交。

但技术上可以合并,不代表组织上应该长期合并。

Repo 2 开始删除 B 业务代码后,Repo 1 还会继续修改这些代码。此时持续把 Repo 1 整体合并到 Repo 2,会反复遇到 modify-delete 冲突,并试图把已经删除的功能重新带回来。冲突不是偶然事故,而是在提醒我们:两个仓库的产品意图已经不同。

如果拆分以后还需要每天同步整个仓库,实际上并没有完成拆分,只是把原来的沟通成本换成了合并成本。

目录分开,不等于依赖分开

两个主要业务已经位于独立目录,这是很好的起点,但对一个十年 Angular 项目而言,目录边界只能算候选架构边界。

除了普通的 TypeScript import,还要检查:

拆分的第一份产物不应该是第二个 Git 仓库,而应该是一张真实依赖图。它需要回答:哪些依赖从 A 指向 B,哪些从 B 指向 A,哪些代码被双方同时引用,以及哪些运行时资源虽然没有 import,却仍然共同使用。

不要把所有 Shared 代码看成一类

现有代码至少应该分成四类:

代码类型拆分后的处理方式
A 团队业务代码只进入 A 项目,由 A 独立维护
B 团队业务代码只进入 B 项目,由 B 独立维护
双方使用的业务规则指定唯一 owner,再通过领域包或 API 提供
纯 common 组件和工具根据稳定性选择复制或独立 npm 包

这里最危险的不是 common 按钮或日期格式化工具,而是双方共同修改的业务规则。

共享使用没有问题,共享修改权才是问题。拆分以后,每个共享业务模块都必须得到明确处理:归 A、归 B、复制后分别演进,或者交给唯一 owner,通过版本化契约提供给另一方。

不应该再创建一个没有 owner 的 shared 区域,让两个团队继续在里面自由修改。那只会在新架构中保留旧问题。

npm 包不一定是通用组件库

B 团队已经发布的 npm 包包含 B 领域业务逻辑,所以它不应该因为发布到了 registry,就被称为普通 shared library。

更准确的定位是:这是一个由 B 团队拥有的版本化业务产品,A 团队可能是它的消费者。

如果 A 不需要其中的 B 业务能力,只是复用了几个通用组件,就不应该依赖整个包。可以把纯通用部分拆成更小的包,也可以复制少量组件,让两个项目分别维护。

如果 A 确实需要 B 的业务能力,则需要建立正式的消费关系:

最后一点对 Angular 尤其重要。如果共享包只支持某个 Angular 主版本,一个团队升级框架时,就可能迫使另一个团队同时升级。仓库虽然分开,发布节奏却仍被绑定。

业务逻辑应该放 npm 包还是后端

这取决于业务规则需要什么样的一致性。

无副作用的纯计算规则,需要在浏览器中执行,允许跟随前端版本发布,可以保留在有明确 owner 的领域 npm 包中。

但如果一条规则涉及权限、资金、合规,或者要求两个在线产品在同一时刻得到完全一致的结果,更适合放到后端领域服务或 BFF 后面的稳定 API。前端包只保留类型、校验和客户端适配。

否则,A 项目何时升级 npm 包,会决定新规则何时生效。两个产品可能在很长时间内对同一输入给出不同结果。

两个月过渡期只做选择性同步

Repo 2 应从 Repo 1 的完整 Git 历史创建,并在分割提交上打标签。保留共同历史是为了审计、追溯和必要时选择性带入提交,不是为了永久做全量同步。

过渡期适合进入同步白名单的变化包括:

B 的普通业务提交不进入 A 的仓库,A 的新功能也不再同时开发两份。

需要同步时,从源仓库 fetch 指定提交,建立独立的 sync/* 分支,通过 cherry-pick -x 保留来源哈希,再走目标仓库自己的 PR、测试和审核流程。

git fetch upstream
git switch -c sync/security-fix origin/main
git cherry-pick -x <source-commit>
git push -u origin sync/security-fix

同步流程必须有结束日期。否则“临时过渡”很容易变成一个永久的跨仓库维护系统。

共用 BFF 不代表 SSO 自动成立

两个前端继续使用同一个 Node.js BFF,确实可以降低迁移成本,但不能因此假设 SSO 自然成立。

至少需要验证:

单点登录不等于共享同一枚应用 Cookie。即使两个前端不能共享 Cookie,也可以分别跳转到同一个身份提供商。身份提供商已有登录会话时,用户通常不需要再次输入密码。

更值得讨论的是 BFF 的所有权。谁能修改和发布它?谁负责兼容两个前端?发生事故时谁响应?如果每次接口变化仍要求两个团队共同排期,仓库拆分就只完成了一半。

不要同时做 Angular 大版本升级

除非当前版本已经造成安全或构建阻塞,否则不建议把 Angular 大版本升级放进仓库拆分的关键路径。

拆分会改变目录、依赖、构建、测试和部署;Angular 升级又会改变编译器、包格式、路由、测试工具和 peer dependency。两组变量叠加以后,出现回归时很难判断问题来自哪里。

更稳妥的做法是先在当前版本建立两个可独立构建、测试、部署和回滚的项目,再由两个团队按照各自节奏升级 Angular。过渡期内,共享包需要明确支持哪些版本,以及支持到什么时候。

一个可执行的拆分顺序

我会把整个过程分为以下几个阶段:

  1. 冻结新增的跨目录依赖,避免拆分期间继续增加耦合。
  2. 梳理静态依赖和运行时依赖,为每个 shared 模块指定归宿。
  3. 从完整历史创建 Repo 2,并记录唯一的分割提交。
  4. 在当前 Angular 版本上建立两套独立构建、测试、部署和回滚流程。
  5. B 业务包由 B 单独拥有,A 只消费公开契约。
  6. 真正的企业共享业务规则由唯一 owner 通过领域包或 API 提供。
  7. 纯 common 组件根据变化方向决定复制还是独立发布。
  8. 过渡期只通过可审计 PR 同步必要修复。
  9. 验证 BFF、SSO、数据迁移、消息事件、监控和回滚。
  10. 设置最终冻结和对账窗口,关闭常规代码同步。

怎样判断拆分真的完成了

不是看两个仓库是否已经建立,也不是看两个项目能否偶尔构建成功。

真正的验收标准是:

这次拆分真正要消除的不是一个 Git remote,而是模糊的共同所有权。

Git 可以帮助两个仓库继续合并,但拆分的终点,应该是绝大多数时候不再需要合并。