Node 项目的清单文件
每个 Node.js 项目的根目录下都有一个 package.json。它是一个普通的 JSON 文件,用来描述项目:名称、版本、依赖什么、提供哪些命令。npm 做任何事情时都会读取它。删掉它,npm 就完全不知道你的项目是什么了。
创建它最快的方式是 npm init:
npm init -y
-y 参数会跳过提示,接受默认值。你会得到类似这样的内容:
{
"name": "my-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
这只是一个最基本的骨架。其中大多数字段单独来看作用不大,等你添加了依赖和脚本,它们才变得有用。
dependencies 与 devDependencies
几乎所有的重活都由两个字段承担:dependencies 和 devDependencies。两者都是从包名到版本范围的映射。
区分它们只有一个原因:dependencies 是代码运行时需要的包。devDependencies 是只在开发时需要的包:测试运行器、代码检查工具、构建工具、类型检查器。当别人把你的包作为他们的依赖安装时,npm 会下载你的 dependencies,跳过你的 devDependencies。
npm 会自动更新这些字段。npm install express 会在 dependencies 中添加一行,npm install --save-dev vitest 会在 devDependencies 中添加一行。你很少需要手动编辑它们。
版本范围:^、~ 与确切版本
像 ^4.19.0 这样的版本字符串并不是确切的版本,而是范围。npm 遵循 semver(语义化版本),它把版本分成 MAJOR.MINOR.PATCH 三部分:
- MAJOR(主版本)升级会破坏向后兼容。
- MINOR(次版本)升级会增加功能,但不会破坏任何东西。
- PATCH(补丁版本)升级用来修复 bug。
你会到处见到的两个运算符:
"express": "^4.19.0" // >= 4.19.0 and < 5.0.0 (any 4.x.x at or above 4.19.0)
"express": "~4.19.0" // >= 4.19.0 and < 4.20.0 (any 4.19.x at or above 4.19.0)
"express": "4.19.0" // exactly 4.19.0
^ 是 npm 安装包时使用的默认符号。它相信次版本和补丁版本的升级会保持兼容。~ 更保守,只接受补丁版本的更新。单独的版本号则会精确锁定。
问题在于:“我刚刚安装的版本”和“范围允许的版本”不是一回事。如果你今天安装了 express@4.19.0,一个月后队友安装你的项目时,^4.19.0 可能会解析成 4.19.5。这就是 package-lock.json 的作用:它记录了实际解析出的确切版本,让每个人都得到同样的依赖树。记得提交它。
scripts:项目的命令入口
scripts 字段用来为常用命令定义快捷方式。放在里面的任何命令都可以用 npm run <name> 运行:
关于脚本,有几点需要了解:
npm start、npm test以及少数几个其他名称不需要run关键字。其他脚本都需要用npm run <name>。- 脚本在一个 shell 中执行,
node_modules/.bin会加入PATH,所以你可以直接调用已安装包中的可执行文件。即使vitest没有全局安装,"test": "vitest"也能运行。 - 脚本可以串联起来:
"build": "npm run lint && npm run compile"。用&&表示“按顺序运行,失败就停止”。 pre<name>和post<name>脚本会自动运行。如果你有prebuild,它会在build之前运行,不需要额外配置。
脚本就是项目的命令入口。一个好的 package.json 意味着新的贡献者可以克隆仓库,运行 npm install,然后运行 npm run dev / npm test,无需阅读任何 wiki。
入口:main、exports 与 type
这些字段告诉 Node(以及打包器)如何加载你的包。
type决定.js文件如何解析。"module"表示 ESM(import/export)。省略它或设为"commonjs"则表示 CommonJS(require)。完整的说明见 CommonJS 与 ESM 那一篇文档。main是旧式的入口,也就是require("my-lib")解析到的文件。较老的工具仍然会使用它。exports是现代的、更严格的替代方案。它精确规定使用者可以导入哪些文件,以及对应的子路径。没有列在这里的文件,导入时会失败,这是特性,不是 bug。公开的 API 由你掌控。
如果你只是在构建一个应用(而不是发布一个包),这几个字段里你大概只需要关心 type。
一个贴近实际的 package.json
把以上内容结合起来,一个小型 Node 应用的 package.json 实际上大致是这样的:
注意 engines.node。它只是建议性的:如果用户的 Node 版本不匹配,npm 会发出警告(开启 engine-strict 时会报错)。对于任何要发布的东西,这都是好习惯。
值得了解的字段
你还会遇到这些字段:
private: true:防止你不小心把包发布到 npm。任何不打算发布的项目都应该设置它。license:SPDX 标识符,比如"MIT"或"ISC"。对任何公开的项目都很重要。repository、bugs、homepage:显示在 npm 仓库的包页面上。bin:如果你的包附带命令行工具,在这里把命令名映射到脚本文件。安装之后,这些命令就可以运行了。workspaces:用于 monorepo;告诉 npm 把子目录当作链接在一起的包来处理。
这些字段你不需要全部用上,只需要用对适合你当前工作的那些。
常见陷阱
几个经常让人犯错的地方:
- 提交
node_modules。不要这样做。把它加入.gitignore。有了package.json和package-lock.json,任何人都可以用npm install重新生成它。 - 不提交
package-lock.json。一定要提交它。没有锁文件,“在我的电脑上能运行”就成了真实存在的问题,因为 semver 范围会随着时间解析成不同的版本。 - 把运行时依赖放进
devDependencies。你的应用在本地可能能运行,因为本地装了开发依赖,但到了跳过它们的生产环境就会出错。只要你发布的代码用到了它,它就属于dependencies。 - 手动修改版本却不重新安装。修改
package.json中的版本后要运行npm install,否则node_modules和锁文件就会不同步。
下一章:Node 运行时
package.json 告诉 Node 你的项目是什么。Node 运行时则决定它如何运行:模块解析、内置模块、全局对象,以及底层的事件循环。这就是下一页的内容。
常见问题
package.json 是用来做什么的?
它是 Node.js 项目的清单文件。它记录项目的名称和版本、依赖哪些包、可以用 npm run 运行哪些脚本,以及入口文件和模块类型等元数据。npm install 会读取它来决定下载什么。
dependencies 和 devDependencies 有什么区别?
dependencies 是代码在运行时需要的包,比如 express 或 react。devDependencies 只在开发或构建时需要,比如测试运行器、打包器和代码检查工具。当别人把你的包作为他们的依赖安装时,npm 会跳过你的 devDependencies。
package.json 版本号中的 ^ 和 ~ 是什么意思?
它们是 semver 版本范围运算符。^1.2.3 允许任何不低于 1.2.3 的 1.x.x 版本(主版本相同)。~1.2.3 更严格,只允许不低于 1.2.3 的 1.2.x 版本(次版本相同)。单独写 1.2.3 会锁定确切的版本。package-lock.json 会记录最终解析出的确切版本,让每次安装都能复现。
如何创建 package.json 文件?
在一个空目录中运行 npm init 并回答提示的问题,或者运行 npm init -y 接受默认值,立刻得到一个文件。你也可以手动编写,它就是一个 JSON 文件。真正必需的字段只有 name 和 version。