前言
在一家低代码平台研发团队的办公室里,一群前端工程师们正聚精会神地讨论着他们下一个版本的开发计划。突然,一个充满热情的开发者闯进办公室,手持一杯冰美式,眼中闪烁着亮光,像是有了重大发现一般。他兴奋地向团队介绍了一个新的工具——PNPM。他用手指在空中划出虚拟的符号链接,比喻 PNPM 就像是魔法棒一样,可以在项目之间轻松共享依赖,彻底解决了他们在开发中遇到的痛点。
pnpm的三连招
- 节省磁盘空间
- 提高安装速度
- 创建一个非扁平的 node_modules 目录
一、pnpm介绍
pnpm全称performant npm,意味“高性能的 npm”,它是新一代的包管理工具。相较于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 | ✔️ | ❌ | ❌ |
速度比较(pnpm 比 npm 快了近 2 倍):
二、pnpm和npm、yarn的差异
- npm@3之前版本的node_modules目录结构
node_modules 中的每个依赖项都有自己的 node_modules 文件夹,在 package.json 中指定了所有依赖项。
比如项目 a 依赖项目 b,项目 c 也依赖项目 b,这样如果 a 和 c 依赖的 b 的版本不一致,也不会出问题。但是这样带来的问题是一个相同的依赖包可能会存在很多分,造成空间浪费,且package 中经常创建太深的依赖树,会导致 Windows 上目录路径过长的问题。
- npm@3+ 和 yarn 中的目录结构
为了解决前一代npm的问题,新一代的npm和yarn都采用了扁平化的node_modules结构,项目中的依赖及依赖使用的依赖都会平铺在node_modules下,解决嵌套层级过深的问题,包也不会被重复被安装,如遇到版本不同的情况,则会进行版本提升。这种方法的缺点是,npm必须首先遍历所有的项目依赖关系,再决定如何生成扁平的node_modules目录结构。npm必须为所有使用到的模块构建一个完整的依赖关系树,这是一个耗时的操作,也是npm安装速度慢的一个很重要的原因。另外由于扁平化的结构,导致即使项目中声明了依赖B,但是没有声明过依赖C,也可以直接使用依赖C,这就是幽灵依赖。
- 模块可以访问没有声明依赖的包
- 扁平化一个依赖树的算法非常复杂且耗时
- 在项目的中有一些包被重复安装
- pnpm的目录结构
pnpm 使用的是铺平和链接的方式安装node_modules结构。执行pnpm install后会将依赖安装到一个全局store中,并通过硬链接的方式链接到虚拟store,即node_modules/.pnpm文件夹。如上图下载了bar@1.0.0依赖后,node_modules下会生成bar@1.0.0和.pnpm文件夹,不会像npm那样将bar@1.0.0下的相关依赖铺平在node_modules下,而是铺平在.pnpm下再硬链接到store,而node_modules下的bar@1.0.0只是一个符号链接,通过软链接到.pnpm/bar@1.0.0。此时项目中要是想引入foo@1.0.0,就会报错,也就解决了npm中幽灵依赖的问题。
三、使用方法
pnpm的使用方法跟npm很相似
安装:npm install -g pnpm
安装完成后,删除本地的node_modules包,输入
pnpm install
即可下载依赖包,跟npm使用过程一样,但是速度变快了
-
基础命令
打包
pnpm run build
下载私服
pnpm install qiqiao-runtime-form
pnpm import
从另一个软件包管理器的 lock 文件生成 pnpm-lock.yaml。
支持的源文件:
- package-lock.json
- npm-shrinkwrap.json
- yarn.lock
移除依赖
pnpm remove ‘xxxx’
删除无用的依赖包
从存储中删除未引用的包。
未引用的包是系统上的任何项目中都未使用的包。在大多数安装操作之后,包有可能会变为未引用状态,例如当依赖项变得多余时。
举例而言,在 pnpm install 期间,包 foo@1.0.0 被更新为 foo@1.0.1。 pnpm 将在存储中保留 foo@1.0.0 ,因为它不会自动除去包。 如果包 foo@1.0.0 没有被其他任何项目使用,它将变为未引用。运行 pnpm store prune 将会把 foo@1.0.0 从存储中删除。
运行 pnpm store prune 是无害的,对您的项目没有副作用。 如果以后的安装需要已经被删除的包,pnpm 将重新下载他们。
最好的做法是 pnpm store prune 来清理存储,但不要太频繁。有时,未引用的包会再次被需要。这可能在切换分支和安装旧的依赖项时发生,在这种情况下,pnpm 需要重新下载所有删除的包,这会暂时减慢安装过程。
请注意,当 pnpm server | pnpm 正在运行时,这个命令是被禁止的。
pnpm store prune
-
构建设置
ignore-scripts
- 默认值: false
- 类型:Boolean
不执行任何项目中 package.json 和它的依赖项中定义的任何脚本。该标记不会阻止执行.pnpmfile.cjs
在部分电脑中下载plus的依赖时发现不能成功下载sentry-cli的依赖包,此时需要用
pnpm add @sentry/cli@1.64.1 --ignore-scripts 来下载
四、幽灵依赖-旧项目迁移pnpm可能会遇到的问题
● 问题讲解
由于在旧项目中,一般使用的都是npm或yarn去下载依赖包,此时按照上面的说法,他们的node_modules将会是扁平化的结构,项目中的依赖及依赖使用的依赖都会平铺在node_modules。此时就有可能会产生一个问题——幽灵依赖。即某个包没有被安装(package.json 中并没有,但是用户却能够引用到这个包)。
如我在项目A中下载了一个叫A-A的依赖包,而A-A依赖包又依赖另一个依赖包A-B,即
此时虽然没有在package.json显示声明A-B依赖包,但由于扁平化的node_modules结构,此时已将各个依赖包提升到了根部,所以此时A项目是能直接引用A-B的。这样就会造成隐患问题。如果依赖A-A中哪天不需要使用依赖A-B了,那我们的node_modules里就会没有依赖A-B,但我们在代码中使用了A-B,这样就会导致项目报错。
而当我们使用pnpm去下载依赖时,各个依赖不再提升到node_modules根部,而是采用软连接的方式去查询依赖包的地址。这就解决了幽灵依赖的问题,所以当你用pnpm去迁移老项目的时候,你大概率会遇到这样的报错信息:
Error: Cannot find module 'xxxxxxx'
Module not found: Error: Can't resolve 'xxxxxxxxxx'
这些都是原先幽灵依赖遗留下的问题,pnpm官方也给出了几种解决方案
● 解决方案
1.根据报错信息去下载确实的依赖,通过 pnpm add ‘xxxxxxx’安装它,并自动将它添加到项目的 package.json 中。
2.使用pnpm提供的钩子函数将缺失的依赖项添加到包的 package.json 中
一个例子是 Not working with Webpack Dashboard (Works now) · Issue #1043 · pnpm/pnpm · GitHub ,它不能与 pnpm 工作。 此后解决了,它现在就与 pnpm 工作了。
它曾经抛出一个错误:
Error: Cannot find module 'babel-traverse'
at /node_modules/inspectpack@2.2.3/node_modules/inspectpack/lib/actions/parse
问题是 babel-traverse 被用于 inspectpack, 然后被用于 webpack-dashboard,但是 babel-traverse 没有在 inspectpack 的 package.json 中。 它仍然与 npm 和 yarn 兼容,因为它们构建的平铺的 node_modules结构。
解决方案是创建一个 .pnpmfile.cjs ,内容如下:
module.exports = {
hooks: {
readPackage: (pkg) => {
if (pkg.name === "inspectpack") {
pkg.dependencies['babel-traverse'] = '^6.26.0';
}
return pkg;
}
}
}
创建 .pnpmfile.cjs 后,仅删除 pnpm-lock.yaml - 不需要 删除 node_modules,因为 pnpm 挂钩仅影响模块解析。 然后,重建依赖项 & 它应该可以工作。
3.如果缺少的依赖包数量少,且能知道其具体包名,可以使用在.npmrc文件中添加 public-hoist-pattern 命令,将匹配的依赖提升至根模块目录中。 提升至根模块目录中意味着应用代码可以访问到幻影依赖,即使他们对解析策略做了不当的修改。
public-hoist-pattern[]=*plugin*
4.如果存在大量依赖包缺失,可以添加shamefully-hoist=true到.npmrc文件中,或使用pnpm i --shamefully-hoist来下载依赖,这会创建一个扁平的 node_modules 结构,类似由 npm 或 yarn 创建的结构,不建议这样做,因为避免这种结构是 pnpm 的 node_modules 结构主要目的。
● 实操问题
如在产品的runtime中
glob是某个依赖包的子依赖包而一起下载下来的,没有在package.json中定义,就直接在项目中使用。由于之前是npm的node_modules扁平化结构,glob依赖包被提升到根目录,此时可以使用。
但若是迁移成pnpm的结构,glob依赖包就不存在node_modules根目录下,而是在.pnpm目录下。此时无法通过软连接的方式查询到该依赖包,导致报错。
此时通过单独安装该依赖包或单独将该依赖包提升到跟目录的方式即可解决。
pnpm add glob@7.1.2
public-hoist-pattern[]=*glob*
五、运用在项目
使用过程:
1.删除本地plus项目的node_modules文件
2.在package.json中添加下面对应项目的依赖包代码,注意放在dependencies下
3.下载依赖包
pnpm install
在实战过程中发现,有些电脑可能因为环境的原因,导致xxxx依赖下载时会一直不成功,所以当用pnpm install 下载依赖后报这个错误,就需要单独去下载这个依赖包。
pnpm addxxxx --ignore-scripts
4.运行项目
pnpm dev
六、小结
就像充满激情的开发者在办公室里展示 PNPM 的那一幕一样,包管理器确实是一种令人惊叹的工具。它通过符号链接的方式,为低代码平台研发团队提供了便捷的依赖共享方式,使他们在项目开发中节省了大量时间和精力。尽管 PNPM 和传统的 npm 和 Yarn 有些许不同,但它的出现为开发者们带来了新的选择和可能性。如同办公室里的那个热情洋溢的场景一般,技术的进步总是充满着活力和创新,而 PNPM 则是这个创新世界中的一股清流,让开发过程更加轻松愉快。
作者介绍:
道一云,成立于2004年,是中国低代码领域的领导厂商、腾讯战略投资企业、腾讯生态核心合作伙伴。拥有自主知识产权管理软件产品百余项,涵盖数字化应用构建低代码平台-七巧、全场景智能业务分析BI-七析、千人千面、数智化办公企业级门户-七星以及30多款开箱即用的场景应用。
欢迎关注:
官网:道一云七巧 - 可视化、智能化、数字化应用构建
免费体验:道一云产品免费试用
公众号:道一云低代码(do1info)