前端依赖一致性的问题分析与最佳实践
# 背景
前端开发经常遇到项目依赖变更的问题,可以归结为时间、空间和人三个方向。
- 时间:没有锁版本(lock),下次安装时部分依赖会更新到最新的小版本
- 空间:系统的 Node.js 版本、包管理工具版本等与目标要求不一致,导致依赖变更
- 人:使用了错误的包管理工具或者命令,导致依赖变更
这些问题不能单纯依靠文档规范来限制,需要有行之有效的工具来避免。
本文会先描述这些常见问题的原因和解决方案,并在最后给出可实施的最佳实践。
# 常见问题
# 1. 不锁版本(lock),依赖包会自动更新
这个应该都懂,需要把 lock 文件加入 git 版本控制之中。
但可能有些同学不知道为什么这么做,这里简单提下。
如果没有锁文件(比如 package-lock.json
),Node 在安装依赖时会选择符合版本范围的最新版本,这个版本范围的设计叫做 semver (opens new window) 规范,其格式为 主版本号.次版本号.修订号
,版本号递增规则如下:
- 主版本号:当你做了不兼容的 API 修改,
- 次版本号:当你做了向下兼容的功能性新增,
- 修订号:当你做了向下兼容的问题修正。
理想情况下大家都照这个要求设计,有不兼容变更就做主版本号
升级,那当然没问题,大家也乐意自动更新来修复漏洞。
然而实际情况上社区的大部分包并不遵守这个约定,有不兼容变更却只是升级了 修订号
或者 次版本号
。。如果选了自动升级,那么准备等待项目报错吧。
因此还是乖乖的 lock 文件加入 git 版本控制吧。
# 2. 锁了版本包也没动,还是更新了依赖
主要表现为没有改动包版本,也使用了 lock 锁定版本,但是后面重装依赖时还是更新了版本和 lock 文件。
如果出现这个原因,是 npm 5.x 时期的某些奇怪设计 (opens new window),更新 npm 包版本即可。
一些相关的讨论:
- Why does "npm install" rewrite package-lock.json? (opens new window)
- Clarify documentation for package-lock.json behaviour (opens new window)
- why is package-lock being ignored? · Issue #17979 · npm/npm (opens new window)
# 3. 混用包管理工具
接到一个新项目,我们会选择什么包管理工具安装依赖?
有经验的同学可能会先看项目说明文档(如果有的话),或者看看项目存在哪种 lock 文件(package-lock.json
、pnpm-lock.json
、yarn.lock
),再决定使用对应的包管理工具。
但总有部分人,经验不足或基于习惯,使用了与项目要求不符的包管理工具。这样的问题在于,该包管理工具没有 lock 文件,依赖是最新的不可置信的。也不可能把这个错误的 lock 文件提交,那会导致管理混乱。
如何解决?把非目标的包管理工具 lock 文件加入 gitignore
,只能解决管理混乱。
可以尝试在安装依赖前(preinstall
/prepare
npm-script 钩子),检查依赖安装命令用的工具是否符合预期,如果不符合预期则中断安装并提示使用正确的命令。
社区也有现成的工具 only-allow (opens new window),可以直接使用,具体可以看官方文档。
"preinstall": "npx only-allow pnpm"
# 4. 安装具体依赖不会触发 prepare
钩子
问题 3 中,我们提到了使用 prepare
钩子来检测依赖安装命令是否符合预期。
然而 prepare
钩子的触发条件是有限制的(preinstall
同),如果 install 命令存在其他参数,将无法触发这个钩子。
npm i lodash
由于 npm 针对单个依赖安装触发钩子的 RFC (opens new window) 还在讨论阶段,故目前还没有根本的解决方案,但我们可以通过一些手段来避免这个问题:
- 如问题 3 所说,把错误的包管理工具 lock 文件加入
gitignore
- 提交合并请求时,在 CI 阶段做一次 lock 文件完整性校验。如果
package.json
的依赖存在变更,但没有加入lock 文件,说明之前的安装方式不正确。
一些相关的讨论:
- Does not seem to work with single package installs · Issue #1 · pnpm/only-allow (opens new window)
- trouble with npm preinstall script (opens new window)
# 5. 自动使用正确的依赖安装命令
问题 3 中,我们提到了使用 prepare
钩子来检测依赖安装命令是否符合预期,如果不符合预期则中断安装并提示使用正确的命令。
那能否调整为:如果不符合预期,则使用正确的命令安装?
一个技巧是,在 prepare
钩子中执行正确的安装命令,在安装后中断脚本运行,避免走到默认的依赖安装。
示例脚本:
# install.sh
# 在 preinstall 中添加如下脚本
# "preinstall": "sh install.sh",
# 无论使用什么包管理工具,最后都改成 pnpm install
echo '已自动改成 pnpm install'
# 跳过 scripts ,避免循环执行
pnpm install --ignore-scripts
rm package-lock.json
rm yarn.lock
echo '安装完成,中断后续执行'
exit 1
2
3
4
5
6
7
8
9
10
11
12
13
以上为测试代码,请勿使用在生产项目中。如果有更好的方案,欢迎分享讨论~
# 6. 限制 Node 和包管理工具的版本
因为设计思路的变化,包管理工具的不同版本也会导致 lock 文件不同。
有一些项目只能运行在特定的 Node.js 版本,又或者使用特定版本的包管理工具安装依赖。
Node.js 好理解,不同版本的语法会有变化。而使用特定版本的包管理工具,通常是和幽灵依赖 (opens new window)相关。某些依赖凑巧使用了另一个版本的依赖,一切凑巧使得项目能够正常运行。如果更新了包管理工具版本,依赖组织方式变了,那项目就不一定能运行了。
彻底的解决方案还是进行升级,但没有时间、收益太低,且这种改动影响面太大,通常需要整体测试。
因此,通常还是选择利用 package.json
的 engines (opens new window) 字段限制 Node 和包管理工具的版本。
{
"engines": {
"node": ">=14",
"npm": "~8",
"pnpm": "~8",
"yarn": "~1",
}
}
2
3
4
5
6
7
8
需要注意的是,engines.npm
默认并不生效(只警告不中断),需要在 .npmrc
文件中添加 engine-strict=true
才行。也有另一种解决方案是在 prepare
钩子中检测 npm 的版本,如果不符合版本要求就中断执行,相关代码可以看这篇文档 (opens new window)。
# 7. 快速切换版本
在问题 6 中,我们谈到了限制 Node 和包管理工具的版本。但有限制就会有快速切换的需求,一个电脑通常不只跑一个项目。
如何快速切换?比如 node 12
切换到 node 14
,又或者 pnpm 6
切换到 pnpm 7
?
我们拆成两个问题来看:
- 快速切换 Node 版本
- 快速切换包管理工具版本
20230709 更新,有个工具 volta (opens new window) 可以更方便的做这个事情
# 快速切换 Node 版本
切换 Node 版本简单,使用 nvm (opens new window) 来管理即可。
还可以在项目根目录创建 .nvmrc
文件并添加版本号,需要切换的时候直接 nvm use
即可,无需记忆版本。
// .nvmrc
v16.0.0
2
还能不能更快?比如我切换了项目目录,就自动帮忙切换版本。
在社区上找到了一款叫 avn (opens new window) 的工具,号称能做这样的事,但看起来很久没迭代了,这种监测应该会有额外的性能影响,没有实际体验过不做评价。
我个人的实践是将 node 版本检测和切换的逻辑放入 prepare
等钩子,在需要的时候再切。
# 快速切换包管理工具版本
通常包管理工具在全局只会存在一个版本,有以下 3 种解决方案:
- corepack: Node.js 16 推出,可以用来管理各种包版本工具,包括 npm/yarn/pnpm,参考文档:Corepack (opens new window)、pnpm using corepack (opens new window)
# 以 pnpm 版本切换为例
# 启用 corepack
corepack enable
# 指定版本
corepack prepare pnpm@<version> --activate
# 在 Node.js v16.17 及以上版本,支持使用 latest tag 安装最新的版本
corepack prepare pnpm@latest --activate
2
3
4
5
6
7
8
9
- pnpm dlx: 使用 pnpm dlx (opens new window) 来指定 pnpm 运行命令使用
pnpm7
,可以参考这篇文章 (opens new window),个人不推荐
pnpm dlx pnpm@7 install
pnpm dlx pnpm@7 run dev
2
- 项目内依赖:在公司内部有个实践,把 pnpm 等包管理工具作为依赖安装在项目中,再通过其他命令调用,把命令转发给这个项目内的 pnpm 。这样做的好处是可以实现包管理工具的版本控制,缺点就是需要改用其他命令。
# 最佳实践
对上面的问题做下整理,可以得到以下最佳实践。
- 使用 lock 来锁住版本,并使用较新版本的 npm 避免一些遗留问题
- 把非目标的包管理工具 lock 文件加入
gitignore
- 使用 prepare 钩子和 only-allow 工具避免混用包管理工具
- 在 CI 阶段再次进行 lock 文件完整性校验
- 使用
package.json
的 engines (opens new window) 字段限制 Node 和包管理工具的版本
# 展望
实际上还存在一些问题,比如问题 5 的「自动使用正确的依赖安装命令」,目前还缺少官方解决方案。以及问题 7 提到的 Corepack 工具,目前还处于实验阶段。
感叹下,前端的基建真挺稀烂的,一些不合理的设计,导致了数百万前端程序员接盘,不过这个情况也在慢慢变好了。
最后,👋🏻 Respect!欢迎一键三连 ~