从零开始:一个前端开发者的 Monorepo 探索笔记

32 阅读5分钟

写在前面:这篇笔记不是教科书式的定义堆砌,而是完全我作为一个新手,从“怎么建目录”开始,一步步踩坑、质疑、直到理解 Monorepo 核心价值的心路历程。如果你也有同样的困惑,这篇文档就是为你准备的。


第一阶段:上手与困惑 —— "这玩意儿怎么跑起来?"

Q1: 我想搞个 Monorepo,第一步该敲什么命令?

不要上来就去学复杂的工具(Nx/Lerna),先用最简单的 npm 原生命令感受一下架构。

标准起手式

  1. 创建根目录npm init -y
  2. 创建子包npm init -w packages/ui -y
    • 注意:不需要手动去创建文件夹再进去初始化,直接用 -w (workspace) 一步到位。

Q2: npm init -w 到底帮我干了什么?

我一开始以为它只是建了个文件夹,后来发现它干了两件大事:

  1. 物理上:创建了 packages/ui 目录和里面的 package.json
  2. 逻辑上:悄悄修改了根目录的 package.json,加了 workspaces 字段。
    • 感悟:这个字段就是“房东”和“租客”的合同,没有它,这就是一堆普通的文件夹。

Q3: 为什么大家都叫 packagessrc?我能改名吗?

答案:能改,但别改。

  • packages: 只是约定俗成。你想叫 libsapps 都可以,只要在 package.jsonworkspaces 里配对就行。但叫 packages 别人一眼就能看懂。
  • src: 更是约定俗成。虽然技术上你可以把源码直接扔根目录,但为了和 dist(构建产物)区分开,用 src 是职业素养的体现。

第二阶段:结构与依赖 —— "依赖到底装在哪里?"

Q4: 根目录和子包的 package.json 区别在哪?

我仔细对比了一下:

  • 子包:就是一个标准的 NPM 包,有 dependencies,有 scripts,跟普通项目没区别。
  • 根目录:它是个“壳”。它通常 private: true(不发布),且多了个灵魂字段 workspaces
    • 本质:根目录不再是代码的家,而是管理代码的基地

Q5: 依赖管理最头大的问题:子包用到的库,根目录要不要装?

困惑:如果 ui 包用了 dayjsutils 包也用了 dayjs,我是不是得装两遍? 解惑

  1. 声明层面必须装两遍。在 uiutils 里都要写上依赖。因为它们理论上是独立的。
  2. 物理层面只存一份。npm 会自动把它们“提升”(Hoist)到根目录的 node_modules 里。
    • 结果:你电脑硬盘里只有一份 dayjs,但两个包都能用。

Q6: 根目录的配置文件(tsconfig/eslint),子包能用吗?

能,而且必须能。 这就是 Monorepo 的一大优势。子包通过 extends 关键字继承根目录的配置。

  • 哲学思考:这是否意味着子包不独立了?
    • 开发时:是的,它依赖根目录的“营养”(配置)。
    • 发布时:它是独立的。构建出的 JS 代码不再包含这些开发配置,别人拿去就能用。

第三阶段:灵魂拷问 —— "我为什么要这么折腾?"

Q7: 如果只是把文件夹放在一起,Monorepo 到底图什么?

这是我最开始的质疑:这不就是把几个项目强行塞在一个文件夹里吗? 直到我遇到了这个场景:

  • 以前:改了组件库 -> build -> publish/link -> 切换到业务项目 -> install/rebuild -> 重启 -> 终于看到效果(甚至可能报错)。
  • 现在:左屏改组件库,右屏业务项目实时热更新
  • 结论:它不是物理上的“放在一起”,它是源码级的联通

第四阶段:工程化与构建 —— "项目多了跑得动吗?"

Q9: 几十个包一起跑,流水线会不会炸?

绝对会炸。如果每次提交都把 20 个包全构建一遍,CI 可能会跑半小时。 这时候就需要引入增量构建的概念。

Q10: Turborepo 是什么?它是怎么救命的?

我本来以为它很难,其实它就是个任务调度器

  • 它的魔法
    1. 看懂关系:它知道 A 依赖 B,所以它会先跑 B 再跑 A(不用你自己写脚本控制顺序)。
    2. 偷懒(缓存):它会算 Hash 值。如果这个包没改过,它直接把上次生成的 dist 扔给你,耗时 0秒
  • 怎么读:/ˈtɜːrboʊ ˈriːpoʊ/ (特波-瑞剖)。

Q11: Lerna 又是谁?

它是“老祖宗”。

  • 以前:它是万能的神,管安装依赖,管发版。
  • 现在:依赖管理被 npm/pnpm 抢了,构建被 Turbo/Nx 抢了。
  • 剩余价值:它现在主要用来发版(自动更新版本号、生成 Changelog)。如果你不需要复杂的发版流程,其实可以不用它。

第五阶段:总结与避坑

Git 是一个还是多个?

一个。 这就是 "Mono" (Single) 的含义。所有代码在一个 Git 仓库里,版本号统一管理,提交记录原子化。

最大的坑是什么?

幽灵依赖 (Phantom Dependencies)

  • 现象:你忘了在子包里装 lodash,但代码居然能跑(因为根目录有)。
  • 后果:上线后报错找不到模块。
  • 对策用 pnpm。它默认禁止这种行为,比 npm 更严格、更安全。

这份笔记真实记录了从迷茫到清晰的过程。Monorepo 不是银弹,但在处理多包关联开发时,它是真正的生产力神器。