第2章:npm核心概念:package.json文件详解、语义化版本控制(SemVer)、依赖类型

好,咱们直接进入正题。package.json 这玩意儿,说白了就是你的项目身份证加购物清单。我见过不少新人,上来就 npm init -y 一把梭,然后啥也不管了。嗯,这样迟早要踩坑。

2.1 package.json 文件详解

每个 Node.js 项目根目录下,几乎都有这个文件。它记录了项目的元数据、脚本、依赖等信息。我个人习惯,创建项目后第一件事就是打开它,把 nameversiondescription 这些字段填完整。

核心字段一览

字段 说明 是否必填
name 项目名称,发布到 npm 时需要唯一
version 当前版本号,遵循 SemVer 规范
description 项目描述,方便别人理解 推荐
main 入口文件,默认 index.js
scripts 可执行的命令别名
dependencies 生产环境依赖
devDependencies 开发环境依赖

重点提醒:nameversion 组合起来,在 npm 生态里是唯一的标识。如果你要发布包,这两个字段必须填对。

scripts 字段的妙用

你想想看,每次都要敲 node server.js 多麻烦。用 scripts 可以简化操作:

{
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "build": "webpack --mode production",
    "test": "jest"
  }
}

然后你只需要 npm run dev 或者 npm start 就行了。我曾经在一个老项目里,看到有人把构建命令写成了 npm run build:prod:with-sourcemap,每次敲都手抖。后来我建议他简化成 npm run build,配合环境变量来控制,清爽多了。

2.2 语义化版本控制(SemVer)

版本号这东西,看着简单,但团队里经常因为「到底该升哪一位」吵起来。SemVer 的格式是 主版本号.次版本号.修订号,规则很明确:

  • 主版本号:做了不兼容的 API 修改。比如从 v1 升到 v2,旧代码可能跑不起来。
  • 次版本号:新增了功能,但向下兼容。你可以放心升级。
  • 修订号:做了 bug 修复,功能没变。闭着眼睛升就行。

我的习惯:每次发布前,先看看这次改了什么。如果有 breaking change,主版本号必须加 1。别偷懒,不然队友会骂娘的。

版本号的范围写法

在 package.json 里,你经常会看到这样的写法:

{
  "dependencies": {
    "lodash": "^4.17.21",
    "react": "~18.2.0",
    "vue": "3.2.0"
  }
}
符号 含义 示例
^ 允许次版本号和修订号升级 ^4.17.21 匹配 4.x.x
~ 只允许修订号升级 ~18.2.0 匹配 18.2.x
无符号 锁定精确版本 3.2.0 就是 3.2.0

踩坑警告:我曾经遇到过,项目里用了 ^1.2.3,结果某天依赖发布了 2.0.0,虽然 ^ 不会自动升主版本,但次版本升级时引入了新 bug。所以,关键依赖建议锁定精确版本,或者用 package-lock.json 来锁定。

2.3 依赖类型:dependencies vs devDependencies

这个区分,说白了就是「线上需要」和「开发需要」的区别。我见过有人把所有依赖都扔进 dependencies,结果生产环境安装了一堆没用的东西,又慢又占空间。

dependencies

项目运行时必须的包。比如:

  • 框架:reactvueexpress
  • 工具库:lodashaxiosdayjs
  • 数据库驱动:mongoosemysql2

安装时用 npm install <package>,默认会加到 dependencies 里。

devDependencies

只在开发阶段需要的包。比如:

  • 构建工具:webpackvitebabel
  • 测试框架:jestmochacypress
  • 代码检查:eslintprettier
  • 类型定义:@types/react@types/node

安装时用 npm install <package> --save-dev 或者 -D

判断标准:你问问自己,这个包在 npm run build 之后,线上代码还需要它吗?如果不需要,就放 devDependencies

一个常见的误区

有人觉得 typescript 是开发依赖,但编译后的代码不需要它,所以放 devDependencies 没错。但如果你在项目里用了 ts-node 直接跑 TypeScript 文件,那 typescript 其实也算运行时依赖。嗯,这里要灵活判断。

2.4 其他依赖类型

除了上面两个,还有几个不太常见但有用的:

  • peerDependencies:宿主环境必须安装的依赖。比如你写了一个 React 组件库,需要用户自己安装 React,就用这个。
  • optionalDependencies:可选依赖,安装失败也不影响主流程。
  • bundleDependencies:打包时一起发布的依赖。

我个人用得最多的是 peerDependencies。曾经写过一个 Vue 插件,如果用户没装 Vue,整个项目就崩了。用 peerDependencies 可以友好地提示用户:「嘿,别忘了装 Vue 哦。」

2.5 实战建议

好了,说了这么多,给你几个实操建议:

  1. 每次安装依赖后,记得提交 package-lock.json。它能保证团队里每个人安装的版本完全一致。
  2. 定期检查过时的依赖。用 npm outdated 看看哪些包该升级了。
  3. 不要手动修改 package-lock.json。它是由 npm 自动生成的,手动改容易出问题。
  4. 删除依赖时用 npm uninstall。直接删 node_modules 里的文件夹,package.json 不会自动更新。

一个小技巧:如果你不确定某个包该放哪,先放 dependencies,等开发完了再审视一遍。把那些只在构建时用到的包挪到 devDependencies 里。这样生产环境安装时能快不少。

嗯,package.json 的内容其实还有很多,比如 enginesprivateworkspaces 等。但今天这几个核心概念,足够你应付大部分日常工作了。下一章我们聊聊 node_modules 的结构和依赖安装的底层原理,那才是真正有意思的地方。