一、核心思路(一句话)
package.json 不只是依赖清单,而是 npm 包对外暴露"模块入口、运行时依赖、开发依赖、兼容要求、发布内容和工程规则"的契约文件。
面试重点抓 4 件事:
入口怎么解析 → 依赖怎么分类 → Tree Shaking 怎么保证 → 发布/版本怎么治理
二、package.json 中最重要的模块入口字段是什么?
核心思路(一句话)
现代项目优先用 exports 定义公共 API 和条件导出,main / module / browser 更多属于兼容性或工具约定,不能简单理解成固定的优先级。
结构图
text
package.json
│
├── exports ──→ 现代 Node.js / 主流打包工具
│ │
│ ├── types
│ ├── import
│ ├── require
│ ├── browser
│ └── default
│
├── main ──────→ CommonJS 传统入口
│
├── module ────→ 社区约定的 ESM 入口
│
└── browser ───→ 浏览器环境替换/入口约定
1. main
传统 Node.js CommonJS 入口:
json
{
"main": "./dist/index.cjs"
}
例如:
js
const pkg = require("my-package");
传统解析可能最终找到:
text
my-package/dist/index.cjs
注意:main 并不是"Node 永远只认它"。现代 Node.js 如果存在 exports,通常会优先使用 exports。
2. module
常见写法:
json
{
"main": "./dist/index.cjs",
"module": "./dist/index.js"
}
通常用于告诉打包器:
"这里有一个 ESM 版本,可以用于构建优化。"
但:
module 不是 Node.js 官方标准入口字段,而是生态中的约定。
Webpack、Rollup 等工具是否读取以及如何处理,取决于具体工具和配置。
3. browser
用于浏览器环境的入口或模块替换。
例如:
json
{
"browser": {
"fs": false
}
}
意思是:
text
Node.js 环境
↓
fs
↓
正常使用 Node.js fs
浏览器构建
↓
fs
↓
替换/禁用
常用于:
同一个 npm 包同时支持 Node.js 和浏览器环境。
三、为什么 exports 是现代 package.json 的核心?
核心思路(一句话)
exports 同时解决"条件导出 + 公共 API 封装 + 深层路径限制"三个问题。
例如:
json
{
"name": "my-lib",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"default": "./dist/index.js"
}
}
}
对应:
text
import
↓
dist/index.js
require
↓
dist/index.cjs
TypeScript
↓
dist/index.d.ts
条件匹配原则
exports 中的条件对象需要按照条件优先级和工具解析规则设计,不能简单概括成"所有条件严格从上到下";但在实际配置中,应把更具体的条件放在前面,避免被更宽泛条件提前匹配。
例如:
json
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"default": "./dist/index.js"
}
}
}
其中:
types:TypeScript 类型入口import:ES Modulerequire:CommonJSdefault:兜底条件
为什么要把 import 和 require 分开?
因为:
js
import x from "my-lib";
和:
js
const x = require("my-lib");
可能需要加载不同格式的产物:
text
import
↓
ESM
require
↓
CommonJS
否则容易出现:
- ESM / CommonJS 互操作问题
- 默认导出行为差异
- 打包器解析异常
- 双重加载等问题
四、exports 如何解决"深层导入"和封装泄漏?
假设包内部:
text
my-lib/
├── dist/
│ ├── index.js
│ ├── Button.js
│ └── utils.js
没有 exports 时,用户可能直接:
js
import Button from "my-lib/dist/Button.js";
这就把内部目录结构暴露给了用户。
使用:
json
{
"exports": {
".": "./dist/index.js"
}
}
用户只能:
js
import { Button } from "my-lib";
而不能随便:
js
import Button from "my-lib/dist/Button.js";
本质
text
没有 exports
↓
用户可以依赖内部文件结构
↓
内部重构容易破坏用户代码
有 exports
↓
只暴露公共 API
↓
内部目录可以自由重构
这也是 exports 非常重要的工程价值:建立模块封装边界。
五、dependencies、devDependencies、peerDependencies 怎么区分?
核心思路(一句话)
不要背定义,直接问:
"这个依赖是谁运行时需要?谁负责提供?"
| 类型 | 谁需要 | 谁安装 | 典型场景 |
|---|---|---|---|
dependencies |
你的代码运行时需要 | 包管理器安装 | axios、lodash |
devDependencies |
开发/构建/测试需要 | 开发项目安装 | webpack、eslint、typescript |
peerDependencies |
宿主项目提供 | 宿主安装 | React、Vue、插件宿主 |
optionalDependencies |
可选运行能力 | 尝试安装 | 平台相关依赖 |
1. dependencies
真正运行时依赖:
json
{
"dependencies": {
"lodash": "^4.17.21"
}
}
你的代码:
js
import _ from "lodash";
发布后用户安装你的包:
bash
npm install my-lib
lodash 也应该作为运行时依赖存在。
2. devDependencies
只服务于开发、构建、测试:
json
{
"devDependencies": {
"typescript": "^5.0.0",
"vitest": "^3.0.0",
"eslint": "^9.0.0"
}
}
例如:
text
源码
↓
TypeScript
↓
构建
↓
dist
TypeScript 是你的构建工具,不是用户运行你的库时必须依赖的运行时库。
六、为什么 React / Vue 组件库通常应该放 peerDependencies?
这是面试非常容易追问的一点。
假设:
json
{
"dependencies": {
"react": "^19.0.0"
}
}
你的组件库:
text
my-ui
└── react
用户项目:
text
app
└── react
可能出现:
text
app
├── react A
└── my-ui
└── react B
于是出现两个 React 实例。React Hooks 等场景可能因此产生:
text
Invalid hook call
所以组件库通常:
json
{
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
}
}
表达的是:
React 是宿主应用提供的,我只声明我兼容哪些版本。
同时开发组件库时:
json
{
"devDependencies": {
"react": "^19.0.0"
}
}
因此:
text
peerDependencies
↓
声明宿主兼容范围
devDependencies
↓
本地开发时实际安装
最容易记的判断方式
"运行时必须跟着我的包走" → dependencies
"只在我开发/构建/测试时需要" → devDependencies
"宿主必须提供,我只要求版本兼容" → peerDependencies
七、optionalDependencies 是什么?
json
{
"optionalDependencies": {
"some-platform-package": "^1.0.0"
}
}
特点:
安装失败不应该阻塞整个项目安装。
典型场景:
text
不同操作系统
↓
需要不同平台二进制实现
↓
当前平台不需要某个依赖
但要注意:
optionalDependencies 并不意味着代码可以无条件使用它。
代码需要自行处理依赖不存在的情况。
八、sideEffects 是什么?为什么可能导致 CSS 被错误 Tree Shaking?
核心思路(一句话)
sideEffects 是告诉打包器:模块被加载时是否可能产生无法仅靠导入导出关系判断的副作用。
例如:
json
{
"sideEffects": false
}
表示:
默认认为模块没有副作用,可以更积极地进行 Tree Shaking。
但是 CSS:
js
import "./index.css";
虽然没有导出:
js
export xxx
但它产生了真实副作用:
text
import CSS
↓
修改页面样式
所以组件库常见:
json
{
"sideEffects": [
"*.css"
]
}
或者根据实际目录结构写更精确的模式。
核心矛盾
text
sideEffects: false
↓
更激进 Tree Shaking
↓
JS 无用代码可以删除
但 CSS
↓
没有 JS 导出
↓
仍然可能影响页面
↓
不能被误判为无副作用
九、type: "module" 是什么?
json
{
"type": "module"
}
主要影响 Node.js 对 .js 文件的模块类型解释。
默认情况下:
text
.js
↓
CommonJS
设置:
json
{
"type": "module"
}
后:
text
.js
↓
ES Module
例如:
js
import fs from "node:fs";
如果需要 CommonJS,可以使用:
text
.cjs
而 ESM 也可以显式使用:
text
.mjs
一定要注意
type: "module" 不是"让浏览器支持 ESM"的开关 。它主要是 Node.js 的模块解析规则。
十、packageManager、engines、overrides 是什么?
1. packageManager
例如:
json
{
"packageManager": "pnpm@10.0.0"
}
用于声明项目推荐/使用的包管理器及版本。
配合团队工具,可以减少:
text
开发者 A → pnpm 9
开发者 B → pnpm 10
CI → npm
导致的环境差异。
2. engines
json
{
"engines": {
"node": ">=20",
"pnpm": ">=10"
}
}
表达:
项目要求什么运行环境。
如果希望包管理器严格执行,还需要结合对应包管理器的配置。
例如 npm 可以通过:
ini
engine-strict=true
控制是否严格按照 engines 检查。engines 本身并不等于"所有环境都会强制阻止安装"。
3. overrides
例如 npm:
json
{
"overrides": {
"some-package": "1.2.3"
}
}
用于统一控制依赖树中的某个版本。
典型场景:
text
A
└── vulnerable-package@1.0
B
└── vulnerable-package@1.1
↓ overrides
统一强制到
vulnerable-package@1.2.3
不同包管理器对应的字段不同:
text
npm → overrides
Yarn → resolutions
pnpm → overrides
不要把它们混成一个通用字段。
十一、发布时如何防止把源码、测试文件一起发布?
核心思路(一句话)
通过 files 控制 npm 包发布白名单,而不是依赖"发布时记得排除"。
例如:
json
{
"files": [
"dist",
"README.md",
"LICENSE"
]
}
最终:
text
npm package
├── dist
├── README.md
└── LICENSE
而不是:
text
src
test
coverage
配置文件
临时文件
...
private
json
{
"private": true
}
常用于:
- Monorepo 根项目
- 内部应用
- 不允许误发布到 npm 的项目
核心作用:
防止误发布。
十二、publishConfig 是什么?
用于控制发布相关配置,例如:
json
{
"publishConfig": {
"registry": "https://registry.example.com"
}
}
企业私有 npm 仓库常见:
text
开发安装
↓
公共/内部 registry
发布
↓
企业私有 registry
它解决的是:
"这个包发布到哪里、以什么发布配置发布?"
十三、Monorepo 中 package.json 怎么治理?
典型结构:
text
repo
├── package.json
├── pnpm-workspace.yaml
└── packages
├── ui
│ └── package.json
├── utils
│ └── package.json
└── hooks
└── package.json
核心治理:
text
workspace
↓
统一管理多个 package
packageManager
↓
统一包管理器版本
engines
↓
统一 Node.js 版本要求
overrides
↓
统一/强制依赖版本
CI
↓
检查版本漂移、锁文件、构建和发布
十四、面试官真正想考什么?
这道题的主要矛盾不是让你背几十个字段,而是看你能不能把字段和工程问题对应起来。
text
package.json
│
├── ① 模块入口
│ ├── exports
│ ├── main
│ ├── module
│ ├── browser
│ └── type
│
├── ② 依赖契约
│ ├── dependencies
│ ├── devDependencies
│ ├── peerDependencies
│ └── optionalDependencies
│
├── ③ 构建优化
│ └── sideEffects
│
├── ④ 环境约束
│ ├── engines
│ └── packageManager
│
├── ⑤ 依赖治理
│ └── overrides / resolutions
│
└── ⑥ 发布治理
├── files
├── private
└── publishConfig
次要矛盾
其他字段可以根据项目补充,但面试首先应该把:
exports+ 依赖分类 +peerDependencies+sideEffects+ 发布治理
讲清楚。
十五、满分答案(背这一版)
面试官:你对 package.json 配置了解多少?
package.json我主要从四个方面理解:模块入口、依赖契约、工程优化和发布治理。第一是模块入口。现代 Node.js 和打包工具更推荐通过
exports定义公共 API 和条件导出,例如通过import、require、types区分 ESM、CommonJS 和 TypeScript 类型入口,同时可以限制用户直接访问包内部路径。main是传统 CommonJS 入口,module更多是 ESM 的生态约定,browser用于浏览器环境适配。第二是依赖分类。
dependencies是运行时真正需要的依赖;devDependencies是开发、构建、测试依赖;peerDependencies表示由宿主环境提供、当前包只声明兼容范围,React、Vue 这类组件库依赖通常属于这一类;optionalDependencies用于可选运行能力。第三是工程优化。
sideEffects可以帮助打包器进行 Tree Shaking,但 CSS 等资源可能存在副作用,所以不能简单设置成全部无副作用。第四是工程治理。
engines和packageManager可以约束运行环境和包管理器版本,overrides或对应包管理器的版本锁定机制可以治理间接依赖,files控制最终发布内容,private防止内部项目误发布,publishConfig可以控制发布 registry 等配置。所以我不会把 package.json 理解成简单的依赖清单,而是把它看成 npm 包对外的模块解析、依赖关系和发布规则契约。