package.json 详解
本教程共 76 篇 · 第 9 篇 · 更新于 2026-07-25 · 约 6 分钟阅读
9. package.json 详解
本节目标:逐字段讲清 package.json,重点 scripts、exports、type 和依赖字段。
package.json 是 Node.js 项目的身份证。它告诉 npm 这个项目叫什么、依赖谁、怎么启动、兼容哪个 Node.js 版本。可以没有 README,但绝不能没有 package.json。
这一章我们不浮于表面,把关键字段一个个拆开讲,顺带聊几个 v24 时代的新玩法。
基础字段:项目的名片
{
"name": "my-awesome-app",
"version": "1.0.0",
"description": "一个做某事的 Node.js 应用",
"main": "index.js",
"type": "module",
"license": "MIT"
}
name:包名,全小写,用连字符分隔。如果要发布到 npm,必须全局唯一。version:遵循语义化版本(SemVer),格式主版本.次版本.修订号。main:入口文件。别人require('your-pkg')或import 'your-pkg'时,Node.js 会加载这个文件。type:"module"表示默认按 ESM 解析.js;"commonjs"或不写,就是传统的 CommonJS。license:许可证。个人项目写MIT最省事,公司项目按法务要求来。
Tip
name如果以@开头,就是「作用域包(Scoped Package)」,比如@company/private-tool。作用域包可以私有化,适合企业内部使用。
语义化版本:^ 和 ~ 的区别
版本号不是随便写的。1.2.3 三段分别代表:
| 位置 | 含义 | 什么时候变 |
|---|---|---|
| 1(MAJOR) | 主版本 | API 不兼容的破坏性变更 |
| 2(MINOR) | 次版本 | 向后兼容的新功能 |
| 3(PATCH) | 修订号 | 向后兼容的 Bug 修复 |
package.json 里常看到 ^ 和 ~,它们控制更新的粒度:
{
"dependencies": {
"express": "^5.1.2", // 允许 5.x.x,但不允许 6.0.0
"lodash": "~4.17.21", // 只允许 4.17.x
"debug": "4.3.4" // 精确锁定 4.3.4
}
}
^:锁定主版本,次版本和修订号可以升。最常用,平衡了稳定性和新特性。~:锁定到次版本,只允许修订号升级。更保守。- 无前缀:精确版本,一点都不许动。
Warning生产环境的关键依赖,我建议精确写死版本号,或者至少用
~。^在升级时可能带入次版本的新功能,而新功能往往伴随着新 Bug。我有过凌晨被^自动升级搞崩服务的惨痛经历。
scripts 与生命周期钩子
scripts 是 package.json 里最高频的字段。除了自定义命令,npm 还内置了几个特殊脚本:
| 命令 | 对应的 script | 说明 |
|---|---|---|
npm start | start | 启动应用 |
npm test | test | 运行测试 |
npm restart | restart | 重启,默认顺序:stop → start |
npm stop | stop | 停止 |
其他脚本都要加 run,比如 npm run build。
生命周期钩子是 scripts 的隐藏玩法。以 build 为例,npm 会自动找 prebuild 和 postbuild:
{
"scripts": {
"prebuild": "npm run clean",
"build": "node build.js",
"postbuild": "npm run compress"
}
}
执行 npm run build,实际跑的是 prebuild → build → postbuild。这个约定省了你写冗长的命令链。
Note
prepare是个特殊钩子,在npm publish之前和npm install(不带参数)时自动执行。很多项目在这里放构建命令,确保发布的包是编译后的产物。
engines:划定 Node.js 版本边界
团队协作时,最怕有人用 Node.js v18 跑你的 v24 项目。engines 字段就是用来划定边界的:
{
"engines": {
"node": ">=20.0.0",
"npm": ">=10.0.0"
}
}
默认情况下,版本不匹配只会输出警告。如果希望强制报错,可以在项目根目录加 .npmrc:
engine-strict=true
这样同事用错 Node.js 版本时,npm install 会直接失败,从源头上杜绝环境差异。
exports:现代包的入口地图
前面章节提过 exports,这里展开讲讲。它是 main 字段的升级版,能精确控制不同导入方式的入口:
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"default": "./dist/index.mjs"
},
"./helpers": {
"import": "./dist/helpers.mjs",
"require": "./dist/helpers.cjs"
},
"./package.json": "./package.json"
}
}
"."是主入口,对应import 'your-pkg'。"./helpers"是子路径入口,对应import 'your-pkg/helpers'。"import"和"require"分别服务 ESM 和 CommonJS。"default"是兜底,当前面的条件都不匹配时走这里。
exports 还有一个隐藏好处:它限制了外部能访问的文件。没有声明在 exports 里的路径,用户无法导入。这相当于给包加了访问控制,避免内部实现细节被外部依赖。
files:控制发布到 npm 的内容
默认情况下,npm publish 会把项目目录里几乎所有文件都上传。通常你只想发布编译产物和必要文档,源码、测试、配置文件不用带上去。
控制方式有两种:
1. files 白名单
{
"files": [
"dist",
"README.md",
"LICENSE"
]
}
只发布 dist 目录和两个文档。
2. .npmignore 黑名单
在项目根目录放 .npmignore,写法同 .gitignore:
src/
tests/
.vscode/
*.log
Tip如果同时有
.gitignore和.npmignore,npm 会忽略.gitignore,只认.npmignore。如果你希望发布规则跟 Git 提交规则一致,可以建一个空的.npmignore,这样 npm 就继续读.gitignore了。
node —run:v24 的脚本新姿势
Node.js v22.13+ / v24 LTS 引入了一个新命令:node --run(简写 node -r)。它的作用和 npm run 类似,但更快。
node --run build # 等价于 npm run build
为什么更快?因为 node --run 跳过了 npm 的初始化过程,直接读取 package.json 里的 scripts 并执行。对于简单的脚本,启动速度能快几百毫秒。
不过它也有局限:不支持 pre/post 钩子,也不处理 npm 的环境变量注入。所以复杂流程还是老老实实 npm run,简单脚本可以尝鲜用 node --run。
Note
node --run自 v22.13.0(LTS)起稳定可用,v24 默认支持。如果你的项目要求兼容旧版本,还是用npm run更稳妥。
一个还算完整的 package.json 示例
把上面的知识点串起来,看一个实战配置:
{
"name": "@myorg/api-server",
"version": "2.1.0",
"description": "内部 API 服务",
"type": "module",
"main": "./dist/index.cjs",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"files": ["dist"],
"scripts": {
"dev": "node --watch src/index.js",
"build": "node build.js",
"start": "node dist/index.mjs",
"test": "node --test",
"lint": "eslint src/"
},
"dependencies": {
"express": "~5.1.0"
},
"devDependencies": {
"eslint": "^9.0.0"
},
"engines": {
"node": ">=22.0.0"
},
"license": "UNLICENSED",
"private": true
}
注意最后的 "private": true,它阻止你 accidentally 执行 npm publish。内部项目一定要加,我听说过太多把私有代码发到公网 npm 的事故了。