重学前端工程化:npm 和 npx 执行原理

面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:yunmz777

在前端开发中,我们经常使用 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

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 一一对应、层次清晰,但在依赖较多时会导致:

  1. node_modules 目录过于庞大

  2. 嵌套层级过深

  3. 重复安装相同依赖

例如,上图中我们的项目 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 在安装过程中遵循以下策略:

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

  2. 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 临时目录:

下载流程如下:

  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 中可能有如下配置:

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

传统方式的缺点

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

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

bash 复制代码
npx fast-create-app create-app xun

npx 的优势

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

执行本地项目依赖

bash 复制代码
# 传统方式
./node_modules/.bin/webpack --version

# npx 方式
npx webpack --version

指定版本执行

bash 复制代码
# 使用特定版本的包
npx [email protected] --init

执行一次性命令

bash 复制代码
# 执行简单测试或一次性任务
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 环境变量中,使项目中安装的可执行文件可以直接运行。例如:

json 复制代码
// 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 文件,记录确切的依赖版本和下载地址

相关推荐
前端九哥几秒前
vue3实现复制到剪切板
前端·vue.js
王小菲2 分钟前
深入解析 JavaScript 闭包机制:从作用域到高阶应用
前端·javascript·面试
chengong998814 分钟前
SassScript:Sass中的编程特性详解
前端·css·sass
简言85628 分钟前
一文搞清楚浏览器js事件循环和渲染事件
前端
yogalin199335 分钟前
网站开发者如何实现反爬虫
前端
bug_kada37 分钟前
ES6 字符串新增方法你了解多少?
前端·javascript
天天扭码38 分钟前
当大模型成为我的赛博嘴替:魔搭社区驯AI实录
前端·人工智能·python
NaZiMeKiY44 分钟前
HTML5前端第四章节
前端·html·html5
超能996要躺平1 小时前
vue3 实现页面锚点,左边控制右边内容区域滚动
前端·vue.js
木木黄木木1 小时前
html5制作2048游戏开发心得与技术分享
前端·html·html5