统一不同的同事之间,本地和 CI 之间的开发环境有利于保证运行效果一致和 bug 可复现,本文聚焦于前端最基本的开发环境配置:nodejs 和 包管理器。
nodejs
首先推荐使用 fnm 管理多版本 nodejs。
对比 nvm:
- 支持
brew
安装,更新方便 - 跨平台,windows 也能用
winget
安装
使用 fnm 一定要记得开启根据当前 .nvmrc 自动切换对应的 nodejs 版本,也就是在在 .zshrc
中加入:
eval "$(fnm env --use-on-cd)"
包管理器
尽管 npm
一直在进步,甚至 7.x 已经原生支持了 workspace。但是我钟爱 pnpm,理由:
- 安全,避免幽灵依赖,不会将依赖的依赖平铺到 node_modules 下
- 快,基于软/硬链接,node_modules 下是软连接,硬链接到 .pnpm 文件夹下的硬链接
- 省磁盘,公司配的 mac 只有 256G
- pnpm 的可配置性很强,配置不够用还可以用
.pnpmfile.js
编写 hooks yarn2
node_modules 都看不到,分析依赖太麻烦了- 公司用的 vue,而 vue3/vite 用 pnpm(政治正确)
推荐使用 Corepack 管理用户的包管理器,其实我一开始知道有 corepack
这个 nodejs 官方的东西的时候,我就在想:为啥不叫 npmm
(node package manager manager) 呢?
corepack
目前官方觉得功能没稳定,所以默认没开启,需要用户通过 corepack enable
手动开启,相关的讨论:enable corepack by default。
有了 corepack
我们就可以轻松的在 npm/yarn/pnpm 中切换,安装和更新不同的版本。还有一个非常方便的特性就是通过在 package.json
中声明 packageManager
字段例如 "[email protected]"
,当我们开启了 corepack
,cd 到该 package.json
所在的 package 的时候,运行 pnpm
时 corepack
会使用 8.14.1
版本的 pnpm
。
corepack
是怎样做到的呢?nodejs
安装文件夹有个的 bin
目录,这个目录会被添加到 path
环境变量,其中包含了 corepack
以及 corepack
支持的包管理器的可执行文件:
❯ tree ../../Library/Caches/fnm_multishells/17992_1705553706619/bin
../../Library/Caches/fnm_multishells/17992_1705553706619/bin
├── corepack -> ../lib/node_modules/corepack/dist/corepack.js
├── node
├── npm -> ../lib/node_modules/npm/bin/npm-cli.js
├── npx -> ../lib/node_modules/npm/bin/npx-cli.js
├── pnpm -> ../lib/node_modules/corepack/dist/pnpm.js
├── pnpx -> ../lib/node_modules/corepack/dist/pnpx.js
├── yarn -> ../lib/node_modules/corepack/dist/yarn.js
└── yarnpkg -> ../lib/node_modules/corepack/dist/yarnpkg.js
可以看到 pnpm
被链接到了 corepack
的一个 js 文件,查看 corepack/dist/pnpm.js
内容:
#!/usr/bin/env node
require('./lib/corepack.cjs').runMain(['pnpm', ...process.argv.slice(2)]);
可以看到其实 corepack
相当于劫持了 pnpm
和 yarn
命令,然后根据 packageManager
字段配置自动切换到对应的包管理器,如果已经安装过就使用缓存,没有就下载。
怎样统一 nodejs 和 包管理器
问题
虽然我在项目中配置了 .nvmrc
文件,在 package.json
中声明了 packageManager
字段,但是用户可能没有安装 fnm
以及配置根据 .nvmrc
自动切换对应的 nodejs,还有可能没有开启 corepack
,所以同事的环境还是有可能和要求的不一致。我一向认为,不应该依靠人的自觉去遵守规范,通过工具强制去约束才能提前发现问题和避免争论。
解决办法
最开始是看到项目中使用了 only-allow 用于限制同事开发时只能用 pnpm
,由此我引发了我一个灵感,为什么我不干脆把事情做绝一点,把 nodejs
的版本也给统一了
于是我写了一个脚本用于检查用户本地的 nodejs
的版本,包管理器的版本必须和要求一致。最近封装成了一个 cli:check-fe-env。使用方式很简单,增加一个 preinstall
script:
{
"scripts": {
"preinstall": "npx check-fe-env"
}
}
工作原理
- 用户在运行
pnpm install
之后,install 依赖之前,包管理器会执行preinstall
脚本 - cli 会检测:
- 用户当前环境的
nodejs
版本和.nvmrc
中声明的是否一样 - 用户当前使用的包管理器种类和版本是否和
package.json
中packageManager
字段一样
- 用户当前环境的
获取当前环境的 nodejs
版本很简单,可以用 process.version
。想要获取执行脚本时的包管理器可以通过环境变量:process.env.npm_config_user_agent
,如果一个 npm script 是通过 pnpm
运行的,那么这个环境变量会被设置为例如 pnpm/8.14.1 npm/? node/v20.11.0 darwin arm64
,由此我们可以获取当前使用的包管理器和版本。
为了加快安装速度,我特意把源码和相关依赖给一起打包了,整个 bundle 大小 8k 左右。
局限性
最新的 npm
和 pnpm
目前貌似都有一个 bug,都是安装完依赖才执行 preinstall
hooks,具体看这: Preinstall script runs after installing dependencies。
这个方案对于 monorepo 项目或者说不需要发包的项目是没啥问题的,但是不适用于一个要发包的项目。原因是 preinstall
script 除了会在本地 pnpm install
时执行,别人安装这个包,也会执行这个 preinstall
script,就和 vue-demi
用的 postinstall
script 一样。主要是确实没找到一个:只会在本地运行 pnpm install
后且在安装依赖前执行的 hook。