NPM & YARN 包版本管理

2021/05/14 npmyarn

几乎所有前端项目中都有 yarn.lock / package-lock.json 文件(以下以 npm 为例)。打开浏览,你会发现它长得类似 package.json 的依赖,但是冗长得多。很多同学可能都不知道他们是干什么用的,甚至部分同学在项目跑不起来的时候还会暴力把 lock 文件给干掉,最后反而导致更严重的错误。

那么,lock 文件到底是干什么用的?为什么我们会需要 lock 文件?

# 为什么需要 lock 文件

{
    "name": "zqd-demo",
    "version": "1.0.0",
    "dependencies": {
        "vue": "^2.5.0"
    },
    "devDependencies": {
        "core-js": "^3.0.0",
        "husky": "^4.0.0"
    },
}

这是一个简单的 package.json 文件,dependencies, devDependencies 记录了这个项目的依赖。我们看到依赖包的版本号前都有 ^ 号,这种写法是因为 npm 使用了语义化版本规范 (opens new window)。这套规范定义了一组简单的规则及条件来约束版本号的配置和增长。semver 约定一个包的版本号必须包含 3 个数字,格式必须为 MAJOR.MINOR.PATCH, 意为 主版本号.小版本号.修订版本号

  • MAJOR 对应大的版本号迭代,做了不兼容旧版的修改时要更新 MAJOR 版本号
  • MINOR 对应小版本迭代,发生兼容旧版 API 的修改或功能更新时,更新 MINOR 版本号
  • PATCH 对应修订版本号,一般针对修复 BUG 的版本号

对于包作者(发布者),npm 要求在 publish 之前,必须更新版本号。npm 提供了 npm version 工具,执行 npm version major|minor|patch 可以简单地将版本号中相应的数字加 1。可以从这里 (opens new window)了解到 npm 是如何使用它的。

如果包是一个 git 仓库,npm version 还会自动创建一条注释为更新后版本号的 git commit 和名为该版本号的 tag

常用的规则示例如下表:

range 含义 示例
^2.2.1 指定的 MAJOR 版本号下, 所有更新的版本 匹配 2.2.3, 2.3.0; 不匹配 1.0.3, 3.0.1
~2.2.1 指定 MAJOR.MINOR 版本号下,所有更新的版本 匹配 2.2.3, 2.2.9; 不匹配 2.3.0, 2.4.5
>=2.1 版本号大于或等于 2.1.0 匹配 2.1.2, 3.1
<=2.2 版本号小于或等于 2.2 匹配 1.0.0, 2.2.1, 2.2.11
1.0.0 - 2.0.0 版本号从 1.0.0 (含) 到 2.0.0 (含) 匹配 1.0.0, 1.3.4, 2.0.0

npm install 执行后,会生成一个 node_modules 树,在理想情况下, 希望对于同一个 package.json 总是生成完全相同 node_modules 树。在某些情况下,确实如此。但在多数情况下,npm 无法做到这一点。有以下两个原因:

  1. 某些依赖项自上次安装以来,可能已发布了新版本。比如:A 包在团队中第一个人安装的时候是 1.0.5 版本,package.json 中的配置项为 A: '^1.0.5';团队中第二个人把代码拉下来的时候,A 包的版本已经升级成了 1.0.8,根据 package.json 中遵循的版本规范,此时第二个人 npm install 后 A 的版本为 1.0.8; 可能会造成因为依赖版本不同而导致的 bug;

  2. 针对 1 中的问题,可能有的小伙伴会想,把 A 的版本号固定为 A: '1.0.5' 不就可以了吗?但是这样的做法其实并没有解决问题,比如 A 的某个依赖在第一个人下载的时候是 2.1.3 版本,但是第二个人下载的时候已经升级到了 2.2.5 版本,此时生成的 node_modules 树依旧不完全相同 ,固定版本只是固定来自身的版本,依赖的版本无法固定。

为了解决上述问题,在 npm 5.0+ 版本,npm install 后都会自动生成一个 package-lock.json 文件。当代码库中有 package-lock.json 文件,执行 npm install,会判断 package.jsonpackage-lock.json 中的版本是否兼容,如果兼容会根据 package-lock.json 中的版本下载;如果不兼容,将会根据 package.json 的版本,更新 package-lock.json 中的版本,以保证 package-lock.json 中的版本兼容 package.json

综上,我们可以得出,lock 文件的存在主要有以下意义:

  • 记录所有的依赖项及相互依赖关系;
  • 优化 npm / yarn 的安装过程:在安装时,npm 会把 node_modules 已有的包和 package-lock.json 进行比较,如果重复的话,就跳过安装;
  • 保证依赖包一致性:在团队错人协作中,确保每个开发同学安装的依赖版本是一致的,确定一棵唯一的 node_modules 树;
  • 利于依赖包的管理维护:node_modules 目录本身是不会被提交到代码库的,但是 package-lock.json 可以而且必须提交到代码库,如果开发人员想要回溯到某一天的目录状态,只需要把 package.jsonpackage-lock.json 这两个文件回退到那一天即可。

# lock 文件的结构

// package-lock.json
{
    "name": "zqd-demo",
    "version": "1.0.0",
    "lockfileVersion": 1,
    "requires": true,
    "dependencies": {
        "@babel/code-frame": {
            "version": "7.8.3",
            "resolved": "https://registry.npm.taobao.org/@babel/code-frame/download/@babel/code-frame-7.8.3.tgz?cache=0&sync_timestamp=1578953126105&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fcode-frame%2Fdownload%2F%40babel%2Fcode-frame-7.8.3.tgz",
            "integrity": "sha1-M+JZA9dIEYFTThLsCiXxa2/PQZ4=",
            "dev": true,
            "requires": {
                "@babel/highlight": "^7.8.3"
            }
        },
        // ...
    }
}

这是一个精简过的 package-lock.json 文件,其中 name, versionpackage.json 中的 name, version 一样,描述了当前包的名字和版本,dependencies 是一个对象,该对象和 node_modules 中的包结构一一对应,对象的 key 为包的名称,值为包的一些描述信息, 根据 package-lock-json 官方文档 (opens new window),主要的结构如下:

  • version:包版本,即这个包当前安装在 node_modules 中的版本
  • resolved:包具体的安装来源
  • integrity:包 hash 值,验证已安装的软件包是否被改动过、是否已失效
  • requires:对应子依赖的依赖,与子依赖的 package.jsondependencies 的依赖项相同
  • dependencies:结构和外层的 dependencies 结构相同,存储安装在子依赖 node_modules 中的依赖包

需要注意的是,并不是所有的子依赖都有 dependencies 属性,只有子依赖的依赖和当前已安装在根目录的 node_modules 中的依赖冲突之后,才会有这个属性。

# 参考资料

上次更新: 2021/8/7 上午3:33:06