npm、yarn 和 pnpm 的lock 机制

问题:如何保证安装依赖的确定性?

在开发中,经常出现这样的场景:

  • 开发、测试的时候好好的,项目上线就报错了。
  • 仔细检查,发现项目在开发和发布之间,刚好某一个依赖包发布了新版本,且这个新版本会导致问题。
  • 这就导致安装的依赖版本相比开发 & 测试时有变化,因此无法提前预知。

在前端项目中,我们使用 package.json 管理项目的依赖,在该文件中,使用 semver 的机制记录了各个依赖的版本。

为了避免这一问题,我们自然地想到,将 package.json 中各个依赖的版本固定下来。

例如,一个名叫 project 的项目依赖了 A 包,的 0.0.1 版本:

1
2
3
4
5
6
{
"name": "project",
"dependencies": {
"A": "0.0.1"
}
}

由于项目的 package.json 是我们控制的,我们可以固定这个包的版本,但这个包同样有自己的 package.json,这个我们控制不了:

1
2
3
4
5
6
{
"name": "A",
"dependencies": {
"B": "^0.0.1"
}
}

A 包依赖了 B 包,且版本声明为 ^0.0.1,这就会导致每次安装都会安装 B 包的最新版本,如果 B 包有新版本发布,就会带来依赖的变化,有出问题的风险。

yarn 的 yarn.lock

这个问题对开发已经造成了严重的干扰,但 npm 一直没有重视,于是,yarn 横空出世,在兼容 npm(即 package.json)的基础之上,通过 yarn.lock 文件固定了每个依赖的版本。

对于上面这个场景,其 yarn.lock 如下:

其实对于每个依赖,除了 versiondependencies 之外,还保存了 resolved(下载链接)和 integrity(校验码),不过这两个与版本控制不相关,因此未列出。

1
2
3
4
5
6
7
A@0.0.1:
version "0.0.1"
dependencies:
B "^0.0.1"

B@^0.0.1:
version "0.0.1"

可以发现,该文件把两个依赖的版本都列了出来。

  • 因为项目依赖了 A0.0.1 版本,因此第一个依赖名为 A@0.0.1,然后它的版本是 0.0.1,且依赖了 ^0.0.1B
  • 然后再看 B,由于 A 声明 B 的版本是 ^0.0.1,所以第二个依赖名为 B^0.0.1,但仔细看它的 version 值,由于 yarn.lock 文件生成时 B 的最新版本是 0.0.1,因此它的值就被固定到了 0.0.1

也就是说,yarn 通过 yarn.lock 记住了每个依赖第一次被添加时的版本。

这样,即使后面 B 发布了 0.0.2 版本,但 yarn.lock 没有变,在安装的时候,查看 B 所记录的版本是 0.0.1,所以仍然会安装 B0.0.1 版本,这就解决了上面的那个问题。

那么下一个问题又来了:假设我就想更新 A 的版本怎么办?

有两种方式:

  • yarn upgrade A@0.0.2

    这种方式会同时更新 yarn.lockpackage.json

  • 手动修改项目中的 package.json

    "A": "0.0.1" 改成 "A": "0.0.2",然后重新 yarnyarn.lock 也会更新。

可能会有一个问题,不是说 yarn.lock 会锁定版本吗,怎么这次又更新了呢?这是因为 yarn 在安装依赖的时候,并不是只看 yarn.lock,也会结合 package.json 来决定到底怎么确定依赖的版本。

对于每一个依赖来说,yarn 会检查 package.json 中的 semver 版本与 yarn.lock 中的固定版本是否匹配。

  • 如果匹配(如 ^0.0.10.0.1),则会直接安装 yarn.lock 中的版本(也就是说,忽略更新的版本)
  • 如果不匹配(如 ^0.1.00.0.1),则会按照 package.json 中的 semver 版本号安装(即 >= 0.1.0< 0.2.0 的最新版本),并更新 yarn.lock
  • 如果 package.json 中列出了 yarn.lock 中不存在的依赖,则参照上一条处理,类似地,使用 yarn add 安装新依赖时也是类似的流程。

当手动修改了 package.json 中的版本号,就与 yarn.lock 中的版本不匹配了,yarn 就会对依赖版本和 yarn.lock 进行更新。

npm 的 package-lock.json

在 yarn 的压力下,npm 不得不行动起来,在 v5 版本时新增了一个 package-lock.json 以实现类似的效果,但 npm 的处理方式经过了多次改变:

  • 5.0.x 版本
    • 如果 package-lock.json 存在,npm 会完全忽略掉 package.json,使用 package-lock.json 中列出的固定版本安装。
    • 这样就带来了一个问题,对于 package.jsonpackage-lock.json 不对应的场景,声明的依赖不会安装,或者安装的版本不对应。
  • 5.1.0 ~ 5.4.2 版本
    • 如果 package.json 中声明的 semver 版本有更新(依赖包发布了新版本),会忽略掉 package-lock.json,按照 package.json 安装,再更新 package-lock.json
    • 实际上是对上一个行为的回滚,很显然,它失去了固定依赖的作用。
  • >5.4.2 版本
    • 与 yarn 保持一致。
    • package.json 中声明的 semver 如果和 package-lock.json 中的固定版本对应,则使用 package-lock.json 中的版本,否则按照 package.json 安装并更新 package-lock.json

截止当前,npm 的最新版本已经到了 8.x,主流的 Node.js 版本(如 14.x、16.x)也内置了 npm 的 7.x 版本,我们可以认为 npm 也可以保证依赖版本的一致性。

pnpm 的 pnpm.lock

作为更优秀的包管理器后继者,pnpm.lock 的行为与 yarn.lock 保持一致。

我们应该怎么做?

npm、yarn、pnpm 都有了自己的机制去保证依赖的版本,我们所能做的,就是不要把 lock 文件放到 .gitignore,同时不要乱动 lock 文件影响项目的稳定性。