前端 CI/CD 上如何保证依赖安装速度达到优解?
一、背景
前端应用在构建部署时长上,通常受代码拉取速度、以及机器配置高低等因素影响。排除这些我们往往不可控的因素外,仍可以通过对 npm 依赖安装方式进行优化,来达成更快的构建速度。以项目存在 50 个以上生产依赖包,最优秀的依赖安装方式比最差的安装方式,往往能够节省 20s 以上的时间。
安装依赖的命令行有:npm install
、npm ci
。在进行真正测试前,先了解下这两命令行的差异,进而明白他们为啥安装速度差异这么大。
二、npm install
首先我们了解下 npm install
这个命令行。上面是执行 npm install
的整个流程,我们看下重点流程的拆解。
PS:本文的 npm 版本特指:v5.4.2 以上版本。低于该版本可能存在不同差异。
1. 检查 config
当执行 npm install
后, npm
首先会从命令行、环境变量和 .npmrc
文件中获取其配置信息。每个 .npmrc
配置文件都是一个 ini
格式的key = value
参数列。我们通常在这配置私服镜像,例如:
npm 读取配置数据遵循如下优先级:
每个项目的配置文件(/path/to/my/project/.npmrc)
每个用户的配置文件 (~/.npmrc)
全局配置文件 ($PREFIX/etc/npmrc)
npm 内置配置文件 (/path/to/npm/npmrc)
2. lock 文件和 package.json
当项目中存在 lock 文件时,会将 lock 文件和 package.json 进行依赖包的信息比对。依赖包信息一致时,则直接使用 lock 文件中的信息进行依赖的安装。否则,则使用 package.json 中的信息进行依赖安装,安装完成后更新 lock 文件。这是两者比对差异的行为。
那么在依赖安装上,首先它会查找本地是否存在缓存,不存在则从网络将资源下载到缓存目录,然后再将资源从缓存目录解析到 node_modules 下。可以如下方式进行缓存目录的查看:
content-v2:存放的是 npm 包资源二进制文件。
index-v5:存放的是文件的索引,根据它来定位包资源文件。
tmp:暂存文件
3. 语义化版本
了解语义化版本 SemVer Range Version 对管理项目中包版本是非常有意思,特别是你们公司存在多个组件库、工具库,并且之间存在相互引用的情况下,它能给你减少很多不必要的麻烦。那么什么是语义化版本呢?
主版本号 X.此版本号 Y.修订号 Z
它的版本存在如下命名区分:
Alpha:内测版
Beta:公测版
Gamma:比较成熟的测试版
RC:发布倒计时,候选版本
Stable:稳定版本
它存在两个非常重要的高级语法:
波浪线(~)
~1.2.3 表示:版本 >=1.2.3 并且 版本<= 1.3.0
补注号(^)
^1.2.3 表示:版本 >=1.2.3 并且 版本<2.0.0。它允许在不修改[major, minor, patch]中最左非零数字的更改。
关于其他语法,可以去官网查看。
那么在项目中,若存在多个依赖依赖了同一个库 Lib 的不同版本,那么在执行 install 的时候,若对该 Lib 库的依赖的版本不兼容的情况下,比如:包 a 需要 1.x 版本,而包 b 需要 2.x 版本,那么按照依赖安装顺序,首个 Lib 1.x 版本会被安装到根目录 node_modules 底下,那么安装包 b 时,因为 2.x 版本与 1.x 版本不兼容,那么 2.x 版本则会被安装到包 b 的 node_modules 底下。 那么会产生什么呢?若项目需要 Lib 库共用一个数据实例的时候,因为各自引用了各自版本的包,那么实例是走不到一起去的。
三、npm ci
npm ci
类似于 npm install
,不同之处在于它主用于自动化环境,例如测试平台、持续集成和部署,或者在希望对依赖项进行全新安装的场景。
npm ci
在以下情况下会更快:
项目有一个
package-lock.json
或npm-shrinkwrap.json
文件项目中不存在
node_modules
文件夹,因为若存在它需要先执行删除操作存在耗费时间。
它和 npm install
的主要区别在于:
npm ci
要求项目中必须具有package-lock.json
或npm-shrinkwrap.json
文件。npm ci
完全根据package-lock.json
文件进行依赖安装,安装过程无需计算求解依赖问题、构建依赖树,因此它的安装速度会更快,同时根据锁包版本安装依赖也保证了整个团队都使用了版本完全一致的依赖。package-lock.json
版本和package.json
依赖版本存在冲突则直接报错,而不会更新锁包。npm ci
执行时,会先删除项目中有的node_modules
,再执行安装。npm ci
一次只能安装整个项目,无法使用它添加单个依赖。npm ci
永远不会改变package.json
文件和package-lock.json
文件。
四、场景验证
上面了解了两个安装命令的执行逻辑,和各自特性。下面进行下,安装速度的比对:
前置条件:
npm 版本 6.14.5
项目中存在
package-lock.json
文件,并提交远程仓,模拟的是 CI。代码提交并不会提交
node_modules
。项目中大概存在 60 个生产依赖。
项目中提交 package-lock.json
文件是为了保证项目依赖版本的一致性。
下面进行 4 组测试组对比,虽然有些能直接知道答案,但还是看下具体提升了多少。
测试组一
无缓存 node_modules
情况下,执行 npm install
。
测试组二
缓存 node_modules
情况下,执行 npm instal
l。
测试组三
无缓存 node_modules
情况下,执行 npm ci
。
测试组四
有缓存 node_modules
情况下,执行 npm ci
。
通过以上 4 组对照组,我们可以得出结论:
1. 无缓存 node_modules
情况下,npm ci
安装依赖速度比 npm install
快,因为 ci 不进行解析构造依赖树操作。
2. 有缓存 node_modules 情况下,npm install
速度比 npm ci
快,ci
需要先删除 node_modules
再从 0 开始进行安装操作。
那么 CI 上,我们没有上传 node_modules
文件,那直接使用 npm ci
命令行是不是最优解?其实不然,CI 上我们仍然可以进行缓存上次安装的依赖 node_modules
目录。
五、Dockerfile
现在部署基本都采用 docker 去构建镜像,看看怎么写缓存依赖安装,下面仅列出核心写法。
了解 rsync
rsync(remote synchronize)是一个远程数据同步工具,可通过 LAN/WAN 快速同步多台主机之间的文件。也可以使用 rsync 同步本地硬盘中的不同目录。其核心思想是:对有变化的文件进行复制;对无变化的文件创建硬链接以减少磁盘占用。
rsync 具有如下的基本特性:
1. 可以镜像保存整个目录树和文件系统
2. 可以很容易做到保持原来文件的权限、时间、软硬链接等
3. 无须特殊权限即可安装
4. 优化的流程,文件传输效率高
5. 可以使用 rsh、ssh 方式来传输文件,当然也可以通过直接的 socket 连接
6. 支持匿名传输,以方便进行网站镜象
参数说明
-a:归档模式,表示以递归方式传输文件,并保持所有文件属性
--delete:删除那些接手端还有而发送端已不存在的文件
六、总结
以上便是对 CI 上依赖安装到一些思考,实际上面 dockerfile 描述的依赖安装速度仍然存在提升速度,比如当缓存目录不存在 node_modules
时,不应该执行 npm install
,而应该去执行 npm ci
。
版权声明: 本文为 InfoQ 作者【梁龙先森】的原创文章。
原文链接:【http://xie.infoq.cn/article/675a4b724b9b504fe17f42eb6】。文章转载请联系作者。
评论