面试题:你对 package.json 了解多少?

一、核心思路(一句话)

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 Module
  • require:CommonJS
  • default:兜底条件

为什么要把 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 包对外的模块解析、依赖关系和发布规则契约。

相关推荐
じ星不离月か5 个月前
package.json中版本号前^和~的区别
版本号·package.json
RFCEO6 个月前
JavaScript基础课程十九、前端工程化基础(npm + Vite)
package.json·javascript前端课程·前端工程化npm基础命令大全·vite创建原生js项目教程·npm安装与卸载依赖包方法·vite启动与打包项目命令·es6模块化在vite中使用
全栈前端老曹9 个月前
【包管理】read-pkg-up 快速上手教程 - 读取最近的 package.json 文件
前端·javascript·npm·node.js·json·nrm·package.json
big tail1 年前
项目依赖版本修改
npm·pnpm·react·yarn·依赖·package.json
高端客户2 年前
package,json 文件中依赖包的说明
npm·file·link·package.json·本地依赖包·版本标识符
wlym1232 年前
npm 中的 package.json 实践
前端·npm·package.json
wlym1232 年前
npm-run-all 使用实践
前端·npm·package.json
仙魁XAN2 年前
Unity 功能 之 创建 【Unity Package】 Manager 自己自定义管理的包的简单整理
unity·游戏引擎·ump·package.json·unitypackage
呀呀夫斯基2 年前
从零开始的<vue2项目脚手架>搭建:vite+vue2+eslint
package.json·npm init