一、为什么需要共享依赖?从“搬家”说起

想象一下,你正在开发一个电商平台。这个平台不是一个大而全的单个应用,而是由好几个独立但又紧密联系的小模块组成的,比如用户中心、商品管理、订单服务和前端展示层。在以前,我们可能会把这些模块都塞进一个巨大的项目里,结果就是代码越写越乱,牵一发而动全身。

现在更流行的做法是“微服务”或者“单体仓库(Monorepo)”,也就是把这些独立的模块放在同一个代码仓库里,但各自管理。这就好比你和几个室友合租了一套大房子,每个人有自己的房间(独立项目),但共享客厅、厨房和卫生间(公共依赖)。

这时候问题就来了:如果每个室友都自己买一台冰箱、装一个洗衣机,不仅浪费钱,房子也塞不下。在项目里,如果每个子模块都自己安装一遍 ReactLodashTypeScript,会导致:

  1. 磁盘空间爆炸node_modules 文件夹被重复安装很多次。
  2. 安装慢如蜗牛:每次 npm installyarn 都要下载和安装大量重复的包。
  3. 版本管理噩梦:今天在A模块升级了 Lodash,明天B模块可能因为版本不一致而出奇怪的Bug。

Yarn Workspaces 就是来解决这个“合租烦恼”的管家。它允许我们在仓库的根目录统一安装依赖,各个子项目可以“链接”到这些共享的依赖,就像大家共用客厅一样。这样既节省空间,又能保证所有模块使用的核心库版本一致。

二、手把手搭建你的第一个 Workspace

让我们抛开理论,直接动手创建一个。我们将使用 Node.js + TypeScript 这个单一技术栈来演示,因为它在前端和后端都极为常见。

第一步:创建项目骨架 首先,我们在空文件夹里初始化项目,并创建子项目的目录。

# 初始化根目录的 package.json
mkdir my-monorepo && cd my-monorepo
yarn init -y

# 创建两个子项目:一个工具包,一个Web应用
mkdir packages
mkdir packages/shared-utils
mkdir packages/web-app

第二步:配置根目录的 package.json 这是启用 Workspaces 功能的关键。我们需要修改根目录的 package.json,告诉 Yarn 我们的“房间”(workspaces)都在哪里。

// 根目录 ./package.json
{
  "name": "my-monorepo",
  "private": true, // 通常Monorepo根目录本身不是要发布的包
  "workspaces": [
    "packages/*" // 使用通配符,表示packages下的所有文件夹都是一个workspace
  ],
  "scripts": {
    "build": "yarn workspaces run build" // 一个便捷命令,在所有子项目中运行build脚本
  }
}

第三步:为子项目配置独立的 package.json 每个子项目都是独立的,拥有自己的名称、版本和依赖声明。

// ./packages/shared-utils/package.json
{
  "name": "@my-monorepo/shared-utils", // 使用@scope命名,清晰且避免公共包名冲突
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts", // 提供TypeScript类型声明
  "scripts": {
    "build": "tsc" // 编译TypeScript
  },
  "dependencies": {
    // 这里通常放运行时依赖,但因为这个是工具包,暂时没有外部依赖
  },
  "devDependencies": {
    // 开发依赖会在根目录统一安装
  }
}
// ./packages/web-app/package.json
{
  "name": "@my-monorepo/web-app",
  "version": "1.0.0",
  "scripts": {
    "start": "node dist/index.js",
    "build": "tsc"
  },
  "dependencies": {
    "@my-monorepo/shared-utils": "1.0.0", // 关键!将兄弟项目作为依赖
    "express": "^4.18.0", // 一个共享的外部依赖
    "lodash": "^4.17.0"   // 另一个共享的外部依赖
  }
}

第四步:在根目录安装所有依赖 现在,我们不需要进入每个子文件夹去 yarn install。只需要在根目录执行一条命令:

yarn install

神奇的事情发生了!Yarn 会:

  1. 扫描所有 workspaces 配置下的 package.json
  2. 将外部依赖(如 express, lodash, typescript提升到根目录的 node_modules 中。
  3. 对于内部依赖(如 @my-monorepo/shared-utils),Yarn 会在根目录的 node_modules 里创建一个符号链接(symlink),直接指向 ./packages/shared-utils 目录。这意味着你在 web-app 里修改 shared-utils 的代码,效果是实时生效的,无需发布或重新安装。

第五步:编写代码并验证共享 让我们写点简单的代码来验证它是否工作。

// ./packages/shared-utils/src/index.ts
/**
 * 一个简单的工具函数,计算两个数的和
 * 这个包将被其他workspace共享使用
 */
export function add(a: number, b: number): number {
  return a + b;
}

// 再导出一个常用的常量
export const APP_NAME = 'My Monorepo App';
// ./packages/web-app/src/index.ts
import express from 'express';
import { add, APP_NAME } from '@my-monorepo/shared-utils'; // 像使用普通npm包一样导入内部包!
import _ from 'lodash';

const app = express();
const port = 3000;

app.get('/', (req, res) => {
  const numbers = [1, 2, 3, 4, 5];
  const sum = _.sum(numbers); // 使用共享的lodash库
  const message = `欢迎来到 ${APP_NAME}。来自共享工具包的计算结果:1 + 2 = ${add(1, 2)}。数组求和(使用Lodash):${sum}`;
  res.send(message);
});

app.listen(port, () => {
  console.log(`${APP_NAME} 的Web应用运行在 http://localhost:${port}`);
});

第六步:构建和运行 在根目录下,我们可以方便地管理所有子项目。

# 方式1:为所有子项目执行build脚本
yarn run build

# 方式2:只为指定的workspace执行命令
yarn workspace @my-monorepo/web-app run start

# 方式3:在指定workspace中添加一个依赖(会自动安装在根目录)
yarn workspace @my-monorepo/web-app add axios

打开浏览器访问 http://localhost:3000,你会看到页面成功调用了共享工具包的函数和常量,也使用了共享的 lodash 库。这证明我们的 Workspace 设置成功了!

三、深入理解:应用场景、优点与坑点

应用场景:

  1. 全栈Monorepo项目:前端(React/Vue)和后端(Node.js)项目共享 TypeScript 配置、ESLint 规则、工具函数等。
  2. 多包库开发:比如你正在开发一个UI组件库,包含核心库 ui-core、React封装 ui-react、Vue封装 ui-vue。它们可以共享构建流程、测试工具和文档生成器。
  3. 微服务前端聚合:多个相对独立但共享设计语言和基础组件的管理后台,可以放在一个仓库里。
  4. 工具链统一:确保公司内所有项目使用的 eslintprettierjest 等版本和配置完全一致。

技术优点:

  1. 极致的依赖管理:根目录一个 node_modules,杜绝重复,安装速度显著提升。
  2. 跨项目链接(Cross-link):内部包通过符号链接引用,开发时修改即生效,极大提升联调效率。
  3. 原子化提交:可以一次性提交涉及多个模块的更改,保持提交历史的逻辑完整性,便于回滚和追踪。
  4. 统一的工具和配置:可以方便地在根目录配置 Husky 钩子、CI/CD 脚本,对所有子项目生效。
  5. 简化发布流程:配合 LernaYarn Changesets 等工具,可以自动化管理内部包的版本号和发布。

需要注意的坑点(避坑指南):

  1. 依赖提升的“幽灵依赖”问题:这是最容易踩的坑!假设 packageA 没有在 package.json 里声明依赖 lodash,但因为根目录的 node_modules 里有,packageA 的代码里 require('lodash') 可能依然能运行。一旦这个项目被单独移出Monorepo,就会立刻报错。务必确保每个包的 package.json 都完整声明其所有依赖。
  2. 不同项目需要不同版本依赖:Workspaces 默认会将依赖提升到根目录,如果 packageA 需要 lodash@^4,而 packageB 需要 lodash@^3,Yarn 会尝试协商一个兼容版本,如果失败,可能会报错或产生不可预知的行为。这时可能需要使用 yarnresolutions 字段或考虑是否适合放在同一个Workspace。
  3. 构建工具和IDE的适配:一些工具(如Webpack, Jest)和IDE(如WebStorm)需要正确配置才能理解这种符号链接。通常需要设置 preserveSymlinks 或使用 jestmoduleNameMapper
  4. 根目录的“私密性”:记得将根目录的 package.json 设置为 "private": true,防止误发布。
  5. 巨型Monorepo的性能:当子项目数量极多时,yarn install 的解析和链接过程可能会变慢。需要良好的目录结构规划和可能的工具优化。

四、搭配Lerna:如虎添翼的版本与发布管理

Yarn Workspaces 解决了依赖安装和链接的问题,但还有一个重要问题:当我要发布 @my-monorepo/shared-utils 的新版本时,如何管理依赖它的 @my-monorepo/web-app 的版本号?

这时,Lerna 就闪亮登场了。它是一个专门为管理带有多个包的JavaScript项目而生的工具,与Yarn Workspaces是天作之合。

基本配置: 在根目录安装Lerna并初始化配置。

yarn add -W -D lerna # 使用 -W 表示安装在根目录workspace
npx lerna init

修改生成的 lerna.json,让它与Yarn Workspaces协同工作。

// ./lerna.json
{
  "$schema": "node_modules/lerna/schemas/lerna-schema.json",
  "version": "independent", // 采用独立版本模式,每个包可以有自己的版本号
  "npmClient": "yarn",     // 指定使用yarn
  "useWorkspaces": true,   // 关键!启用对yarn workspaces的支持
  "packages": [
    "packages/*"
  ]
}

Lerna的威力:

  1. 一键安装所有依赖lerna bootstrap 已经被 yarn install 替代,更高效。
  2. 运行所有包的脚本lerna run build 和我们的 yarn workspaces run build 类似。
  3. 强大的版本发布:这是Lerna的核心。
    • lerna changed:查看自上次发布以来哪些包发生了更改。
    • lerna version:交互式地更新发生更改的包的版本号,并打上Git Tag。
    • lerna publish from-package:将新版本的包发布到NPM仓库。
  4. 跨包操作lerna add axios --scope=@my-monorepo/web-app 可以方便地为指定包添加依赖。

通过结合Yarn Workspaces和Lerna,你就能获得一个从本地开发、依赖管理到版本发布的全流程高效Monorepo解决方案。

五、总结与最佳实践清单

Yarn Workspaces 是现代前端和Node.js工程化中管理复杂项目结构的利器。它通过共享依赖和符号链接,将多个独立又关联的项目凝聚成一个有机整体。

最后,送上一份简洁的最佳实践清单,助你用好Workspaces:

  1. 规划先行:设计清晰的目录结构,如 packages/(业务包)、libs/(基础库)、apps/(应用)。
  2. 命名规范:内部包使用统一的scope,如 @my-org/package-name,避免命名冲突。
  3. 显式声明:每个包的 package.json 必须完整、准确地声明其所有依赖,杜绝“幽灵依赖”。
  4. 善用根脚本:在根目录 package.json 中定义常用的组合命令,如 build:alltest:all,提升团队效率。
  5. 版本策略:对于紧密关联的包,考虑使用“固定模式”;对于独立演进的包,使用“独立模式”。使用 LernaChangesets 来管理发布流程。
  6. CI/CD集成:在CI流水线中,可以利用 lerna changed 或类似工具实现按需构建和部署,只对发生变更的包进行操作,加快流水线速度。
  7. 团队共识:确保团队成员都理解Workspaces的工作原理,特别是符号链接和依赖提升的概念,并建立相应的开发、提交和发布规范。

拥抱Yarn Workspaces,就像是给你的项目团队分配了一位高效的管家,它让依赖管理变得井井有条,让跨项目开发变得流畅自然。从今天开始,尝试将你的下一个项目拆分成Workspaces,亲身体验这种高效与优雅吧。