果断放弃npm切换到pnpm--节约磁盘空间(256G硬盘救星)

2021-09-10 10:11:23 浏览数 (1)

团队成立初期我们采用 npm3 来管理项目依赖,后续我们研发了自己组件库、图表库、工具库,采用了 monorepo 管理,依赖管理也由 npm3 切换成了 yarn(yarn workspace)。不管是 npm3 还是 yarn 都采用扁平化的 node_modules 文件夹方式,以此避免引入层级过深、相同依赖版本重复等问题。

随着公司业务不断壮大,团队支撑的项目越来越多。由于依赖是跟随项目的,导致磁盘空间占用严重。

由于上述原因,开始尝试使用 pnpm 来进行管理。

节约磁盘空间

pnpm 依赖项将存储在一个全局内容可寻址的仓库中(${os.homedir}/.pnpm-store),具体项目中使用依赖采用硬链接方式,而不是进行复制。对于每个模块的每个版本只保留一个副本。如:本地有10个项目依赖相同 vue 版本,如果使用 npm 或 yarn 时本地磁盘需要有 10 个 vue 的副本;而 pnpm 只有1个。

  1. 如果你用到了某依赖项的不同版本,那么只会将有差异的文件添加到仓库(公共仓库)。
  2. 所有文件都会存储在硬盘上的同一位置。 当多个包(package)被安装时,所有文件都会从同一位置创建硬链接,不会占用额外的磁盘空间。 这允许跨项目共享同一版本的依赖。
代码语言:javascript复制
$ pnpm install

Packages:  1585
                                                                                                          
Packages are hard linked from the content-addressable store to the virtual store.
  Content-addressable store is at: /Users/ligang/.pnpm-store/v3
  Virtual store is at:             node_modules/.pnpm
Progress: resolved 1585, reused 1585, downloaded 0, added 1585, done

可以发现:

  • 内容可寻址存储在 /Users/ligang/.pnpm-store/v3
  • 虚拟存储目录 node_modules/.pnpm
  • downloaded 0,这样极大的提升了 install 速度
代码语言:javascript复制
ll node_modules

lrwxr-xr-x   1 ligang  staff    44B  9  1 17:59 deepmerge -> .pnpm/deepmerge@3.3.0/node_modules/deepmerge
lrwxr-xr-x   1 ligang  staff    72B  9  1 17:59 element-resize-detector -> .pnpm/element-resize-detector@1.2.2/node_modules/element-resize-detector
lrwxr-xr-x   1 ligang  staff    58B  9  1 17:59 element-ui -> .pnpm/element-ui@2.13.1_vue@2.6.12/node_modules/element-ui
lrwxr-xr-x   1 ligang  staff    39B  9  1 17:59 eslint -> .pnpm/eslint@5.16.0/node_modules/eslint

node_modules 目录下的文件全部被软链到了虚拟存储路径下 .pnpm.pnpm/ 以平铺的形式储存着所有的包(格式:.pnpm/@/node_modules/)。.pnpm 目录下的包会硬链到全局仓库中(/Users/ligang/.pnpm-store/v3)。

关于「硬链」、「软链」可以查看上篇博文。

以项目中依赖 element-ui 为例:

代码语言:javascript复制
cd node_modules

ls -li element-ui
8643474522 lrwxr-xr-x  1 ligang  staff  58  9  1 17:59 element-ui -> .pnpm/element-ui@2.13.1_vue@2.6.12/node_modules/element-ui

ls -li .pnpm/element-ui@2.13.1_vue@2.6.12/node_modules/
8643424956 drwxr-xr-x  13 ligang  staff  416  9  1 17:59 element-ui

node_modules 目录下,element-ui软链到了 .pnpm 对应的目录下 element-ui;.pnpm目录下,element-ui硬链接( link count 13)。

非扁平化的 node_modules 文件夹

回归一下 node_modules 结构历史:

第一阶段:npm@3 之前版本

代码语言:javascript复制
node_modules
└─ foo
   ├─ index.js
   ├─ package.json
   └─ node_modules
      └─ bar
         ├─ index.js
         └─ package.json
  • 依赖树层级太深,会导致 Windows 上的目录路径过长问题
  • 相同包在不同的依赖项中需要时,会存在多个相同副本

第二阶段:npm@3 版本,扁平化处理

主要是解决上述两个问题

代码语言:javascript复制
node_modules
├─ foo
|  ├─ index.js
|  └─ package.json
└─ bar
   ├─ index.js
   └─ package.json

第三阶段:pnpm

由于扁平化算法的极其复杂,以及会存在多项目间相同依赖副本的情况。pnpm 在尝试解决这些问题时,放弃了扁平化处理 node_modules 的方式。而是采用 硬链 软链 方式。

代码语言:javascript复制
node_modules
├─ .pnpm
|  ├─ foo@1.0.0/node_modules/foo
|  |  └─ index.js
|  └─ bar@2.0.0/node_modules/bar
├─ foo -> .pnpm/foo@1.0.0/node_modules/foo
└─ bar -> .pnpm/bar@2.0.0/node_modules/bar

node_modules 根目录中的包只是一个符号链接。require('foo') 将执行 node_modules/.pnpm/foo@1.0.0/node_modules/foo/indexjs 中的文件(这里是硬链接),而不是 node_modules/foo/index.js 中的文件。

好处

这种布局结构的一大好处是只有真正在依赖项中(package.json dependences)的包才能访问。使用扁平化的 node_modules 结构,所有提升的包都可以访问。

npm@3/yarn 采用扁平化的方式管理 node_modules

示例:以 chokidar 为例

代码语言:javascript复制
"dependencies": {
  "chokidar": "^3.5.2"
}

项目中依赖了 chokidar 用于监听文件夹内容变化,通过 npm 安装后结构

依赖包如此之多,正是由于扁平化处理而来。chokidar 依赖包以及其依赖的依赖包都被提取到了一级目录下。这种方式会导致没有明确被依赖的包也可以被引用。

代码语言:javascript复制
const isNumber = require('is-number')
console.log(isNumber(123), isNumber('abc'))

上述可以正常引用到!

采用 pnpm 重新安装

执行上面代码,会报错:Error: Cannot find module ‘is-number’

问题

扁平化 node_modules 导致了上述错误。如果存在这种情况,需要切换成 pnpm 我们应该如何处理?

方案1:

通过 pnpm add 添加依赖

方案2:

通过相关 hooks 添加相关的依赖

.pnpmfile.cjs

代码语言:javascript复制
module.exports = {
  hooks: {
    readPackage: (pkg) => {
      if (pkg.name === "inspectpack") {
        pkg.dependencies['babel-traverse'] = '^6.26.0'
      }
      return pkg
    }
  }
}

方案3:

如果缺少依赖太多,可以使用提升选项。此选项官方不推荐。

代码语言:javascript复制
pnpm install --shamefully-hoist

由于 cli3 对于 pnpm 支持不够完善(在 cli4 中已完全支持),我们采用了这种方式。 相关 Issue

总结

pnpm 方式的实现精髓

  1. 通过软链的形式,使得 require 可以正常引用;同时对非真正依赖的项目做隔离(避免引用依赖的混乱)
  2. .pnpm 的存在避免了循环引用和层级过深的问题(都在其第一层)
  3. 硬链使得不同项目相同依赖只存在一个副本,减少磁盘空间

参考链接

  • https://www.kochan.io/nodejs/pnpms-strictness-helps-to-avoid-silly-bugs.html
  • https://www.kochan.io/nodejs/why-should-we-use-pnpm.html
  • https://github.com/vuejs/vue-cli/issues/2703
  • https://pnpm.io/zh/faq

0 人点赞