前端工程化:深入理解 npm 依赖管理机制
从
dependenciesvspeerDependencies到 npm 包构建与分发,解密前端依赖管理的完整链路。
问题的起源
在 Monorepo 项目中,我遇到了这样的困惑:
// element-plus/package.json
{
"dependencies": {
"lodash-unified": "^1.0.0",
"@vueuse/shared": "^10.0.0"
}
}疑问:
- element-plus 配置的这些依赖,是打包在 element-plus 里面的吗?
- 我安装 element-plus 时,是 element-plus 自己去安装这些依赖吗?
- 为什么我的项目需要配置
optimizeDeps.include才能找到这些依赖? dependencies和peerDependencies到底有什么区别?- peerDependencies 指定 vue,是不是用父项目的?这个声明只是看与父项目的版本差异是否巨大?
- element-plus 的依赖我在 node_modules 根目录找不到,但 pnpm-lock.yaml 里可以看到,这是为什么?
带着这些问题,我深入研究了 npm 依赖管理的完整机制。
一、dependencies vs peerDependencies
1.1 核心区别
| 配置项 | dependencies | peerDependencies |
|---|---|---|
| 含义 | "我需要这个依赖,请自动安装" | "我需要这个依赖,但让父项目提供" |
| 安装行为 | 每个包独立安装,可能有多个版本 | 使用父项目的版本,全局唯一 |
| 适用场景 | 工具库(lodash、dayjs、axios) | 框架、插件(vue、react、pinia) |
1.2 为什么需要 peerDependencies?
场景:element-plus 需要 vue
❌ 错误方式:使用 dependencies
// element-plus/package.json
{
"dependencies": {
"vue": "^3.5.0"
}
}
// 项目里面 apps/uni/package.json
{
"dependencies": {
"vue": "^3.5.13",
"@zkzq/hooks": "workspace:*"
}
}结果:两个 vue 实例!
node_modules/
├── [email protected] # apps/uni 的
└── element-plus/
└── node_modules/
└── [email protected] # element-plus 自己的问题:
- ❌ Vue 响应式系统失效(ref 在一个实例创建,但在另一个实例使用)
- ❌ 运行时报错:
[Vue warn]: Invalid VNode type - ❌ 打包体积增大(两份 vue)
✅ 正确方式:使用 peerDependencies
// element-plus/package.json
{
"peerDependencies": {
"vue": "^3.5.0" // 告诉父项目:"我需要 vue,但你来装"
}
}
// apps/uni/package.json
{
"dependencies": {
"vue": "^3.5.13",
"@zkzq/hooks": "workspace:*"
}
}结果:只有一个 vue 实例!
node_modules/
├── [email protected] # 唯一的 vue
└── element-plus/ (symlink,共享父项目的 vue)优势:
- ✅ 只有一个 vue 实例
- ✅ 响应式系统正常工作
- ✅ 打包体积最小
1.3 类比理解
dependencies(自己带工具):
你是木匠(element-plus),需要锤子(vue)
"我自己带一把锤子"
结果:
- 你带了锤子 3.5.0
- 工地(apps/uni)有锤子 3.5.13
- 两把锤子可能用错 → 出问题 ❌peerDependencies(用工地的工具):
你是木匠(element-plus),需要锤子(vue)
"我需要锤子,但用工地的那把"
结果:
- 检查工地有没有锤子
- 有一把 3.5.13
- 大家都用这把 → 不会出问题 ✅1.4 选择规则
| 依赖类型 | 使用场景 | 应该用什么 |
|---|---|---|
| 框架库 | vue, react, angular | peerDependencies(必须单例) |
| 状态管理 | pinia, vuex, redux | peerDependencies(必须单例) |
| 路由 | vue-router, react-router | peerDependencies(必须单例) |
| UI 组件库 | element-plus, ant-design | peerDependencies(避免重复打包) |
| 工具库 | lodash, dayjs, axios | dependencies(可以多个版本) |
二、npm 包的构建与分发机制
2.1 核心问题:element-plus 的依赖打包在哪里?
element-plus 发布到 npm 时的结构
element-plus/
├── package.json
├── dist/
│ ├── index.mjs # ESM 格式(已构建)
│ ├── index.cjs # CommonJS 格式
│ └── index.d.ts # TypeScript 类型
└── es/
└── components/
└── button/
└── index.mjsdist/index.mjs 的内容(简化示例)
// element-plus/dist/index.mjs
// ❌ 注意:lodash-unified 没有被打包进来!
import { isString } from "lodash-unified"; // ← 仍然是 import 语句
import { useVModel } from "@vueuse/shared"; // ← 仍然是 import 语句
export function ElButton(props) {
if (isString(props.type)) {
// ...
}
}关键发现:element-plus 的 dist/ 文件保留了 import 语句,依赖没有被打包进去!
2.2 为什么不把依赖打包进去?
策略对比
策略 1:打包所有依赖(Bundle)❌
// 如果打包了所有依赖,dist/index.mjs 会变成:
function isString(value) {
// lodash-unified 的代码内联
return typeof value === "string";
}
export function ElButton(props) {
if (isString(props.type)) {
// ...
}
}问题:
- ❌ 文件巨大(element-plus.mjs 可能 5MB+)
- ❌ 如果你的项目也用 lodash-unified,会打包两份
- ❌ 无法 tree-shaking(按需加载失效)
策略 2:保留 import 语句(External)✅
// element-plus 实际采用的方式
import { isString } from "lodash-unified"; // ← 保留 import
export function ElButton(props) {
if (isString(props.type)) {
// ...
}
}优势:
- ✅ element-plus 包很小(只有自己的代码)
- ✅ 依赖由用户项目统一管理
- ✅ 支持 tree-shaking(只打包用到的代码)
- ✅ 多个库共享依赖时,只会打包一份
2.3 npm 包的两种构建策略对比
| 对比项 | Library(库) | Application(应用) |
|---|---|---|
| 典型代表 | element-plus, lodash, vue | 你的项目 |
| 构建目标 | 给别人用 | 给用户访问 |
| 依赖处理 | External(保留 import) | Bundle(完全打包) |
| 发布到 npm | ✅ 是 | ❌ 否 |
| 是否包含依赖 | ❌ 否(在 dependencies 声明) | ✅ 是(打包到 dist/) |
三、依赖的完整生命周期
3.1 阶段 1:安装时(pnpm install)
cd apps/uni
pnpm add element-pluspnpm 做的事:
1. 下载 element-plus 包(包含 dist/ 构建产物)
↓
2. 读取 element-plus/package.json 的 dependencies:
{
"lodash-unified": "^1.0.0",
"@vueuse/shared": "^10.0.0",
"@ctrl/tinycolor": "^3.0.0"
}
↓
3. 递归下载并安装这些依赖
↓
4. 所有依赖都安装到 node_modules/
node_modules/
├── element-plus/
│ └── dist/index.mjs (已构建,但保留了 import)
├── lodash-unified/
├── @vueuse/shared/
└── @ctrl/tinycolor/此时:element-plus 和它的所有依赖都在 node_modules 中,但还没有打包到你的项目。
3.2 阶段 2:开发时(pnpm dev)
// apps/uni/src/main.ts
import { ElButton } from "element-plus";Vite 做的事:
1. 读取你的代码:import { ElButton } from 'element-plus'
↓
2. 找到 node_modules/element-plus/dist/index.mjs
↓
3. 读取 element-plus 的代码:
import { isString } from 'lodash-unified';
↓
4. 继续找到 node_modules/lodash-unified/
↓
5. 预构建(optimizeDeps):
- 普通 Vite 项目:✅ 自动预构建 lodash-unified
- uni-app 项目:❌ 不会预构建 → 报错
↓
6. 开发服务器提供所有模块3.3 阶段 3:生产构建时(pnpm build)
pnpm buildVite + Rollup 做的事:
1. 分析依赖树
main.ts
→ element-plus/dist/index.mjs
→ lodash-unified/dist/index.mjs
→ @vueuse/shared/dist/index.mjs
↓
2. 把所有代码打包到一起
dist/assets/index-abc123.js
(包含:你的代码 + element-plus + lodash-unified + ...)
↓
3. Tree-shaking:只打包用到的代码
- 只用了 ElButton → 不打包 ElTable
- lodash-unified 只用了 isString → 只打包 isString
↓
4. ✅ 最终产物:优化后的 .js 文件最终 dist/ 目录:
dist/
├── index.html
└── assets/
└── index-abc123.js # 包含所有依赖的完整代码此时:所有依赖都被打包进去了,node_modules 不需要部署。
3.4 完整流程图
npm 包发布者(element-plus) 用户项目(apps/uni)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
源码阶段
├─ src/button.vue
├─ src/table.vue
└─ dependencies:
lodash-unified
构建阶段(vite build)
├─ 编译 Vue 组件
├─ 保留 import 语句 (External)
└─ 生成 dist/index.mjs
发布到 npm
npm publish
1. pnpm add element-plus
━━━━━━━━━━━━━━━━━━━━━━
下载 element-plus
下载 lodash-unified
安装到 node_modules/
2. pnpm dev (开发)
━━━━━━━━━━━━━━━━━━
预构建依赖
启动开发服务器
3. pnpm build (生产)
━━━━━━━━━━━━━━━━━━
打包所有依赖
Tree-shaking
生成 dist/assets/四、Vite 的依赖预构建机制
4.1 什么是依赖预构建?
官方定义(来自 Vite 文档):
Vite 会将以 CommonJS 或 UMD 发布的依赖项转换为 ESM,并将有许多内部模块的 ESM 依赖项转换为单个模块。
作用:
- 格式转换:CommonJS → ESM(浏览器可用)
- 性能优化:合并多个模块为一个(减少网络请求)
- 缓存优化:预构建结果缓存在
node_modules/.vite/
4.2 普通 Vite 项目 vs uni-app 项目
普通 Vite 项目(全量扫描)
// apps/web/vite.config.ts
export default defineConfig({
plugins: [vue()]
});行为:
你的代码 import element-plus
↓
Vite 扫描 element-plus/package.json → dependencies
↓
发现 lodash-unified、@vueuse/shared、@ctrl/tinycolor
↓
递归扫描这些包的 dependencies
↓
全部预构建 → ✅ 不会报错uni-app 项目(选择性扫描)
// apps/uni/vite.config.ts
export default defineConfig({
plugins: [uni()] // @dcloudio/vite-plugin-uni
});行为:
你的代码 import element-plus
↓
uni() 插件只预构建 element-plus 本身
↓
❌ 不会扫描 element-plus 的 dependencies
↓
lodash-unified 等子依赖没有被预构建
↓
构建时找不到 → 报错uni-app 为什么这样设计?
- 目标环境复杂(H5、微信小程序、支付宝小程序、App)
- 小程序有代码包大小限制(2MB 主包、20MB 分包)
- 需要更严格的依赖控制
4.3 解决方案:optimizeDeps.include
// apps/uni/vite.config.ts
export default defineConfig({
plugins: [uni()],
optimizeDeps: {
include: [
"element-plus", // 你直接 import 的
"lodash-unified", // element-plus 间接依赖的
"@vueuse/shared", // element-plus 间接依赖的
"@ctrl/tinycolor" // element-plus 间接依赖的
]
}
});作用:告诉 Vite "这些包也要预构建,即使我没有直接 import"。
五、Monorepo 中的特殊场景
5.1 workspace 包的源码引用
你的项目采用源码直接引用模式:
// packages/hooks/package.json
{
"main": "index.ts", // ← 指向源码 .ts 文件
"types": "index.ts"
}与 npm 包的区别:
| 对比项 | npm 包(element-plus) | workspace 包(packages/hooks) |
|---|---|---|
| main 指向 | "./dist/index.mjs" | "./index.ts" |
| 分发形式 | 构建产物 | 源码 |
| 是否预构建 | 是(在开发服务器启动时) | 否(实时编译) |
| 依赖处理 | dependencies | peerDependencies(框架类) |
5.2 workspace 包的依赖问题
场景:packages/hooks 缺少 vue 依赖声明
// packages/hooks/package.json
{
"dependencies": {
"@zkzq/services": "workspace:*"
// ❌ 缺少 vue 依赖声明
}
}// packages/hooks/src/useTable.ts
import { ref } from "vue"; // ❌ TypeScript: 找不到模块 'vue'问题链:
1. apps/uni 引用 @zkzq/hooks
↓
2. hooks/src/useTable.ts 中 import { ref } from 'vue'
↓
3. uni-app 构建时在 hooks 的依赖树中找 vue
↓
4. hooks 没有声明 vue(无论是 dependencies 还是 peerDependencies)
↓
5. ❌ 报错:"Cannot find module 'vue'"解决方案:使用 peerDependencies
// packages/hooks/package.json
{
"peerDependencies": {
"vue": "^3.5.0" // ← 告诉父项目:"我需要 vue,但让你提供"
},
"dependencies": {
"@zkzq/services": "workspace:*"
}
}
// apps/uni/package.json
{
"dependencies": {
"vue": "^3.5.13", // ← 提供 vue
"@zkzq/hooks": "workspace:*"
}
}效果:
1. apps/uni 引用 @zkzq/hooks
↓
2. hooks 的 peerDependencies 要求 vue
↓
3. uni-app 构建时在 apps/uni 的依赖树中找 vue
↓
4. ✅ 找到了 [email protected]
↓
5. hooks 使用 uni 提供的 vue → 只有一个 vue 实例六、常见问题解答
Q1: 为什么删除 node_modules 重新安装能解决问题?
原因:
- Vite 的预构建缓存在
node_modules/.vite/,可能出现旧缓存 - pnpm 的 symlink 可能在某些情况下创建失败
- 重新安装会重新解析依赖树,重新创建 symlink
更好的方法:
# 只清除 Vite 缓存
rm -rf node_modules/.vite
pnpm devQ2: 为什么 Web 项目正常,uni-app 报错?
根本原因:@dcloudio/vite-plugin-uni 关闭了全量依赖扫描。
| 项目类型 | 依赖扫描策略 | 子依赖预构建 |
|---|---|---|
| Web(标准 Vite) | 全量扫描 | ✅ 自动预构建 |
| uni-app | 选择性扫描 | ❌ 不会自动预构建 |
解决方法:使用 optimizeDeps.include 手动指定子依赖。
Q3: dependencies 到底会不会把依赖打包进去?
答案:看发布者是谁。
npm 包发布者(element-plus):
- ❌ 不会打包依赖到 npm 包(dist/)
- ✅ 依赖保留为
import语句 - ✅ 在
dependencies中声明,让用户自动安装
用户项目(apps/uni):
- ✅ 会打包所有依赖到最终产物(dist/assets/)
- ✅ 包含 element-plus + lodash-unified + 所有依赖
- ❌ 部署时不需要 node_modules
Q4: peerDependencies 未满足会怎么样?
pnpm 会发出警告:
pnpm install
WARN Issues with peer dependencies found
.
└─┬ @zkzq/hooks
└── ✕ missing peer vue@^3.5.0后果:
- ⚠️ TypeScript 类型检查可能报错
- ⚠️ 运行时可能找不到模块
- ⚠️ 如果有多个版本,可能出现实例冲突
Q5: peerDependencies 指定 vue,是不是用父项目的?这个声明只是看与父项目的版本差异是否巨大?
第一个问题:是否使用父项目的依赖?
✅ 是的,peerDependencies 会使用父项目的依赖。
// packages/hooks/package.json
{
"peerDependencies": {
"vue": "^3.5.0"
}
}
// apps/uni/package.json
{
"dependencies": {
"vue": "^3.5.13"
}
}结果:
- ✅
@zkzq/hooks会使用apps/uni安装的[email protected] - ✅ 不会重复安装 vue
- ✅ 全局只有一个 vue 实例
第二个问题:不是"差异是否巨大",而是严格的版本范围匹配
❌ 错误理解:"只是看版本差异是否巨大"
✅ 正确理解:"检查父项目版本是否满足指定的版本范围(semver)"
pnpm install 时的检查逻辑
1. 读取 @zkzq/hooks 的 peerDependencies: { "vue": "^3.5.0" }
↓
2. 检查父项目(apps/uni)是否安装了 vue
↓
3. 检查父项目的 vue 版本是否满足 "^3.5.0" 这个范围
↓
4. 如果满足 → ✅ 不报警告
如果不满足 → ⚠️ 发出警告版本范围的含义(semver)
^3.5.0 的含义:
^3.5.0 表示:>= 3.5.0 且 < 4.0.0
✅ 满足的版本:
- 3.5.0
- 3.5.13
- 3.6.0
- 3.99.99
❌ 不满足的版本:
- 3.4.9 (小于 3.5.0)
- 4.0.0 (主版本号变化)
- 2.7.0 (主版本号变化)其他常见范围符号
| 写法 | 含义 | 示例 |
|---|---|---|
^3.5.0 | >= 3.5.0 且 < 4.0.0 | 3.5.x, 3.6.x ✅ 4.0.0 ❌ |
~3.5.0 | >= 3.5.0 且 < 3.6.0 | 3.5.x ✅ 3.6.0 ❌ |
>=3.5.0 | >= 3.5.0(无上限) | 3.5.0, 4.0.0 ✅ |
3.5.0 | 精确匹配 | 只有 3.5.0 ✅ |
^2.0.0 || ^3.0.0 | 2.x 或 3.x | 2.7.0, 3.5.0 ✅ |
版本不匹配的实际后果
场景 1:版本满足要求 ✅
// packages/hooks → peerDependencies: { "vue": "^3.5.0" }
// apps/uni → dependencies: { "vue": "3.6.2" }pnpm install
# ✅ 无警告,3.6.2 满足 ^3.5.0场景 2:版本不满足要求 ❌
// packages/hooks → peerDependencies: { "vue": "^3.5.0" }
// apps/uni → dependencies: { "vue": "3.4.0" }pnpm install
WARN Issues with peer dependencies found
.
└─┬ @zkzq/hooks
└── ✕ unmet peer vue@^3.5.0: found 3.4.0后果:
- ⚠️ 可能存在 API 不兼容(vue 3.4 缺少 3.5 的新特性)
- ❌ 运行时可能报错
场景 3:版本相差"巨大" ❌
// packages/hooks → peerDependencies: { "vue": "^3.5.0" }
// apps/uni → dependencies: { "vue": "2.7.16" }pnpm install
ERR Issues with peer dependencies found
.
└─┬ @zkzq/hooks
└── ✕ unmet peer vue@^3.5.0: found 2.7.16后果:
- ❌ API 完全不兼容(Vue 2 vs Vue 3)
- ❌ 运行时直接报错
为什么这样设计?
不是"巨大"的模糊概念,而是 API 兼容性
Vue 的版本规则(semver):
主版本号.次版本号.补丁号
X . Y . Z
X 变化 → 破坏性变更(API 不兼容)
Y 变化 → 新增功能(向后兼容)
Z 变化 → Bug 修复(向后兼容)举例:
| peerDependencies | 父项目版本 | 是否兼容 | 原因 |
|---|---|---|---|
^3.5.0 | 3.5.13 | ✅ 完全兼容 | 只是 bug 修复 |
^3.5.0 | 3.6.0 | ✅ 兼容 | 新增功能,旧 API 仍可用 |
^3.5.0 | 3.4.9 | ⚠️ 可能不兼容 | hooks 可能用了 3.5 的新特性 |
^3.5.0 | 4.0.0 | ❌ 不兼容 | 主版本变化,破坏性更新 |
^3.5.0 | 2.7.0 | ❌ 完全不兼容 | Vue 2 vs Vue 3 |
总结
| 你的理解 | 实际情况 |
|---|---|
| "使用父项目的依赖" | ✅ 完全正确 |
| "只是看版本差异是否巨大" | ❌ 不准确 |
| 正确理解 | 检查父项目版本是否满足指定的版本范围(semver) |
核心要点:
- ✅ peerDependencies 确实会使用父项目的依赖
- ✅ 版本号(如
^3.5.0)是一个精确的范围要求,不是模糊的"巨大"概念 - ✅ 检查的是 API 兼容性,而不是"差异大小"
- ✅ 如果不满足范围,会发出警告,可能导致运行时错误
Q6: element-plus 的依赖我在 node_modules 根目录找不到,但 pnpm-lock.yaml 里可以看到,这是为什么?
你观察到的现象
# 在 node_modules 根目录执行
ls node_modules/
# 输出:
element-plus/
vue/
pinia/
@zkzq/
# ❌ 没有看到 lodash-unified、@vueuse/shared
# 但在 pnpm-lock.yaml 里可以看到
pnpm-lock.yaml:
element-plus:
dependencies:
lodash-unified: 1.0.3
@vueuse/shared: 10.7.0
@ctrl/tinycolor: 3.6.1疑问:这些依赖到底装没装?装在哪里了?
pnpm 的特殊依赖管理机制
npm/yarn 的方式(扁平化)
node_modules/
├── element-plus/ # 你安装的
├── lodash-unified/ # element-plus 的依赖(提升到根目录)
├── @vueuse/shared/ # element-plus 的依赖(提升到根目录)
├── @ctrl/tinycolor/ # element-plus 的依赖(提升到根目录)
└── vue/特点:
- ✅ 所有依赖都在根目录,容易看到
- ❌ 幽灵依赖(你没声明也能用)
- ❌ 依赖重复(多个版本会有冗余)
pnpm 的方式(严格隔离)
Monorepo 项目的结构(重要纠正):
zkzq/ # ← Monorepo 根目录
├── node_modules/ # ← .pnpm 在根目录的 node_modules
│ ├── element-plus/ # symlink
│ ├── vue/ # symlink
│ ├── pinia/ # symlink
│ └── .pnpm/ # ← 真正的依赖存储位置(根目录)
│ ├── [email protected]/
│ │ └── node_modules/
│ │ ├── element-plus/
│ │ ├── lodash-unified/ # ← lodash-unified 在这里!
│ │ ├── @vueuse/shared/ # ← @vueuse/shared 在这里!
│ │ └── @ctrl/tinycolor/
│ ├── [email protected]/
│ └── [email protected]/
├── apps/
│ ├── web/
│ └── uni/
└── packages/
├── hooks/
└── core/关键点:
- ✅ Monorepo 中,
.pnpm在根目录的node_modules/.pnpm/(不是每个子项目都有) - ✅
node_modules/element-plus是一个 symlink(符号链接) - ✅ 真实文件在
node_modules/.pnpm/[email protected]/node_modules/element-plus - ✅
lodash-unified等依赖也在.pnpm/[email protected]/node_modules/下 - ✅ 严格隔离,你的代码不能直接
import 'lodash-unified'(除非你显式声明)
为什么这样设计?
npm/yarn 的问题:幽灵依赖
// 你的 package.json
{
"dependencies": {
"element-plus": "^2.11.2"
// ❌ 没有声明 lodash-unified
}
}// 你的代码
import { isString } from "lodash-unified"; // ❌ 居然能用!
// 因为 npm/yarn 把 lodash-unified 提升到了 node_modules 根目录问题:
- ❌ 你没声明依赖,却能用(幽灵依赖)
- ❌ element-plus 哪天不用 lodash-unified 了,你的代码就崩了
- ❌ 依赖关系不清晰
pnpm 的解决方案:严格隔离
你的代码只能访问:
node_modules/
├── element-plus/ ✅ 你声明了,可以用
├── vue/ ✅ 你声明了,可以用
└── .pnpm/ ❌ 隐藏目录,不能直接访问
你不能访问:
node_modules/.pnpm/[email protected]/node_modules/lodash-unified/
↑ 这是 element-plus 的私有依赖,你没声明,不能用如果你尝试:
// 你的代码
import { isString } from "lodash-unified";结果:
✘ [ERROR] Could not resolve "lodash-unified"正确做法:
# 如果你真的需要用,必须显式声明
pnpm add lodash-unifiedpnpm-lock.yaml 的作用
# pnpm-lock.yaml(简化)
lockfileVersion: "6.0"
dependencies:
element-plus:
specifier: ^2.11.2
version: 2.11.2([email protected])
packages:
/@ctrl/[email protected]:
resolution: { integrity: sha512-xxx }
/[email protected]:
resolution: { integrity: sha512-xxx }
dependencies:
"@ctrl/tinycolor": 3.6.1
"@vueuse/core": 10.7.0
lodash-unified: 1.0.3 # ← 记录了子依赖作用:
- 锁定完整的依赖树:记录了所有包(包括子依赖)的确切版本
- 确保一致性:团队成员、CI/CD 安装的版本完全一致
- 记录依赖关系:哪个包依赖了哪个包的哪个版本
验证方法
# 1. 查看根目录(只有直接依赖)
ls node_modules/
# 2. 查看 .pnpm 目录(所有真实依赖)
ls node_modules/.pnpm/
# 3. 查看 element-plus 的依赖
ls node_modules/.pnpm/[email protected]*/node_modules/
# 4. 查看 symlink
ls -la node_modules/element-plus
# 输出:element-plus -> .pnpm/[email protected][email protected]/node_modules/element-plus
# 5. 查看全局 store
pnpm store path
# 输出:~/.pnpm-store/v3总结
你的问题的答案:
- ✅ 这些依赖确实被安装了,在
node_modules/.pnpm/隐藏目录里 - ✅ pnpm-lock.yaml 记录了完整的依赖树,包括子依赖的确切版本
- ✅ 根目录看不到,是因为 pnpm 采用严格隔离机制,子依赖只能被声明它们的包访问
- ✅ 这是设计行为,避免幽灵依赖,确保依赖关系清晰
- ✅ Monorepo 中,
.pnpm在根目录的node_modules/.pnpm/,所有子项目共享
pnpm vs npm/yarn 对比表:
| 对比项 | npm/yarn | pnpm |
|---|---|---|
| 依赖位置 | 全部扁平化到根目录 | 隔离在 .pnpm/ |
| 根目录可见 | ✅ 所有依赖都可见 | ❌ 只有直接依赖可见 |
| 幽灵依赖 | ❌ 可以用未声明的包 | ✅ 不能用未声明的包 |
| 磁盘占用 | 每个项目独立存储 | 全局硬链接共享 |
| 安装速度 | 较慢 | 很快 |
| Monorepo | 每个子项目有 node_modules | 根目录统一管理 |
七、最佳实践总结
7.1 依赖声明规范
npm 包发布(packages/hooks、packages/core)
{
"peerDependencies": {
"vue": "^3.5.0", // 框架类必须用 peer
"pinia": "^2.0.0 || ^3.0.0" // 状态管理必须用 peer
},
"dependencies": {
"dayjs": "^1.11.0", // 工具库用 dependencies
"@zkzq/utils": "workspace:*" // workspace 包用 dependencies
}
}应用层(apps/web、apps/uni)
{
"dependencies": {
"vue": "^3.5.13", // 提供 peerDependencies 要求的依赖
"pinia": "^2.1.7",
"element-plus": "^2.11.2", // UI 库可以在应用层
"@zkzq/hooks": "workspace:*"
}
}7.2 uni-app 项目配置
// apps/uni/vite.config.ts
export default defineConfig({
plugins: [uni()],
resolve: {
dedupe: ["vue", "pinia"] // 防止多实例
},
optimizeDeps: {
include: [
"vue",
"pinia",
// 如果使用了 npm 包,手动添加其子依赖
"element-plus",
"lodash-unified",
"@vueuse/shared"
],
exclude: [
// workspace 包排除预构建(使用源码)
"@zkzq/core",
"@zkzq/hooks",
"@zkzq/store"
]
}
});7.3 依赖选择原则
| 依赖类型 | 使用场景 | 应该用什么 |
|---|---|---|
| 框架 | vue, react | peerDependencies |
| 状态管理 | pinia, redux | peerDependencies |
| 路由 | vue-router | peerDependencies |
| UI 组件库 | element-plus | peerDependencies(库)/ dependencies(应用) |
| 工具库 | lodash, dayjs | dependencies |
| workspace 包 | @zkzq/* | dependencies (workspace:*) |
7.4 Monorepo 架构分层
packages/core (跨端业务逻辑)
✅ 可依赖:dayjs、lodash、axios(纯 JS)
❌ 不可依赖:element-plus、uni-ui(平台专属)
apps/web (Web 应用)
✅ 可依赖:element-plus、echarts
apps/uni (uni-app 应用)
✅ 可依赖:@dcloudio/uni-ui、uqrcodejs八、总结
8.1 关键概念回顾
dependencies:
- "我需要这个依赖,请自动安装"
- npm 包不会把依赖打包进去,只是声明"用户需要安装这些"
- 用户项目 build 时才会把所有依赖打包到 dist/
peerDependencies:
- "我需要这个依赖,但让父项目提供"
- 确保全局只有一个版本(框架、插件必须用)
- 避免多实例问题
npm 包的分发:
- 发布的是构建产物(dist/),但保留
import语句 - 依赖不打包,在
dependencies中声明 - 用户项目负责最终打包
- 发布的是构建产物(dist/),但保留
Vite 预构建:
- 普通项目:全量扫描,自动预构建所有依赖
- uni-app:选择性扫描,需要手动
optimizeDeps.include
8.2 核心规律
npm 包构建流程:
源码 → 编译 → dist/(保留 import)→ npm publish
用户项目使用流程:
pnpm install → 安装所有依赖到 node_modules
→ pnpm dev → Vite 预构建(开发模式)
→ pnpm build → 打包所有依赖到 dist/(生产模式)
最终产物:
dist/assets/index.js (包含你的代码 + 所有依赖)8.3 实战建议
Monorepo 中的 workspace 包:
- 框架类依赖用 peerDependencies
- 保持源码引用(不要构建)
- 让应用层提供框架依赖
uni-app 项目:
- 避免在共享包中使用平台专属库
- 手动配置
optimizeDeps.include - 使用 uni-app 兼容的库(uqrcodejs、uni-ui)
遇到依赖问题:
- 优先清除 Vite 缓存:
rm -rf node_modules/.vite - 检查 peerDependencies 是否满足
- 检查是否需要手动 include 子依赖
- 优先清除 Vite 缓存:
参考资料
本文基于实际 Monorepo 项目开发经验总结,涵盖了从依赖管理到构建打包的完整链路。希望能帮助你深入理解前端工程化中的依赖管理机制。