先记住这个答案
monorepo 用单一仓库管理多包,跨包改动可在一个提交内原子完成,避免临时中间状态,但仓库历史大、全量克隆慢。git submodule 把每个子包独立仓库,主仓库只记录子模块提交指针,改动子包需要先推子仓库再更新主仓库指针,两步操作断裂会让 CI 或同事拿到不一致代码。工程上 monorepo 付出仓库规模与工具链成本,submodule 付出指针同步与跨仓联调成本。
- monorepo 跨包变更原子,submodule 需两步提交。
- submodule 指针不同步会造成缺代码或错版本。
- 仓库规模大时 submodule 隔离克隆,monorepo 需浅克隆。
提交粒度和同步机制的不同
monorepo 把所有前端包放在同一个 Git 仓库,工作区里各包只是目录。开发者一次 git commit 就能同时修改 packages/ui 和 packages/app,提交记录天然含全部改动。CI 在特定提交上构建,拿到的一定是这些包的一致版本,不会出现主仓引用了尚未发布子仓代码的窗口。
submodule 下主仓库的 Git 对象里保存的不是子目录内容,而是子模块仓库的一个提交 ID。子包内容变了,主仓库不会自动跟随,必须先在子仓库推送,再到主仓库执行 git add 子模块路径 和 git commit 来提升指针。这个两步动作如果漏掉,主仓库指针停在旧提交,别人 clone 或 fetch 后 checkout 会得到旧版子包。
# 在子模块目录内修改并推送
cd packages/design-system
git commit -am "feat: update button"
git push origin main
sleep 1
# 回到主仓库,更新指针并提交
cd ../..
git add packages/design-system
git commit -m "chore: bump design-system pointer"
git push origin main此命令展示 submodule 下每次子包变更都必须额外执行一次主仓指针提升,破坏原子性。
一个跨包改动需求的实践场景
设想一个有 6 个前端包(组件库、工具函数、管理后台、官网、BFF、配置)的中型团队,日常常有‘改按钮组件同时改后台引用’的联动需求。如果采用 submodule,把组件库和工具函数拆成独立子仓库,后台仓库记录组件库指针。假设开发者早上拉了后台仓库,组件库同事下午改了 API 并推了子仓,但没有后台仓库的人提交指针。此时后台仓库的指针仍指向旧提交,CI 和同事按主仓库指针都会检出旧版组件库;而开发者本地可能手动在子模块目录拉取了最新代码,导致本地与 CI/线上版本不一致。若后台新代码依赖组件库新 API,就会在线上出现半个接口不匹配。
处理是必须约定每次子包发布后立即在所有依赖方仓库更新指针,并让 CI 在依赖路径上禁止本地缓存。即便如此,由于没有原子提交,编辑器打开后台仓库时子模块可能自动 fetch 到新提交但主仓指针没动,出现工作区与索引不一致的『脏状态』,又需要人肉判断。改用 monorepo 后,这类改动一次提交解决,CI 的产出天然带一致版本,省去跨仓协调。
适用边界与失效条件
submodule 的边界在于它假设子包稳定独立,你的团队能接受子包改动分两次推并承担指针同步纪律。一旦联动频繁,这种纪律容易在 deadline 下失效。另外 git submodule update --remote 会自动拉取子仓远端分支的最新提交,但这并非受控的固定版本,因此多数工程仍会选择显式固定提交指针并手动同步。
monorepo 的边界在于仓库体积和权限粒度。所有代码历史集中后 .git 快速膨胀,克隆和检索变慢。权限上整个仓库对所有人可见,不开放可只给某包写权限的模型。应对是用 --filter=blob:none 部分克隆和 sparse-checkout 只取需要的目录,但初次搭建 CI 和本地缓存策略需要额外投资。若组织存在不允许共享代码或外包隔离要求,submodule 反而可用作物理隔离。
容易答错的地方
- 误认为 submodule 能自动跟随子仓更新
- 常见错误是以为
git submodule update会像普通依赖一样拉子仓最新。实际上它只把子模块切到主仓库记录的提交。若不显式在子仓 fetch/merge 再提升指针,拿到的永远是主仓库里那个固定 ID,不是远程分支最新。 - 以为 monorepo 必定慢到不可用
- 有人因为历史听过 '大仓库慢' 就拒绝 monorepo,实际用
--single-branch、浅克隆和部分克隆(blob:none)能让初始克隆只取必要历史。慢的根源不是仓库大小而是对象加载方式,很多 monorepo 团队通过 commit-graph 和包管理器 workspace 保持可接受速度。
面试官还会怎么问?
monorepo 里如何保证只构建改动的包?
需要包管理器(如 npm/pnpm/yarn workspaces)结合工具计算变更。通常用 git diff --name-only 找出改动路径,按目录映射到对应工作区包,再在 CI 里跳过未受影响包的测试或构建。注意每个提交的基线要选 merge-base,否则 diff 会被错误折叠。
submodule 的指针不刚好,用什么命令诊断?
在主仓库跑 git submodule status,若某一行的第一列为 +,说明该子模块当前检出的提交与主仓库记录的提交不一致。用 git submodule update --init --recursive 强制切回记录提交;若想更新指针,需进入子模块手动 fetch/checkout 再回到主仓 git add 提交。
子包要独立发版到 npm,monorepo 和 submodule 哪个更方便?
monorepo 更适合做语义化发布。工具如 changesets 能按包检测变更、组合版本,并可逐个发布。submodule 下子仓仍可独立发版,但每次发版都要同步多个主仓指针,流程更繁琐。若不要求一次改多包,submodule 也不差,但发版后所有依赖方更新指针往往是强约束。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。