一、什么是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');

为什么能直接引用呢?因为:

  1. 其他package依赖了lodash,被提升到根node_modules
  2. 项目根package.json可能依赖了shared-utils
  3. Node.js模块查找机制会向上搜索node_modules

这会导致三个典型问题:

  1. 代码在本地运行正常,但单独发布package时爆炸💥
  2. 不同环境安装的依赖版本可能不同
  3. 团队新成员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"
  }
}

这相当于给依赖提升戴上了紧箍咒。

四、最佳实践与经验总结

经过多个大型项目实战,我总结出这些黄金法则:

  1. 新项目:直接用Yarn Berry,它的PnP模式能彻底解决依赖提升问题
  2. 存量项目:先用dependency-check扫雷,再逐步添加nohoist规则
  3. 组件库开发:每个package必须通过dependency-check才能发布
  4. 团队协作:在README最显眼位置注明依赖安装的特殊要求
  5. 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'"
  }
}

最后提醒三个常见坑:

  1. 某些VSCode插件会依赖提升后的模块,导致nohoist配置下插件报错
  2. Docker构建时如果缓存了node_modules,可能会忽略nohoist规则
  3. 如果用了React Native,metro打包器对依赖提升有特殊要求

记住:依赖管理就像整理房间,看起来整齐的(提升)不一定好用,关键是要建立清晰的归属关系!