什么是 Monorepo ?
Monorepo是一种代码管理模式,指在一个项目仓库 (repo) 中管理多个模块/包 (package)。与 Monorepo 相对的是 Multirepo(或Polyrepo),也就是我们常见的每个模块建一个 仓库。 Google、Facebook、微软等公司已经使用了很多年,Vue3、Yarn2 等知名项目现在也改用了 Monorepo。
一个 Monorepo 项目目录可能是这样的:
├── CHANGELOG.md
├── README.md // monorepo 配置,注册子项目
├── package.json
├── packages // 子项目目录
│ ├── package1
│ │ └── package.json
│ └── package2
│ │ └── package.json
│ └── package3
│ └── package.json
为什么使用 Monorepo ?
当你需要维护多个项目,多个项目之间有依赖,这些项目共用相同的基础设施(构建工具、lint)的时候,使用monorepo 会带来很多好处。
- 常用的包管理工具像 yarn、npm 都会做依赖提升,使用 monorepo 能减少依赖安装时间,同时也减少空间占用。
- 有依赖的项目之间调试非常方便,上层应用能够感知其依赖的变化,可以很方便的对依赖项进行修改和调试。
- 几个项目共用基础设施,不用重复配置。
Workspaces
Workspaces 是设置包架构的一种新方式。他的目的是更方便地使用 monorepo,具体就是能让你的多个项目集中在在同一个仓库,并能够相互引用 -- 被依赖的项目代码修改会实时反馈到依赖项目中。Monorepo 中的子项目称为一个 workspace,多个 workspace 构成 workspaces。
使用 workspaces(以 yarn 为例)好处:
- 依赖包可以被 linked 到一起,这意味着你的工作区可以相互依赖,代码是实时更新的。这是比 `yarn link`` 更好的方式因为这只会影响工作区部分,不会影响整个文件系统。
- 所有项目的依赖会被一起安装,这让 Yarn 更方便的优化安装依赖。
- Yarn 只有一个 lock 文件,而不是每个子项目就有一个,这意味着更少的冲突。
hoist
为什么使用 monorepo 能减少项目依赖安装时间和所占的空间呢 ?我们先回顾一下独立项目依赖安装的过程。
npm v3之前,依赖安装规则很简单,在安装依赖时将依赖放到项目的 node_modules 文件中;同时如果某个直接依赖 A 还依赖其他模块 B,作为间接依赖,模块 B 将会被下载到 A 的 node_modules 文件夹中,依此递归执行,最终形成了一颗巨大的依赖模块树。
这样的 node_modules 结构,简单明了、符合预期,但对于大型项目来说可能会安装很多重复的包,比如项目直接依赖 A 和 B,但 A 和 B 都依赖相同版本的模块 C,那么 C 会重复出现在 A 和 B 依赖的 node_modules 中。
这种重复问题使得安装结果浪费了较大的空间资源,也使得安装过程过慢。因此 npm v3 之后,node_modules 的结构改成了扁平结构。
扁平化安装
假设项目直接依赖 A 和 C ,他们分别依赖 B v1.0 和 B v2.0,他们的依赖树结构如下图:
扁平化过程如上图所示。
一个思考题:
还是上面那个项目,如果此时项目原始结构是:
那么扁平化安装的结构是下图哪个呢?
图3-1 ?
图3-2 ?
两种情况都有可能取决于依赖 A 和 C 的安装顺序。如果先安装了 A,所以 A 的依赖 B v1.0 率先被安装在顶层 node_modules 中,接着 C 和 D 依次被安装,C 和 D 的依赖 B v2.0 就不得不安装在 C 和 D 的 node_modules 当中了。
如果项目中先依赖并安装了 A ,然后又新增依赖 B 和 D,就会变成图3-1,从图中可以看出 B v2.0 明显被重复安装了多次。这时我们可以删除 node_modules 并重新安装得到更清爽的结构 图 3-2, 也可以使用 npm dedupe 命令。而 Yarn 在安装依赖时会自动执行 dedupe 命令。
hoist in workspaces
在 monorepo 项目中引进了一种新的层级结构,这种结构不再强依赖于 node_modules
来建立模块间的依赖关系。如下图所示:
通过将子项目和依赖提升到父项目的node_modules
中(monorepo/node_modules
),我们减少了 B v1.0 的重复安装。
同时 package-1 和 package-2 也被提升到了根 node_modules
中,这就是 monorepo 的 workspace 能让你像正常使用 npm 包一样使用其他子项目的秘密了。同时node_modules
中的子项目软链到 packages/
目录,所以被依赖子项目代码的变化也会实时更新到依赖子项目中。比如想在 package-1 中调试 package-2,只需要在 package-1 的package.json 文件中加上 ‘package-2’ 依赖就OK了,完全不用诸如 npm link
、yalc 工具 、拷贝package-2的打包文件到 node_modules 中等操作,调试过程变得非常丝滑~
一个小 tips,package-2 的包名是/package-2/package.json#name
的字段值而不是文件夹名。
可能遇到的问题
找不到 xxx module
出现这个问题的时候通常有两个原因:
-
没有在子项目 package.json 中声明
如果子项目 A 和 B 都依赖了包 axios,但是在 A 的package.json 中没有声明 ‘axios’ 依赖。你本地开发没有问题因为其他 packages 依赖了这个包。但是在上线构建的时候就会报错,因为在编译的时候将会取消 Package Manager 的 workspace 状态,变成根据子项目的 package.json 从 npm 中拉取依赖进行编译。因为此时 package.json 中的依赖表不完整,就会有找不到依赖的错误。
这个问题可配置插件来检查提醒。
-
某个依赖不支持 monorepo 模式
这种情况应该比较少出现。Node 是递归查找模块的: 当需要找 package A 的时候,查找顺序是:
node_modules/A
--> ../node_modules/A
--> ../../node_modules/A
... 直到查找到全局的node_modules
目录。
依赖被 hoist 之后,遵循此规范的工具可也以正常工作。但是也有一些工具不是按照这种规范查找的,而是假设依赖位于本项目下的 node_modules
中。解决这个问题一个可能的方案是将根目录下的依赖包都软链到子项目的node_modules
中。package manage 没有自动去这样做,更建议工具维护者能迁移到兼容模式。
hoist 的问题及破解: pnpm
上面提到,依赖提升可能会导致本地开发正常,线上无法编译的问题。当时我们的方案是使用插件 lint,提醒我们及时声明依赖。但是依靠插件始终是不足够可靠的,有没有其他方式可以解决该问题?
答案是有的,那就是pnpm。pnpm 开创了一套与npm v7、yarn 完全不同的新的依赖管理机制,这也是在这里单独讲 pnpm 的原因。
pnpm 节约更多的磁盘空间和更快的安装速度
monorepo 把子项目的依赖提升到根目录中,减少了部分依赖重复安装。但是如果有多个 monorepo 项目,并且多个monorepo 之中有相同的依赖,那么依赖依然会被安装多次。所以依赖安装还有优化空间,pnpm 就把这个事情做到了极致,他将依赖存储在一个可内容寻址的 store 中,基于此:
- 如果使用了同一个依赖的不同版本,只会把不同的文件存储到 store 中。比如,如果 express 有 100个文件,一次版本更新只改了其中1个文件,
pnpm update
只会把这个新的文件加入到 store 中,而不会克隆整个依赖。 - 所有的文件都被存储到磁盘上一个单独的地方。当依赖被安装的时候,这些文件就会被硬链接到那个地方,而不用消耗额外的磁盘空间。这就能让我们跨项目的复用相同版本的依赖
pnpm 全新的依赖管理机制
我们用 pnpm 初始化一个项目,并且安装一个依赖
pnpm init -y
pnpm install express
看一下此时的 node_modules 里的内容:
.pnpm
express
.modules.yaml
node_modules
里只有文件夹 .npm
和 express
,详细结构可以在这里看。
这样就保证项目中只能访问到我们安装过的 express
,不会再出现没有在 package.json
声明但是能使用的情况。
pnpm 与 monorepo
- 一致的 node_modules 结构:项目声明的依赖只在项目的 node_modules 下存在,peerDependencies 也能被正确 resolve
- 不同项目的 lock 项是独立的。pnpm 为每一个版本的依赖都单独维护了一个 lock 项,对于上面的
react@^16.13.1
的问题,在 pnpm 将pattern
和version
分开了,如果一个项目的 package.json 没有改变过,那么其依赖就不会变(PS: rushjs 的 lock 也是基于特定的工具,所以如果 rush 用 yarn 装依赖,也会有类似问题)。 - pnpm 原生支持只对特定子项目安装依赖,同时保持一致的 node_modules 结构
- pnpm 更快,依赖安装速度是 yarn 的 2-3 倍
参考资料
- classic.yarnpkg.com/en/docs/wor…
- yarnpkg.com/features/wo…
- github.com/lerna/lerna…
- npm workspaces
- pnpm.io/blog/2020/0…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!