Gahing's blog Gahing's blog
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Gahing / francecil

To be best
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 前端基础

  • 应用框架

  • 工程能力

    • 工程效率

    • 编译构建

    • 工程管理

      • monorepo

      • npm

        • npm link 存在的问题
        • npm供应链攻击
        • npm资料
        • pnpm介绍
        • 前端依赖管理-前世今生及未来
        • npm version 命令
        • npm 发包过程
        • 关于 lockfile 合并冲突的问题分析与最佳实践
        • 前端依赖一致性的问题分析与最佳实践
          • 背景
          • 常见问题
            • 1. 不锁版本(lock),依赖包会自动更新
            • 2. 锁了版本包也没动,还是更新了依赖
            • 3. 混用包管理工具
            • 4. 安装具体依赖不会触发 prepare 钩子
            • 5. 自动使用正确的依赖安装命令
            • 6. 限制 Node 和包管理工具的版本
            • 7. 快速切换版本
            • 快速切换 Node 版本
            • 快速切换包管理工具版本
          • 最佳实践
          • 展望
        • 浅谈 package-lock.json 合并冲突修复算法
    • JS 模块化

    • CSS 模块化

    • 工程质量

    • 前端测试

    • CI&CD

  • 应用基础

  • 专业领域

  • 业务场景

  • 大前端
  • 工程能力
  • 工程管理
  • npm
gahing
2023-07-01
目录

前端依赖一致性的问题分析与最佳实践

# 背景

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

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

这些问题不能单纯依靠文档规范来限制,需要有行之有效的工具来避免。

本文会先描述这些常见问题的原因和解决方案,并在最后给出可实施的最佳实践。

# 常见问题

image.png

# 1. 不锁版本(lock),依赖包会自动更新

这个应该都懂,需要把 lock 文件加入 git 版本控制之中。

但可能有些同学不知道为什么这么做,这里简单提下。

如果没有锁文件(比如 package-lock.json),Node 在安装依赖时会选择符合版本范围的最新版本,这个版本范围的设计叫做 semver (opens new window) 规范,其格式为 主版本号.次版本号.修订号 ,版本号递增规则如下:

  1. 主版本号:当你做了不兼容的 API 修改,
  2. 次版本号:当你做了向下兼容的功能性新增,
  3. 修订号:当你做了向下兼容的问题修正。

理想情况下大家都照这个要求设计,有不兼容变更就做主版本号升级,那当然没问题,大家也乐意自动更新来修复漏洞。

然而实际情况上社区的大部分包并不遵守这个约定,有不兼容变更却只是升级了 修订号 或者 次版本号。。如果选了自动升级,那么准备等待项目报错吧。

因此还是乖乖的 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"
1

# 4. 安装具体依赖不会触发 prepare 钩子

问题 3 中,我们提到了使用 prepare 钩子来检测依赖安装命令是否符合预期。

然而 prepare 钩子的触发条件是有限制的(preinstall 同),如果 install 命令存在其他参数,将无法触发这个钩子。

npm i lodash
1

官方文档 (opens new window)也有说明:

由于 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
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", 
  }
}
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 ?

我们拆成两个问题来看:

  1. 快速切换 Node 版本
  2. 快速切换包管理工具版本

20230709 更新,有个工具 volta (opens new window) 可以更方便的做这个事情

# 快速切换 Node 版本

切换 Node 版本简单,使用 nvm (opens new window) 来管理即可。

还可以在项目根目录创建 .nvmrc 文件并添加版本号,需要切换的时候直接 nvm use 即可,无需记忆版本。

// .nvmrc
v16.0.0
1
2

还能不能更快?比如我切换了项目目录,就自动帮忙切换版本。

在社区上找到了一款叫 avn (opens new window) 的工具,号称能做这样的事,但看起来很久没迭代了,这种监测应该会有额外的性能影响,没有实际体验过不做评价。

我个人的实践是将 node 版本检测和切换的逻辑放入 prepare 等钩子,在需要的时候再切。

# 快速切换包管理工具版本

通常包管理工具在全局只会存在一个版本,有以下 3 种解决方案:

  1. 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
1
2
3
4
5
6
7
8
9
  1. pnpm dlx: 使用 pnpm dlx (opens new window) 来指定 pnpm 运行命令使用 pnpm7 ,可以参考这篇文章 (opens new window),个人不推荐
pnpm dlx pnpm@7 install
pnpm dlx pnpm@7 run dev
1
2
  1. 项目内依赖:在公司内部有个实践,把 pnpm 等包管理工具作为依赖安装在项目中,再通过其他命令调用,把命令转发给这个项目内的 pnpm 。这样做的好处是可以实现包管理工具的版本控制,缺点就是需要改用其他命令。

# 最佳实践

对上面的问题做下整理,可以得到以下最佳实践。

  1. 使用 lock 来锁住版本,并使用较新版本的 npm 避免一些遗留问题
  2. 把非目标的包管理工具 lock 文件加入 gitignore
  3. 使用 prepare 钩子和 only-allow 工具避免混用包管理工具
  4. 在 CI 阶段再次进行 lock 文件完整性校验
  5. 使用 package.json 的 engines (opens new window) 字段限制 Node 和包管理工具的版本

# 展望

实际上还存在一些问题,比如问题 5 的「自动使用正确的依赖安装命令」,目前还缺少官方解决方案。以及问题 7 提到的 Corepack 工具,目前还处于实验阶段。

感叹下,前端的基建真挺稀烂的,一些不合理的设计,导致了数百万前端程序员接盘,不过这个情况也在慢慢变好了。


最后,👋🏻 Respect!欢迎一键三连 ~

编辑 (opens new window)
#最佳实践
上次更新: 2024/09/01, 23:56:56
关于 lockfile 合并冲突的问题分析与最佳实践
浅谈 package-lock.json 合并冲突修复算法

← 关于 lockfile 合并冲突的问题分析与最佳实践 浅谈 package-lock.json 合并冲突修复算法→

最近更新
01
浅谈代码质量与量化指标
08-27
02
快速理解 JS 装饰器
08-26
03
Vue 项目中的 data-v-xxx 是怎么生成的
09-19
更多文章>
Theme by Vdoing | Copyright © 2016-2024 Gahing | 闽ICP备19024221号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式