CI项目锁定依赖包版本

本篇文章使用总结的背景是2021年,如有相应变化,需要参考npm官方文档。

1. 问题提出背景

连续几个项目本地启动或发版部署报错,排查了原因之后发现是由于依赖包的版本问题导致的,所以决定锁定项目的依赖包版本。 由于我们的项目的依赖包是由npm管理的,所以只考虑使用npm锁定依赖包版本的方案。 npm锁定版本可以使用的方法:

  • 在package.json中写死版本号
  • 使用package-lock.json管理

第一种方法有太多的局限性,无法保证间接的依赖包版本,而且大部分package.json中的依赖包版本号都是^,支持次版本升级的,所以还是考虑使用package-lock.json。在实际中发现尽管使用了package-lock.json,有时候也无法避免package-lock.json文件被改变,为了解决这个问题,研究了可能引起改变的原因,同时给出了一个能够最大程度保证项目团队在各自的本地安装的node_modules是一致的方案。

2.npm锁定依赖包版本的方案
  • 在初始化项目的时候,项目创建者记录当前使用的node版本号,npm版本号(npm建议最新版)以及npm镜像源地址,同时记录项目工程可使用的node版本范围和npm版本范围(不同版本的npm会导致package.json和package-lock.json的文件结构不一致) 注意: 1.创建一个项目的时候决定使用node的那个版本,需要从两个方面考虑,一是当前node官网主推的长期维护的版本,二是自己公司线上环境使用的node版本 2.之所以需要npm的版本范围也指定是考虑到npm的版本会影响到package-lock文件的结构。
    • 关于node版本和npm版本的指定,可以写在package.json中
{
  ...,
   "engines" : {
    "node" : ">=10.16.0 <=16.13.1",
    "npm" : ">=6.9.0"
  }
}
  • 关于npm镜像源的配置,可以通过项目工程内配置.npmrc文件来实现每个项目工程的单独npm控制,也可以项目团队成员自己通过nrm管理 。
  • 工程内配置.npmrc文件的engine-strict字段为true可以严格npm i或者npm ci的时候校验node和npm的版本号范围
  • 初始化项目中创建的package.json和package-lock.json都必须要上传至gitlab
  • 项目团队成员在拉取项目代码之后,使用nvm(nvmw)切换到对应的项目工程node版本,同时确保npm的版本在项目允许的版本范围之内。
  • 项目团队成员执行npm ci,根据package-lock.json安装整个项目工程的依赖包,然后进行项目的启动开发。
    • 如果项目团队成员需要单独升级某个依赖包的版本: 执行npm update packageName单独安装某个依赖包,此时package-lock.json会被更新覆盖。在本地测试没有问题之后,将package.json和package-lock.json都上传至仓库中。
    • 如果项目团队成员需要安装一个新的依赖包 执行npm install packageName单独安装某个依赖包,此时package-lock.json会被更新覆盖。在本地测试没有问题之后,将package.json和package-lock.json都上传至仓库中。
    • 如果项目工程需要整个升级所有的依赖包
      • 第一种方式:执行npm update,此时package-lock.json会被更新覆盖。在本地测试没有问题之后,将package.json和package-lock.json都上传至仓库中。
      • 第二种方式:删除node_modules和package-lock.json文件,重新执行npm install 注意:
        1. 每种方式的更新都需要经过充分的本地测试,再将package.json和package-lock.json都上传至仓库使团队成员使用
        2. 两种方式的区别是:第一种方式更新既会安装满足package.json中的语义版本的当前的最新版本,同时也会同步更改package.json中的依赖版本的最低版本为当前最新的版本,如果不需要更改package.json,需要执行npm update --no-save;而第二种方式不会更改package.json
        3. 推荐第一种,可以逐步升级
  • 项目定期更新依赖关系(可做一个重复任务 dependabot),执行之后充分进行测试然后上传至仓库。确保逐步更新替换使用较新的依赖包,以免长时间不更新造成最后升级的时候一大堆技术债。 ----------方案待定
3.关于npm中的package-lock.json
  • 存在意义 package-lock.json是npm@5.x.x之后 根据 package.json 自动生成 的lockfiles 文件。它是package.json安装的依赖关系的映射,描述了一个精确的树关系,以便后续的安装能够生成相同的树,而不用考虑中间的依赖关系更新。更多的目的可参考阅读npm的官方文档:package-lock.json | npm Docs (npmjs.com)
  • 生成逻辑
    • 5.0.x版本 不管package.json中的依赖是否有更新,npm install都会根据package-lock.json下载。 package-lock.json file not updated after package.json file is changed · Issue #16866 · npm/npm · GitHu…
    • 5.1.0版本后 当package.json中的依赖项有更新版本时,npm install会无视package-lock.json去下载新版本的依赖项并且更新package-lock.json。 why is package-lock being ignored? · Issue #17979 · npm/npm · GitHub
    • 5.4.2版本后
      • 如果之后一个package.json文件,运行npm install会根据它生成一个package-lock.json文件,这个文件相当于本次install的一个快照,不仅记录了package.json直接依赖的版本,也记录了间接依赖的版本。
      • 如果package-lock.json中的版本包含在package.json的版本范围内,即使此时package.json中有新的版本,执行npm install也会根据package-lock.json下载。
      • 如果手动修改了package.json的版本,并且和package-lock.json中版本不兼容,那么执行npm install时package-lock.json将会更新到兼容package.json的版本。
  • 被修改的可能原因
    • 新增或者删除了一些包,但是没有及时 install
    • 挪动了包的位置(例如从 dependencies 移动到 devDependencies这种操作,这种会更新某些字段)
    • npm registry 的影响
4.不同版本的npm的差异(主要指lockfile相关)
  • 为了避免重复处理node_modules目录,npm@7开始会在node_modules目录下生成一个“隐藏的”lock文件.package-lock.json
  • package-lock.json文件格式的不同
    • lockfileVersion字段取值的不同 lockfileVersion 是一个整数字段值,目前有1,2,3三个可能的取值。
      • 1:npm@6及之前的版本生成的package-lock.json中的lockfileVersion的值
      • 2:npm@7生成的package-lock.json中的lockfileVersion的值,向后兼容v1版本
      • 3:npm@7生成的package-lock.json中的lockfileVersion的值,不再向后兼容以前的lockfile的版本,目前还没有应用,可能在npm@6不再支持之后启用。
    • npm@7增加了packages字段
      • 这是一个对象,它将包位置映射到包含有关该包的信息的对象
      • 根项目通常以“”的键列出,而所有其他包都列出了它们从根项目文件夹的相对路径
    • npm@6中的dependences字段在npm@7之后也会有,但是只是为了兼容而存在,在使用npm@7之后的版本会被忽略掉
5.其他补充
  • node和npm版本对应关系 (npm view npm versions) | node | npm | 备注(npm版本范围) | | --- | --- | --- | | <=14.18.3 | 6.x | 6.0.0-6.14.16 | | >=15.0.0 && <=16.10.0 | 7.x | 7.0.0-7.24.2 | | >=16.11.0 | 8.x | 8.0.0-8.5.0 |

  • 语义版本控制

    • 指定版本:比如 1.2.2 ,遵循“主版本.次版本.修订(补丁)版本”的格式规定,安装时只安装指定版本。
    • 波浪号(tilde)+指定版本:比如 ~1.2.2 ,表示安装 1.2.x 的最新版本(不低于1.2.2),但是不安装 1.3.x,也就是说安装时不改变主版本号和次版本号。
    • 插入号(caret)+指定版本:比如 ˆ1.2.2,表示安装 1.x.x 的最新版本(不低于 1.2.2),但是不安装 2.x.x,也就是说安装时不改变主版本号。需要注意的是,如果大版本号为 0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。
    • latest:安装最新版本。
  • 如果这样子的情况下还会出现问题的话那就具体问题具体分析解决。