背景
开发monorepo项目时,因为文件结构较为复杂,时间久了往往会出现一些在所难免的问题,导致项目冗余度高,性能差,主要分为以下几类问题:
- 依赖管理不当,项目中存在大量重复依赖,无用依赖
- 存在多个不同版本的同一包
- 合码后pnpm-lock被忽略更新,导致团队开发时版本冲突
复现场景:
大量无用依赖和重复依赖怎么来的?
无用依赖(Unused Dependencies)
原因:
- 依赖未清理:项目中可能有一些包已经不再使用某些依赖,但没有及时从
package.json中删除这些不再使用的依赖,导致它们被保留在项目中。 - 历史包依赖未清理:在开发过程中,如果有些包或者模块被拆分、重构或删除,之前的依赖可能仍然保留在
package.json中,但这些依赖已经不再被任何代码实际使用。
场景示例:
假设原来项目中有包 C 和 D,其中包 C 使用了 axios,但后来包 C 被拆分成两个小模块,且这两个模块中都不再使用 axios 了。此时 axios 依赖依然留在包 C 的 package.json 中,但实际上不再被任何代码使用。
// package.json of C (已不再使用 axios)
{
"dependencies": {
"axios": "^0.21.1"
}
}
依赖版本不一致(Version Inconsistency)
原因:
- 不同子包依赖不同版本的同一个库:如果不同的子包依赖同一库的不同版本,pnpm 会倾向于安装多个版本,以避免版本冲突。这可能会导致在 monorepo 中出现多个版本的相同库,增加了冗余。
场景示例:
如果包 A 和包 B 都依赖 react,但版本不同,pnpm 可能会安装两个版本的 react,这会导致重复依赖。
// package.json of A
{
"dependencies": {
"react": "^18.0.0"
}
}
// package.json of B
{
"dependencies": {
"react": "^17.0.0"
}
}
在这种情况下,pnpm 会将 react 18 和 17 的版本都安装到 node_modules 中。
pnpm 的 Hoisting 设置问题
原因:
- Hoisting 配置不当:pnpm 默认使用 hoisting(提升)来将共享的依赖提升到根目录的
node_modules中。如果 hoisting 配置不当,某些依赖可能不会被提升到根目录,而是被分散在各个子包中,从而导致重复的依赖和包体积冗余。
场景示例:
如果 pnpm 的 workspace 配置中,某些依赖没有正确提升到顶层,可能会导致每个子包都各自安装一份相同的依赖,而不是通过提升机制共享依赖。
// pnpm-workspace.yaml
packages:
- 'packages/*'
- 'libs/*'
hoist:
- 'react'
- 'lodash'
如果 react 和 lodash 在所有子包中都使用相同版本,pnpm 应该将它们提升到顶层。如果没有正确配置或不一致的配置,会导致重复安装。
忽视 Workspace Dependency(未共享依赖)
原因:
- 跨包依赖未共享:如果某些子包之间的依赖没有共享,而是每个包都独立安装,可能导致重复安装和依赖不一致。例如,如果包 A 和包 B 都使用了包 C,但各自独立地安装了包 C 而没有通过 workspace 管理共享这些依赖,就会导致冗余依赖。
场景示例:
假设包 A 和包 B 都依赖于包 C,但它们没有在 pnpm-workspace.yaml 中正确配置工作区共享,导致包 A 和包 B 都安装了包 C。
// package.json of A
{
"dependencies": {
"C": "^1.0.0"
}
}
// package.json of B
{
"dependencies": {
"C": "^1.0.0"
}
}
如果没有通过 workspace 共享,而是让每个子包独立安装依赖,C 会在每个包中重复安装。
pnpm-lock被忽略更新
pnpm-lock是什么?忽略他有什么后果?可以参考我的另外一篇文章:pnpm.lock.yaml,看似无关紧要,实则······
如何解决问题(手动)
1. 清理无用依赖
步骤:
-
手动检查和删除无用依赖:检查每个包的
package.json,确保只保留当前实际需要的依赖。如果发现某些依赖不再使用,手动删除它们。 -
使用工具自动检测无用依赖:可以借助一些工具自动检测项目中的无用依赖,避免遗漏。
-
depcheck:一个常用的工具,它可以帮助找出未使用的依赖和丢失的依赖。npm install -g depcheck depcheck -
npm-check:另一个有用的工具,它不仅能检查无用依赖,还能检查过时的依赖。npm install -g npm-check npm-check
-
操作:
- 删除
package.json中未使用的依赖,并在代码中确认移除相关的导入语句。 - 更新
pnpm-lock.yaml文件,确保没有残留的冗余依赖。
2. 解决版本不一致
步骤:
-
统一版本管理:确保所有子包使用相同版本的核心依赖,避免出现多个版本的冲突。如果子包依赖同一个库的不同版本,可以选择统一版本或使用
resolutions来强制指定版本。// 根目录的 package.json { "resolutions": { "react": "^18.0.0", "lodash": "^4.17.21" } } -
检查
pnpm-lock.yaml:确保pnpm-lock.yaml中的依赖版本一致,手动或通过pnpm install重新生成lock文件。
操作:
- 执行
pnpm install重新整理依赖,确保版本一致性。 - 使用
pnpm list检查项目中安装的依赖树,确认是否有重复版本的依赖。
3. 优化 pnpm 的 Hoisting 设置
步骤:
-
配置 Hoisting 规则:确保
pnpm的工作区配置正确,避免重复依赖。可以通过调整pnpm-workspace.yaml中的hoist配置来实现依赖的提升。// pnpm-workspace.yaml packages: - 'packages/*' - 'libs/*' hoist: - 'react' - 'lodash'上面的配置会确保
react和lodash等共享依赖被提升到根目录的node_modules中,避免重复安装。 -
避免不必要的 Hoisting:虽然 Hoisting 有时能解决问题,但过度提升可能会导致某些包的版本不兼容。在某些情况下,避免将某些依赖提升到根目录可能更有利于避免版本冲突。
操作:
- 通过
pnpm install更新依赖,确保依赖被正确提升。 - 使用
pnpm list --depth 3等命令检查是否有不必要的重复安装。
4. 使用 Workspace 共享依赖
步骤:
-
正确配置工作区依赖:确保
pnpm-workspace.yaml中的各个包正确配置共享依赖,避免包之间的重复安装。例如,如果 A 和 B 包都依赖 C,确保通过workspace来共享这个依赖。// pnpm-workspace.yaml packages: - 'packages/*' - 'libs/*' -
集中管理依赖:将通用依赖(如 React、Lodash 等)移动到根
package.json中,确保它们在所有包之间共享,而不是每个子包单独安装。
操作:
- 使用
pnpm install执行工作区依赖的统一安装,确保共享依赖被正确配置。
5. 更新 pnpm-lock 文件
步骤:
-
重新生成
pnpm-lock.yaml文件:在很多情况下,pnpm-lock.yaml文件可能会被忽略更新,导致团队开发时出现版本冲突。定期清理和更新pnpm-lock.yaml文件,确保其中的依赖是最新且一致的。pnpm install --frozen-lockfile -
使用
pnpm audit检查锁文件:运行pnpm audit可以检查项目中的依赖漏洞和潜在的安全问题,确保锁文件和依赖是最新的。
操作:
- 删除
node_modules和pnpm-lock.yaml,然后重新执行pnpm install。 - 使用
pnpm update更新所有依赖并确保锁文件与package.json一致。
6. 定期维护和监控
步骤:
-
定期清理:定期执行
pnpm prune命令,清理不再需要的依赖,保持依赖树干净。pnpm prune -
监控依赖:使用
pnpm outdated定期检查过时的依赖,并及时更新。pnpm outdated -
添加自动化检测:为项目添加 CI/CD 流水线,定期运行依赖检查工具(如
depcheck、npm-check),在代码提交前自动检测和清理无用依赖。
操作:
- 在 CI 中集成依赖管理工具,定期检测和清理冗余依赖。
如何解决问题(自动化)
1. GitHub Actions CLI 示例
这个 GitHub Actions 配置脚本将帮助你自动化清理无用依赖、更新依赖版本、优化 pnpm-lock.yaml 文件等工作。
不知道什么是 GitHub Actions,可以看我另外一篇文章: github actions--最好的打工仔,帮你干脏活累活
什么是yaml?这篇文章有讲解:pnpm.lock.yaml,看似无关紧要,实则······
GitHub Actions 配置文件 ci.yml
name: zhuozhuoのCI
#触发条件
on:
push:
branches:
- dev-ywz
pull_request:
branches:
- dev-ywz
#定义工作流中的任务
jobs:
depcheck:
runs-on: ubuntu-latest #指定工作流运行的系统
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up pnpm
uses: pnpm/action-setup@v2
with:
version: 9.12.3
- name: Install dependencies
run: pnpm install
- name: Run depcheck to find unused dependencies
run: |
npx depcheck --ignore-path=pnpm.lock.yaml
continue-on-error: true
- name: Report unused dependencies
run: |
echo "depcheck执行成功"
if [ -f "depcheck-report.txt"]; then
echo "auv,您这儿有没用的依赖"
cat depcheck-report.txt
exit 1
fi
version-consistency:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up pnpm
uses: pnpm/action-setup@v2
with:
version: 9.12.3
- name: Check and fix version consistency
run: |
echo "version-consistency执行成功"
pnpm install --prefer-offline --frozen-lockfile
# 前面执行了强制操作,audit兜底
pnpm audit
lockfile-updata:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up pnpm
uses: pnpm/action-setup@v2
with:
version: 9.12.3
- name: Set up pnpm
run: pnpm install
- name: Clean upp unused dependencies
run: pnpm prune
- name: Update lock file
run: |
echo "lockile-update执行成功"
git config --global user.email "3098448071@qq.com"
git config --global user.name "PaiduiXiaowangzi"
pnpm install
git add pnpm-lock.yaml
git commit -m'gh自动更新了 pnpm-lock'
git push
dependencies-update:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up pnpm
uses: pnpm/action-setup@v2
with:
version: 9.12.3
- name: Install depencies
run: |
pnpm install
- name: Update outdated dependencies
run: |
echo "dependencies-update执行成功"
pnpm outdated
pnpm update
pnpm test
- name: Commit update code
#上面成功了才执行
if: success()
run: |
pnpm add .
pnpm commit -m'gh自动更新依赖'
说明:
- depcheck:检查项目中的无用依赖,并报告,如果有无用依赖,CI 会失败,提醒开发者清理。
- version-consistency:通过
pnpm audit检查依赖版本的一致性,并确保所有子包使用相同版本的依赖。 - lockfile-update:清理无用依赖并确保
pnpm-lock.yaml文件是最新的,避免出现版本冲突。 - dependency-update:检查并更新所有过时的依赖,确保项目始终使用最新的依赖版本。