4.1 Yarn install命令源码实现
本文发布于掘金专栏
文章原文位于github
本文严重依赖2. 使用 vscode 以及 chrome 调试 yarn 源码
install 命令是 yarn 命令中比较核心的一个命令,弄懂 install 其他很多命令都会明白,因为存在互相调用函数实现。
调试准备
进入一个 debug 文件夹并初始化一个npm
包,安装react@18
,删除node_modules
只留下package.json
和yarn.lock
,命令在下面
bash
yarn init -y
yarn add react@18 # 这里用18是因为react18有依赖项
rm -rf node_modules
在vscode
的launch.json
中的配置添加调试 install 相关的配置
json
{
"type": "node",
"request": "launch",
"name": "debug yarn install",
"skipFiles": ["<node_internals>/**"],
"console": "internalConsole",
"outFiles": ["${workspaceFolder}/**/*.(m|c|)js", "!**/node_modules/**"],
"program": "${workspaceFolder}/mybuild/cli/index.js",
"cwd": "${workspaceFolder}/../yarn-source-dev",
"args": ["install"]
}
选择debug yarn install
即可调试。具体的调试方法见使用 vscode 以及 chrome 调试 yarn 源码,也就是本书的第 2 章。
install 命令运行流程
install 命令源码位于src/cli/commands/install.js
,查看 install 命令源码可以得到下面的运行流程图。
运行流程主要分为四个函数的执行。
run 函数
run
函数是导出给commands
对象的一个函数,这是install
命令具体实现的入口,下面是简洁代码,run
函数主要做了以下事
js
if (flags.lockfile === false) {
lockfile = new Lockfile();
} else {
lockfile = await Lockfile.fromDirectory(config.lockfileFolder, reporter);
}
wrapLifecycle();
- 生成
Lockfile
实例,如果命令行参数中带有--lockfile false
的话会生成一个全新的Lockfile
实例,如果没有--lockfile
参数的话就从当前的yarn.lock
文件生成实例,这也是默认的选项。下面是从yarn.lock
中生成的Lockfile
实例。可以看到Lockfile
是解析yarn.lock
文件到对应的数据结构的类。
- 调用
wrapLifecycle
。实际这里是先调用的install
函数install
函数再调用的wrapLifecycle
。下面是install
的函数实现。
wrapLifecycle 函数
这个函数的参数是一个factory
函数,factory
函数会被放到一系列的lifecyclescript
执行中执行。下面是 wrapLifecycle 函数的简洁声明
js
async function wrapLifecycle(config: Config, flags: Object, factory) {
await config.executeLifecycleScript("preinstall");
await factory();
await config.executeLifecycleScript("install");
await config.executeLifecycleScript("postinstall");
if (!config.production) {
if (!config.disablePrepublish) {
await config.executeLifecycleScript("prepublish");
}
await config.executeLifecycleScript("prepare");
}
}
所谓的lifecyclescript
实际上就是在package.json
中的scripts
字段中声明的script
。这里执行script
的函数是config.executeLifecycleScript
。这个后续在yarn run
命令的章节中会具体讲解。这里可以看到在执行完preinstall
script 会就会立即执行factory
函数。
factory 函数
factory 函数首先实例话了Install
类,这个类的构造函数里面是一些属性的初始化。然后这个函数会调用Install
实例上的init
方法。init
方法里面是后学install
命令的具体实现。
init 方法
init
方法的代码比较多,这里简洁分为三部分。
第一部分是执行yarn
升级检测,如果yarn
有版本的更新输出中会有提示,这里的yarn
版本指的是yarn1
的版本,20 年之前使用的人会经常看到这个提示,现在(2024)年因为yarn1
的版本基本没有更新所有很难看到这个提示了。
第二部分是执行fetchRequestFromCwd
函数,这个函数是获取包的入口的函数。此函数会把package.json
里面依赖的包都收集到一个数组里面。这里查看代码可以看到yarn
默认是安装三种类型的依赖dependencies
devDependencies
optionalDependencies
,其中devDependencies
在!this.config.production === false
时是不会安装的(加上--procution 命令会使 this.config.production === true)。同时这个函数还会返回当前包的一些信息比如workspace
以及package.json
。
第三部分则是steps
,在上文中的流程图中带有step
的节点都是steps
中的一个运行节点,运行节点是先收集需要运行的节点,然后对节点依次执行,如果当前节点执行报错则直接退出不执行后续的节点。steps
的执行函数会使用init
方法中的一些变量并进行修改,在整个步骤完成后init
函数中的变量会被置为相应的值。
第四部分则是保存对应的package.json
以及yarn.lock
。第三部分结束后整个node_modules
都已经构建好了。
steps
steps是init
方法的核心部分。这里按照出现顺序依次介绍对应的step
。
在运行install命令的时候控制台会输出当前在哪一个step
checkCompatibilityStep
这个step
是选择加入到steps
中的,其中的执行条件是package.json
中有os
cpu
engines
字段中的其中一个且命令行参数中没有对应的ignore
参数,比如os
和cpu
对应--ignore-platform
参数,engines
对应--ignore-engines
参数。具体检测的函数实现代码位于src/package-compatibility.js
。
resolveStep
这个第一个必执行的step
,resolveStep
的目标是找到所有包的具体信息,包括依赖的依赖的包。找到的具体信息主要是包的具体版本是什么,从哪里下载,包依赖的具体依赖包具体信息。一般情况下是在npm远端源
上下载的包文件。下面时step
执行函数的内容
js
await this.resolver.init(depRequests, {
isFlat: this.flags.flat,
isFrozen: this.flags.frozenLockfile,
workspaceLayout,
});
topLevelPatterns = this.preparePatterns(rawPatterns);
flattenedTopLevelPatterns = await this.flatten(topLevelPatterns);
npm远端源
的包文件是一个tarball
文件,它是一个以.tgz
结尾的一个压缩文件。访问源的api,比如https://registry.npmjs.org/react/18.2.0
,可以在响应中的dist/tarball
中找到包文件的链接。
this.resolver.init
会找到所有的包的准确版本以及下载包的npm源链接
。
这个函数内容的逻辑很复杂,概括大致能分成yarn.lock
文件中存在以及在yarn.lock
文件中不存在两种。如果包对应的版本在yarn.lock
中存在,yarn
会直接使用lock
文件中的包版本以及lock
文件中声明的tarball
文件链接。如果包在yarn.lock
中不存在,yarn
会请求npm远端源
去获取对应的信息,知道所有的依赖都被正确解析。
相关代码在
src/package-resolver
step
执行函数中剩下两步很简单,把最底层的依赖拍平。
经过这一部所有包的信息,包括依赖的依赖,也就是将要安装的所有包的信息都被解析到this.resolver
中。为下一步进行下载对应的包做准备。
下面是resolveStep
运行完之后解析到的react@18
及其所有的包的信息。
作者因为有
yarn.lock
文件所有所有的包都是直接通过lock
文件解析的,通过lock
文件解析的包的fresh
字段是false
,通过源解析的是true
auditStep
这个step
满足参数中有--audit
才执行,因为yarn audit
命令并没有修复的子命令,所有install
命令这里可以加--audit
来实现差不多的功能。这个在之后的yarn audit
命令中会讲这里先跳过。
fetchStep
这是一个必执行的step
。在上面的resolveStep
把所有包的信息都解析出,这一步主要是下载包到对应的cache
文件夹。逻辑也很简单,如果cache
文件夹中存在对应的包,则无需操作,只需要返回包cache
对应的信息,如果cache
文件夹中不存在包,则下载包的tarball
文件并进行解压到包的cache
文件夹。
使用
yarn cache dir
命令可以查看全局的cache
文件夹
进入cache
文件夹中搜索react-18.2.0
会发现存在react@18.2.0
版本的cache
文件夹。这是因为作者之前已经安装过react@18.2.0
导致cache
命中。
linkStep
这是一个必执行的step
。这个step
开始构建当前目录下的node_modules
。这个step
简单来讲就是先把第一步解析到的所有包扁平化成一个数组,找到每个包对于的cache
文件夹位置以及在当前目录node_modules
下的位置,然后进行文件的复制。由于resolveStep
和fetchStep
已经知道所有的信息这里可以直接复制。
在把所有包从cache
中的位置复制到node_modules
之后,yarn
还会把有bin
的包的bin执行文件
创建软链接到node_modules/.bin
目录下。
pnpStep
这个step
需要条件满足才执行,这里需要打开pnp
插件的开关才能执行。这个不做多讲后续讲pnp
会补充。
buildStep
这是一个必执行的step
。这个step
一般是在linkStep
后被执行,同时这个step
一般也是steps
的最后一个。这个step
主要是将之前resolveStep
中收集到的所有包的scripts
进行执行。这里会先按照当前包的依赖中的scripts
进行执行,执行完当前的再执行依赖的,整体的执行顺序是一个拓扑序。这里执行的scripts
是preinstall
install
postinstall
,其中的声明在依赖包的scripts
中的script不会被执行。
到了这里基本的step
就走完,剩下的都是满足条件才执行的step
savingHarStep
这个step
是一个满足条件才执行的step
。这个step
通过在命令行参数中加入--har
来启用,启用之后在命令完成后会生成一个.har
文件,这个文件可以使用网络分析工具进行分析。这个step
在网络有问题排查十分有用。
cleaningModulesStep
这个step
是一个满足条件才执行的step
。启用这个step
的条件是当前目录下存在.yarnclean
文件。这个step
和yarn autoclean
命令有关后续会讲解。
总结
Yarn的install命令实现中最重要的是四个step
。resolveStep
负责找到包具体信息和来源,fetchStep
把包从远端拉到本地的cache
文件夹,linkStep
负责构建完整的node_modules
,buildStep
负责按照拓扑序执行所有依赖包的scripts
。
author: xiaochuan
date: 2025.1.1