一、为什么需要共享依赖?从“搬家”说起
想象一下,你正在开发一个电商平台。这个平台不是一个大而全的单个应用,而是由好几个独立但又紧密联系的小模块组成的,比如用户中心、商品管理、订单服务和前端展示层。在以前,我们可能会把这些模块都塞进一个巨大的项目里,结果就是代码越写越乱,牵一发而动全身。
现在更流行的做法是“微服务”或者“单体仓库(Monorepo)”,也就是把这些独立的模块放在同一个代码仓库里,但各自管理。这就好比你和几个室友合租了一套大房子,每个人有自己的房间(独立项目),但共享客厅、厨房和卫生间(公共依赖)。
这时候问题就来了:如果每个室友都自己买一台冰箱、装一个洗衣机,不仅浪费钱,房子也塞不下。在项目里,如果每个子模块都自己安装一遍 React、Lodash 或 TypeScript,会导致:
- 磁盘空间爆炸:
node_modules文件夹被重复安装很多次。 - 安装慢如蜗牛:每次
npm install或yarn都要下载和安装大量重复的包。 - 版本管理噩梦:今天在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 会:
- 扫描所有
workspaces配置下的package.json。 - 将外部依赖(如
express,lodash,typescript)提升到根目录的node_modules中。 - 对于内部依赖(如
@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 设置成功了!
三、深入理解:应用场景、优点与坑点
应用场景:
- 全栈Monorepo项目:前端(React/Vue)和后端(Node.js)项目共享
TypeScript配置、ESLint规则、工具函数等。 - 多包库开发:比如你正在开发一个UI组件库,包含核心库
ui-core、React封装ui-react、Vue封装ui-vue。它们可以共享构建流程、测试工具和文档生成器。 - 微服务前端聚合:多个相对独立但共享设计语言和基础组件的管理后台,可以放在一个仓库里。
- 工具链统一:确保公司内所有项目使用的
eslint、prettier、jest等版本和配置完全一致。
技术优点:
- 极致的依赖管理:根目录一个
node_modules,杜绝重复,安装速度显著提升。 - 跨项目链接(Cross-link):内部包通过符号链接引用,开发时修改即生效,极大提升联调效率。
- 原子化提交:可以一次性提交涉及多个模块的更改,保持提交历史的逻辑完整性,便于回滚和追踪。
- 统一的工具和配置:可以方便地在根目录配置
Husky钩子、CI/CD脚本,对所有子项目生效。 - 简化发布流程:配合
Lerna或Yarn Changesets等工具,可以自动化管理内部包的版本号和发布。
需要注意的坑点(避坑指南):
- 依赖提升的“幽灵依赖”问题:这是最容易踩的坑!假设
packageA没有在package.json里声明依赖lodash,但因为根目录的node_modules里有,packageA的代码里require('lodash')可能依然能运行。一旦这个项目被单独移出Monorepo,就会立刻报错。务必确保每个包的package.json都完整声明其所有依赖。 - 不同项目需要不同版本依赖:Workspaces 默认会将依赖提升到根目录,如果
packageA需要lodash@^4,而packageB需要lodash@^3,Yarn 会尝试协商一个兼容版本,如果失败,可能会报错或产生不可预知的行为。这时可能需要使用yarn的resolutions字段或考虑是否适合放在同一个Workspace。 - 构建工具和IDE的适配:一些工具(如Webpack, Jest)和IDE(如WebStorm)需要正确配置才能理解这种符号链接。通常需要设置
preserveSymlinks或使用jest的moduleNameMapper。 - 根目录的“私密性”:记得将根目录的
package.json设置为"private": true,防止误发布。 - 巨型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的威力:
- 一键安装所有依赖:
lerna bootstrap已经被yarn install替代,更高效。 - 运行所有包的脚本:
lerna run build和我们的yarn workspaces run build类似。 - 强大的版本发布:这是Lerna的核心。
lerna changed:查看自上次发布以来哪些包发生了更改。lerna version:交互式地更新发生更改的包的版本号,并打上Git Tag。lerna publish from-package:将新版本的包发布到NPM仓库。
- 跨包操作:
lerna add axios --scope=@my-monorepo/web-app可以方便地为指定包添加依赖。
通过结合Yarn Workspaces和Lerna,你就能获得一个从本地开发、依赖管理到版本发布的全流程高效Monorepo解决方案。
五、总结与最佳实践清单
Yarn Workspaces 是现代前端和Node.js工程化中管理复杂项目结构的利器。它通过共享依赖和符号链接,将多个独立又关联的项目凝聚成一个有机整体。
最后,送上一份简洁的最佳实践清单,助你用好Workspaces:
- 规划先行:设计清晰的目录结构,如
packages/(业务包)、libs/(基础库)、apps/(应用)。 - 命名规范:内部包使用统一的scope,如
@my-org/package-name,避免命名冲突。 - 显式声明:每个包的
package.json必须完整、准确地声明其所有依赖,杜绝“幽灵依赖”。 - 善用根脚本:在根目录
package.json中定义常用的组合命令,如build:all、test:all,提升团队效率。 - 版本策略:对于紧密关联的包,考虑使用“固定模式”;对于独立演进的包,使用“独立模式”。使用
Lerna或Changesets来管理发布流程。 - CI/CD集成:在CI流水线中,可以利用
lerna changed或类似工具实现按需构建和部署,只对发生变更的包进行操作,加快流水线速度。 - 团队共识:确保团队成员都理解Workspaces的工作原理,特别是符号链接和依赖提升的概念,并建立相应的开发、提交和发布规范。
拥抱Yarn Workspaces,就像是给你的项目团队分配了一位高效的管家,它让依赖管理变得井井有条,让跨项目开发变得流畅自然。从今天开始,尝试将你的下一个项目拆分成Workspaces,亲身体验这种高效与优雅吧。
评论