第 65 期 - npm install 原理全解析
摘要
本文讲述了 npm install 中 ActualTree 和 IdealTree 两棵树的构建流程、比较、Diff 树操作、package.json 和 package - lock.json 文件更新,还有本地调试方法和参考文档等内容。
一、整体流程
1. npm install 流程涉及两棵树
- npm install 流程涉及 ActualTree 和 IdealTree。ActualTree 根据本地 package.json 和 node_modules 目录下的依赖构建。IdealTree 根据本地 package.json、package - lock.json 和命令行输入的依赖构建。两棵树构建完会进行比较,构建 Diff 树,然后遍历 Diff 树中的 Node 节点,创建对应目录、下载产物、执行 script 脚本命令。
- 之后收集更新 package.json 文件的 Node 节点并遍历更新内容,还要遍历根 Node 节点 inventory 属性中的 Node 节点,创建 Shrinkwrap 对象并更新到 package - lock.json 文件中。
2. ActualTree 创建流程
- 首先解析项目 package.json 文件内容并创建根 Node 节点。然后遍历 package.json 中的依赖创建 Edge 对象添加到归属 Node 节点的 edgesOut 属性。接着遍历当前 Node 节点的 node_modules 目录中的依赖创建子 Node 节点并建立父子引用关系,最后递归遍历子 Node 节点构建 Node 节点树。
- 例如在创建根 Node 节点相关代码中:
async # loadActual (options) {
// 创建根 Node 节点
this.# actualTree = await this.# loadFSNode({ path: this.path });
// 解析本地 package - lock.json 文件内容
const meta = await Shrinkwrap.load();
this.# actualTree.meta = meta;
// 递归遍历 Node 节点,构建 Node 节点树
await this.# loadFSTree(this.# actualTree);
}
3. IdealTree 创建流程
- 先解析项目 package.json 文件内容创建根 Node 节点,遍历 package.json 中的依赖创建 Edge 对象添加到归属 Node 节点的 edgesOut 属性。再遍历 package - lock.json 中的依赖创建子 Node 节点建立父子引用关系,解析命令行输入的依赖信息更新 tree package 属性,遍历其依赖创建新 Edge 对象添加到 edgesOut 属性。
- 最后从# depsQueue 获取需要更新的 Node 节点,遍历 edgesOut 属性中有问题的 Edge 对象创建子 Node 节点建立父子引用关系,递归遍历# depsQueue 队列的 Node 节点构建 Node 节点树。
- 如在创建根 Node 节点部分代码:
async # initTree () {
let root;
// 获取项目的 package.json 文件内容
const pkg = await rpj(this.path + '/package.json');
// 创建根 Node 节点
root = await this.rootNodeFromPackage(pkg);
// 遍历 package - lock.json 中的依赖,创建其对应的子 Node 节点,并建立父子引用关系
return this.loadVirtual({ root });
}
二、数据引用图谱
1. ActualTree
- 项目根 Node 节点的 package 属性存储 package.json 文件数据,package.json 依赖对应的 Edge 对象归属到根 Node 节点的 edgesOut 属性,package - lock.json 文件对应的 Shrinkwrap 对象归属根 Node 节点的 meta 属性。
- 项目 node_modules 下的 package 对应子 Node 节点,package 目录下 package.json 文件数据存储在子 Node 节点的 parent 属性,其依赖对应的 Edge 对象归属到子 Node 节点的 edgesOut 属性,子 Node 节点的 parent 属性指向根 Node 节点,根 Node 节点的 children 属性指向子 Node 节点。若 package 下有 node_modules 目录也会创建 Node 节点建立父子关系形成 Node 节点树。
2. IdealTree
- 项目根 Node 节点的 package 属性存储 package.json 文件数据,package.json 依赖对应的 Edge 对象归属根 Node 节点的 edgesOut 属性,package - lock.json 文件对应的 Shrinkwrap 对象归属根 Node 节点的 meta 属性。
- 项目 package - lock.json 文件中的 packages 字段包含 package 信息,根据其创建子 Node 节点,子 Node 节点的 parent 属性存储该信息,根据 package 信息中的 dependencies 字段创建 Edge 对象归属到子 Node 节点的 edgesOut 属性,子 Node 节点的 parent 属性指向根 Node 节点,根 Node 节点的 children 属性指向子 Node 节点。解析命令行输入依赖更新根 Node 节点 package 属性重新创建 Edge 对象等操作最终形成 Node 节点树。
三、核心代码
1. Reifier.reify
- 此函数构建 ActualTree 和 IdealTree,对比两者差异构建 Diff 树,遍历 Diff 树中的 Node 节点创建对应目录、下载产物、执行 script 脚本命令,还更新 package.json 配置文件内容和保存 package - lock.json 文件。
- 代码如下:
async reify (options) {
// 创建 ActualTree 和 IdealTree
await this[_loadTrees](options);
// 对比 ActualTree 和 IdealTree 的差异,创建 Diff 树
await this[_diffTrees]();
// 遍历 Diff 树中 Node 节点,创建其对应的目录,下载其对应的产物,执行其 script 脚本命令,例如 postinstall
await this[_reifyPackages]();
// 更新 package.json 配置文件内容和保存 package - lock.json 文件
await this[_saveIdealTree](options);
}
2. 创建 ActualTree 相关函数
- 如 ActualLoader.# loadActual 创建根 Node 节点、解析本地 package - lock.json 文件内容、递归遍历 Node 节点构建 Node 节点树。
- 其中 ActualLoader.# loadFSTree 递归遍历 Node 节点依赖创建子 Node 节点建立父子引用关系构建 Node 节点树,ActualLoader.# loadFSChildren 遍历当前 Node 节点的 node_modules 目录中的依赖创建子 Node 节点建立父子引用关系,ActualLoader.# loadFSNode 获取当前 Node 节点的 package.json 文件内容创建 Node 节点等操作。
3. 创建 IdealTree 相关函数
- IdealTreeBuilder.# initTree 获取项目 package.json 文件内容创建根 Node 节点、遍历 package - lock.json 中的依赖创建子 Node 节点建立父子引用关系。
- IdealTreeBuilder.# applyUserRequestsToNode 解析命令行输入的依赖信息更新 tree package 属性、遍历依赖创建新 Edge 对象添加到 edgesOut 属性,IdealTreeBuilder.# buildDepStep 递归遍历# depsQueue 获取需要更新的 Node 节点构建 Node 节点树等操作。
4. 对比 ActualTree 和 IdealTree 差异相关函数
- Diff.calculate 对比两棵树差异构建 Diff 树,getChildren 遍历对比 ActualTree children 和 IdealTree children 的差异创建 Diff 节点,leave 建立 Diff 节点父子引用关系构建 Diff 树,diffNode 对比 Node 节点差异。
5. 遍历 Diff 树相关函数
- Reifier._retireShallowNodes 收集需要删除的 Node 节点,Reifier._removeTrash 遍历要删除节点集合删除对应目录,Reifier._createSparseTree 遍历 Diff 树中 Node 节点创建对应目录,Reifier._unpackNewModules 遍历 Diff 树中 Node 节点下载产物,Builder.# build 遍历 Diff 树中 Node 节点执行 script 脚本命令。
6. 更新 package.json 和保存 package - lock.json 相关函数
- Reifier._saveIdealTree 收集需要更新 package.json 的 Node 节点遍历更新内容,还保存 package - lock.json 文件。
四、本地调试
- 首先克隆 npm cli 项目:
git clone https://github.com/npm/cli.git
。 - 然后安装 cli 项目依赖:进入 cli 目录执行
npm install
。 - 接着新建调试项目:在与 cli 项目平级目录下新建目录,进入该目录初始化 package.json(
node../cli/index.js init -y
),安装依赖(node --inspect../cli/index.js install [package]
)。 - 最后在 Google 浏览器中输入
chrome://inspect
回车进行调试。
五、参考文档
- 给出了如“彻底了解 npm ——架构、进化史及原理解析”和“【前端调试】如何优雅调试 Node.js”等参考文档。
Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有