一文讲清楚 npm 包里的 `dependencies` 和 `devDependencies`

6 阅读8分钟

刚接触 npm 包管理时,很多人都会被两个字段绕住:

  • dependencies
  • devDependencies

表面看只差了一个 dev,但实际背后是两种完全不同的角色。

很多人会先形成一个直觉:

devDependencies 是开发时依赖,dependencies 是运行时依赖。

这个理解方向没错,但如果只停在这里,实际写项目时还是容易分错。

这篇文章就把这个问题彻底掰开讲明白。


一、先看结论

最简单的判断标准是这一句:

包在真正运行时还需要它,就放 dependencies
只在开发、测试、构建、打包阶段需要它,就放 devDependencies

换句话说:

dependencies

表示运行时依赖

也就是:

  • 项目启动时需要
  • 代码执行时需要
  • 用户真正使用功能时需要

devDependencies

表示开发时依赖

也就是:

  • 本地开发时需要
  • 测试时需要
  • 构建时需要
  • 打包发布时需要
  • 代码检查时需要

二、为什么这个问题总让人混淆

因为“开发时会用到”这句话,范围太大了。

你写代码的时候,当然什么都在开发时用到了:

  • 你会用 axios
  • 你会用 lodash
  • 你会用 typescript
  • 你会用 eslint

但它们的性质并不一样。

这里真正该问的,不是:

我开发时有没有用到它?

而是:

我把包发布出去后,别人安装并运行这个包时,还需不需要它?

这才是判断关键。

三、先用一个生活化的比喻理解

可以把开发 npm 包想成开一家小餐馆。

dependencies 是什么

像端上桌的食材:

  • 调料

没有这些,顾客吃不到东西。

devDependencies 是什么

像后厨工具:

  • 菜刀
  • 烤箱
  • 清洁工具

它们对做菜很重要,但顾客不需要把这些工具一起买走。

所以:

  • 跟着“成品”一起发挥作用的,是 dependencies
  • 只在“制作过程”中发挥作用的,是 devDependencies

四、最常见的理解方式

很多文章会这样解释:

dependencies

项目运行时要用的依赖

devDependencies

开发这个项目时要用的依赖

这句话没错,但还不够完整。

更准确一点,应该改成:

dependencies

项目在实际运行时必须存在的依赖

devDependencies

项目在开发、测试、构建、打包、发布时使用的依赖

注意这里多出来几个关键词:

  • 测试
  • 构建
  • 打包
  • 发布

这几个词很重要,因为很多初学者只理解了“开发”,没理解“构建”。

五、什么叫“构建时需要”?

很多人第一次看到这句话会疑惑:

TypeScript、Babel 明明不参与业务运行,为什么它们很重要?

原因很简单:

你平时写的源码,不一定是最终发布给别人运行的代码。

比如你写的是:

const add = (a: number, b: number) => a + b
export default add

这里有 TypeScript 类型标注,运行环境并不能直接拿这些类型来执行。

所以发布前,通常会经过一轮处理,变成:

const add = (a, b) => a + b
export default add

或者再进一步转成更兼容旧环境的版本。

这个“把源码处理成可发布产物”的过程,就是构建或编译。

所以:

  • typescript
  • babel
  • rollup
  • webpack
  • vite

这些通常属于 devDependencies

因为它们负责的是“做菜过程”,不是“上桌后的食物”。

六、最容易分清的一种办法

每次遇到一个依赖,不妨问自己一句:

如果把项目构建完、发布出去,用户真正使用功能时,还要不要这个依赖?

如果要

放进 dependencies

如果不要,只是帮助你开发和打包

放进 devDependencies

这个方法很稳。

七、通过例子理解

例子 1:工具函数库里用了 dayjs

import dayjs from 'dayjs'

export function formatDate(date) {
  return dayjs(date).format('YYYY-MM-DD')
}

这里 dayjs 应该放哪?

答案是:dependencies

因为你的函数真正运行时,需要调用 dayjs

不是你开发时用了它一次就结束了,而是你包的使用者在调用 formatDate 时,底层还要依赖 dayjs

例子 2:用了 eslint 做代码检查

开发时你装了:

npm install eslint -D

它是用来做什么的?

  • 检查代码规范
  • 提示潜在问题
  • 统一团队风格

项目真正运行时,需要 eslint 吗?

不需要。

所以它应该放进 devDependencies

例子 3:用了 jestvitest 做测试

测试工具只在测试阶段执行。

用户安装你的包,并不会因为要调用某个功能而去运行 jest

所以这类依赖一般都属于 devDependencies

例子 4:用了 typescript 写源码

你源码可能是 .ts 文件,但最终发布的包往往是已经编译好的 .js 文件。

也就是说,用户使用你包时,用到的是产物,不是你本地的 TypeScript 编译器。

所以 typescript 一般属于 devDependencies

例子 5:项目里使用 axios 发请求

如果你的业务代码里直接这样写:

import axios from 'axios'

export function getUser() {
  return axios.get('/api/user')
}

axios 是实际运行逻辑的一部分。

因此它通常属于 dependencies

八、为什么很多人会把依赖放错

常见原因主要有三个。

1)只从“我开发时有没有用过”来判断

这会导致判断范围过大。

因为你开发时什么都用到了,但不是所有东西都会进入运行阶段。


2)没有区分“源码”和“发布产物”

这是个很常见的坑。

很多工具只作用在源码阶段,比如:

  • 类型检查
  • 代码转换
  • 打包压缩
  • 生成声明文件

这些工具非常重要,但它们的重要性停留在“生产过程”,不是“运行过程”。

3)把“业务依赖”误认为“开发依赖”

比如平时有的产品业务里明确用到了:

  • axios
  • lodash
  • dayjs

这些都不应该因为“开发时也在用”就被扔进 devDependencies

它们是运行逻辑本身的一部分。

九、从包作者和包使用者两个视角看,会更清楚

这个问题最容易绕,是因为视角没切换。

站在包作者视角

你会觉得:

  • 我开发时用了 typescript
  • 我开发时也用了 axios
  • 我开发时也用了 eslint

感觉它们都是“开发中会用到的东西”。

没错,但这不是 npm 关心的重点。

站在包使用者视角

npm 更关心的是:

  • 用户安装你的包后,哪些依赖必须存在,代码才能跑
  • 哪些依赖只是你内部研发流程要用

这样一看,边界就清楚了:

  • 运行必须的 → dependencies
  • 研发辅助的 → devDependencies

十、一个最小的项目例子

假设你写了一个日期格式化工具包。

目录可能是这样:

src/
  index.ts
package.json
tsconfig.json

源码:

import dayjs from 'dayjs'

export function formatDate(date: string): string {
  return dayjs(date).format('YYYY-MM-DD')
}

你在开发时可能安装了这些包:

  • dayjs
  • typescript
  • rollup
  • vitest
  • eslint

那该怎么分?

放到 dependencies

  • dayjs

因为真正运行 formatDate 时要用它。

放到 devDependencies

  • typescript
  • rollup
  • vitest
  • eslint

因为它们只是帮助你:

  • 写 TypeScript
  • 打包代码
  • 测试功能
  • 检查规范

用户运行 formatDate 本身,不需要这些工具参与。

十一、一个典型的 package.json 示例

{
  "name": "demo-utils",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "dependencies": {
    "dayjs": "^1.11.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "rollup": "^4.0.0",
    "eslint": "^9.0.0",
    "vitest": "^1.0.0"
  }
}

看到这个配置,可以这样理解:

  • dayjs 是成品运行时要吃的“食材”
  • typescriptrollupeslintvitest 是制作和检查过程中的“工具”

十二、再说几个常见包,方便形成直觉

常见的 dependencies

这类包通常会直接出现在业务代码执行路径里:

  • axios
  • lodash
  • dayjs
  • uuid
  • react(很多应用项目里是这样)
  • vue(很多应用项目里是这样)

常见的 devDependencies

这类包通常服务于开发流程:

  • typescript
  • eslint
  • prettier
  • jest
  • vitest
  • webpack
  • vite
  • rollup
  • babel
  • sass(很多情况下)
  • 各类构建插件、测试插件、lint 插件

十三、一个很实用的口诀

可以记一句很接地气的话:

跟着产物跑的,放 dependencies
只陪你开发的,放 devDependencies

或者再换一种说法:

用户运行时要用的,是 dependencies
你写代码时要用的工具,是 devDependencies

十四、容易踩坑的地方

坑 1:把运行依赖误放进 devDependencies

结果可能是:

  • 本地开发一切正常
  • 发布后别人安装使用时报错
  • 因为真正运行时缺依赖

这就像你做了一碗面,结果把“面”放进了工具箱,而不是放进食材清单里。

坑 2:把开发工具误放进 dependencies

结果通常不会立刻炸,但会带来一些问题:

  • 安装体积变大
  • 用户下载了不必要的包
  • 依赖树更复杂
  • 包管理更混乱

这就像你卖一碗面,还强行把菜刀、案板、烤箱一起塞给顾客。

十五、最后总结

把这件事说到最本质,其实就一句话:

dependencies 是项目运行时真正要依赖的库;
devDependencies 是项目开发、测试、构建、打包时使用的工具依赖。

判断时别问:

我开发时有没有用到它?

而要问:

用户真正运行这份代码时,还需不需要它?

需要,就是 dependencies
不需要,就是 devDependencies

十六、结尾

刚学 npm 时,dependenciesdevDependencies 看起来只是两个字段的区别;
但理解透了之后,你会发现它本质上是在帮你区分:

  • 哪些是“产品的一部分”
  • 哪些是“生产产品的工具”

这个边界一旦建立起来,很多工程化问题都会顺。

因为写代码这件事,说到底也很像开店:

食材要分清,工具也要分清。
不然厨房会乱,顾客也吃不好。


如果喜欢这篇文章,请给我点个赞:)

祝大家:码上有钱 --Larry