在 Node.js 项目中,package.json 并不仅仅是一个记录依赖的清单,它更像是整个模块的“身份证”和“配置中心”。无论是作为发布到 npm 的包,还是团队内部维护的服务,package.json 中的每一个字段都直接影响依赖安装、模块解析、脚本运行甚至发布行为。本节将按使用频率和重要程度,逐一拆解这些核心字段的真实含义和最佳实践。
15.2.1 必填与基础字段
name & version
{
"name": "@myorg/user-service",
"version": "2.1.3"
}
这两个字段是所有 npm 包的最基本标识符,合在一起构成了一个包的唯一性。name 支持作用域(如 @myorg/),用于组织私有包或避免名称冲突。版本号需要遵循语义化版本规范(semver),在发布时不能重复。即使项目不打算发布,这两个字段通常也都会保留,因为各种工具和脚本都可能依赖它们。
private
"private": true
当设为 true 时,npm 会拒绝发布这个包。企业内部的服务项目、Monorepo 根目录等不需要发到公共仓库的包,都应该加这个字段以防误操作。
description & keywords
描述性字段,npm 网站搜索和展示时会用到。虽然不改变运行时行为,但对于开源项目来说,清晰的描述和关键词是重要的文档入口。
15.2.2 入口与模块解析
main
"main": "./dist/index.js"
最初定义的是被 require('包名') 时的入口文件。如果包同时提供 CommonJS 和 ESM 输出,main 通常指向 CJS 版本,因为默认 require 遵循此字段。如果省略,默认为项目根目录下的 index.js。
module (非官方,但约定俗成)
"module": "./dist/index.esm.js"
这个字段并非 npm 官方规范,但被绝大多数打包工具(如 webpack、Rollup、Vite)识别。当包消费者使用 ES module 语法(import)时,构建工具会优先使用 module 字段指向的文件,通常它是一个 ESM 版本,有助于 Tree Shaking。
exports
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./helpers": "./dist/helpers.js"
}
exports 是 Node.js 12.7+ 引入的现代化入口映射方案,它比 main/browser/module 更强大且严格:
- 可以声明条件导出,比如区分
import和require、区分browser和node。 - 可以精确控制哪些子路径可以被外部访问,未在
exports中声明的路径将被 Node.js 直接拒绝,从而防止消费者直接require('your-package/src/internal.js')。 - 它替代了
main的大部分功能,当exports和main同时存在时,exports优先级更高。
实际项目如果同时需要支持 CJS 和 ESM,强烈建议使用 exports 明确各入口。
type
"type": "module"
当设置为 "module" 时,.js 文件将默认被 Node.js 视为 ES Module。如果希望继续使用 CommonJS,需要将文件后缀名显式改为 .cjs。这个字段影响整个包内所有 .js 文件的解析方式,因此从 CJS 迁移到 ESM 时需要特别小心。如果没有设置,默认为 "commonjs"。
browser
"browser": {
"./lib/server.js": "./lib/browser.js",
"fs": false
}
这个字段主要供打包工具(如 webpack、esbuild)在打包前端代码时使用,用来指定哪些模块需要替换为浏览器版本,或者哪些 Node.js 核心模块(如 fs)在浏览器端不可用需要标记为空。
15.2.3 脚本与生命周期
scripts
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "vitest",
"lint": "eslint src/"
}
scripts 是开发过程中使用频率最高的字段,通过 npm run <脚本名> 执行。npm 内置了一些生命周期钩子,可以自动触发:
pre<脚本名>和post<脚本名>会在脚本执行前后自动运行,例如prebuild、postbuild。- 特殊的
prepare脚本在npm install之后执行,常用于编译本地包(如husky的初始化)。 - 不要把复杂的多任务逻辑直接写在一条命令中,可以组合
&&,或者使用concurrently、npm-run-all等工具。
15.2.4 依赖管理字段
dependencies & devDependencies
"dependencies": {
"express": "^4.18.2",
"prisma": "^5.0.0"
},
"devDependencies": {
"typescript": "^5.3.0",
"vitest": "^1.0.0"
}
dependencies:生产环境下运行时必需的包,会在npm install时被安装。devDependencies:仅在本地开发和测试时需要的包(如测试框架、编译器、类型声明),当使用npm install --production或设置NODE_ENV=production时不会被安装。
区分两者的关键是运行时是否需要:Express 是运行时必需的,所以放 dependencies;TypeScript 和 Vitest 只有开发时用到,放 devDependencies。依赖的版本号前面通常会带前缀符号:
^:允许更新到向后兼容的次版本和补丁版本(推荐用于稳定第三方库)。~:只允许更新补丁版本。*或空:接受任何版本(不推荐)。- 精确版本:
"2.1.3",常用于内部协同包。
peerDependencies
"peerDependencies": {
"react": ">=18.0.0"
}
当你的包作为一个插件或扩展提供,而宿主项目已经安装了某个主库时,应使用 peerDependencies 声明对宿主库版本的兼容范围。npm 从 v7 开始会自动安装 peerDependencies,但在库开发中,仍建议不要将主框架放到 dependencies 里,以免造成多实例冲突。
optionalDependencies
"optionalDependencies": {
"fsevents": "^2.0.0"
}
这些依赖如果安装失败,不会导致整个 npm install 中止。通常用于平台相关的可选功能模块。
bundledDependencies
这是一个数组,列举的那些依赖会在发布时将实际的包文件打包进最终的 tgz 文件,避免用户安装时再次下载。用于离线分发或需要精确控制依赖时。
engines
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
}
声明项目所需的 Node.js 和 npm 最低版本。仅作为警告提示,不会自动安装对应版本,但可以配合 .nvmrc 和 CI 检查来确保执行环境一致。
15.2.5 发布与文件控制
files
"files": ["dist", "README.md", "bin"]
当包被发布到 npm 时,只有 files 字段列出的文件和目录会被上传。此外,以下文件始终会被包含:package.json、README、CHANGELOG、LICENSE。而 .gitignore 中列出的文件会被排除。为了安全起见,通常只在 files 中放入编译后的产物和必要文档,不要把源码、测试、配置文件暴露到公开发布的包中。
publishConfig
"publishConfig": {
"registry": "https://npm.pkg.github.com",
"access": "public"
}
在发布时覆盖一部分 package.json 的配置,指定私有仓库地址或包的访问级别(public 或 restricted)。非常适合企业内部仓库发布使用。
private
如之前所述,防止意外公开。
15.2.6 可执行文件与 CLI
bin
"bin": {
"my-cli": "./dist/cli.js"
}
声明后,当包被全局安装(npm install -g)时,npm 会创建一个同名的符号链接,指向指定的 JS 文件,该文件需要以 #!/usr/bin/env node 开头。对于本地安装的包,npx 可以执行它。bin 是开发命令行工具的核心字段。
15.2.7 工作空间与 Monorepo
workspaces
"workspaces": [
"packages/*",
"apps/*"
]
这个字段将当前项目标记为 Monorepo 的根目录,并声明哪些子目录中的包属于此工作空间。配合 npm、yarn 或 pnpm 的工作空间功能,可以实现依赖统一安装、跨包引用(例如 @myorg/ui 可以直接被 @myorg/app import),以及统一执行脚本。workspaces 是管理多包项目的基石。
15.2.8 其他常用字段
license
"license": "MIT"
声明包的许可证,方便使用者了解法律条款。开源社区中最常见的是 MIT。
repository
"repository": {
"type": "git",
"url": "https://github.com/user/repo.git"
}
指向代码仓库,npm 网站会展示此链接。
bugs
"bugs": {
"url": "https://github.com/user/repo/issues"
}
让报 Bug 更容易找对地方。
funding
展示赞助信息,npm 页面会显示赞助按钮。
overrides / resolutions (npm/yarn)
当你的依赖树中有某个子依赖存在安全漏洞或者需要强制使用特定版本时,可以用 overrides(npm)或 resolutions(yarn)来强制重写依赖版本。这是一种救急手段,应谨慎使用并尽量推动上游修复。
"overrides": {
"lodash": "4.17.21"
}
15.2.9 小结:一个完整项目的 package.json 骨架
下面是一个典型的 TypeScript 全栈项目(服务端为主)的 package.json 核心部分,展示上述字段如何组合:
{
"name": "my-api-service",
"version": "1.2.0",
"private": true,
"description": "用户中心微服务",
"main": "./dist/main.js",
"type": "module",
"engines": {
"node": ">=18"
},
"scripts": {
"dev": "tsx watch src/main.ts",
"build": "tsc",
"start": "node dist/main.js",
"test": "vitest",
"lint": "eslint src/"
},
"dependencies": {
"fastify": "^4.0.0",
"prisma": "^5.0.0",
"zod": "^3.0.0"
},
"devDependencies": {
"@types/node": "^20",
"typescript": "^5.3.0",
"tsx": "^4.0.0",
"vitest": "^1.0.0",
"eslint": "^8.0.0"
},
"bin": {
"user-cli": "./dist/cli.js"
},
"files": ["dist", "prisma/schema.prisma"]
}
package.json 虽然看起来只是一份声明文件,但它深刻地定义了项目如何被构建、安装、运行和发布。理解每个字段的实际作用,能够让开发者在自定义工具链、处理依赖冲突、配置多端入口时更加游刃有余。随着 Node.js 和 npm 的演进,一些新字段(如 exports)逐渐取代了老字段的职责,保持对这些变化的跟进同样重要。