关于项目npm迁移到pnpm的一些踩坑

2,731 阅读5分钟

迁移优势

官网:www.pnpm.cn/

  • 安装更快速,磁盘空间占用更小:pnpm的依赖包实际上是存储在公共库,项目里的依赖包是通过硬链接软链接(相当于快捷方式)指向依赖包的。这就可以使得不同的项目可以共享相同版本的依赖包,减少磁盘空间的使用,以及避免重复下载。(我的项目在替换成pnpm后,依赖包的下载速度提升了不少)
  • 更安全:pnpm 创建的 node_modules 默认并非扁平结构,因此代码无法对任意软件包进行访问

迁移步骤

迁移顺利的条件是每个步骤都成功执行,实际操作过程中,由于pnpm与npm的依赖包有不同的管理策略,会出现依赖冲突,资源路径引用错误等问题,后面会讲到我在升级过程中踩的坑

  1. 通过npm全局安装pnpm
npm install -g pnpm
  1. 删除项目中的node_modules(项目中的package-lock.json升级成功后也可以删除)
rm -rf node_modules
  1. 执行pnpm i
pnpm install
  1. 启动项目并访问,确认项目正常运行

npm与pnpm的安装策略

npm的本地依赖是直接安装到项目根目录的node_modules下,npm3以前依赖下的依赖是层层嵌套的,npm3以后为了解决这种嵌套过深且相同依赖无法复用的问题,把依赖下的依赖提升到node_modules目录,这就导致一些非直接依赖暴露给了项目,隐式依赖会带来安全隐患,并且如果统一依赖存在不同版本,也只能提升其中一个版本,其它版本还是存在嵌套的问题。

假设当前项目依赖如下两个npm包:foo(1.0.0) bar(1.0.0),foo和bar也有各自的依赖包,详见下面的的目录结构(重点在于有两个版本的B依赖包)

npm3以前的依赖包结构

node_modules
├──foo(1.0.0)
│ ├──A(1.0.0)
│ │ ├──C(1.0.0)
│ ├──B(1.0.0)
├──bar(1.0.0)
│ ├──A(1.0.0)
│ │ ├──C(1.0.0)
│ ├──B(1.0.1)

npm3以后的依赖包结构

node_modules
├──foo(1.0.0)
├──bar(1.0.0)
│ ├──B(1.0.1)
├──B(1.0.0)
├──A(1.0.0)
├──C(1.0.0)

pnpm依赖包的目录结构

在了解pnpm包的目录结构前,我们先了解一下软链接和硬链接

软链接

如我当前有个myapp项目,项目的开发依赖包有vite

image.png 我们打开node_modules,可以看到vite文件夹左下角是有个箭头的,这个就是标识当前文件是一个软链接

image.png

实际地址可以通过命令行看到 image.png

硬链接

硬链接的本质是多个文件名指向磁盘上的同一块数据。

通过上面的软链接我们知道vite的实际地址是“...\myapp\node_modules.pnpm\vite@7.1.12\node_modules\vite”,这个vite实际上就是一个硬链接,指向pnpm的全局存储。

我们复制一个跟myapp一样的项目myapp1(复制处理node_modules外的其它文件),执行pnpm install,我们可以看到两次安装时间的差距。正是因为这些硬链接实际上下载过一次就不需要再次下载了,大大节省磁盘空间和安装时间。

image.png

image.png

上面示例的项目在pnpm管理器的目录结构如下

node_modules
├──.pnpm
│ ├──foo@1.0.0
│ │ ├──node_modules
│ │ │ ├──foo(硬链接,指向文件在公共库的实际地址)
│ │ │ ├──A (软链接,指向 .pnpm/A@1.0.0)
│ │ │ ├──B (软链接,指向 .pnpm/B@1.0.0)
│ ├──bar@1.0.0
│ ├──A@1.0.0
│ │ ├──node_modules
│ │ │ ├──A(硬链接)
│ │ │ ├──C(软链接,指向 .pnpm/C@1.0.0)
│ ├──B@1.0.0
│ │ ├──node_modules
│ │ │ ├──B(硬链接)
│ ├──B@1.0.1
│ │ ├──node_modules
│ │ │ ├──B(硬链接)
│ ├──C@1.0.0
│ │ ├──node_modules
│ │ │ ├──C(硬链接)
│ ├──C@1.0.1
│ │ ├──node_modules
│ │ │ ├──C(硬链接)
│ ├──bar@1.0.0
│ │ ├──node_modules
│ │ │ ├──bar(硬链接)
│ │ │ ├──A (软链接,指向 .pnpm/A@1.0.0)
│ │ │ ├──B (软链接,指向 .pnpm/B@1.0.1)
├──foo(软链接,指向 .pnpm/foo@1.0.0)
├──bar(软链接,指向 .pnpm/bar@1.0.0)

迁移中遇到的问题

依赖冲突

我的项目在迁移的过程中,启动项目的时候,出现了报错,报错内容大概如下:

error in ./node_modules/.pnpm/xxx@1.6.13/node_modules/@yyy/xxx/components/login/errorToast.vue?&vue&type=style&index=0&id=2abec1bb&lang=css&

Syntax Error: Error: PostCSS plugin autoprefixer requires PostCSS 8. Migration guide for end-users: github.com/postcss/pos…

解决思路: yyy包组件在编译css的时候报错了,报错内容是autoprefixer需要8版本以上的PostCSS(yyyy依赖的autoprefixer是高版本的),说明在编译的过程中,编译css的工具包使用了高版本的autoprefixer低版本的PostCSS,由于xxx包使用的是@vue/cli-service编译项目,通过查找pnpm-lock.yaml,发现@vue/cli-service依赖postcss-loader: 3.0.0,这个包里面依赖的postcss版本为"postcss": "^7.0.0",这个依赖关系我们是没办法改变的,所以我们可以通过降低autoprefixer的版本来达到兼容

pnpm i autoprefixer@9.8.8 -D

swiper样式路径错误

由于我的项目中使用了vue-awesome-swipe,这个依赖包在项目使用中,css是引入swiper这个依赖包的,代码如下

import { swiper, swiperSlide } from 'vue-awesome-swiper'
import 'swiper/dist/css/swiper.css'

解决方案: 这也是因为代码引用未声明包引起的错误(package.json没有声明,但是可以某个依赖包依赖了这个swiper,所以swiper出现在node_modules的根目录下使得项目可以引用)。我的解决方案是显式安装swiper

pnpm i swiper@4.5.1

git hooks钩子报错

在项目正常运行后,准备提交代码,发现git报错了,报错内容是

.git/hooks/pre-commit: line 51: ../../.pnpm/run-node@1.0.0/node_modules/run-node/run-node: No such file or directory

报错原因:pnpm与npm存储本地依赖包的路径发生变化,导致原先通过npm安装husky生成的钩子文件没有更新,并且在新的目录clone的此项目,.git/hooks目录并没有生成钩子

解决办法: 升级husky为8以上,新版本的husky已经修改了钩子的存储目录

首先,安装新版本的husky

pnpm i husky@latest

修改package.json

{
    "scripts": {
        "prepare": "husky install"
    }
}

删除package.json中对husky的定义,如下

{
    "husky": {
        "hooks": {
              "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
              "pre-commit": "lint-staged"
        }
    }
}

通过命令行,添加pre-commit、commit-msg相关钩子

npx husky add .husky/pre-commit "npx lint-staged $1"
npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1 && bash "$(dirname "$0")/../bash/wizard.sh""

结语

至此,npm迁移pnpm完成