包管理器
npm,yarn,pnpm
npm
npm 使用最多的功能是作为一个在线的包管理工具。npm 本身不能执行任何包,对于本地项目的包,需要写入 package.json 文件,然后通过 npm 解析 package.json 文件,解析到包的 .bin 目录下,在 bash 中执行。
.bin文件夹存储了当前项目里使用的所有模块的软链接,连接到对应模块的安装目录。
npm scripts 工作原理
自定义脚本命令。
局部安装的包,直接在 terminal 中使用会无法找到。
npm run 命令,会新建一个 shell ,将当前项目中 node_modules/.bin 的绝对路径加入环境变量中,执行完语句再删掉新加的环境变量。
指令钩子
在执行 npm scripts 命令(无论是自定义还是内置)时,都经历了 pre 和 post 两个钩子,在这两个钩子中可以定义某个命令执行前后的命令。
比如在执行 npm run serve 命令时,会依次执行 npm run preserve、npm run serve、npm run postserve。如果没有指定则会跳过。
"scripts": {
"preserve": "xxxxx",
"serve": "vue-cli-service serve",
"postserve": "xxxxxx"
}
npx
npm 内置了 npx 的包,可以直接使用。
npx 算是一个简单的 cli 工具,可以更方便地执行一些 npm 的包,也可以减少对环境变量的污染。
npx 原理:运行时检查node_modules/.bin路径以及环境变量。
npx 功能:
-
不安装包的情况下直接执行一些包,减少对磁盘的使用。
下载到临时目录,过一段时间会自动清除。
-
方便切换 node 版本,临时执行一些命令。
-
可以直接执行 GitHub 的模块源码。(必须是包含
package.json和入口的模块代码)npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32
node_modules 结构
npm@3 之前,node_modules 结构是干净、可预测的。node_modules 下的每个依赖都有自己的 node_modules 文件夹,且在 package.json 中指定了所有的依赖。
node_modules
└─ foo
├─ index.js
├─ package.json
└─ node_modules
└─ bar
├─ index.js
└─ package.json
产生的问题:
- 嵌套安装,node_modules 依赖层级过深,可能超出操作系统最长路径限制。依赖层级过深也导致文件查找复杂度上升,影响性能。
- 当多个不同的依赖依赖同一个依赖时,相同的依赖会被多次安装,占用大量的空间资源。
npm@3+ 和 yarn 之后,node_modules 结构发生了变化,变成了扁平化结构,但产生了幽灵依赖的问题。
node_modules
├─ foo
| ├─ index.js
| └─ package.json
└─ bar
├─ index.js
└─ package.json
如果一个包的多个版本在项目中被依赖,node_modules 会提升该包第一个安装的版本到顶层,而其他的版本还是按照之前的方式会被放在各自的依赖里。

这种提升第一个安装的包到顶层的方式会导致依赖结构不确定的问题,也是后面 lock 文件诞生的原因。
npm@5+之后,添加 lock 文件记录依赖树信息,进行依赖锁定,保证依赖安装确定性。
yarn
yarn 也是包管理器,与 npm 没有本质的区别,都是管理和安装包的,解决了早期 npm 的一些问题并提升了管理包的效率。但在最新版的 npm 和 yarn 安装速度和使用体验并没有太大的差距。
早期的 yarn 相对于 npm 比较大的优势:
- 采用缓存机制,支持离线安装(npm@5 已支持)
- 依赖扁平化结构(npm@3 已支持)
- 依赖安装确定性 yarn.lock(npm@5 增加了 package-lock.json)
- 安装速度快,并行下载
- 安装失败自动重试
yarn add [pkg]
yarn remove [pkg]
npm 和 yarn 存在的问题
phantom dependencies
phantom dependencies (幽灵依赖): 某个包没有在 package.json中被依赖,但用户还是可以引用到这个包。
原因是node_modules的扁平结构。如果使用 npm 或 yarn 安装项目依赖,间接依赖(第三方包的依赖)会被提升在node_modules顶层目录下。
依赖多次重复安装
无论在 node_modules 的嵌套结构,或是改进后的扁平化结构,大量的包都会被重复安装多次,占用大量的空间。
pnpm
一 个更新的包管理器,使用软链与平铺目录构建的嵌套结构。
不会重复安装依赖:基于内容寻址的存储,所有依赖都会安装在磁盘一个单独的目录下,当依赖被安装时,其中的文件会硬链接到这一位置,不会占用额外的磁盘空间。
pnpm 的 node_modules 结构:
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo //软链接,类似于快捷方式
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar //硬链接
└── foo@1.0.0
└── node_modules
├── foo -> <store>/foo
└── bar -> ../../bar@1.0.0/node_modules/bar
.pnpm 文件夹中保存着每个包的虚拟存储目录:基于内容可寻址存储的硬链接。
包的依赖项与依赖包的实际位置位于同一目录级别:如 node_modules/.pnpm/foo@1.0.0/node_modules 。所有依赖都软链接到 node_modules/.pnpm/ 下对应的目录。
优势:
-
包安装速度快。
-
磁盘空间利用非常高效。
不会重复安装同一个包,即使有一个包的不同版本,pnpm 也会极大程度地复用之前版本的代码。
-
支持 monorepo。
pnpm workspace,yarn workspace,lerna 等多种 monorepo 策略。
-
继承了 npm 与 yarn 的其他优势,比如安装确定性与离线模式。
-
安全性高,可以规避非法访问依赖的风险,比如幽灵依赖。
了解更多:
- Why should we use pnpm? by @ZoltanKochan
- 平铺的结构不是 node_modules 的唯一实现方式 | pnpm
- 基于符号链接的 node_modules 结构 | pnpm
- All in one:项目级 monorepo 策略最佳实践 - SegmentFault 思否