Skip to content

pnpm 依赖类型:dependencies、devDependencies 与 peerDependencies

在前端工程里,dependenciesdevDependenciespeerDependencies 经常被混在一起理解。如果只按“能不能装上、能不能跑”判断,很容易埋下后续维护问题。

这篇文章只讨论依赖字段本身:它们分别表达什么语义,会影响哪个阶段,以及在应用和库开发中应该如何选择。

依赖字段的核心区别

最重要的三个字段是:

txt
dependencies      = 运行我的代码需要的包
devDependencies   = 开发、构建、测试、lint 时需要的包
peerDependencies  = 我不自己带这个包,但要求使用我的项目提供它

例如普通 Vue 应用:

json
{
  "dependencies": {
    "vue": "^3.5.0",
    "pinia": "^2.3.0",
    "axios": "^1.7.0"
  },
  "devDependencies": {
    "vite": "^6.0.0",
    "typescript": "^5.0.0",
    "eslint": "^9.0.0"
  }
}

vuepiniaaxios 是业务运行时依赖。vitetypescripteslint 是开发构建工具。

但如果写的是 Vue 组件库,情况不同:

json
{
  "peerDependencies": {
    "vue": "^3.4.0 || ^3.5.0"
  },
  "devDependencies": {
    "vue": "^3.5.0",
    "vite": "^6.0.0",
    "typescript": "^5.0.0"
  }
}

这里 vue 同时出现在 peerDependenciesdevDependencies,不是重复,而是两个角色:

txt
peerDependencies:告诉使用者,你的项目里必须有 Vue
devDependencies:我开发这个组件库时,本地也需要 Vue

dependencies:生产运行需要的直接依赖

dependencies 表示项目在生产运行时需要的包。

典型例子:

json
{
  "dependencies": {
    "axios": "^1.7.0",
    "dayjs": "^1.11.0",
    "lodash-es": "^4.17.21"
  }
}

如果代码里有直接运行时导入:

ts
import axios from 'axios'
import dayjs from 'dayjs'

这些包通常应该放进 dependencies

判断标准不是“这个包是不是前端包”,而是:

txt
生产环境运行这段代码时,是否需要这个包存在?

如果答案是需要,它通常就不应该只放在 devDependencies

devDependencies:开发和构建工具依赖

devDependencies 表示开发阶段、构建阶段、测试阶段、格式化阶段需要的包。

典型例子:

json
{
  "devDependencies": {
    "vite": "^6.0.0",
    "typescript": "^5.0.0",
    "eslint": "^9.0.0",
    "prettier": "^3.0.0",
    "vitest": "^2.0.0"
  }
}

这些包通常不会成为业务代码的运行时依赖。它们的作用是帮助项目被开发、检查、测试或构建。

常见判断方式:

txt
只在 npm scripts、构建配置、测试配置、lint 配置里使用:devDependencies
业务代码运行时直接 import 并依赖其功能:dependencies

例如:

json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "vitest",
    "lint": "eslint ."
  }
}

这里的 vitevitesteslint 是工具链依赖,通常放在 devDependencies

peerDependencies:要求宿主项目提供的依赖

peerDependencies 常见于库、插件、组件库,而不是普通业务应用。

它表达的是:

txt
我这个包需要和宿主项目共用某个依赖,而不是自己安装一份独立副本。

典型场景:

json
{
  "peerDependencies": {
    "vue": "^3.4.0 || ^3.5.0"
  }
}

这表示组件库需要宿主项目提供 Vue。

为什么不直接放进 dependencies?因为 Vue、React 这类框架往往存在全局上下文、组件实例、插件注册、Hooks、响应式系统等运行时关系。如果组件库自己带一份 Vue,而宿主应用也带一份 Vue,就可能出现两套运行时实例。

为什么组件库不应该把 Vue/React 放进 dependencies

假设组件库这样写:

json
{
  "dependencies": {
    "vue": "^3.5.0"
  }
}

用户项目自己也装了 Vue:

json
{
  "dependencies": {
    "vue": "3.3.0",
    "my-ui": "1.0.0"
  }
}

可能形成:

txt
app
├── vue@3.3.0
└── my-ui
    └── vue@3.5.0

这意味着主应用和组件库可能使用两份 Vue。对 lodashdayjs 这类普通工具库,两份通常问题不大;但对 Vue、React、Webpack、Vite、ESLint、Express 这类“宿主型依赖”,两份实例容易导致上下文不一致、插件注册不一致、React Hooks 报错、构建插件行为异常等问题。

所以判断规则是:

txt
普通工具库,库内部自己用:dependencies
宿主框架、插件宿主、运行时上下文:peerDependencies
本地开发这个库也需要:再放 devDependencies

同一个包可以同时出现在 peerDependencies 和 devDependencies

对库项目来说,这是正常现象。

例如组件库:

json
{
  "peerDependencies": {
    "vue": "^3.4.0 || ^3.5.0"
  },
  "devDependencies": {
    "vue": "^3.5.0"
  }
}

含义是:

txt
peerDependencies:发布后要求使用者安装 Vue
devDependencies:开发这个库时,本地也需要 Vue 来构建、测试和类型检查

不要把它理解成“重复安装”。它们解决的是两个不同阶段的问题。

前端打包时,dependencies 并不直接决定是否进入 bundle

Vite、Webpack、Rollup 打包前端代码时,看的是 import 依赖图,不是看包在 dependencies 还是 devDependencies

例如:

ts
import { createApp } from 'vue'

只要构建时 node_modules 能解析到 vue,它就可能被打进最终产物。dependenciesdevDependencies 真正影响的是包管理器安装阶段,例如:

bash
pnpm install --prod

这时 devDependencies 不会被安装。如果某个生产运行或构建必须用到的包只放在 devDependencies,就可能在 CI、Docker 或生产环境中出问题。

更准确的理解是:

txt
包管理器安装阶段:看 dependencies/devDependencies
打包器构建阶段:看 import 图和 external 配置
浏览器运行阶段:只看打包后的 JS/CSS
Node 生产运行阶段:看生产环境实际安装了哪些包

应用项目和库项目的选择差异

普通应用项目的重点是:生产运行需要什么,就放进 dependencies;开发工具链需要什么,就放进 devDependencies

txt
应用项目:
- vue/react/pinia/axios/dayjs:通常是 dependencies
- vite/typescript/eslint/vitest/prettier:通常是 devDependencies

库项目的重点是:哪些依赖应该由使用者提供,哪些依赖应该由库自己带。

txt
库项目:
- lodash/dayjs 等内部工具:通常是 dependencies
- vue/react 等宿主框架:通常是 peerDependencies + devDependencies
- vite/typescript/eslint/vitest:通常是 devDependencies

常见误区

误区一:devDependencies 一定不会进入前端产物

不准确。

前端产物由打包器的依赖图决定。如果业务代码 import 了某个包,只要构建时能解析到,它就可能进入 bundle。devDependencies 只是安装语义,不是打包排除规则。

误区二:dependencies 一定会进入 bundle

也不准确。

如果某个依赖没有被代码 import,或者被打包配置 external 掉,它就不会进入最终 bundle。dependencies 说明它是生产依赖,但不等价于“一定被打包”。

误区三:peerDependencies 不需要本地安装

不准确。

peerDependencies 是给使用者看的约束。本地开发库时,如果代码、测试、类型检查、构建都需要这个包,仍然应该把它放进 devDependencies

误区四:所有直接 import 的包都应该放 dependencies

对应用项目大体成立,但对库项目不一定成立。

库项目直接 import Vue/React,也不代表应该把 Vue/React 放进 dependencies。如果这个依赖必须和宿主项目共享运行时上下文,就应该优先考虑 peerDependencies

最终判断表

场景推荐字段
应用运行时直接依赖dependencies
构建、测试、lint、格式化工具devDependencies
组件库依赖 Vue/ReactpeerDependencies + devDependencies
库内部使用 lodash/dayjs 等普通工具dependencies
CLI 工具运行时依赖 commander/chalk 等包dependencies
类型、测试、打包时才需要的工具devDependencies

最短总结

txt
dependencies 是“我运行时自己需要”。
devDependencies 是“我开发、构建、测试时需要”。
peerDependencies 是“你用我时必须提供”。

再补一层:

txt
安装阶段:主要看 dependencies/devDependencies/peerDependencies
打包阶段:主要看 import 图和 external 配置
运行阶段:主要看最终产物和生产环境实际安装的包

把这几层分清,依赖字段就不会只停留在“能不能装上”的层面。

Last updated:

Powered by VitePress. Written with AI, reviewed by human.