npm、yarn 和 pnpm 的lock 机制
问题:如何保证安装依赖的确定性?
在开发中,经常出现这样的场景:
- 开发、测试的时候好好的,项目上线就报错了。
- 仔细检查,发现项目在开发和发布之间,刚好某一个依赖包发布了新版本,且这个新版本会导致问题。
- 这就导致安装的依赖版本相比开发 & 测试时有变化,因此无法提前预知。
在前端项目中,我们使用 package.json
管理项目的依赖,在该文件中,使用 semver 的机制记录了各个依赖的版本。
为了避免这一问题,我们自然地想到,将 package.json
中各个依赖的版本固定下来。
例如,一个名叫 project
的项目依赖了 A
包,的 0.0.1
版本:
1 |
|
由于项目的 package.json
是我们控制的,我们可以固定这个包的版本,但这个包同样有自己的 package.json
,这个我们控制不了:
1 |
|
A
包依赖了 B
包,且版本声明为 ^0.0.1
,这就会导致每次安装都会安装 B
包的最新版本,如果 B
包有新版本发布,就会带来依赖的变化,有出问题的风险。
yarn 的 yarn.lock
这个问题对开发已经造成了严重的干扰,但 npm 一直没有重视,于是,yarn 横空出世,在兼容 npm(即 package.json
)的基础之上,通过 yarn.lock
文件固定了每个依赖的版本。
对于上面这个场景,其 yarn.lock
如下:
其实对于每个依赖,除了
version
和dependencies
之外,还保存了resolved
(下载链接)和integrity
(校验码),不过这两个与版本控制不相关,因此未列出。
1 |
|
可以发现,该文件把两个依赖的版本都列了出来。
- 因为项目依赖了
A
的0.0.1
版本,因此第一个依赖名为A@0.0.1
,然后它的版本是0.0.1
,且依赖了^0.0.1
的B
。 - 然后再看
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
,所以仍然会安装 B
的 0.0.1
版本,这就解决了上面的那个问题。
那么下一个问题又来了:假设我就想更新 A
的版本怎么办?
有两种方式:
yarn upgrade A@0.0.2
这种方式会同时更新
yarn.lock
和package.json
。手动修改项目中的
package.json
将
"A": "0.0.1"
改成"A": "0.0.2"
,然后重新yarn
,yarn.lock
也会更新。
可能会有一个问题,不是说 yarn.lock
会锁定版本吗,怎么这次又更新了呢?这是因为 yarn 在安装依赖的时候,并不是只看 yarn.lock
,也会结合 package.json
来决定到底怎么确定依赖的版本。
对于每一个依赖来说,yarn 会检查
package.json
中的 semver 版本与yarn.lock
中的固定版本是否匹配。
- 如果匹配(如
^0.0.1
和0.0.1
),则会直接安装yarn.lock
中的版本(也就是说,忽略更新的版本)- 如果不匹配(如
^0.1.0
和0.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.json
和package-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 文件影响项目的稳定性。