前端应该知道的pnpm+monorepo知识点
什么是pnpm?
pnpm是一款现代化的包管理工具,在github上已经有了接近25k的star,应该说pnpm是包管理工具的未来。
pnpm官网介绍说pnpm是一个快速的,节省磁盘空间的包管理工具,那么pnpm到底强在哪里呢?
pnpm强在哪里
1. 节省磁盘空间
使用 npm 时,依赖每次被不同的项目使用,都会重复安装一次。而在使用 pnpm 时,依赖会被存储在内容可寻址的存储中,所以:
- 如果在不同的项目中使用了同样的包,在使用npm/yarn时,这个包会被安装多次,也就是说磁盘中会多次写入这块相同的内容,占用空间。而pnpm会将所有的文件存储在磁盘上的某个位置,在包被安装时,包里的文件会通过
hardlink
(硬链接)到这个位置,也就是说,在不同的项目复用同一个包时,pnpm只会安装一次,并且多次复用。实现了跨项目对同一版本包的共享。 - 对于包的不同版本,pnpm会将不同版本间有差异的文件添加至磁盘仓库中,使用
pnpm update
时也只会更新差异文件,极大程度上复用了之前版本的代码,不会因为部分文件的差异改变整个包的内容。
因此,pnpm在磁盘上节省了大量空间,这与项目和依赖项的数量成正比,并且安装速度要快得多!
2. 更快的安装速度
pnpm 分三个阶段执行安装:
- 依赖解析。 仓库中没有的依赖都被识别并获取到仓库。
- 目录结构计算。
node_modules
目录结构是根据依赖计算出来的。 - 链接依赖项。 所有以前安装过的依赖项都会直接从仓库中获取并链接到
node_modules
。
得益于优秀的包管理机制(见下文)和链接依赖,pnpm的安装速度远超npm/yarn。
3. 非扁平的 node_modules 目录
包是从全局 store 硬连接到虚拟 store 的,这里的虚拟 store 就是 node_modules/.pnpm。
我们打开 node_modules 看一下:
例如在项目中使用 pnpm 安装了一个叫做 express 的依赖,那么最后会在 node_modules 中形成这样两个目录结构:
node_modules/express/...
node_modules/.pnpm/express@4.17.1/node_modules/xxx
其中第一个路径是 nodejs 正常寻找路径会去找的一个目录,如果去查看这个目录下的内容,会发现里面连个 node_modules 文件都没有:
▾ express
▸ lib
History.md
index.js
LICENSE
package.json
Readme.md
实际上这个文件只是个软链接,它会形成一个到第二个目录的一个软链接(类似于软件的快捷方式),这样 node 在找路径的时候,最终会找到 .pnpm 这个目录下的内容。
其中这个 .pnpm 是个虚拟磁盘目录,然后 express 这个依赖的一些依赖会被平铺到 .pnpm/express@4.17.1/node_modules/
这个目录下面,这样保证了依赖能够 require 到,同时也不会形成很深的依赖层级。
也就是说,所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm 下,然后之间通过软链接来相互依赖。
pnpm较npm,yarn解决了什么问题
依赖嵌套
在npm1、npm2中,node_modules
的目录结构是这样的
node_modules
└─ foo
├─ index.js
├─ package.json
└─ node_modules
└─ bar
├─ index.js
└─ package.json
如果一个包依赖于另外一个包,会进行依赖嵌套,如果依赖包过多,会造成大量重复的包被安装。 更大的问题是在windows系统中的文件路径最长是 260 多个字符,这样嵌套是会超过 windows 路径的长度限制的。
于是在npm3和yarn都使用了扁平化依赖
来解决这个问题。
可以看到我们安装了一个包后,node_modules多了很多依赖,这就是扁平化依赖的结果。
现在的依赖层级类似于这种
node_modules
├─ foo
| ├─ index.js
| └─ package.json
└─ bar
├─ index.js
└─ package.json
yarn将所有的依赖都拍平到node_modules里,这样在安装新的包时,只需要在node_modules中寻找即可,如果找到相同版本的包就直接复用,解决了包大量重复安装的问题。但是扁平化依赖带来了什么问题呢?最主要的问题是:幽灵依赖。
幽灵依赖
什么是幽灵依赖? 举个例子:在项目中,我们可能会使用一个包A,A可能依赖B、C这两个包,在使用npm3/yarn安装A时,由于其采用扁平化依赖管理,A、B、C这三个包都会被安装在node_modules下,而事实上,我们项目的package.json是这样的
{
"name": "tese",
"version": "1.0.0",
"main": "lib/index.js",
"dependencies": {
"A": "^1.0.0",
}
}
也就是说package.json
中只存在一个包,而NodeJS 的 require()
函数能够在依赖目录找到A、B、C三个包,因为 require()
在查找文件夹时 根本不会受 package.json 文件影响。
现在,你大概知道可能会发生什么事情了:在项目开发中,我们可能会直接使用B/C这些幽灵依赖,如果项目迭代中A不再被使用,使用幽灵依赖会因为无法找到依赖而报错。
而pnpm使用软硬链接的方式,解决了这个问题。(见上文)
什么是monorepo?
Monorepo 其实就是将多个项目 (pacakage 软件包)放到同一个仓库 (Repo) 中进行管理。这种代码组织形式可以更好地管理多 Package 项目。主要的优点有:
-
可见性 (Visibility): 每个开发者都可以方便地查看多个包的代码,方便修改跨 Package 的 Bug。比如开发 Admin 的时候发现UI 有问题,随手就可以修改。
-
更简单的包管理方式(Simpler dependency management): 由于共享依赖简单,因此所有模块都托管在同一个存储库中,因此都不需要私有包管理器。
-
唯一依赖源(Single source of truth): 每个依赖只有一个版本,可以防止版本冲突,没有依赖地狱。
-
原子提交: 方便大规模重构,开发者可以一次提交多个包(package)。
在一个monorepo项目中,代码结构大致是这样的
├── packages
| ├── pkg1
| | ├── package.json
| ├── pkg2
| | ├── package.json
├── package.json
在 packages 存放多个子项目,并且每个子项目都有自己的package.json
Monorepo相较于Multirepo的优缺点
场景 | MultiRepo | MonoRepo |
---|---|---|
代码可见性 | ✅ 代码隔离,研发者只需关注自己负责的仓库 ❌ 包管理按照各自owner划分,当出现问题时,需要到依赖包中进行判断并解决。 | ✅ 一个仓库中多个相关项目,很容易看到整个代码库的变化趋势,更好的团队协作。 ❌ 增加了非owner改动代码的风险 |
依赖管理 | ❌ 多个仓库都有自己的 node_modules,存在依赖重复安装情况,占用磁盘内存大。 | ✅ 多项目代码都在一个仓库中,相同版本依赖提升到顶层只安装一次,节省磁盘内存, |
代码权限 | ✅ 各项目单独仓库,不会出现代码被误改的情况,单个项目出现问题不会影响其他项目。 | ❌ 多个项目代码都在一个仓库中,没有项目粒度的权限管控,一个项目出问题,可能影响所有项目。 |
开发迭代 | ✅ 仓库体积小,模块划分清晰,可维护性强。 ❌ 多仓库来回切换(编辑器及命令行),项目多的话效率很低。多仓库见存在依赖时,需要手动 npm link ,操作繁琐。 ❌ 依赖管理不便,多个依赖可能在多个仓库中存在不同版本,重复安装,npm link 时不同项目的依赖会存在冲突。 | ✅ 多个项目都在一个仓库中,可看到相关项目全貌,编码非常方便。 ✅ 代码复用高,方便进行代码重构。 ❌ 多项目在一个仓库中,代码体积多大几个 G,git clone 时间较长。 ✅ 依赖调试方便,依赖包迭代场景下,借助工具自动 npm link,直接使用最新版本依赖,简化了操作流程。 |
工程配置 | ❌ 各项目构建、打包、代码校验都各自维护,不一致时会导致代码差异或构建差异。 | ✅ 多项目在一个仓库,工程配置一致,代码质量标准及风格也很容易一致。 |
构建部署 | ❌ 多个项目间存在依赖,部署时需要手动到不同的仓库根据先后顺序去修改版本及进行部署,操作繁琐效率低。 | ✅ 构建性 Monorepo 工具可以配置依赖项目的构建优先级,可以实现一次命令完成所有的部署。 |
使用pnpm构建monorepo的优点
monorepo由于自身特性,仓库里的模块需要处理与内部模块之间的关系,基于此,pnpm由于其优秀的依赖管理机制以及对workspace的支持可以说是轻量化构建monorepo的最好方式。
实战
我们使用pnpm Workspace来创建
- 首先,你需要pnpm这个工具,我们直接使用npm来安装
npm i pnpm -g
- 然后,在根目录重新初始化一个package.json。
pnpm init
- 初始化工作空间 在monorepo项目中,每个软件包会存放在工作空间,方便管理。
首先需要创建一个 pnpm-workspace.yaml
,这个文件用于声明所有软件包全部存放在 packages 目录之中。其实目前 monorepo 风格的项目也普遍使用 packages 作为默认软件包目录位置。
packages:
# all packages in direct subdirs of packages/
- 'packages/*'
monorepo存在的缺点
上文我们谈到了那么多monorepo的优点,但是不可避免的,monorepo缺点也十分明显:
- 性能问题。在仓库规模足够大时,git操作会花费更长时间,安装成本也较高
- 权限问题。多人开发时权限管理较为困难,同时代码合并冲突问题会更复杂。 3.打包构建:随着项目数量的增加,构建和打包的时间可能会较长,影响部署速度,需要专门优化。
参考资料
转载自:https://juejin.cn/post/7267585231791341605