• 前端工程师的困扰:为什么会出现包依赖不一致的情况?
  • 发布于 2个月前
  • 309 热度
    0 评论
背景

前端开发经常遇到项目依赖变更的问题,可以归结为时间、空间和人三个方向。

时间:没有锁版本(lock),下次安装时部分依赖会更新到最新的小版本
空间:系统的 Node.js 版本、包管理工具版本等与目标要求不一致,导致依赖变更
人:使用了错误的包管理工具或者命令,导致依赖变更

这些问题不能单纯依靠文档规范来限制,需要有行之有效的工具来避免。本文会先描述这些常见问题的原因和解决方案,并在最后给出可实施的最佳实践。
常见问题
1.  不锁版本(lock),依赖包会自动更新
这个应该都懂,需要把 lock 文件加入 git 版本控制之中。但可能有些同学不知道为什么这么做,这里简单提下。如果没有锁文件(比如 package-lock.json),Node 在安装依赖时会选择符合版本范围的最新版本,这个版本范围的设计叫做 semver 规范,其格式为 主版本号.次版本号.修订号 ,版本号递增规则如下:
主版本号:当你做了不兼容的 API 修改,
次版本号:当你做了向下兼容的功能性新增,
修订号:当你做了向下兼容的问题修正。

理想情况下大家都照这个要求设计,有不兼容变更就做主版本号升级,那当然没问题,大家也乐意自动更新来修复漏洞。然而实际情况上社区的大部分包并不遵守这个约定,有不兼容变更却只是升级了 修订号 或者 次版本号。。如果选了自动升级,那么准备等待项目报错吧。因此还是乖乖的 lock 文件加入 git 版本控制吧。
2.  锁了版本包也没动,还是更新了依赖
主要表现为没有改动包版本,也使用了 lock 锁定版本,但是后面重装依赖时还是更新了版本和 lock 文件。如果出现这个原因,是 npm 5.x 时期的某些奇怪设计,更新 npm 包版本即可。
3.  使用了错误的包管理工具

接到一个新项目,我们会选择什么包管理工具安装依赖?有经验的同学可能会先看项目说明文档(如果有的话),或者看看项目存在哪种 lock 文件(package-lock.json、pnpm-lock.json 、yarn.lock),再决定使用对应的包管理工具。但总有部分人,经验不足或基于习惯,使用了与项目要求不符的包管理工具。这样的问题在于,该包管理工具没有 lock 文件,依赖是最新的不可置信的。也不可能把这个错误的 lock 文件提交,那会导致管理混乱。


如何解决?把非目标的包管理工具 lock 文件加入 gitignore ,只能解决管理混乱。可以尝试在安装依赖前(preinstall/prepare npm-script 钩子),检查依赖安装命令用的工具是否符合预期,如果不符合预期则中断安装并提示使用正确的命令。
4.  安装具体依赖不会触发 prepare 钩子
问题 3 中,我们提到了使用 prepare 钩子来检测依赖安装命令是否符合预期。然而 prepare 钩子的触发条件是有限制的(preinstall 同),如果 install 命令存在其他参数,将无法触发这个钩子。
npm i lodash
官方文档也有说明:由于 npm 针对单个依赖安装触发钩子的 RFC 还在讨论阶段,故目前还没有根本的解决方案,但我们可以通过一些手段来避免这个问题:
.如问题 3 所说,把错误的包管理工具 lock 文件加入 gitignore
.提交合并请求时,在 CI 阶段做一次 lock 文件完整性校验。如果 package.json 的依赖存在变更,但没有加入lock 文件,说明之前的安装方式不正确。

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
备注:以上为测试代码,请勿使用在生产项目中。如果有更好的方案,欢迎分享讨论~

6.  限制 Node 和包管理工具的版本
有一些项目只能运行在特定的 Node.js 版本,又或者使用特定版本的包管理工具安装依赖。Node.js 好理解,不同版本的语法会有变化。而使用特定版本的包管理工具,通常是和幽灵依赖相关。某些依赖凑齐使用了另一个版本的依赖,一切凑齐使得项目能够正常运行。如果更新了包管理工具版本,依赖组织方式变了,那项目就不一定能运行了。彻底的解决方案还是进行升级,但没有时间、收益太低,且这种改动影响面太大,通常需要整体测试。
因此,通常还是选择利用 package.json 的 engines 字段限制 Node 和包管理工具的版本。
{
  "engines": { 
    "node": ">=14",
    "npm": "~8",
    "pnpm": "~8", 
    "yarn": "~1", 
  }
}
需要注意的是,engines.npm 默认并不生效(只警告不中断),需要在 .npmrc 文件中添加 engine-strict=true 才行。也有另一种解决方案是在 prepare 钩子中检测 npm 的版本,如果不符合版本要求就中断执行,相关代码可以看这篇文档。
7.  快速切换版本
在问题 6 中,我们谈到了限制 Node 和包管理工具的版本。但有限制就会有快速切换的需求,一个电脑通常不只跑一个项目。如何快速切换?比如 node 12 切换到 node 14,又或者 pnpm 6 切换到 pnpm 7 ?
我们拆成两个问题来看:
.快速切换 Node 版本
.快速切换包管理工具版本

快速切换 Node 版本
切换 Node 版本简单,使用 nvm 来管理即可。还可以在项目根目录创建 .nvmrc 文件并添加版本号,需要切换的时候直接 nvm use 即可,无需记忆版本。
// .nvmrc
v16.0.0
还能不能更快?比如我切换了项目目录,就自动帮忙切换版本。在社区上找到了一款叫 avn 的工具,号称能做这样的事,但看起来很久没迭代了,这种监测应该会有额外的性能影响,没有实际体验过不做评价。我个人的实践是将 node 版本检测和切换的逻辑放入 prepare 等钩子,在需要的时候再切。
快速切换包管理工具版本
通常包管理工具在全局只会存在一个版本,有以下 3 种解决方案:
1.corepack: Node.js 16 推出,可以用来管理各种包版本工具,包括 npm/yarn/pnpm
# 以 pnpm 版本切换为例

# 启用 corepack
corepack enable

# 指定版本
corepack prepare pnpm@<version> --activate
# 在 Node.js v16.17 及以上版本,支持使用 latest tag 安装最新的版本
corepack prepare pnpm@latest --activate
2.pnpm dlx: 使用 pnpm dlx 来指定 pnpm 运行命令使用 pnpm7 ,可以参考这篇文章,个人不推荐
pnpm dlx pnpm@7 install
pnpm dlx pnpm@7 run dev
3.项目内依赖:在公司内部有个实践,把 pnpm 等包管理工具作为依赖安装在项目中,再通过其他命令调用,把命令转发给这个项目内的 pnpm 。这样做的好处是可以实现包管理工具的版本控制,缺点就是需要改用其他命令。

最佳实践
对上面的问题做下整理,可以得到以下最佳实践。
1.使用 lock 来锁住版本,并使用较新版本的 npm 避免一些遗留问题
2.把非目标的包管理工具 lock 文件加入 gitignore
3.使用 prepare 钩子和 only-allow 工具
4.在 CI 阶段再次进行 lock 文件完整性校验
5.使用 package.json 的 engines 字段限制 Node 和包管理工具的版本

展望
实际上还存在一些问题,比如问题 5 的「自动使用正确的依赖安装命令」,目前还缺少官方解决方案。以及问题 7 提到的 Corepack 工具,目前还处于实验阶段。感叹下,前端的基建真挺稀烂的,一些不合理的设计,导致了数百万前端程序员接盘,不过这个情况也在慢慢变好了。

用户评论