包管理工具 —— 更推荐的 pnpm

·  阅读 1247

前言

一般我们接触的包管理工具有 npm、yarn 以及pnpm,小柒在工作中基本上新项目都采用了pnpm 来作为包管理工具,那我们就来聊一聊换成 pnpm 的好处。

npm

npm 从 v1 -v3- v5 版本的迭代都有重大的改变,一起来下看吧~。

npm v1 嵌套

npm 在 v3 之前 node_modules 里的包都是嵌套的。

node_modules
├── A@1.0.0
│   └── node_modules
│       └── B@1.0.0
├── C@1.0.0
│   └── node_modules
│       └── B@2.0.0
└── D@1.0.0
    └── node_modules
        └── B@1.0.0
复制代码

随着项目越来越大,依赖包越来越多,这样也会带来一系列问题。

  • 嵌套的层级加深,文件路径过长。
  • 大量的包被重复安装。比如上面例子中的 B@1.0.0 就会被装两份。

npm v3 扁平

在 v3 版本,实现了扁平化安装依赖的模式, node_modules 中的包成打平状态。

node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0
    └── node_modules
        └── B@2.0.0
├── D@1.0.0
复制代码

npm v3 的变化,虽然避免了嵌套过深以及重复安装的问题(注意多个版本的包只能有一个版本被提升),但是其存在很多不确定性(即生成的 node_modules 结构不确定)。

举个🌰:假设 A@1.0.0 依赖 C@1.0.1B@1.0.0 依赖 C@1.0.2,那么生成的 node_modules 结构什么样的呢?

node_modules
├── A@1.0.0
├── B@1.0.0
    └── node_modules
        └── C@1.0.2
├── C@1.0.1
// 还是下面的情况呢
node_modules
├── A@1.0.0
    └── node_modules
        └── C@1.0.1
├── B@1.0.0
├── C@1.0.2
复制代码

其实是都有可能,这就依赖于 A 和 B 在 package.json中的位置。

npm v5 扁平 + lock

为了解决 node_modules 结构的不确定性,于是在 v5 版本中默认会生成 package-lock.json文件 。我们来看看只安装 swiper 这个库对应的 package-lock.json 文件。

// package.json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "license": "ISC",
  "dependencies": {
    "swiper": "^8.0.7"
  }
}
// package-lock.json
{
  "name": "test",
  "version": "1.0.0",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "dom7": {
      "version": "4.0.4",
      "resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.4.tgz",
      "integrity": "sha512-DSSgBzQ4rJWQp1u6o+3FVwMNnT5bzQbMb+o31TjYYeRi05uAcpF8koxdfzeoe5ElzPmua7W7N28YJhF7iEKqIw==",
      "requires": {
        "ssr-window": "^4.0.0"
      }
    },
    "ssr-window": {
      "version": "4.0.2",
      "resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz",
      "integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ=="
    },
    "swiper": {
      "version": "8.0.7",
      "resolved": "https://registry.npmjs.org/swiper/-/swiper-8.0.7.tgz",
      "integrity": "sha512-GHjDfxSZdupfU7LrSVOpaNaT7R1D2zxopPGBFz1UOXOtsYvVJLg0k6NvkTAD7qn0ASl5pTti82qoYwvYvIkg4g==",
      "requires": {
        "dom7": "^4.0.4",
        "ssr-window": "^4.0.2"
      }
    }
  }
}
复制代码

package-lock.json 文件可以帮我们记录安装的每一个包版本和其所依赖的其他包版本,这样在下一次安装的时候就可以通过这个文件来安装。由 package-lock.json 文件和 package.json 文件能确保始终得到一致的 node_modules 目录结构,这样就保证了安装依赖的确定性。

yarn

yarn 1

yarn1 的出现是为了解决 npm v3 的问题,那时候还没有 npm v5。yarn install 生成的 node_modules 目录结构与 npm v5 相同,同时默认会生成一个 yarn.lock 文件。只要 yarn 的版本相同,yarn 安装依赖的确定性就能保证

同前面的例子一样,我们使用 yarn 安装 swiper 生成的 yarn.lock 文件如下:

# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
​
​
dom7@^4.0.4:
  version "4.0.4"
  resolved "https://registry.npmjs.org/dom7/-/dom7-4.0.4.tgz#8b68c5d8e5e2ed0fddb1cb93e433bc9060c8f3fb"
  integrity sha512-DSSgBzQ4rJWQp1u6o+3FVwMNnT5bzQbMb+o31TjYYeRi05uAcpF8koxdfzeoe5ElzPmua7W7N28YJhF7iEKqIw==
  dependencies:
    ssr-window "^4.0.0"
​
ssr-window@^4.0.0, ssr-window@^4.0.2:
  version "4.0.2"
  resolved "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz#dc6b3ee37be86ac0e3ddc60030f7b3bc9b8553be"
  integrity sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==
​
swiper@^8.0.7:
  version "8.0.7"
  resolved "https://registry.npmjs.org/swiper/-/swiper-8.0.7.tgz#9eefe26c703e627a6dc7237c0109e172ce06e3f6"
  integrity sha512-GHjDfxSZdupfU7LrSVOpaNaT7R1D2zxopPGBFz1UOXOtsYvVJLg0k6NvkTAD7qn0ASl5pTti82qoYwvYvIkg4g==
  dependencies:
    dom7 "^4.0.4"
    ssr-window "^4.0.2"复制代码

对比一下 yarn.lock 文件和 package-lock.json 文件,我们可以发现一下几点不同:

  • package-lock.json 与 yarn.lock 格式上有差异。

  • npm v5 中只需要 package-lock.json 就可以保正确的 node_modules 目录结构,而 yarn 需要同时拥有 yarn.lock 文件和 package.json文件。(可参考Yarn 的确定性

在使用 yarn 作为包管理工具时,我们也需要主要以下几点:

  • yarn.lock 是自动生成的,不要手动修改它
  • 将 yarn.lock 文件上传到 git
  • 升级依赖时,使用yarn upgrade命令,不要手动修改 package.json 和 yarn.lock 文件
  • 不得以不要把 lock 文件删掉,整个重装。这样会造成原本锁住的版本都放开了,执行yarn install的时候会根据 package.json 里定义的版本区间去找最新版,可能会造成你预期外的依赖也被更新了, 有可能会引入 bug。

yarn2

yarn2 版本是无 node_modules 模式,可以加快项目安装速度,同时大大缩减删除一整个项目的速度。[这里顺带提一嘴,不过多介绍 🤡]

npm install -g yarn@berry
复制代码

pnpm

pnpm(perfomance npm) 现代包管理工具,其性能上有很大的提高。

基本使用

npm install -g pnpm // 全局安装 pnpm 
​
pnpm add axios // 添加至dependencies
pnpm add axios -D   // 添加至devDependencies
pnpm add -O [package] //保存到optionalDependencies
​
pnpm update  // 更新
​
pnpm remove/uninstall // 删除
​
pnpm dlx  // 从源中获取包而不将其安装为依赖项,热加载,并运行它公开的任何默认命令二进制文件。
​
pnpm link // 将本地项目连接到另一个项目,这里是硬连接。
复制代码

基本特性

  • 本地安装包速度快: 相比于npm / yarn 快 2-3 倍。
  • 磁盘空间利用高效: 及时版本不同也不会重复安装同一个包。
  • 安全性高:避免了npm/yarn 非法访问依赖二重身的风险

pnpm 是如何提升性能的?

一句话概括:pnpm 在安装依赖时使用了 hard link 机制,使得用户可以通过不同的路径去寻找某个文件。pnpm 会在全局的 store 目录下存储 node_modules 文件的 hard link。

下面先简单讲讲几个概念: hard link 、symlink 以及全局的 store 目录。

什么是 hard link 和 symlink

本质上都是文件访问的方式。

hard link(硬链接):如果 A 是 B 的硬链接,则 A 的 indexNode(可以理解为指针) 与 B 的 indexNode 指向的是同一个。删除其中任何一个都不会影响另外一个的访问。作用:允许一个文件拥有多个有效路径,这样用户可以避免误删。

symlink(软链接或符号链接symbolic link):类似于桌面快捷方式。比如 A 是 B 的软连接(A 和 B 都是文件名),A 和 B 的 indexNode 不相同,但 A 中只是存放这 B 的路径,访问 A 时,系统会自动找到 B。删掉 A 与 B 没有影响,相反删掉 B,A 依然存在,但它的指向是一个无效链接。

store 目录

store 目录一般在${os.homedir}/.pnpm-store/v3/files 这个目录下。 由于 pnpm 会在全局的 store 目录下存储 node_modules 文件的 hard link,这样在不同项目中安装同一个依赖的时候,不需要每次都去下载,只需要安装一次就行,避免了二次安装的消耗。这点 npm 、yarn 在不同项目上使用,都需要重新下载安装。

image.png

store 目录也会随着安装的包的数量越来越大,使用 pnpm store prune 命令可以删除不再被引用的包。(不推荐频繁使用)

pnpm 网状+平铺的node_modules 结构

我们同样使用 pnpm 来安装一下 swiper 包,此时会自动生成一个 pnpm-lock.yaml 文件。接着我们来看看在 pnpm 中 node_modules 结构与 npm 和 yarn 有什么不同。

image.png

安装 swiper 包后,根 node_modules 下会存在两个目录, 一个是 .pnpm 虚拟磁盘目录,用户不能直接从中 require;另一个 swiper 目录,正常node require 的路径, 这个 swiper 我们称之为 swiper 的软链 。当 node 解析依赖时,会通过这个软链来找到 swiper 的真实位置,swiper 真实的位置在 .pnpm/swiper@8.0.7/node_modules/swiper 下,这个文件称为 swiper 的硬链,会真实的链接到全局的 store 中。

image.png

由于兼容性问题,没有使用 symlink 代替 hard link 。实际上存在 store 目录里面的依赖也是可以通过软链接去找到的,node.js 本身提供了一个 --preserve-symlinks 的参数来支持 symlink ,但实际这个参数对应 symlink 的支持并不好,所以作者放弃了。

npm 与 yarn 的 共性问题

npm 与 yarn 在安装依赖虽然也实现了包打平,但还是存在两个问题:phatomdoppelgangers

image.png

  • phatom (非法访问依赖):package.json 中只声明了 A, 但 A 的 depdencies 有 B, 这样安装在 A 时 B 也会被安装,项目中还是可以 require 到 B。
  • doppelgangers (二重身): 一个包的不同版本还是会重复安装(不能打平同一个包的不同版本),能会造成同一个包重复安装,性能还是会损失。

pnpm 中不会出现这两种情况,首先依赖的打平是在 .pnpm 的 node_modules 中,而.pnpm 是一个虚拟的磁盘目录,用户不能 require 到;其次pnpm 安装的依赖始终都是存在全局 store 目录下的 hard links,一份不同的依赖始终都只会被安装一次。

pnpm 独有的特点

pnpm 除了能解决上述 npm 与 yarn 的 共性问题外,其独有的特点如下。

  • 管理 Node.js 版本:在 .npmrc 文件下,可配置运行项目的 node 版本。 pnpm 将自动安装指定版本的 Node.js 并将其用于执行 pnpm run 命令或 pnpm node 命令。(可参考npmrc
  • 内容可寻址存储(CAS):是一种存储机制,其中为固定数据分配了硬盘上的永久位置,并使用唯一的内容名称,标识符或地址进行寻址。

pnpm 的 Monorepo 策略

pnpm 对 Monorepo 也有很好的支持。那么如何使用 pnpm 来构建 Monorepo 项目?

  • 全局配置 pnpm-workspace.yaml

    packages:
      # all packages in subdirs of packages/ and components/
      - 'packages/**'
    复制代码
  • 使用 pnpm 安装全局公用的包

    pnpm install react react-dom -w // 根目录
    pnpm i dayjs -r --filter packageName  // 安装在指定的 packages下
    复制代码
    • -w 将包安装在根目录下的 node_modules

    • -r 将包安装在指定 package 下,并需要配合指定的参数 --filter, 后面接 package 的name

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改