npm:package.json 详解
package.json 是 Node.js 项目和 npm 包的清单:写清身份、入口、脚本和依赖。它必须是 纯 JSON(不能是带注释的 JavaScript 对象字面量)。npm install 根据它和 lockfile 装依赖。
不打算发布的应用也可以没有严格的 name、version;要发布到 registry,这两个字段是必填的,二者合起来构成包的唯一标识。
创建 package.json
有两种方式:
- 手动创建:在项目根目录新建
package.json,写入 JSON。 - 自动创建:在项目根目录执行
npm init,按提示填写;npm init -y(或--yes)用默认值直接生成。
一个简单示例:
{
"name": "project-name",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
package.json 配置说明
name
包名称。要发布则必填,否则可选。
规则:
- 长度不超过 214 个字符(含范围包的 scope,如
@babel/core里的@babel); - 范围包的名称可以以
.或_开头,非范围包不行; - 新包名称 不得包含大写字母;
- 名称会进入 URL、命令行参数和文件夹名,因此不能包含非 URL 安全字符。
不要和 Node.js 核心模块重名。范围包写作 @scope/name。
version
包版本号。要发布则必填,否则可选。必须是 node-semver 能解析的字符串。
标准版本号是 X.Y.Z,均为非负整数,禁止数字前方补零:
- X 主版本号(major):不向后兼容的重大变更。
- Y 次版本号(minor):向后兼容的功能增加。
- Z 修订号(patch):向后兼容的缺陷修复。
不稳定、尚未承诺兼容时,在标准版本号后用 - 接预发布标识,标识之间用 . 分隔(可再加 + 编译信息):
- 内部版本(alpha),例如
2.0.0-alpha.1+8ad8as.20220129 - 公测版本(beta),例如
2.0.0-beta.1 - 正式版候选(rc,Release Candidate),例如
2.0.0-rc.1
查看 registry 上某包的版本:
npm view <package-name> version
npm view <package-name> versions
description
一段描述,字符串。便于用户理解,也参与 npm search。
keywords
关键字,字符串数组。同样便于搜索发现。
homepage
项目主页 URL。npm docs 会打开文档、主页(不是仓库)。
{
"homepage": "https://github.com/owner/project#readme"
}
bugs
缺陷跟踪的 URL 和(或)报告问题的邮箱。
{
"bugs": {
"url": "https://github.com/owner/project/issues",
"email": "project@hostname.com"
}
}
可以只给其中一项。只提供 URL 时,值可以直接是字符串:
{
"bugs": "https://github.com/owner/project/issues"
}
在包目录执行 npm bugs 会在浏览器打开该 URL;没有 URL 则打开该包在 npmjs.com 上的页面。
license
许可证,让使用者知道权利和限制。常用 SPDX 标识:https://spdx.org/licenses/。多许可证可用 SPDX 表达式,例如 (ISC OR MIT)。
{
"license": "ISC"
}
ISC:允许出于任何目的使用、复制、修改和分发,是否收费均可,条件是副本中保留版权声明和本许可声明。
{
"license": "UNLICENSED"
}
UNLICENSED(注意拼写,不是 Unlicensed):不在任何条款下授权他人使用该私有或未发布包。自定义正文可用 "SEE LICENSE IN <filename>"。
旧的 license 对象、licenses 数组写法已废弃,改用 SPDX 字符串。
author、contributors
author 是一个人,contributors 是一组人。字段结构相同:author 是对象,contributors 是对象数组。email、url 可选。
{
"name": "Barney Rubble",
"email": "b@rubble.com",
"url": "http://barnyrubble.tumblr.com/"
}
也可写成一行:
{
"author": "Barney Rubble <b@rubble.com> (http://barnyrubble.tumblr.com/)"
}
funding
包的资助方式:对象、URL 字符串或数组。
{
"funding": {
"type": "individual",
"url": "http://example.com/donate"
}
}
{
"funding": "http://example.com/donate"
}
{
"funding": [
{
"type": "individual",
"url": "http://example.com/donate"
},
"http://example.com/donateAlso",
{
"type": "patreon",
"url": "https://www.patreon.com/my-account"
}
]
}
npm fund 列出当前项目依赖树里的资助信息;npm fund <package-name> 看某一个包(多个 URL 时列出第一个)。
files
作为依赖安装时,tarball 要包含 哪些路径:文件、目录或 glob(*、**/* 等)。语法像 .gitignore,含义相反:这里是白名单。省略该字段时,效果等同 ["*"](再减去忽略规则),即默认打包几乎全部文件。
也可在根目录或子目录放 .npmignore:
- 没有
.npmignore时,才用.gitignore当忽略清单。 - 两者都有 时,打包以
.npmignore为准,不是「有.gitignore就忽略.npmignore」。 - 根目录的
.npmignore不会覆盖files白名单;子目录里的.npmignore仍会排除该子树中、即使已被files选中的文件。
根级 files 列出的文件,不能靠根目录的 .npmignore、.gitignore 再排除掉。
无论怎样配置,下列文件 始终打包:package.json、README、LICENSE、LICENCE、main 指定的文件、bin 指定的文件。README、LICENSE 的大小写和扩展名不限。
下列路径 默认不进包(其中一部分即使写进 files 也带不走):.git、CVS、.svn、.hg、.lock-wscript、.wafpickle-N、.*.swp、.DS_Store、._*、npm-debug.log、.npmrc、node_modules、config.gypi、*.orig、package-lock.json、pnpm-lock.yaml、yarn.lock、bun.lockb。
.git、.npmrc、node_modules 以及各类 lockfile 无法靠 files 强行打进去。若发布时需要锁版本,用 npm-shrinkwrap.json 代替 package-lock.json。
main
CommonJS 主入口。包名叫 project-name 时,require("project-name") 得到该文件的 exports。路径相对包根目录,例如 dist/index.js。未设置则默认根目录 index.js。
type
告诉 Node.js 如何解释本包里的 .js 文件。npm 自己不用这个字段。
"commonjs"(默认):.js当 CommonJS。"module":.js当 ESM。.cjs、.mjs仍按后缀强制。
exports
现行主入口字段,用来代替只写一个 main。可以声明多个入口、按 import、require、node、default 等条件导出,并且 没写进 exports 的路径默认不能从包外引用。
{
"type": "module",
"main": "./dist/index.cjs",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./package.json": "./package.json"
}
}
同时写了 exports 和 main 时,Node.js 解析入口以 exports 为准。细节见 Node.js 的 包入口。
imports
包 内部 的子路径映射,键必须以 # 开头,只在本包代码里生效,不会开放给使用者。
{
"imports": {
"#utils": "./src/utils.js"
}
}
browser
类似 main,但面向客户端(浏览器)。打包工具会读它。写在这里等于提示:这份入口会依赖 window 等 Node.js 里没有的 API。Node.js 运行时本身不按 browser 解析。
bin
.js 可以用 node 执行:
/* bin_test.js */
console.log('abcd');
在该文件所在目录执行 node bin_test.js 会打印 abcd。每次都要拼完整路径,命令行工具就不方便。
bin 把「指令名」映射到可执行文件:
- 全局安装时,Unix 会链到全局 bin 目录(常见
/usr/local/bin,用了 nvm 则在对应 Node 前缀下);Windows 会在全局前缀生成.cmd(常见%AppData%\npm\command-name.cmd)。 - 作为依赖装进项目时,会在该项目的
node_modules/.bin生成同名命令。npm run、npx会自动把这个目录加进 PATH。也可直接跑./node_modules/.bin/command-name。
{
"bin": {
"command-name": "./bin/index"
}
}
全局安装 npm i my-program -g 之后,终端输入 command-name 即执行该文件。
只有一个可执行文件且命令名等于包名时,可以写成字符串:
{
"name": "my-program",
"version": "1.2.5",
"bin": "./path/to/program"
}
等价于:
{
"name": "my-program",
"version": "1.2.5",
"bin": {
"my-program": "./path/to/program"
}
}
Unix 上请保证这些文件首行是 #!/usr/bin/env node,否则系统不会用 Node 解释。Windows 的 .cmd 垫片不依赖 shebang,但跨平台包仍应写上。
#!在 Unix 里叫 shebang。普通文件以它开头时,内核按后面的解释器执行。#在不少脚本里是注释,解释器会跳过这一行。
/usr/bin/env node:在PATH里找名为node的解释器,避免写死/usr/bin/node。
man
man 是 manual 的简写,给 man 程序提供帮助页。值为一个文件或多个文件名组成的数组。
只指定一个文件时,无论文件名是什么,最终命令都是包名:
{
"name": "my-program",
"man": "./man/doc.1"
}
实际执行的是 man my-program。
文件名不以包名开头时,包名会作为前缀:
{
"name": "my-program",
"man": [
"./man/my-program.1",
"./man/othername.1"
]
}
对应 man my-program 和 man my-program-othername。
man 文件必须以数字结尾,压缩后可加 .gz。数字表示安装到 man 的哪一节:
{
"name": "my-program",
"man": [
"./man/my-program.1",
"./man/my-program.2"
]
}
对应 man my-program 和 man 2 my-program。
directories
为 bin、doc、lib、man 等标明目录。directories.bin、directories.man 会被 npm 用来展开该目录下的文件;doc、lib 主要是元数据。
{
"directories": {
"bin": "./bin",
"doc": "./doc",
"lib": "./lib",
"man": "./man"
}
}
不要同时写一个具体的 bin 文件路径和 directories.bin。单个文件用 bin;整目录用 directories.bin。
repository
源码仓库地址。GitHub 上的仓库可用 npm repo 打开(现行文档不再把这件事记在 npm docs 上;npm docs 打开的是 homepage、文档)。
{
"repository": {
"type": "git",
"url": "git+https://github.com/npm/cli.git"
}
}
url 应是可以直接交给 Git 的地址,不要填浏览器里的 HTML 项目页。发布时 npm 会把简写规范化成对象;新包建议直接写对象,避免 publish 警告。
GitHub、gist、Bitbucket、GitLab 也可以用和 npm install 相同的简写(下列是几种写法,不是同一个 JSON 里的重复键):
npm/npm
github:user/repo
gist:11081aaa281
bitbucket:user/repo
gitlab:user/repo
package.json 不在仓库根目录时(monorepo 子包),用 directory 指出所在目录:
{
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",
"directory": "packages/react-dom"
}
}
scripts
内置生命周期脚本、以及任意自定义脚本。用 npm run-script 或 npm run 执行。名为 preX、postX 的脚本会夹在 X 前后跑(如 precompress、compress、postcompress)。依赖包里的脚本可用 npm explore <pkg> -- npm run <script>。
{
"scripts": {
"precompress": "echo before",
"compress": "echo compress",
"postcompress": "echo after"
}
}
运行 npm run compress 时,这三个脚本都会执行。
npm start、npm test、npm stop、npm restart 是少数不必写 run 的内置命令,同样走 pre、post。
只在特定情况下触发的生命周期
- prepare(npm 4 起)
npm publish、npm pack打包之前;- 在包根目录执行 不带包名 的
npm install(可带--omit=dev等旗标;npm install express这种加包名的不会跑); - 通过 git URL 安装、且该包声明了
prepare时:会装它的dependencies和devDependencies,并在打包、安装时执行prepare; - 从 npm 7 起这些脚本默认在后台跑,要看输出加
--foreground-scripts。
- prepublish:已不推荐。历史原因是
npm install也会跑它,名字容易误解。新脚本用prepare或prepublishOnly。 - prepublishOnly:只在
npm publish时、准备打包之前跑。 - prepack:打 tarball 之前(
npm pack、npm publish、安装 git 依赖时)。注意npm run pack只是你自定义的脚本名,和 CLI 的npm pack不是一回事。 - postpack:生成压缩包之后、挪到最终位置之前(
publish不会把 tarball 留在本地)。
常见命令的生命周期顺序
- npm cache add、npm diff:
prepare - npm ci、npm install(无包名):
preinstall→install→postinstall→prepublish→preprepare→prepare→postprepare - npm pack:
prepack→prepare→postpack - npm publish:
prepublishOnly→prepack→prepare→postpack→publish→postpublish - npm rebuild:
preinstall→install→postinstall→prepare - npm start、npm stop、npm test、npm restart:
pre<名称>→<名称>→post<名称> - npm run \<自定义脚本>:
pre<名称>→<名称>→post<名称>
prepublish 出现在 install 流程里,不会出现在 publish 流程里。不要把两条链混成「prepare 夹在 prepublish 和 prepublishOnly 之间」这一句话。
npm 7 起 不再 提供 uninstall 生命周期脚本。
在当前包下调用依赖里的可执行文件
{
"name": "foo",
"dependencies": {
"bar": "0.1.x"
},
"scripts": {
"start": "bar ./test"
}
}
npm start 会执行 bar ./test。npm install 之后,依赖的 bin 会出现在 node_modules/.bin,npm run 能直接叫到命令名。
package.json 变量
package.json 顶层字段会加上 npm_package_ 前缀进入环境变量,嵌套键用 _ 连接:
/* var.js */
console.log(process.env.npm_package_name, process.env.npm_package_version);
console.log(process.env.npm_package_scripts_test);
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"var": "node ./var.js"
}
}
运行 npm run var 查看输出。
退出
脚本交给 sh(Windows 上是 cmd,可用 script-shell 改)。退出码非 0 会中止后续流程。
脚本不必是 Node.js,但必须是某种可执行文件。
config
给包脚本用的一组参数:
{
"name": "foo",
"config": {
"port": "8080"
}
}
脚本里读环境变量 npm_package_config_port。用户也可用 npm config set foo:port 80 覆盖(foo 是包名)。
dependencies(项目依赖)
当前包运行所需的依赖,是「包名 → 版本范围或 URL」的对象。版本范围遵循 semver,多个范围用空格分隔。
不要把测试、构建、仅开发期用的包装进这里,那些放 devDependencies。
指定版本范围
- version: 完全匹配
- >version: 大于 version
- >=version: 大于等于 version
- <version: 小于 version
- <=version: 小于等于 version
- ~version: 若写了次版本,则只允许补丁级变动;若只写到主版本,则允许次版本变动
~1.2.3 等价于 >=1.2.3 <1.3.0-0
~1.2 等价于 >=1.2.0 <1.3.0-0 同 1.2.x
~1 等价于 >=1.0.0 <2.0.0-0 同 1.x
~0.2 等价于 >=0.2.0 <0.3.0-0 同 0.2.x
~1.2.3-beta.2 等价于 >=1.2.3-beta.2 <1.3.0-0
只允许同一补丁号下、预发布 >=beta.2 的版本,如 1.2.3-beta.4;1.2.4-beta.2 不行
- ^version: 不允许改动最左边那个 非零 数字。因此
^1.2.3不跨主版本;^0.2.3不跨次版本;^0.0.3只允许补丁(再往下的预发布规则见下)
^1.2.3 等价于 >=1.2.3 <2.0.0-0
^0.2.3 等价于 >=0.2.3 <0.3.0-0
^0.0.3 等价于 >=0.0.3 <0.0.4-0
^1.2.3-beta.2 等价于 >=1.2.3-beta.2 <2.0.0-0
^0.0.3-beta 等价于 >=0.0.3-beta <0.0.4-0
^1.x 等价于 >=1.0.0 <2.0.0-0
^0.x 等价于 >=0.0.0 <1.0.0-0
- 1.2.x: 1.2.0、1.2.1 等,不含 1.3.0
- version1 - version2: 等价于
>=version1 <=version2 - range1 || range2: 满足其一即可
- tag: 发行标签,如
latest - * 或空字符串: 匹配任何版本
- tarball URL、Git URL、GitHub 简写、本地路径
tarball URL
安装时下载该压缩包:
{
"dependencies": {
"asd": "http://asdf.com/asdf.tar.gz"
}
}
Git URL
git+ssh://git@github.com:npm/cli.git#v1.0.27
git+ssh://git@github.com:npm/cli#semver:^5.0
git+https://isaacs@github.com/npm/cli.git
git://github.com/npm/cli.git#v1.0.27
GitHub URL
expressjs/express
mochajs/mocha#4727d357ea
user/repo#feature/branch
本地路径
../foo/bar
~/foo/bar
./foo/bar
/foo/bar
npm install -S ../foo/bar 会把路径规范成相对路径写进 package.json(npm 5 起 -S、--save 已是默认,写不写效果相同):
{
"dependencies": {
"bar": "file:../foo/bar"
}
}
适合本地联调尚未发布的包。
devDependencies(开发依赖)
开发期依赖,写法与 dependencies 相同,放测试工具、编译器、打包器等。
别人把你的包装进他们的项目时(npm install your-program),只会装你的 dependencies,不会装 devDependencies。
若他们克隆了你的仓库,在仓库根目录执行 npm install,则会把 dependencies 和 devDependencies 都装上,以便开发和测试。生产或 CI 若只要运行时依赖,用 npm install --omit=dev(旧旗标 --production 仍可用)。
peerDependencies(对等依赖)
表达与 宿主 的关系:插件 A1 必须在包 A 的环境下跑,则 A 是宿主,A1 把 A 写在 peerDependencies。
{
"name": "tea-latte",
"version": "1.3.5",
"peerDependencies": {
"tea": "2.x"
}
}
含义:我只在宿主 tea 的 2.x 下工作;请保证安装我时,tea 也在同一层 node_modules,而不是嵌在 tea-latte 里面。
├── tea-latte@1.3.5
└── tea@2.2.0
当前树里已有 tea 但版本对不上 2.x 时,再 npm install tea-latte 会失败(npm 7+)或警告(npm 3-6)。
自动安装行为:
- npm 1、2:会自动装 peer。
- npm 3-6:不自动装,只警告。
- npm 7 起:默认再自动装。树里已有冲突版本则安装失败。临时可用
--legacy-peer-deps回到 3-6 那种不强制装 peer 的行为。
插件的 peer 范围尽量宽,不要锁死到某一个补丁号,否则宿主稍一升级就会和别的插件冲突。
peerDependenciesMeta
为 peer 补充元数据。把某项标成 optional: true 后,用户没装这个 peer 时 npm 不再警告(该项仍应出现在 peerDependencies 里)。
{
"name": "tea-latte",
"version": "1.3.5",
"peerDependencies": {
"tea": "2.x",
"soy-milk": "1.2"
},
"peerDependenciesMeta": {
"soy-milk": {
"optional": true
}
}
}
bundledDependencies(捆绑依赖)
官方字段名是 bundleDependencies,bundledDependencies 同样认。值为包名数组(版本以 dependencies 里的为准),也可为 true(捆全部依赖)或 false。
本地要保留这些包装进 tarball、或只分发一个文件时,列出包名后执行 npm pack。
{
"name": "awesome-web-framework",
"version": "1.0.0",
"bundledDependencies": [
"renderized",
"super-streams"
]
}
npm pack 得到 awesome-web-framework-1.0.0.tgz,内含 renderized、super-streams。再用 npm install awesome-web-framework-1.0.0.tgz 装到新项目。
optionalDependencies(可选依赖)
这些依赖装不上或找不到时,npm 继续,不把整次安装判失败。使用方要自己做好缺失时的分支。
let foo = null;
let fooVersion;
try {
foo = require('foo');
fooVersion = require('foo/package.json').version;
} catch (er) {
foo = null;
}
if (notGoodFooVersion(fooVersion)) {
foo = null;
}
if (foo) {
foo.doFooThings();
}
npm install --no-optional 会跳过它们。
optionalDependencies 会覆盖 dependencies 里的同名项,通常只在一处声明。
overrides
只在 项目根(workspace 根)的 package.json 生效,用来把依赖树里某个包换成指定版本或另一个包。嵌套可深可浅。
无论直接依赖声明了什么,都让 foo 装成 1.0.0:
{
"overrides": {
"foo": "1.0.0"
}
}
完整对象可以同时改包自身和它的子依赖。"." 表示 foo 自己:
{
"overrides": {
"foo": {
".": "1.0.0",
"bar": "1.0.0"
}
}
}
只改子孙里的 foo:
{
"overrides": {
"bar": {
"foo": "1.0.0"
}
}
}
也可以按版本选择再嵌套:
{
"overrides": {
"baz@2.0.0": {
"bar": {
"foo": "1.0.0"
}
}
}
}
直接依赖的范围和 override 对不上会报 EOVERRIDE。不要把 "foo": "^1.0.0" 的直接依赖覆盖成 ^2.0.0。范围一致则允许;更稳妥的是引用自身声明,不必再对一次版本:
{
"dependencies": {
"foo": "^1.0.0"
},
"overrides": {
"foo": "$foo"
}
}
overrides 改的是「已经声明的那条边解析到哪个版本」。若上游 package.json 漏写了依赖,要用根上的 packageExtensions(npm 较新版本)去补元数据,而不是用 overrides 硬塞一个不存在的边。该字段同样只认仓库根,非 private 的包若带上它,npm 会拒绝发布。
engines(引擎)
声明适用的 Node.js 或 npm 版本,默认 *(不限制):
{
"engines": {
"node": ">=18.0.0 <25",
"npm": ">=10"
}
}
除非用户设置了 engine-strict(npm config set engine-strict true),否则这只是建议:作为依赖安装时最多警告,不会阻止安装。
devEngines(较新的 npm)形状不同,用来约束 正在开发这份源码 的人用哪套 runtime、包管理器,在 install、ci、run 前检查;和面向「安装你这个包的用户」的 engines 不是一回事。
os
允许或禁止的操作系统。值来自 process.platform。! 表示排除。
{
"os": [
"darwin",
"linux",
"!win32"
]
}
cpu
允许或禁止的 CPU 架构。值来自 process.arch。
{
"cpu": [
"x64",
"ia32",
"!arm",
"!mips"
]
}
Linux 上若还要限制 C 库,可用 libc(如 "glibc"、"musl"),且通常配合 "os": "linux"。
private
"private": true 时,npm publish 会拒绝。用来防止把私有项目发到公共 registry。
若允许发布、但只准发到指定仓库(如内网),用下面的 publishConfig.registry。
publishConfig
发布时使用的配置,常见 tag、registry、access。范围包默认 restricted,要发到公共源需 "access": "public"。
可覆盖的项见 npm 的 config 文档。
workspaces
工作区路径或 glob。每个工作区目录里要有有效的 package.json,安装时会链到顶层 node_modules。
{
"name": "workspace-example",
"workspaces": [
"./packages/*"
]
}
packageManager
给 Corepack 看的包管理器及精确版本,例如 "packageManager": "npm@10.9.2"。团队用来钉死 npm、pnpm、Yarn 的版本,避免各人 CLI 不一致。
默认值
npm 会根据包内容补一些默认:
"scripts": { "start": "node server.js" }
根目录有server.js且未定义start时。"scripts": { "install": "node-gyp rebuild" }
根目录有binding.gyp,且没有定义install或preinstall时。"contributors": [...]
根目录有AUTHORS时,每行按Name <email> (url)解析,email 和 url 可选。以#开头或空行忽略。
常见问题
npm ci 和 npm install 的区别
npm ci 面向自动化:测试、CI、部署,或你要一次不掺杂质的全新安装。有 lockfile、且 node_modules 为空或不存在时,通常比反复决策版本的 npm install 更快。
主要区别:
- 必须已有
package-lock.json或npm-shrinkwrap.json,否则npm ci不能用; - lockfile 与
package.json对不上就 退出报错,不会去改 lockfile; - 一次只装整个项目,不能顺便
npm ci lodash这种加单个包; - 开始前会删除
node_modules; - 不改
package.json和 lockfile。
持续集成和生产发布优先 npm ci:按 lockfile 里的精确版本和 integrity 安装,避免「我这机能装、流水线对不上」。
dependencies 和 devDependencies 的区别
dependencies: 运行或发布后仍需要的包,例如框架 Vue、UI 组件库。npm install <packageName>(可写 -S、--save,npm 5 起已是默认)写进这里。不写版本则按 registry 当时符合默认保存规则的版本写入(通常是带 ^ 的当前最新)。指定版本写成 一个 参数:npm install vue@3.0.1。写成 vue @3.0.1 会被当成两个包名。
devDependencies: 只在开发期需要,例如 webpack、babel、测试框架。npm install <packageName> -D。
业务应用里执行
npm install时两类都会下载。会不会进最终产物,取决于打包器有没有分析到你的源码import、require了它,不是看它写在哪一个字段。发布 npm 包时:别人安装你的包,只会带上你的
dependencies(以及 peer 等),不会带上你的devDependencies。
package.json 和 package-lock.json 的区别
package.json 写的是 允许的范围(如 ^1.2.3)。package-lock.json 写的是某一次解析得到的 整棵精确树(版本、完整性哈希、下载地址)。删除 node_modules 再 npm install 时,有 lockfile 就能复现同一棵树,也少打一些「再选一遍最新补丁」的决策。lockfile 应提交进版本库;发布到 registry 的 tarball 默认 不含 package-lock.json。