一、什么是Yarn工作区依赖提升
咱们前端同学在用Yarn管理项目时,经常会遇到多个子项目共用依赖的情况。这时候Yarn Workspaces(工作区)就派上用场了,它允许我们在一个根目录下管理多个package,并且可以共享node_modules。但这里有个特别有意思的现象叫"依赖提升"(Hoisting),简单说就是Yarn会尽量把相同的依赖提到根目录的node_modules里,避免重复安装。
举个栗子🌰,假设我们有个monorepo项目结构如下:
project-root/
├── package.json
├── packages/
│ ├── component-a/
│ │ └── package.json
│ └── component-b/
│ └── package.json
当component-a和component-b都依赖lodash时,Yarn默认会把lodash提升到根目录的node_modules,而不是在每个子package里都装一份。这就像把公共图书放在学校图书馆,而不是每个班级都买一套。
二、依赖提升带来的问题
但是!这个看起来很聪明的机制有时候会带来麻烦。最常见的问题就是"幽灵依赖"(Phantom Dependency)—— 某些没在package.json里声明的依赖,居然能在代码里直接require进来!
来看个真实案例(技术栈:Node.js + React):
// packages/component-a/index.js
const _ = require('lodash'); // 注意:component-a的package.json里没声明lodash!
const shared = require('shared-utils'); // 这个也没声明!
module.exports = () => _.capitalize('hello');
为什么能直接引用呢?因为:
- 其他package依赖了lodash,被提升到根node_modules
- 项目根package.json可能依赖了shared-utils
- Node.js模块查找机制会向上搜索node_modules
这会导致三个典型问题:
- 代码在本地运行正常,但单独发布package时爆炸💥
- 不同环境安装的依赖版本可能不同
- 团队新成员clone项目后一脸懵逼:"这依赖哪来的?"
三、问题解决方案大全
方案1:禁用依赖提升
在根package.json里加个配置:
{
"workspaces": {
"nohoist": ["**"]
}
}
这相当于告诉Yarn:"别自作聪明提升依赖,老老实实每个package都装自己的"。缺点是node_modules体积会变大,安装变慢。
方案2:精确控制提升白名单
更优雅的做法是只提升确定安全的依赖:
{
"workspaces": {
"nohoist": [
"**/react",
"**/react-dom",
"**/lodash"
]
}
}
这样只有明确列出的依赖会被提升,其他都乖乖待在各自package里。
方案3:使用工具强制检查
安装dependency-check工具:
yarn add -D dependency-check
然后在CI脚本里加检查:
# 检查所有package是否有幽灵依赖
find packages -name 'package.json' | xargs -I {} dirname {} | xargs -I {} sh -c 'cd {} && dependency-check ./ --missing'
方案4:升级到Yarn Berry
Yarn 2+(Berry版本)引入了更严格的依赖解析模式:
yarn set version berry
echo "nodeLinker: node-modules" >> .yarnrc.yml
然后在每个package里添加installConfig:
{
"installConfig": {
"hoistingLimits": "workspaces"
}
}
这相当于给依赖提升戴上了紧箍咒。
四、最佳实践与经验总结
经过多个大型项目实战,我总结出这些黄金法则:
- 新项目:直接用Yarn Berry,它的PnP模式能彻底解决依赖提升问题
- 存量项目:先用dependency-check扫雷,再逐步添加nohoist规则
- 组件库开发:每个package必须通过
dependency-check才能发布 - 团队协作:在README最显眼位置注明依赖安装的特殊要求
- CI/CD:必须包含幽灵依赖检查步骤
举个完整示例(技术栈:React + TypeScript monorepo):
// 根package.json
{
"private": true,
"workspaces": {
"packages": ["packages/*"],
"nohoist": [
"**/eslint",
"**/webpack" // 这些工具链依赖不提升
]
},
"scripts": {
"postinstall": "find packages -name package.json | xargs -I {} dirname {} | xargs -I {} sh -c 'cd {} && dependency-check ./ --missing'"
}
}
最后提醒三个常见坑:
- 某些VSCode插件会依赖提升后的模块,导致nohoist配置下插件报错
- Docker构建时如果缓存了node_modules,可能会忽略nohoist规则
- 如果用了React Native,metro打包器对依赖提升有特殊要求
记住:依赖管理就像整理房间,看起来整齐的(提升)不一定好用,关键是要建立清晰的归属关系!
评论