面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:
yunmz777
。
在前端开发中,我们经常使用 npm install <package>
来安装依赖包。这个命令会从默认的 registry 源查找包,并将其下载解压到项目的 node_modules
目录中。
你可能注意到了两个现象:
- 从网上拷贝的新项目首次运行
npm install
速度较慢,而清空node_modules
后再次安装却快了很多 node_modules
内可能嵌套着其他node_modules
目录
虽然使用时无需关注 node_modules
的内部结构,但了解它可以帮助我们更好地理解 npm 的工作原理。接下来我们将深入探讨 npm install
的工作机制。
npm install 原理
要在目录中执行 npm install
命令,当前目录必须存在 package.json
文件,否则命令将无法正常执行。如下图所示:
我们先通过下图概览 npm install
的完整流程,以便在后续详细介绍时能更清晰地理解每个步骤的作用:
依赖包之间的嵌套
开始前,让我们先了解 package-lock.json
文件的关键字段:
-
version
: 包的版本号 -
resolved
: 包的安装源地址 -
integrity
: 包的哈希值,用于验证包的完整性 -
requires
: 依赖包需要的所有依赖项 -
dependencies
: 子依赖的依赖包信息
早期版本的 npm 采用递归方式处理依赖,按照各个包的 package.json
结构将依赖安装到各自的 node_modules
目录中,直到所有依赖关系都被满足。 以下是一个依赖嵌套的示例:
项目 moment
依赖 axios
:
json
{
"name": "moment",
"version": "1.0.0",
"dependencies": {
"axios": "^1.3.1"
}
}
axios
依赖以下包:
json
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
form-data
又依赖:
json
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
上述依赖关系仅为示例部分。执行 npm install
命令后,生成的 node_modules
目录结构如下:
这种递归安装方式存在明显缺点:虽然目录结构与 package.json
一一对应、层次清晰,但在依赖较多时会导致:
-
node_modules
目录过于庞大 -
嵌套层级过深
-
重复安装相同依赖
例如,上图中我们的项目 moment
依赖 axios
,而 axios
又依赖 proxy-from-env
。如果我们在项目中也直接使用 proxy-from-env
,npm 会在项目根目录的 node_modules
中再安装一次,导致系统中存在两份相同的依赖包。
扁平结构
为了解决上述问题,npm 在后续版本中采用了扁平化的依赖结构。扁平化的概念类似于数组的扁平化处理:
js
const array = [1, 2, 3, [4, [5, [6, [7, [8, [4]]]]]]];
console.log(array); // [ 1, 2, 3, [ 4, [ 5, [Array] ] ] ] ]
const result = array.flat(Infinity);
console.log(result); // [1, 2, 3, 4, 5, 6, 7, 8, 4];
采用类似思想,npm 现在将依赖安装时遵循以下原则:无论是直接依赖还是子依赖,都优先安装在 node_modules
根目录下。执行 npm install
后,项目的目录结构变为:
上图展示了扁平化后的依赖结构。npm 在安装过程中遵循以下策略:
-
当遇到相同包的相同版本时,直接复用已安装的版本,避免重复安装
-
npm 会尝试寻找依赖间的兼容版本。依靠 semver(语义化版本)规则,即使版本号不完全一致,只要版本范围有交集,就可以使用单一版本满足多处依赖需求
这种扁平化策略有效减少了重复依赖,大幅降低了项目体积。
检查缓存
npm 首先检查本地是否存在缓存:
-
若存在,直接将缓存内容解压到
node_modules
目录下 -
若不存在,则通过网络下载所需的包
npm 缓存
npm 会将从 registry 下载的包缓存在本地。可以通过以下命令查看缓存目录位置:
sh
npm config get cache
执行后会显示缓存路径(例如 D:\node\node_cache
)。该路径因个人 Node.js 安装位置而异。
打开缓存目录,可以看到如下结构:
缓存目录中:
-
content-v2
存放实际的包内容 -
index-v5
存放依赖的索引信息
index-v5
目录内容示例(格式化后):
json
{
"key": "make-fetch-happen:request-cache:https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.0.0.tgz",
"integrity": "sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w==",
"time": 1675299618116,
"size": 1987,
"metadata": {
"time": 1667289376213,
"url": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.0.0.tgz",
"reqHeaders": {},
"resHeaders": {
"cache-control": "public, must-revalidate, max-age=31557600",
"content-type": "application/octet-stream",
"date": "Tue, 01 Nov 2022 07:56:15 GMT",
"etag": "\"7fb248fc0c589b12896e0572085d0b7a\"",
"last-modified": "Mon, 27 Aug 2018 21:44:38 GMT",
"vary": "Accept-Encoding"
},
"options": {
"compress": true
}
}
}
索引文件中的关键字段:
-
key
: SHA256 生成的哈希值,用于在content-v2
中查找对应文件 -
integrity
: 用于校验文件完整性 -
size
: 包的大小 -
url
: 依赖包的远程地址 -
resHeaders
: HTTP 响应头信息
而 content-v2
目录中存放的是二进制文件。将这些文件重命名为 .tgz
后解压,即可得到完整的依赖包内容。
上图展示了将缓存中的二进制文件重命名为 .tgz
并解压后得到的依赖包内容,可以看到依赖包包含了完整的源代码文件和配置文件(如图中的 package.json
、JavaScript 文件和各种配置文件等)。这正是 npm 在安装过程中实际解压到 node_modules
目录中的内容。
下载包
如果本地缓存中不存在所需依赖,npm 会通过网络请求下载。下载链接来自 package-lock.json
文件中的 resolved
字段。例如,axios 的下载链接为:
url
https://registry.npmjs.org/axios/-/axios-1.3.1.tgz
查看浏览器下载的内容,会发现有以下文件:
当我们从命令行中执行 npm install <package>
的时候,包会经过上面缓存目录下的 tmp
临时目录:
下载流程如下:
-
npm 验证从 registry 下载的包的完整性(通过对比 integrity 哈希值)
-
验证失败时会重新下载
-
验证成功后,将包添加到缓存并解压到
node_modules
目录 -
最后生成或更新
package-lock.json
文件
什么是 npx
npx 是 npm 5.2.0 版本引入的一个工具,npm 官方将其定义为:
从本地或远程 npm 包中运行命令。
npx 会随 npm 一起安装,两者版本保持一致:
npx 核心功能
npx 是一个 npm 包执行器,它能帮你执行依赖包中的二进制文件,主要优势包括:
-
临时安装可执行依赖包:无需全局安装,避免环境污染
-
自动执行依赖包命令:安装完成后自动运行
-
自动加载 node_modules 中的依赖 :无需手动指定
$PATH
-
可执行特定版本的命令:便于测试不同版本
-
支持执行 GitHub 代码仓库:扩展了使用场景
如果 package.json
的 bin
字段只有一个入口,执行 npx 将从该入口开始运行依赖包。npx 会自动查找当前依赖包中 bin
字段指定的入口文件并执行它。
例如,一个包的 package.json
中可能有如下配置:
json
"bin": {
"moment": "./index.js"
}
这种情况下,执行 npx moment
时,npx 会自动找到并运行该包的 ./index.js
文件。如果 bin
字段包含多个命令,则需要明确指定要运行的命令名称。
npx 使用示例
传统方式 vs npx 方式
传统方式:先全局安装脚手架工具,再使用
bash
# 安装
npm install fast-create-app -g
# 使用
fast-create-app create-app xun
传统方式的缺点:
- 全局污染:全局安装的包会影响整个系统环境
- 版本冲突:不同项目可能需要同一工具的不同版本
- 权限问题:全局安装通常需要管理员权限
- 更新困难:全局包不会随项目依赖自动更新
- 存储占用:长期积累大量不再使用的全局包
npx 方式:一步到位,无需全局安装
bash
npx fast-create-app create-app xun
npx 的优势:
- 按需临时安装,用完即删
- 自动使用最新版本的包
- 可同时使用不同版本的工具
- 不需要额外的管理员权限
- 降低了初学者的使用门槛
执行本地项目依赖
bash
# 传统方式
./node_modules/.bin/webpack --version
# npx 方式
npx webpack --version
指定版本执行
bash
# 使用特定版本的包
npx [email protected] --init
执行一次性命令
bash
# 执行简单测试或一次性任务
npx cowsay "Hello npx!"
通过 npx,开发者可以更灵活地使用 npm 生态中的工具,无需担心全局环境污染,特别适合尝试新工具、执行一次性命令和管理多版本依赖的场景。
npx 原理
npx 的工作原理可以概括为以下几个步骤:
-
查找执行路径 :当执行
npx xxx
命令时,npx 会按照以下顺序查找可执行文件:-
首先检查
$PATH
环境变量中是否存在该命令 -
然后查找当前项目的
node_modules/.bin
目录 -
如果都没找到,则临时从 npm registry 安装该包
-
-
临时安装机制 :若需要临时安装,包会被下载到特殊的缓存目录(如
D:\node\node_cache\_npx
)中 -
执行完自动清理:命令执行完成后,临时安装的包会被自动删除,不会留下任何痕迹
命名上,npm 中的 "m" 代表 "Management"(管理),而 npx 中的 "x" 可理解为 "eXecute"(执行)。
npm scripts 与 npx 的区别
有一个常见疑问:为什么在终端中需要用 npx xxx test
,而在 package.json
的 scripts 中只需要写 xxx test
就能直接运行?
这是因为 npm 在执行 scripts 时会自动将 node_modules/.bin
目录添加到 PATH 环境变量中,使项目中安装的可执行文件可以直接运行。例如:
json
// package.json
{
"scripts": {
"test": "jest" // 不需要写成 npx jest
}
}
这样执行 npm run test
时,npm 会自动查找 node_modules/.bin
目录下的 jest 可执行文件并运行。
而在终端中直接执行时,系统并不会自动查找 node_modules/.bin
,所以需要使用 npx 来处理这个路径问题。这也是 npx 存在的主要价值之一 - 简化本地命令行工具的使用。
总结
npm install 的工作原理可以总结为以下几点:
-
首先检查 package.json 和 package-lock.json,确定依赖关系树
-
检查本地缓存,若缓存存在且完整则直接使用,否则从 registry 下载依赖包
-
将下载的包存入本地缓存并校验完整性,然后解压到 node_modules 目录
-
应用扁平化处理策略,尽可能避免依赖冗余和嵌套过深的问题
-
生成或更新 package-lock.json 文件,记录确切的依赖版本和下载地址