npm
嵌套的 node_modules 结构(v1 / v2)
存在的问题
存在依赖地狱的问题和重复依赖安装的问题。
扁平化 (v3)
采用扁平化的方式,将子依赖「提升」(hoist),解决了依赖地狱的问题。
依赖查找算法:在安装新的包时,会不停往上级node_modules中查找。如果找到相同版本的包就不会重新安装,在遇到版本冲突时才会在模块下的 node_modules 目录下存放该模块子依赖,解决了大量包重复安装的问题,依赖的层级也不会太深。
存在的问题
幽灵依赖问题
指在 package.json 中未定义的依赖,但项目中依然可以正确地被引用到,这会存在潜在的问题
不确定性
同样的 package.json 文件,install 依赖后可能不会得到同样的 node_modules 目录结构,这是其依赖安装算法决定的。
依赖重复问题
依然存在重复的依赖问题(因为同一父级的 node_modules 只能存在一个公共依赖)。这种情况下,如果依赖自身存在副作用,那么就会导致多次副作用。
扁平化 + lock (v5)
在npm v5中新增了package-lock.json。当项目有package.json文件并首次执行npm install安装后,会自动生成一个package-lock.json文件,该文件里面记录了package.json依赖的模块,以及模块的子依赖。并且给每个依赖标明了版本、获取地址和验证模块完整性哈希值。通过package-lock.json,保障了依赖包安装的确定性与兼容性,使得每次安装都会出现相同的结果。
yarn
提升安装速度
在 npm 中安装依赖时,安装任务是串行的,会按包顺序逐个执行安装,这意味着它会等待一个包完全安装,然后再继续下一个。yarn 采用了并行操作,在性能上有显著的提高。而且在缓存机制上,yarn 会将每个包缓存在磁盘上,在下一次安装这个包时,可以脱离网络实现从磁盘离线安装。
lockfile 解决不确定性
在依赖安装时,会根据 package.josn 生成一份 yarn.lock 文件。 lockfile 里记录了依赖,以及依赖的子依赖,依赖的版本,获取地址与验证模块完整性的 hash。 即使是不同的安装顺序,相同的依赖关系在任何的环境和容器中,都能得到稳定的 node_modules 目录结构,保证了依赖安装的确定性。
与 npm 一样的弊端
yarn 依然和 npm 一样是扁平化的 node_modules 结构,没有解决幽灵依赖和依赖分身问题。
yarn lock vs npm lock
- 文件格式不同,npm v5 使用的是 json 格式,yarn 使用的是自定义格式
- package-lock.json 文件里记录的依赖的版本都是确定的,不会出现语义化版本范围符号(~ ^ *),而 yarn.lock 文件里仍然会出现语义化版本范围符号
- package-lock.json 文件内容更丰富,实现了更密集的锁文件,包括子依赖的提升信息
- npm v5 只需要 package.lock 文件就可以确定 node_modules 目录结构
- yarn.lock 无法确定顶层依赖,需要 package.json 和 yarn.lock 两个文件才能确定 node_modules 目录结构。node_modules 目录中 package 的位置是在 yarn 的内部计算出来的,在使用不同版本的 yarn 时可能会引起不确定性。
pnpm
在不考虑循环依赖的情况下,实际的项目依赖结构图为有向无环图(DAG),但是npm和yarn通过文件目录和node resolve算法模拟的实际上是有向无环图的一个超集(多出了很多错误祖先节点和兄弟节点之间的链接),这导致了很多的问题。pnpm通过硬链接与符号链接结合的方式,更加精确的模拟DAG来解决yarn和npm的问题。
非扁平化的node_modules
硬链接(hard link)节约磁盘空间
硬链接以理解为源文件的副本,使得用户可以通过不同的路径引用方式去找到某个文件,他和源文件一样的大小但是事实上却不占任何空间。pnpm 会在全局 store 目录里存储项目 node_modules 文件的硬链接。硬链接可以使得不同的项目可以从全局 store 寻找到同一个依赖,大大节省了磁盘空间。该策略会将包安装在系统的全局 store 中,依赖的每个版本只会在系统中安装一次。
符号链接(symbolic link)创建嵌套结构
软链接可以理解为快捷方式,pnpm在引用依赖时通过符号链接去找到对应磁盘目录(.pnpm)下的依赖地址。非扁平化结构。
优点
这套全新的机制设计地十分巧妙,不仅兼容 node 的依赖解析,同时也解决了:
- 幽灵依赖问题:只有直接依赖会平铺在 node_modules 下,子依赖不会被提升,不会产生幽灵依赖。
- 依赖分身问题:相同的依赖只会在全局 store 中安装一次。项目中的都是源文件的副本,几乎不占用任何空间,没有了依赖分身。
- 同时,由于链接的优势,pnpm 的安装速度在大多数场景都比 npm 和 yarn 快 2 倍,节省的磁盘空间也更多。
缺点
- 由于 pnpm 创建的 node_modules 依赖软链接,因此在不支持软链接的环境中,无法使用 pnpm,比如 Electron 应用。
- 忽略了 package-lock.json。npm 的锁文件旨在反映平铺的 node_modules 布局,但是 pnpm 默认创建隔离布局,无法由 npm 的锁文件格式反映出来,而是使用自身的锁文件pnpm-lock.yaml。
- 因为依赖源文件是安装在 store 中,调试依赖或 patch-package 给依赖打补丁也不太方便,可能会影响其他项目。
- 子依赖提升到同级的目录结构,虽然由于 Node.js 的父目录上溯寻址逻辑,可以实现兼容。但对于类似 Egg、Webpack 的插件加载逻辑,在用到相对路径的地方,需要去适配。
useful posts
- 关于依赖管理的真相 — 前端包管理器探究
- JavaScript package managers compared: npm, Yarn, or pnpm?
- 包管理器的发展史
- Pnpm: 最先进的包管理工具
- 关于现代包管理器的深度思考——为什么现在我更推荐 pnpm 而不是 npm/yarn?
- Yarn Plug'n'Play可否助你脱离node_modules苦海?
- 字节的一个小问题 npm 和 yarn不一样吗?
- package-lock.json和yarn.lock的包依赖区别
- npm 和 yarn 缓存策略对比
- 基于符号链接的 node_modules 结构
- 平铺的结构不是 node_modules 的唯一实现方式