Skip to Content

面试导航 - 程序员面试题库大全 | 前端后端面试真题 | 面试

在前端开发中,我们经常使用 npm install <package> 来安装依赖包。这个命令会从默认的 registry 源查找包,并将其下载解压到项目的 node_modules 目录中。

你可能注意到了两个现象:

  1. 从网上拷贝的新项目首次运行 npm install 速度较慢,而清空 node_modules 后再次安装却快了很多
  2. 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

{ "name": "moment", "version": "1.0.0", "dependencies": { "axios": "^1.3.1" } }

axios 依赖以下包:

"dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" }

form-data 又依赖:

"dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" }

上述依赖关系仅为示例部分。执行 npm install 命令后,生成的 node_modules 目录结构如下:

这种递归安装方式存在明显缺点:虽然目录结构与 package.json 一一对应、层次清晰,但在依赖较多时会导致:

  1. node_modules 目录过于庞大

  2. 嵌套层级过深

  3. 重复安装相同依赖

例如,上图中我们的项目 moment 依赖 axios,而 axios 又依赖 proxy-from-env。如果我们在项目中也直接使用 proxy-from-env,npm 会在项目根目录的 node_modules 中再安装一次,导致系统中存在两份相同的依赖包。

扁平结构

为了解决上述问题,npm 在后续版本中采用了扁平化的依赖结构。扁平化的概念类似于数组的扁平化处理:

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 在安装过程中遵循以下策略:

  1. 当遇到相同包的相同版本时,直接复用已安装的版本,避免重复安装

  2. npm 会尝试寻找依赖间的兼容版本。依靠 semver(语义化版本)规则,即使版本号不完全一致,只要版本范围有交集,就可以使用单一版本满足多处依赖需求

这种扁平化策略有效减少了重复依赖,大幅降低了项目体积。

检查缓存

npm 首先检查本地是否存在缓存:

  • 若存在,直接将缓存内容解压到 node_modules 目录下

  • 若不存在,则通过网络下载所需的包

npm 缓存

npm 会将从 registry 下载的包缓存在本地。可以通过以下命令查看缓存目录位置:

npm config get cache

执行后会显示缓存路径(例如 D:\node\node_cache)。该路径因个人 Node.js 安装位置而异。

打开缓存目录,可以看到如下结构:

缓存目录中:

  • content-v2 存放实际的包内容

  • index-v5 存放依赖的索引信息

index-v5 目录内容示例(格式化后):

{ "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 的下载链接为:

https://registry.npmjs.org/axios/-/axios-1.3.1.tgz

查看浏览器下载的内容,会发现有以下文件:

当我们从命令行中执行 npm install <package> 的时候,包会经过上面缓存目录下的 tmp 临时目录:

下载流程如下:

  1. npm 验证从 registry 下载的包的完整性(通过对比 integrity 哈希值)

  2. 验证失败时会重新下载

  3. 验证成功后,将包添加到缓存并解压到 node_modules 目录

  4. 最后生成或更新 package-lock.json 文件

什么是 npx

npx 是 npm 5.2.0 版本引入的一个工具,npm 官方将其定义为:

从本地或远程 npm 包中运行命令。

npx 会随 npm 一起安装,两者版本保持一致:

npx 核心功能

npx 是一个 npm 包执行器,它能帮你执行依赖包中的二进制文件,主要优势包括:

  1. 临时安装可执行依赖包:无需全局安装,避免环境污染

  2. 自动执行依赖包命令:安装完成后自动运行

  3. 自动加载 node_modules 中的依赖:无需手动指定 $PATH

  4. 可执行特定版本的命令:便于测试不同版本

  5. 支持执行 GitHub 代码仓库:扩展了使用场景

如果 package.jsonbin 字段只有一个入口,执行 npx 将从该入口开始运行依赖包。npx 会自动查找当前依赖包中 bin 字段指定的入口文件并执行它。

例如,一个包的 package.json 中可能有如下配置:

"bin": { "moment": "./index.js" }

这种情况下,执行 npx moment 时,npx 会自动找到并运行该包的 ./index.js 文件。如果 bin 字段包含多个命令,则需要明确指定要运行的命令名称。

npx 使用示例

传统方式 vs npx 方式

传统方式:先全局安装脚手架工具,再使用

# 安装 npm install fast-create-app -g # 使用 fast-create-app create-app xun

传统方式的缺点

  1. 全局污染:全局安装的包会影响整个系统环境
  2. 版本冲突:不同项目可能需要同一工具的不同版本
  3. 权限问题:全局安装通常需要管理员权限
  4. 更新困难:全局包不会随项目依赖自动更新
  5. 存储占用:长期积累大量不再使用的全局包

npx 方式:一步到位,无需全局安装

npx fast-create-app create-app xun

npx 的优势

  1. 按需临时安装,用完即删
  2. 自动使用最新版本的包
  3. 可同时使用不同版本的工具
  4. 不需要额外的管理员权限
  5. 降低了初学者的使用门槛

执行本地项目依赖

# 传统方式 ./node_modules/.bin/webpack --version # npx 方式 npx webpack --version

指定版本执行

# 使用特定版本的包 npx eslint@7.0.0 --init

执行一次性命令

# 执行简单测试或一次性任务 npx cowsay "Hello npx!"

通过 npx,开发者可以更灵活地使用 npm 生态中的工具,无需担心全局环境污染,特别适合尝试新工具、执行一次性命令和管理多版本依赖的场景。

npx 原理

npx 的工作原理可以概括为以下几个步骤:

  1. 查找执行路径:当执行 npx xxx 命令时,npx 会按照以下顺序查找可执行文件:

    • 首先检查 $PATH 环境变量中是否存在该命令

    • 然后查找当前项目的 node_modules/.bin 目录

    • 如果都没找到,则临时从 npm registry 安装该包

  2. 临时安装机制:若需要临时安装,包会被下载到特殊的缓存目录(如 D:\node\node_cache\_npx)中

  3. 执行完自动清理:命令执行完成后,临时安装的包会被自动删除,不会留下任何痕迹

命名上,npm 中的 “m” 代表 “Management”(管理),而 npx 中的 “x” 可理解为 “eXecute”(执行)。

npm scripts 与 npx 的区别

有一个常见疑问:为什么在终端中需要用 npx xxx test,而在 package.json 的 scripts 中只需要写 xxx test 就能直接运行?

这是因为 npm 在执行 scripts 时会自动将 node_modules/.bin 目录添加到 PATH 环境变量中,使项目中安装的可执行文件可以直接运行。例如:

// package.json { "scripts": { "test": "jest" // 不需要写成 npx jest } }

这样执行 npm run test 时,npm 会自动查找 node_modules/.bin 目录下的 jest 可执行文件并运行。

而在终端中直接执行时,系统并不会自动查找 node_modules/.bin,所以需要使用 npx 来处理这个路径问题。这也是 npx 存在的主要价值之一 - 简化本地命令行工具的使用。

总结

npm install 的工作原理可以总结为以下几点:

  1. 首先检查 package.json 和 package-lock.json,确定依赖关系树

  2. 检查本地缓存,若缓存存在且完整则直接使用,否则从 registry 下载依赖包

  3. 将下载的包存入本地缓存并校验完整性,然后解压到 node_modules 目录

  4. 应用扁平化处理策略,尽可能避免依赖冗余和嵌套过深的问题

  5. 生成或更新 package-lock.json 文件,记录确切的依赖版本和下载地址

最后更新于:
Copyright © 2025Moment版权所有粤ICP备2025376666