# 包管理器

2023年10月17日10分钟阅读

- FormalSciences/ComputerScience
- Webdevelopment/Tools

## Npm

npm
是 JavaScript 编写的软件包管理工具，同时也是 node.js 的默认包管理工具。

### 早期版本

因为早期 npm 版本采用嵌套结构，所以存在依赖地狱和存储空间占用过大问题；同时 Windows 环境下路径限制 256 字符，可能会导致运行出错。

### V3 版本

V3 版本采用扁平结构来避免过深的依赖树和包冗余，子依赖会尽量平铺在主依赖所在的目录中（同一包但版本号不同还是会放至子目录中）。

    node_modules
    ├── A@1.0.0
    ├── B@1.0.0
    └── C@1.0.0
        └── node_modules
            └── B@2.0.0

虽然解决了依赖地狱的问题，但是又形成了新的问题。

#### 幽灵依赖

幽灵依赖是指在 package.json 中未定义的依赖，但项目中依然可以正确地被引用到。

    {
      "dependencies": {
        "A": "^1.0.0",
        "C": "^1.0.0"
      }
    }

例如我们只安装了 A 和 C，虽然 A 引用了 B，但是因为平铺在同一目录，所以在项目中引用 B 还是可以正常工作的。将来如果某个版本的 A 依赖不再依赖 B 或者 B 的版本发生了变化，那么就会造成依赖缺失或兼容性问题。

#### 不确定性

    node_modules
    ├── A@1.0.0
    ├── B@1.0.0
    └── C@1.0.0
        └── node_modules
            └── B@2.0.0

    node_modules
    ├── A@1.0.0
    │   └── node_modules
    │       └── B@1.0.0
    ├── B@2.0.0
    └── C@1.0.0

不确定性是指同样的 package.json 文件，install 依赖后可能不会得到同样的 node_modules 目录结构。如果 A 依赖 B@1.0，C 依赖 B@2.0；依赖安装后究竟该提升 B 为 1.0 还是 2.0 取决于用户的安装顺序。

#### 依赖分身

假设继续再安装依赖 B@1.0 的 D 模块和依赖 @B2.0 的 E 模块，此时：A 和 D 依赖 B@1.0；C 和 E 依赖 B@2.0。

    node_modules
    ├── A@1.0.0
    ├── B@1.0.0
    ├── D@1.0.0
    ├── C@1.0.0
    │   └── node_modules
    │       └── B@2.0.0
    └── E@1.0.0
        └── node_modules
            └── B@2.0.0

可以看到 B@2.0 会被安装两次，实际上无论提升 B@1.0 还是 B@2.0，都会存在重复版本的 B 被安装，这两个重复安装的 B 就叫依赖分身。

### 安装方式

可以通过 node.js 官网
下载安装，或者直接使用如下脚本。

    curl -qL https://www.npmjs.com/install.sh | sh

### 常用命令

命令 | 功能
--- | ---
npm help | 帮助文档
npm -v | npm 版本
npm config list -l | npm 配置
npm init | 初始化引导创建 package.json 文件
npm set <变量名> <值> | 设置环境变量
npm search <关键词> [-g] | 检索模块
npm list | 局部查看模块
npm list -g —depth 0 | 全局安装的模块，目录深度为 0。
npm install | 读取 package.json 安装模块
npm uninstall <模块> [-g] | 卸载局部模块或全局模块
npm update <模块> [-g] | 升级局部模块或全局的指定模块
npm run <脚本> | 运行 package.json 脚本

## Yarn

yarn
同样采用扁平结构，它的出现是为了解决 npm V3 依赖安装速度慢和不确定性。

### 提升安装速度

yarn 采用并行模式替代 npm 串行模式安装包，并且利用全局缓存可以提升较大安装速度。

### Lockfile 解决不确定性

安装依赖时，根据 package.josn 生成一份 yarn.lock 文件。因为 lockfile 里记录了依赖，以及依赖的子依赖、依赖的版本、获取地址、验证模块完整性的 hash；所以即使是不同的安装顺序，相同的依赖关系在任何的环境和容器中，都能得到统一的 node_modules 目录结构，保证了依赖安装的确定性。

npm v5 才发布 package-lock.json。

### 存在的问题

因为 yarn 和 npm 一样是扁平结构的 node_modules ，所以并没有解决幽灵依赖和依赖分身问题。

### 安装方式

    npm install --global yarn

### 常用命令

命令 | 功能
--- | ---
yarn help | 帮助文档
yarn init | 初始化项目
yarn install | 安装所有依赖项
yarn add [package] | 安装指定依赖项
yarn add [package]@[version | tag] | 安装指定版本依赖
yarn up [package] | 升级指定依赖
yarn up [package]@[version | tag] | 升级指定版本依赖
yarn remove [package] | 删除指定依赖

## Pnpm

pnpm
不同于 npm 和 yarn 使用的扁平结构，而是采用了内容寻址存储。pnpm 通过设置全局 store，然后在项目中通过使用硬链接与符号链接引用依赖。为了实现此功能，node_modules 目录下会多出 .pnpm 目录，而且是非扁平结构。

- 硬链接 Hard link：硬链接可以理解为源文件的副本。
- 符号链接 Symbolic link：符号链接（软连接）可以理解为快捷方式。

    <store>/xxx 开头的路径是硬链接，指向全局 store 中安装的依赖。其余的是符号链接，指向依赖的快捷方式。

    node_modules
    ├── .pnpm
    │   ├── A@1.0.0
    │   │   └── node_modules
    │   │       ├── A => <store>/A@1.0.0
    │   │       └── B => ../../B@1.0.0
    │   ├── B@1.0.0
    │   │   └── node_modules
    │   │       └── B => <store>/B@1.0.0
    │   ├── B@2.0.0
    │   │   └── node_modules
    │   │       └── B => <store>/B@2.0.0
    │   └── C@1.0.0
    │       └── node_modules
    │           ├── C => <store>/C@1.0.0
    │           └── B => ../../B@2.0.0
    │
    ├── A => .pnpm/A@1.0.0/node_modules/A
    └── C => .pnpm/C@1.0.0/node_modules/C

### 未来可期

pnpm 这套全新的机制设计地十分巧妙，不仅兼容 node 的依赖解析，同时也解决了如下问题：

- 幽灵依赖：只有直接依赖会平铺在 node_modules 下，子依赖不会被提升，不会产生幽灵依赖。

- 依赖分身：相同的依赖只会在全局 store 中安装一次。项目中的都是源文件的副本，几乎不占用任何空间，没有了依赖分身。

同时由于链接的优势，pnpm 的安装速度在大多数场景比 npm 和 yarn 快 2 倍，同时也节省更多的磁盘空间。但也存在一些弊端：

- 因为 pnpm 创建的 node_modules 依赖软链接，所以在不支持软链接的环境中无法使用 pnpm；比如 Electron 应用。

- 因为依赖源文件是安装在 store 中，调试依赖或 patch-package 给依赖打补丁也不太方便，可能会影响其他项目。

### 安装方式

#### Npm

    npm install -g pnpm

#### Curl

    curl -fsSL https://get.pnpm.io/install.sh | sh -

### 常用命令

命令 | 功能
--- | ---
`pnpm install` | 安装 package.json 文件中的所有依赖项
`pnpm add <pkg>` | 安装指定的依赖项
`pnpm update` | 更新所有的依赖项
`pnpm uninstall` | 移除指定的依赖项
`pnpm list` | 列出已安装的所有依赖项
`pnpm run` | 运行在 package.json 文件中定义的脚本
`pnpm test` | 运行测试
`pnpm link` | 创建或者删除一个软链接到全局安装的依赖项
`pnpm prune` | 移除无用的依赖项
`pnpm publish` | 将你的包发布到 npm 注册表
`pnpm root` | 打印全局安装的依赖项的位置
`pnpm store` | 控制共享的包存储
`pnpm outdated` | 检查哪些依赖项有新版本可以更新
`pnpm rebuild` | 重新编译包
`pnpm import` | 从 npm 转换一个项目
`pnpm fetch` | 预下载所有从注册表下载的依赖项到本地的存储库
`pnpm audit` | 检查项目依赖项中的已知的漏洞

## npm，yarn，pnpm 功能比较

功能 | pnpm | Yarn | npm
--- | --- | --- | ---
工作空间支持（monorepo） | ✔️ | ✔️ | ✔️
隔离的 `node_modules` | ✔️ - 默认 | ✔️ | ❌
提升的 `node_modules` | ✔️ | ✔️ | ✔️ - 默认
自动安装 peers | ✔️ - 通过 auto-install-peers=true | ❌ | ✔️
Plug’n’Play | ✔️ | ✔️ - 默认 | ❌
零安装 | ❌ | ✔️ | ❌
修补依赖项 | ✔️ | ✔️ | ❌
管理 Node.js 版本 | ✔️ | ❌ | ❌
有锁文件 | ✔️ - `pnpm-lock.yaml` | ✔️ - `yarn.lock` | ✔️ - `package-lock.json`
支持覆盖 | ✔️ | ✔️ - 通过 resolutions | ✔️
内容可寻址存储 | ✔️ | ❌ | ❌
动态包执行 | ✔️ - 通过 `pnpm dlx` | ✔️ - 通过 `yarn dlx` | ✔️ - 通过 `npx`
Side-effects cache | ✔️ | ❌ | ❌