前端包管理工具之从NPM到PNPM

前端包管理工具之从NPM到PNPM | DLLCNX的博客

近几年的前端开发者肯定了解npm是什么,而且使用过程中也开始接触yarnpnpm等等。那么到底为什么会有这么多的包管理工具,它们好像可以交替使用,但是好像又有点不同。我这边按照我的理解,整理搜索一下包管理器发展的历史。

一、包管理工具的发展

2010 年 1 月,一款名为 npm 的包管理器诞生。它确立了包管理器工作的核心原则。

npm 的发布诞生了一场革命,在此之前,项目依赖项都是手动下载和管理的。npm 引入了文件和元数据字段,将依赖项列表存储在 package.json 文件中,并且将下载的文件保存到 node_modules 文件夹中。

随后,又因为npm的缺陷或者旧版本的不足,大牛们造出一个又一个替代npm来进行包管理的轮子,例如:yarnyarn2pnpm等等。

1.1 NPM

是 Node.js 自带的包管理工具,也是最常用的包管理工具之一。它可以方便地安装、升级、卸载依赖包,还可以发布自己的包到 NPM 仓库。

很多人认为 npm 是 node package manager 的缩写,但其实不是,官方对此有过辟谣。

npm已经迭代过很多版本,也尝试解决大家对它缺陷或者不足的吐槽。

1.1.1 npm v1 & v2

此时期主要是采用简单的递归依赖 方法,最后形成高度嵌套的依赖树。这种模式虽然模块依赖关系比较清晰,但是造成的问题更大。

  • 重复依赖嵌套地狱,空间资源浪费:大量重复的包被安装,文件体积超级大。比如跟 foo 同级目录下有一个baz,两者都依赖于同一个版本的lodash,那么 lodash 会分别在两者的 node_modules 中被安装,也就是重复安装。

  • 安装速度过慢

  • 文件路径过长:尤其在 window 系统下,路径过长会导致爆错,最多260多个字符。

  • 模块实例不能共享:比如 React 有一些内部变量,在两个不同包引入的 React 不是同一个模块实例,因此无法共享内部变量,导致一些不可预知的 bug。

**例如:**项目依赖了A1.0和 B1.0,而 A1.0 和 B1.0依赖了不同版本的 C@1.0 和 C@2.0,node_modules 结构如下:

注意:为了方便,后边版本号都简单以两位数字表示,不是正确的语义化版本号

kotlin 复制代码
node_modules
├── A@1.0
│   └── node_modules
│       └── C@1.0
└── B@1.0
    └── node_modules
        └── C@2.0

在我们真实使用过程中,随着依赖的增多,重复冗余的包会越来越多,最终,node_modules会大量的占用磁盘。而且依赖嵌套的深度也会十分可怕,这个就是我们常说的依赖地狱(Dependency Hell)

1.1.2 npm v3

npm v3 版本作了较大的更新,开始采取扁平化的依赖结构。

为了将嵌套的依赖尽量打平,避免过深的依赖树和包冗余,npm v3 将子依赖「提升」(hoist),采用扁平的 node_modules 结构,子依赖会尽量平铺安装在主依赖项所在的目录中。

kotlin 复制代码
node_modules
├── A@1.0
├── B@1.0
    └── node_modules
        └── C@2.0
└── C@1.0

可以看到 A 的子依赖的 C@1 不再放在 A 的 node_modules 下了,而是与 A 同层级。

而 B 依赖的 C@2 因为版本号原因还是嵌套在 B 的 node_modules 下。

这样的依赖结构可以很好的解决重复依赖的依赖地狱问题,层级也不会太深,但也形成了新的问题。

​ 比如上方的示例其实我们项目只安装了 A 和 B,C是A的依赖:

json 复制代码
// package.json dependencies
{
  "dependencies": {
    "A": "^1.0",
    "B": "^1.0"
  }

​ 由于 B 在安装时被提升到了和 A 同样的层级,所以在项目中引用 B 还是能正常工作的。

​ 幽灵依赖是由依赖的声明丢失造成的,如果某天某个版本的 A 依赖不再依赖 B 或者 B 的版本发生了变化,那么就会造成依赖缺失或兼容性问题。

  • 不确定性 Non-Determinism:同样的 package.json 文件,install 依赖后可能不会得到同样的 node_modules 目录结构。还是之前的例子,A 依赖 C@1.0,B依赖 C@2.0,依赖安装后究竟应该提升 C 的 1.0 还是 2.0。取决于用户的安装顺序。如果有 package.json 变更,本地需要删除 node_modules 重新 install,否则可能会导致生产环境与开发环境 node_modules 结构不同,代码无法正常运行。

  • 依赖分身 Doppelgangers:假设继续再安装依赖 C@1.0 的 D 模块和依赖 C@2.0 的 E 模块,此时:

    • A 和 D 依赖 C@1.0
    • B 和 E 依赖 C@2.0

    以下是提升 C@1.0 的 node_modules 结构:

kotlin 复制代码
node_modules
├── A@1.0
├── B@1.0
│   └── node_modules
│       └── C@2.0
├── C@1.0
├── D@1.0
└── E@1.0
    └── node_modules
        └── C@2.0

​ 可以看到 C@2.0 会被安装两次,实际上无论提升 C@1.0 还是 C@2.0,都会存在重复版本的 C 被安装,这两个重复安装的 C 就叫 依赖分身 doppelgangers。这会导致一些隐含的问题:

1. 破坏单例模式:假如模块B、E中引入了模块C2.0中导出的一个单例对象,但其实引用的不是同一个 C2.0,即使代码里看起来加载的是同一模块的同一版本,但实际解析加载的是不同的module,引入的也是不同的对象。如果同时对该对象进行缓存或副作用操作,就会产生问题。

2. types冲突:虽然各个package的代码不会相互污染,但是他们的types仍然可以相互影响,因此版本重复可能会导致全局的types命名冲突。

当时npm还没解决这些问题的时候,社区就出来一个新的解决方案了,那就是 yarn

1.1.3 npm v5

为了解决上面出现的扁平化依赖算法耗时长问题,npm 引入 package-lock.json 机制,package-lock.json 的作用是锁定项目的依赖结构,保证依赖的稳定性。

当项目有package.json文件并首次执行npm install安装后,会自动生成一个package-lock.json文件,该文件里面记录了package.json依赖的模块,以及模块的子依赖。并且给每个依赖标明了版本、获取地址和验证模块完整性哈希值。通过package-lock.json,保障了依赖包安装的确定性与兼容性,使得每次安装都会出现相同的结果。

感兴趣的话可以具体看看官方文档:package.json | npm Docs (npmjs.com)

**注:**其实在 package-lock.json 机制出现之前,可以通过 npm-shrinkwrap 实现锁定依赖结构,但是 npm-shrinkwrap 默认关闭,需要主动执行。

一致性

考虑上文案例,初始时安装生成package-lock.json如左图所示,depedencies对象中列出的依赖都是提升的,每个依赖项中的requires对象中为子依赖项。此时更新A依赖到2.0版本,如右图所示,并不会改变提升的子依赖版本。因此重新生成的node_modules目录结构将不会发生变化。

兼容性

语义化版本(Semantic Versioning)

依赖版本兼容性就不得不提到npm使用的SemVer版本规范,版本格式如下:

  • 主版本号:不兼容的 API 修改
  • 次版本号:向下兼容的功能性新增
  • 修订号:向下兼容的问题修正

在使用第三方依赖时,我们通常会在package.json中指定依赖的版本范围,语义化版本范围规定:

  • ~:只升级修订号
  • ^:升级次版本号和修订号
  • *:升级到最新版本

语义化版本规则定义了一种理想的版本号更新规则,希望所有的依赖更新都能遵循这个规则,但是往往会有许多依赖不是严格遵循这些规定的。因此一些依赖模块子依赖不经意的升级,可能就会导致不兼容的问题产生。因此package-lock.json给每个模块子依赖标明了确定的版本,避免不兼容问题的产生。

1.2 Yarn

1.2.1 yarn v1(yarn classic)

2016 年,yarn 发布,yarn 也采用扁平化 node_modules 结构 。它的出现是为了解决 npm v3 几个最为迫在眉睫的问题:依赖安装速度慢,不确定性

yarn的一些特性是走在npm的前边的。 yarn 出现时,此时 npm 处于 v3 时期,其实当时 yarn 解决的问题基本就是 npm v5 解决的问题,包括使用 yarn.lock 等机制,锁定版本依赖,实现并发网络请求,最大化网络资源利用率,其次还有利用缓存机制,实现了离线模式

与npm v5之后推出的package-lock.json不同,yarn并没有采用JSON格式的文件,而是使用了自定义的格式,名字就叫做yarn.lock,与前者不同,后者的lockfile目录结构并不能复制出完完全全一样的node_modules拓扑结构,他只是把依赖到的所有库 flat 成根目录级别,这样更方便做diff。

安装速度
  • 并行:在 npm 中安装依赖时,安装任务是串行的,会按包顺序逐个执行安装,这意味着它会等待一个包完全安装,然后再继续下一个。为了加快包安装速度,yarn 采用了并行操作,在性能上有显著的提高。
  • 离线缓存:像npm一样,yarn使用本地缓存。与npm不一样的是,yarn的缓存机制是将每个包缓存在磁盘上,在下一次安装这个包时,无需互联网链接就能安装本地缓存的依赖项,它提供了离线模式。这个功能在2012年的npm项目中就被提出来过,但一直没有实现。
lockfile

yarn 更大的贡献是发明了 yarn.lock。

在依赖安装时,会根据 package.josn 生成一份 yarn.lock 文件。

lockfile 里记录了依赖,以及依赖的子依赖,依赖的版本,获取地址与验证模块完整性的 hash。

即使是不同的安装顺序,相同的依赖关系在任何的环境和容器中,都能得到稳定的 node_modules 目录结构,保证了依赖安装的确定性。

所以 yarn 在出现时被定义为快速、安全、可靠的依赖管理。而 npm 在一年后的 v5 才发布了 package-lock.json。

其实后面npm v5上能看到 yarn 的机制的影子,上面的机制目前 npm 基本也都实现了,就目前而言 npm 和 yarn 其实并没有差异很大,具体使用 npm 还是 yarn 可以看个人需求。

弊端

yarn 依然和 npm 一样是扁平化的 node_modules 结构,并没有解决幽灵依赖依赖分身问题。

1.2.2 yarn v2(yarn berry)

在 pnpm 之后, yarn 感受到了对手的挑战,于是在 2020 年, yarn 2诞生了

yarn 2(也叫yarn berry,v1 叫 yarn classic)。它是对 yarn 的一次重大升级,其中一项重要更新就是 Plug'n'Play(Plug'n'Play = Plug and Play = PnP,即插即用)。

尽管yarn1看似并没有对node_modules作出太大改动,但是他们的团队并不是没有意识到node_modules的缺憾,他们做出了Plug'n'Play的尝试。npm 与 yarn 的依赖安装与依赖解析都涉及大量的文件 I/O,效率不高。开发 Plug'n'Play 最直接的原因就是依赖引用慢,依赖安装慢。

首先node_modules本身的局限性在于解析、安装依赖时产生的大量IO操作:

  1. 解析:当require某个第三方文件时,首先在当前目录寻找node_modules,找不到再去父级,找到之后,再去这个node_modules的子目录去寻找,直到找到该文件。因为node不认识包,只认识文件,而node_moduls的设计也就注定了他不允许包管理工具正确的删除重复的包数据
  2. 安装:解析出某个具体的版本号,下载 tar 包到离线镜像,从镜像解压到本地缓存;从缓存拷贝到node_modules,即使是pnpm的hard link,也只是优化了最后一步。

因此,berry做出了修改,与其让node去查找软件包,不如直接简明扼要的告诉node应该在哪里找到这个包。Plug'n'Play 特性应运而生,他其实是省略了node_modules的拷贝,转而生成了一个**.pnp.js的文件去记录包的版本,以及映射到的磁盘位置,即把每个包看作整体,压缩成一个zip;一个.yarn文件夹,里面又有 cacheunplugged**目录,前者存放压缩过的依赖包,后者可以通过unplugin指令解压某个想要手动修改的包。

berry一定程度解决了一些问题:

  1. 之前介绍的npm存在的两个问题,berry因为不会生成node_modules目录,因此不存在phantom dependency的问题,同时他采用的.pnp.js的静态映射而不是copy的方式也避免了重复安装依赖的问题。
  2. 基于 .pnp.js 和 zip loading 实现的零安装,即将.pnp.js及.yarn文件夹全部上传至gitlab,在有些情况下是可行的,但是这里使用 create-react-app 进行实测,yarn 为144Mb,berry为62Mb,只是正常的压缩体积的优化;随后拿React,Vue等包做了下实验,也基本都是这个比例(7.9Mb VS 5.1Mb)(17Mb VS 8.5Mb)。
  3. 最后一点说一下一些新的特性,如插件机制,方便我们在对berry的核心代码并不熟悉的情况下开发基于berry的扩展功能,官方实现的官方实现的typescript插件,在yarn add 时自动添加@types等。

当然也存在一些问题,最明显的就是首次安装依赖的时间并没有感觉到缩短,其次还有上面所说的.yarn/cache到底要不要放到远程仓库中也是有待商榷的事。

berry的改变

  1. 抛弃 node_modules

无论是 npm 还是 yarn,都具备缓存的功能,大多数情况下安装依赖时,其实是将缓存中的相关包复制到项目目录中 node_modules 里。而 yarn PnP 则不会进行拷贝这一步,而是在项目里维护一张静态映射表 pnp.cjs。

pnp.cjs 会记录依赖在缓存中的具体位置,所有依赖都存在全局缓存中。同时自建了一个解析器,在依赖引用时,帮助 node 从全局缓存目录中发现依赖,而不是查找 node_modules。

这样就避免了大量的 I/O 操作同时项目目录也不会有 node_modules 目录生成,同版本的依赖在全局也只会有一份,依赖的安装速度和解析速度都有较大提升。

注:pnpm 在 2020 年底的 v5.9 也支持了 PnP

  1. 脱离 node 生态

pnp 比较明显的缺点是脱离了 node 生态。

  • 因为使用 PnP 不会再有 node_modules 了,但是 Webpack,Babel 等各种前端工具都依赖 node_modules。虽然很多工具比如 pnp-webpack-plugin 已经在解决了,但难免会有兼容性风险。
  • PnP 自建了依赖解析器,所有的依赖引用都必须由解析器执行,因此只能通过 yarn 命令来执行 node 脚本。

1.3 pnpm

pnpm - performant npm,在 2017 年正式发布,定义为快速的,节省磁盘空间的包管理工具,开创了一套新的依赖管理机制,成为了包管理的后起之秀。

与依赖提升和扁平化的 node_modules 不同,pnpm 引入了另一套依赖管理策略:内容寻址存储。该策略会将包安装在系统的全局 store 中,依赖的每个版本只会在系统中安装一次。

内容寻址存储

pnpm 内部使用基于内容寻址的文件系统来存储磁盘上所有的文件,这样可以做到不会出现重复安装,在项目中需要使用到依赖的时候,pnpm 只会安装一次,之后再次使用都会直接硬链接指向该依赖,极大节省磁盘空间,并且加快安装速度。

注:硬链接是多个文件名指向同一个文件的实际内容,而软链接(符号链接)是一个独立的文件,指向另一个文件或目录的路径

在引用项目 node_modules 的依赖时,会通过硬链接与符号链接在全局 store 中找到这个文件。为了实现此过程,node_modules 下会多出 .pnpm 目录,而且是非扁平化结构。

  • 硬链接 Hard link :硬链接可以理解为源文件的副本,项目里安装的其实是副本,它使得用户可以通过路径引用查找到全局 store 中的源文件,而且这个副本根本不占任何空间。同时,pnpm 会在全局 store 里存储硬链接,不同的项目可以从全局 store 寻找到同一个依赖,大大地节省了磁盘空间。
  • 符号链接 Symbolic link :也叫软连接,可以理解为快捷方式,pnpm 可以通过它找到对应磁盘目录下的依赖地址。

还是使用上面 A,B,C 模块的示例,使用 pnpm 安装依赖后 node_modules 结构如下:

js 复制代码
node_modules
├── .pnpm
│   ├── A@1.0
│   │   └── node_modules
│   │       ├── A => <store>/A@1.0
│   │       └── B => ../../B@1.0
│   ├── B@1.0
│   │   └── node_modules
│   │       └── B => <store>/B@1.0
│   ├── B@2.0
│   │   └── node_modules
│   │       └── B => <store>/B@2.0
│   └── C@1.0
│       └── node_modules
│           ├── C => <store>/C@1.0
│           └── B => ../../B@2.0
│
├── A => .pnpm/A@1.0.0/node_modules/A
└── C => .pnpm/C@1.0.0/node_modules/C

<store>/xxx 开头的路径是硬链接,指向全局 store 中安装的依赖。

其余的是符号链接,指向依赖的快捷方式。

未来可期

这套全新的机制设计地十分巧妙,不仅兼容 node 的依赖解析,同时也解决了:

  1. 幽灵依赖问题:只有直接依赖会平铺在 node_modules 下,子依赖不会被提升,不会产生幽灵依赖。
  2. 依赖分身问题:相同的依赖只会在全局 store 中安装一次。项目中的都是源文件的副本,几乎不占用任何空间,没有了依赖分身。

同时,由于链接的优势,pnpm 的安装速度在大多数场景都比 npm 和 yarn 快 2 倍,节省的磁盘空间也更多。

但是,其实这种模式也存在一些弊端:

  1. 由于 pnpm 创建的 node_modules 依赖软链接,因此在不支持软链接的环境中,无法使用 pnpm,比如 Electron 应用。
  2. 因为依赖源文件是安装在 store 中,调试依赖或 patch-package 给依赖打补丁也不太方便,可能会影响其他项目。

扩展

也许有人说 yarn 默认也是扁平化安装方式,但是 yarn 有独特的 PnP 安装方式 ,可以直接去掉 node_modules,将依赖包内容写在磁盘,节省了 node 文件 I/O 的开销,这样也能提升安装速度,但是 yarn PnP 和 pnpm 机制是不同的 ,且总体来说安装速度 pnpm 是要快于 yarn PnP 的,详情请看下面官方文档

最后就是 pnpm 是默认支持 monorepo 多项目管理的,在日渐复杂的前端多项目开发中尤其适用,也就说我们不再需要 lerna 来管理多包项目,可以使用 pnpm + Turborepo 作为我们的项目管理环境

配置工作空间官方文档:工作空间(Workspace) | pnpm

还有就是 pnpm 还能管理 nodejs 版本,可以直接替代 nvm,命令如下所示

text 复制代码
# 安装 LTS 版本
pnpm env use --global lts
# 安装指定版本
pnpm env use --global 16

1.4 npx

npx想要解决的主要问题,就是调用项目内部安装的模块。比如项目内部安装了测试工具webpack,我们要使用的话需要通过node-modules/.bin/webpack -v这样来使用,但是有了npx,我们可以直接npx webpack -v就能使用。此外,对于一些全局命令,如果不存在,它会自动下载安装到一个临时目录,然后使用,不会污染全局空间。

首先 npx 是一个工具,旨在帮助完善使用npm注册表中的软件包的体验-npm使得超级容易安装和管理注册表中托管的依赖项,npx使得使用CLI工具和托管在该注册表中的其他可执行文件变得容易 注册表。 到目前为止,它大大简化了许多需要使用纯npm进行一些程序化的事情:

  1. 直接调用项目安装的模块
ruby 复制代码
npm install -D mocha // 项目中安装mocha测试模块
// 一般来说,调用 Mocha ,只能在项目脚本和 package.json 的scripts字段里面,
//  如果想在命令行下调用,必须像下面这样。
// 项目的根目录下执行 node-modules/.bin/mocha --version

// 如果使用npx
npx mocha --version
// npx运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。
  1. 控制调用本地/远程模块
  • 强制使用本地模块,不下载远程模块 npx --no-install http-server
  • 忽略本地同名模块,强制安装并使用远程模块 npx --ignore-existing create-react-app my-react-app
  1. 执行一次性命令,避免全局安装
lua 复制代码
npx create-react-app my-app 安装一个临时的 create-react-app 并且调用它,不会污染全局安装

场景: 比如尝试一个cli工具,可能这辈子只能用到这一次,不想这次使用完后还一直存在电脑硬盘里占用内存。 步骤:

  • 当不在$PATH中时调用npx将自动从npm注册表安装一个具有该名称的包,并调用它。

  • npx 将create-react-app下载到一个临时目录,使用以后再删除。

  • 完成后,安装的软件包将会删除,而不会出现在globals中,不必担心全局污染。

  1. 使用不同版本的Node(同样适用于其他库的指定版本)
css 复制代码
npx -p node@ node -v 可以用于一次性运行node版本
  1. 执行Github源码
shell 复制代码
# 执行 Gist 代码
$ npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32
# 执行仓库代码
$ npx github:piuccio/cowsay hello

1.5 总结

pnpm 起初看起来像 npm,因为它们的 CLI 用法相似,但管理依赖项却大不相同;pnpm 的方法带来更好的性能和最佳的磁盘空间效率。Yarn Classic 仍然很受欢迎,但它被认为是遗留软件,并且在不久的将来可能会放弃支持。Yarn Berry PnP 是新贵,但尚未看到它彻底改变包管理器领域的潜力。

目前还没有完美的依赖管理方案,可以看到在依赖管理的发展过程中,出现了:

  • 不同的 node_modules 结构,有嵌套,扁平,甚至没有 node_modules,不同的结构也伴随着兼容与安全问题。
  • 不同的依赖存储方式来节约磁盘空间,提升安装速度。
  • 每种管理器都伴随新的工具和命令,不同程度的可配置性和扩展性,影响开发者体验。
  • 这些包管理器也对 monorepo 有不同程度的支持,会直接影响项目的可维护性和速度。

库与开发者能够在这样优化与创新的发展过程中互相学习,站在巨人的肩膀上继续前进,不断推动前端工程领域的发展。

多年来,许多用户询问谁使用哪些包管理器,总体而言,人们似乎对 Yarn Berry PnP 的成熟度和采用特别感兴趣。但是国内我们能看到,pnpm似乎更受欢迎。

二、pnpm迁移

迁移过程中主要有如下问题:因为使用 npm 或 yarn 安装依赖项时,所有包都被提升到模块目录的根目录。因此,源代码可以访问未作为依赖项添加到项目的依赖项 。但是默认情况下,pnpm 使用链接仅将项目的直接依赖项添加到模块目录的根目录中

这意味着如果 package.json 没有引用的依赖,那么它将无法解析。这是迁移中的最大障碍。可以使用 auto-install-peers设置自动执行此操作(默认情况下是false)

对于多个使用 npm 安装依赖的项目,单独删除依赖包很耗时间,我们可以使用 npkill ,该工具可以列出系统中的任何 node_modules 目录以及它们占用的空间。然后可以选择要删除的依赖以释放空间

迁移流程

首先全局安装包

text 复制代码
npm i -g pnpm

迁移步骤如下

1.首先使用 npkill 删除 node_modules 依赖包

2.项目根目录创建 .npmrc,填写如下内容

text 复制代码
auto-install-peers=true

3.导入依赖锁定文件(pnpm-lock.yaml)

保证根目录有如下依赖锁定文件(npm-shrinkwrap.json,package-lock.json,yarn.lock)

然后执行如下命令

text 复制代码
pnpm import pnpm-lock.yaml

4,最后执行 pnpm i 安装依赖

问题

生成依赖文件警告

官方 issue 解释:Unmet peer dependencies and The command -- pnpm/pnpm (github.com)

生成 pnpm-lock.yaml 文件时出现如下警告

text 复制代码
 WARN  Issues with peer dependencies found
.
└─┬ vuepress 1.9.9
  └─┬ @vuepress/core 1.9.9
    └─┬ vue-loader 15.10.1
      └─┬ @vue/component-compiler-utils 3.3.0
        └─┬ consolidate 0.15.1
          ├── ✕ unmet peer react-dom@^16.13.1: found 15.7.0
          └── ✕ unmet peer react@^16.13.1: found 15.7.0

这是因为在 npm 3 中,不会再强制安装 peerDependencies (对等依赖)中所指定的包,而是通过警告的方式来提示我们。pnpm 会在全局缓存已经下载过的依赖包,如果全局缓存的依赖版本与项目 package.json 中指定的版本不一致,就会出现这种 hint 警告

我们可以在项目的 package.json 中配置 peerDependencyRules 忽略对应的警告提示

text 复制代码
{
  "pnpm": {
    "peerDependencyRules": {
      "ignoreMissing": [
        "react"
      ]
    }
  }
}

或者说直接在 .npmrc 配置文件中直接关闭严格的对等依赖模式,可以添加 strict-peer-dependencies=false 到配置文件中,或者执行如下命令

text 复制代码
npm config set strict-peer-dependencies=false

然后也可能会出现警告 deprecated subdependencies found,暂时可以忽略

幽灵依赖问题

在最后安装依赖的时候可能会出现幽灵依赖问题,幽灵依赖就是没有在package.json中,但是项目中,或者引用的包中使用到的依赖

举个例子,比如我们现在使用 npm 安装了 v-viewer 依赖,同时 viewerjsv-viewer 的依赖项,由于扁平化依赖机制,我们可以在 node_modules/v-viewer/package.json 中看到声明的 viewerjs 依赖,即使项目根目录下的 package.json 没有声明 viewerjs 依赖,我们仍旧可以使用,这就是幽灵依赖

而现在我们切换为 pnpm 后,在默认情况下不允许访问未声明的依赖,有以下两种解决方案

1.自行安装未声明依赖项

幽灵依赖自动扫描工具:@sugarat/ghost - npm (npmjs.com)

text 复制代码
pnpm i -S viewerjs

或者说某些版本 pnpm 会自动爆出幽灵依赖错误 missing peer ...,也可以直接不使用上面的扫描工具,直接自行安装后面的 ... 依赖

2.找到.npmrc 文件,在其中配置 public-hoist-pattern 或者 shamefully-hoist 字段,将依赖提升到根node_modules 目录下解决,也就是所谓的依赖提升

依赖提升官方文档:.npmrc | pnpm

text 复制代码
# .npmrc
# 提升含有 eslint(模糊匹配)、prettier(模糊匹配)、viewerjs(精确匹配) 的依赖包到根 node_modules 目录下
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=viewerjs

# 提升所有依赖到根 node_modules 目录下,相当于 public-hoist-pattern[]=*,与上面一种方式一般二选一使用
shamefully-hoist=true

当然,极不推荐用这样的方式解决依赖问题 ,这样没有充分利用 pnpm 依赖访问安全性的优势,又走回了 npm / yarn 的老路。

三、参考文章

pnpm、npm、yarn 包管理工具『优劣对比』及『环境迁移』 - 知乎 (zhihu.com)

深入浅出 npm & yarn & pnpm 包管理机制-CSDN博客

yarn yarn2 and pnpm的一些总结 | 码农家园 (codenong.com)

相关推荐
栈老师不回家8 分钟前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙14 分钟前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠18 分钟前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds38 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app
bysking3 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓3 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4113 小时前
无网络安装ionic和运行
前端·npm