先记住这个答案
exports 字段声明了一个包对外暴露的所有入口。有了它,别人导入 pkg 或 pkg/子路径 时,Node.js 只查这张表,没写进去的路径一律报 ERR_PACKAGE_PATH_NOT_EXPORTED,包的内部文件就被封装起来了。每个入口还可以写成条件对象,比如 import 条件给 ESM 文件、require 条件给 CommonJS 文件。Node.js 按对象里的书写顺序逐个检查条件,用第一个命中的,所以顺序很重要。
- 定义了
exports后,没列出的子路径都不能导入 - 条件按对象里的书写顺序匹配,第一个命中就用
default放最后兜底,types放最前面exports存在时优先于main
一个同时发布 ESM 和 CommonJS 的包
这是库作者最常写的配置,同时暴露了主入口、一个工具目录和 package.json 本身。
{
"name": "pkg",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"default": "./dist/index.mjs"
},
"./utils/*": "./dist/utils/*.mjs",
"./package.json": "./package.json"
}
}有了这份配置,import a from "pkg" 拿到 index.mjs,require("pkg") 拿到 index.cjs。import f from "pkg/utils/format" 会通过通配规则映射到 dist/utils/format.mjs。而 import x from "pkg/dist/internal.mjs" 会报 ERR_PACKAGE_PATH_NOT_EXPORTED,哪怕这个文件实际存在。
"./package.json" 那一行不是多余的。有些工具需要读取包的 package.json,没导出它就会读取失败。
条件为什么要讲顺序
Node.js 拿到条件对象后,从上往下看每个键,当前环境满足哪个条件就用哪个,找到第一个就停。它不会把所有条件放在一起比较优先级。
- 把
default写错位置 - 我实测过一份配置,把
"default"写在"require"前面,结果require("pkg")拿到的是default指向的 ESM 文件,require条件形同虚设。default在任何环境下都匹配,所以它只能放在最后兜底。 同样的道理,TypeScript 解析类型时要求types放在第一位,放在后面可能先被import条件截走,找不到类型声明。 常见的条件有这几个:import和require看导入方式,node表示 Node.js 环境,default兜底。browser、development、production这些是社区约定,打包器会识别,Node.js 里可以用--conditions=development这类参数启用自定义条件。
容易答错的地方
- 加上
exports以为只是多了一个选择 - 加上
exports是一个破坏性改动。以前用户可以随便导入pkg/lib/xxx.js,加上以后这些路径全部失效。所以很多库是在大版本升级时才加这个字段。 - 以为有了
exports还会看main - 在支持
exports的 Node.js 里,两者同时存在时只用exports。保留main只是为了照顾不认识exports的老工具。 - 双格式包的状态被复制两份
- 同一个包的
import和require入口是两个不同的文件,也就是两个模块实例。如果应用里一部分代码import它,另一部分require它,包里的单例、缓存就会有两份。这叫双包风险,常见的规避办法是让 ESM 入口只做一层包装,实际状态都放在 CommonJS 那份里。
面试官还会怎么问?
包内部怎么用简短路径引用自己的文件?
用 imports 字段,键必须以 # 开头,比如 "#utils/*": "./src/utils/*.js",包内代码写 import x from "#utils/format.js"。它只对包自己可见,外部导入不到。
打包器也认 exports 吗?
Webpack 5、Vite、Rollup 的 Node 解析插件都支持,而且会额外匹配 browser、module 等条件。条件的书写顺序在打包器里同样生效。
包能不能导入自己的公开路径?
可以,这叫自引用。包里写 import x from "pkg/utils/format",只要包定义了 exports,Node.js 就会按包自己的 exports 解析,和外部用户看到的结果一致。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。