yliu

时来天地皆同力,运去英雄不自由

yarn.lock 引发的血案


最近开发项目的时候遇到一个神奇的 bug,在回滚了无数次之后终于定位到了问题,就是 yarn.lock 引起的,当时升级相关依赖版本不小心把 yanr.lock 文件给干掉了,导致依赖引用的模块有问题。

不过这也引起了我的思考为什么 yarn 要有一个 yarn.lock 文件呢?或者说为什么现代的包安装都需要有 lock 文件呢?例如 npm 的叫做 package-lock.json,pnpm 的叫做 pnpm-lock.yaml

后面调查了一下发现其实是为了确认依赖关系,这里以 npm 的发展历史进行讲解。

扁平化依赖安装

下面的例子都以这张依赖图为例

relyon.jpeg

在 npm5 之前也就是还没有 package-lock.json 之前,会将所有的依赖都下载到 node_modules 目录下,如果依赖还依赖其他依赖就会继续下载到依赖本身下的 node_modules下,例如上图所示最终会生成如下依赖关系。

node_modules
  modules1
    a:1.0
    b:1.0
  modules2
    a:1.0
    b:2.0

这样做虽然不会遇到依赖管理问题,但是很明显占用磁盘空间太大了。 例如上面有一个共同模块是 a:1.0,如果能把相同的依赖提取出来不就是可以大大节省安装时长嘛?

而且最关键的时因为包的文件体积很小,依赖一旦多起来反复读写反而成为性能的瓶颈。

npm5 之后就尝试了扁平化安装,它的思想很简单就是安装的时候如果有子依赖就安装到根目录下的 node_modules ,如果还有依赖跟根目录下的依赖冲突就和 npm5 之前的处理方式一样,在子依赖的目录下新建 node_modules 然后重复。 以上面的例子演示

npm i modules1 modules2

会得到这样一个依赖

node_modules
  modules1
  a:1.0
  b:1.0
  modules2
    b:2.0

这样做很明显改善了安装时长和节省了磁盘空间,但是带来了一个安装顺序的问题,就是上面是安装 module1 为开始,但是假设安装 module2 为开始呢?

npm i modules2 modules1

会得到这样一个依赖

node_modules
  modules1
  a:1.0
  b:2.0
  modules2
    b:1.0

注意,上面不考虑网速等额外情况

会发现结构已经发生了改变。

温习一下查找模块的方式:如果在当前 node_modules 找不到时会怎么查找?会继续向上查找,直到最顶层为止。

而 module1 import 的很明显是期待 b:1.0 这个依赖,最终却得到了 b:2.0 依赖。这就给代码新增了不确定性,我最开始遇到的问题就是这个原因得来的。

解决方式

上面演示了扁平化依赖可能遇到的依赖不确定问题,那能不能通过新增一个文件来记录模块之间的依赖关系呢,之后安装的时候复用?这就是 yanr.lock、package-lock.lock 和 pnpm-lock.yml 的由来。

不过除了锁定 lock 文件的这种方式还有什么新的办法呢?pnpm 给出了答案,可以继续采用 npm5 之前的安装方式 + 软连接来解决。

上面说到因为安装重复的模块导致安装效率很低,才被迫采用提取公共模块的扁平化安装,但是如果把当前项目的模块放到最顶的磁盘文件内,每次安装新的模块先搜寻有没有对应的,如果有给一个软链接而不生成真正文件,这样不就是大大节省了安装效率嘛?虽然 lock 文件还是存在但是已经改善重复磁盘的读写问题。

新的展望

最近在知乎看到了 如何看待蚂蚁在 SEEConf 分享的 NPM 安装优化,提速 3 倍之多? 我觉得这可能是后面的网速不断提升但是包安装依然很慢的解决方案,但是目前还未开源所以暂未能体验。

最后

如果有什么错误欢迎指出。

参考了一些文章: