Skip to content

包管理工具

1 package.json

1.1 概念

  • package.json 是 Node.js/前端工程的核心配置文件。

  • 它是一个 JSON 文件,用来 描述项目的基本信息、依赖清单、脚本命令和工程配置。

  • 相当于项目的“说明书”和“依赖管理账本”。

1.2 作用

  1. 项目说明:描述项目名称、版本、作者、许可证等。

  2. 依赖管理:记录项目运行/开发所需的第三方库及版本。

  3. 脚本管理:定义常用命令(npm run build、npm run test 等)。

  4. 模块解析:指定包的入口文件、类型声明文件等。

  5. 工程配置:为构建工具、测试工具、编译器等提供内联配置。

  6. 团队协作:结合 lock 文件,保证在不同机器安装依赖的一致性。

1.3 常见的字段

1.3.1 基本信息

  • name:包名,发布到 npm 时的唯一标识。

  • version:遵循语义化版本号 (semver)。

  • description:描述信息。

  • author:作者信息。

  • license:许可证。

  • private:是否为私有包,true 时禁止发布到 npm。

1.3.2 依赖管理

  • dependencies:生产环境依赖。

  • devDependencies:开发环境依赖(构建、测试工具等)。

  • peerDependencies:宿主项目必须安装的依赖(多用于库开发)。

  • optionalDependencies:可选依赖,安装失败不影响整体。

  • bundledDependencies:发布时打包进去的依赖。

INFO

👉 npm 的包通常需要遵从 semver 版本规范,semver 版本规范是 X.Y.Z:

  • X 主版本号(major):当你做了不兼容的 API 修改(可能不兼容之前的版本)
  • Y 次版本号(minor):当你添加了向下兼容的新功能(新功能增加,但是兼容之前的版本)
  • Z 修订号(patch):当你修复了向下兼容的问题(没有新功能,修复了之前版本的 bug)

^1.2.3:允许更新次版本和补丁版本(1.x.x)。

~1.2.3:允许更新补丁版本(1.2.x)。

1.2.3:锁定版本。

*:允许任意版本。

1.3.3 脚本

  • scripts:定义可运行命令。
json
{
    "scripts": {
        "start": "node index.js",
        "build": "webpack",
        "test": "jest"
    }
}

运行方式:npm run build / yarn build / pnpm build

INFO

内置的 4 个命令(start、test、restart、stop) → 可以直接用,不写 run

其他脚本(如 build、lint 等) → 必须写 npm run xxx

如果写 npm build,npm 会调用它自己的 内部命令(构建 npm 包),而不是执行 scripts.build

👉 所以 npm build 是错的,应该写 npm run build

yarn 的命令解析更宽松:

yarn <script> 会优先查找 package.json 里的 scripts

找到就执行;没找到才去执行 yarn 自己的命令。

因此 yarn build ✅ 可以直接运行 scripts.build。

yarn run build 也能用,但 run 可以省略。

和 yarn 类似:

pnpm <script> 会直接执行 package.json 里的脚本。

所以 pnpm build ✅ 可以直接运行 scripts.build

pnpm run build 也可以,但 run 可以省略。

工具执行 scripts.build 的写法是否可省略 run
npmnpm run build ✅ / npm build❌ 不能省略
yarnyarn build ✅ / yarn run build✅ 可以省略
pnpmpnpm build ✅ / pnpm run build✅ 可以省略

1.3.4 模块解析

  • main:CommonJS 入口文件。

  • module:ESM 入口(给现代打包器用)。

  • exports:推荐方式,精细控制导出路径。

  • bin:命令行工具入口。

  • types / typings:TS 类型声明文件入口。

👉 还常见的一个就是:

  • type:用来声明整个包是 CommonJS 还是 ES Module。

    • "type": "commonjs"(默认值)

    • "type": "module" → 表示 .js 文件走 ESM 语法,.cjs 文件才是 CJS。

1.3.5 工程配置

  • engines:指定 Node.js/npm 的版本。

  • browserslist:目标浏览器范围(babel/postcss 使用)。

  • 工具相关配置:eslintConfigbabeljest 等,可直接写在 package.json 内。

1.4 模块解析流程举例(★)

现在完整的梳理一个引入第三方库的模块解析流程

用一个具体的例子(import axios from "react")说明从 代码写法 → 查找 → 解析 → 最终引入 的全过程。

1.4.1 确认模块名

  • "react" 是一个 裸模块(bare module specifier),不是路径(如 ./utils.js),也不是绝对地址(如 https://)。

  • 这类模块解析规则是:去 node_modules 找对应的包。

1.4.2 从 node_modules 开始查找

  • Node.js / bundler(如 webpack、Vite)会先在当前项目目录下查找:
bash
./node_modules/react
  • 如果没找到,会向上一级目录查找:
bash
../node_modules/react
../../node_modules/react
...

直到找到或到达磁盘根目录。

1.4.3 读取包的 package.json

找到 node_modules/react/ 后,会先读取它的 package.json。 里面有几个关键字段决定用哪个文件作为入口:

  • main(CommonJS 默认入口):
json
{ "main": "index.js" }
  • module(ESM 入口,现代打包器优先使用):
json
{ "module": "index.module.js" }
  • exports(推荐写法,可精细控制导出):
json
{
    "exports": {
        ".": {
            "import": "./index.modern.mjs",
            "require": "./index.js"
        }
    }
}

👉 React 实际上在它的 package.json 中写了:

json
{
    "main": "index.js",
    "exports": {
        ".": {
            "react-server": "./react.shared-subset.js",
            "default": "./index.js"
        }
    }
}

所以默认会走到 node_modules/react/index.js

1.4.4 解析入口文件

入口文件 index.js 可能是一个“转发文件”,它会把核心 API 从别的路径导出,例如:

js
"use strict";
module.exports = require("./cjs/react.development.js");

这样最终就会定位到 react.development.js,里面包含 React 的核心实现。

1.4.5 bundler 的优化(如 webpack/Vite)

优先使用 exports/module 字段 → 选择 ESM 版本,支持 tree-shaking,减少打包体积。

如果没有 exports/module,才会回退到 main 字段。

如果 package.json 没写入口,默认尝试 index.js

1.4.6 最终加载到代码中

Node.js 环境下 → require('react') 会拿到导出的对象(React API)。

浏览器打包后 → bundler 会把 React 相关代码打进 bundle,供 import React 使用。

🔎 总结为“模块解析流程”

  1. 识别模块名(裸模块 → 去 node_modules)。

  2. 逐级向上查找 node_modules。

  3. 读取对应包的 package.json。

    • 优先看 exports 字段。
    • 如果没有 exports,再看 module 字段。
    • 如果没有 module,再看 main 字段。
  4. 定位入口文件。

  5. 如果入口再转发,就继续跟进直到真实文件。

  6. 最终导出 API 给你的代码使用。

当我写 import React from "react" 时,Node.js 或 bundler 会去当前目录的 node_modules 查找 react 包,读取它的 package.json,根据 exports/module/main 字段确定入口文件。如果入口文件再导出其他内容,则继续解析。最终将 React 的 API 暴露给我的代码。

alt text

2 npm

2.1 什么是 npm

npm (Node Package Manager):Node.js 官方的包管理工具。

  • 下载/安装第三方库(如 React、Express)。

  • 管理项目依赖版本。

  • 执行脚本命令(如构建、测试)。

  • 发布/共享自己的包。

其中 node_modules 目录就是 npm 用来局部安装依赖的地方,使得不同的项目可以使用不同版本的包,而不会互相干扰。

  • 随着 npm 的快速成长,一些问题也随之而来,比如 node_modules 随着依赖的嵌套,体积越来越大。

2.2 npm 的嵌套依赖模型

在 npm2 及以前,每个包会将其依赖安装在自己的 node_modules 目录下,这意味着每个依赖也会带上自己的依赖,形成一个嵌套的结构,结构如下:

alt text

这样的结构虽然解决了版本冲突、依赖隔离等问题,但却有几个致命的缺点:

  • :每个依赖都会安装自己的依赖,导致了大量的重复,特别是在多个包共享同一依赖的场景下。

  • :这种嵌套结构在文件系统中造成了非常长的路径,然而大多数 Windows 工具、实用程序和 shell 最多只能处理长达 260 个字符的文件和文件夹路径。一旦超过,安装脚本就会开始出错,而且无法再使用常规方法删除 node_modules 文件夹。相关 issue:github.com/nodejs/node…

  • :每次安装或更新依赖时,npm 需要处理和解析整个依赖树,过程非常缓慢。

2.3 npm3 架构升级

为了解决这些问题,npm 在第三个版本进行了重构,引入了扁平化的依赖模型。 通过将依赖扁平化,尽可能地减少重复的包版本,有效减少了项目地总体积,同时也避免了 npm 早期的深层嵌套问题。 扁平化结构如下:

alt text 可以看到还是会有一定可能产生嵌套问题,因为根目录只能存放某个包的一个版本,而某个包的依赖可能会有多个版本,所以会产生嵌套问题。

2.4 npm5 架构升级

为了解决 npm3 中的一些问题,npm 在第五个版本进行了重构,引入了 package-lock.json 文件。

这个锁文件确保了依赖的一致性。无论是在哪个环境下运行 npm install,都能确保安装相同版本的依赖,解决了因版本不匹配导致的问题。

2.5 幽灵依赖

2.5.1 什么是幽灵依赖

新建一个文件夹,在终端初始化项目,执行 npm init -y,然后安装一个包,比如 lodash,执行 npm install express,这个时候node_modules文件夹下会发现什么呢?如下图所示:

alt text

我只安装了express,为什么 node_modules 下会出现那么多的包呢?那是因为 express 依赖了一些包,而依赖的这些包又会依赖其它包...npm 则是把这些包拍平了放到了 node_modules 下,这也就导致 node_modules 里出现了这么多包。

那么问题又来了,那 node_modules 下的这些包我没有去主动安装我能不导入呢?答案是肯定的。比如新建一个 index.js,然后导入 node_modules 下的 body-parser

javascript
import bd from "body-parser";

console.log(bd);

执行一下这个文件(注意:这里需要在 package.json 中加个字段"type": "module",这样才能用 es6 语法) alt text

发现是可以使用的,但是我们并没有去安装 body-parser。而这种依赖包就被称为为

2.5.2 幽灵依赖存在哪些问题?

首先幽灵依赖会带来 。 举个例子:

假设 小李 在项目中安装了依赖包 A1,而 A1 又依赖 B1。 后来 小王 在开发时需要使用 B1,发现项目里能直接引用,于是就没有在 dependencies 中显式声明。

某一天,小李将 A1 升级为 A2,而 A2 要么依赖的是新版 B2,要么已经不再依赖 B1。 这样一来:

  • B1 可能会被移除(因为不再被任何依赖声明)。

  • 或者 B1 被替换为与现有代码不兼容的 B2

结果就是:小王的代码会因为依赖丢失或版本不兼容而报错。

  1. 由于该模块未在 package.json 中显式声明,在其他环境(如测试、生产或其他开发者的机器)部署时,无法自动安装所需模块,导致环境不一致,从而引发运行错误。

  2. 未明确声明依赖版本,不同环境可能安装到不同的版本,造成功能在某些环境下无法正常运行,甚至出现不可预期的错误。

  3. 开发人员无法通过 package.json 明确了解项目实际依赖,增加了代码理解和维护的难度。

INFO

此时 node_modules 就会有两个一样的 B 包分别在 A 文件和 D 文件下,这样就会造成磁盘空间的浪费。这也就是后面 npm 升级成扁平化包管理的方式的原因(导致幽灵依赖出现)。

那么有没有方案既能解决幽灵依赖,有能节省磁盘空间呢? 答案是肯定的,这时候就要介绍一下大名鼎鼎的包管理器 pnpm 是如何解决这些问题的。

参考链接:

3 pnpm

3.1 什么是 pnpm

pnpm 是一个快速、节省磁盘空间的包管理器。它通过 的方式来管理依赖,避免了 npm 中出现的幽灵依赖问题。 alt text