一、从一个常见的烦恼说起
想象一下,你正在开发一个大型前端应用,这个应用由好几个部分组成:一个用户看到的主网站(web-app)、一个给管理员用的后台(admin-panel),还有好几个被它们共同使用的、自己写的工具包(比如 shared-ui 组件库、 utils 通用函数库)。
传统的做法,是把每个部分都当成一个独立的项目,各自有一个自己的代码仓库。这下麻烦就来了:当你在 shared-ui 里改了一个按钮的样式,为了在 web-app 里用上这个新样式,你需要:
- 在
shared-ui里发布一个新版本(比如1.0.1)。 - 跑到
web-app的项目里,更新package.json,把shared-ui的版本号改成1.0.1。 - 运行
npm install或yarn install来安装新版本。
如果 admin-panel 也用到了这个组件,你还得把第2、3步再重复一遍。这还只是两个项目,如果项目再多一些,这种重复劳动和版本同步会让人非常头疼。而且,web-app 和 admin-panel 可能都安装了 React、Lodash 这些相同的库,这意味着你的电脑里会存在多份一模一样的 React 代码,既占空间,安装也慢。
有没有一种办法,能把所有相关的项目放在一个“大仓库”里统一管理,让它们能轻松地互相引用,并且共享依赖呢?这就是 Monorepo(单一仓库) 的概念。而 Yarn Workspaces,就是 Yarn 这个包管理工具为我们提供的、用来高效管理 Monorepo 的“利器”。
二、初识Yarn Workspaces:它到底是什么?
你可以把 Yarn Workspaces 理解为一个“智能的管家”。这个管家管理着一个大宅子(Monorepo 仓库),宅子里有好几个房间(每个子项目,我们称之为 workspace 或 工作区)。
这个管家的核心本领有三点:
- 依赖提升与共享:如果好几个房间都需要同样的家具(比如
React这个库),管家不会给每个房间都买一套。他会买一套放在公共的客厅(仓库根目录的node_modules)里,所有房间共用。这大大节省了空间(磁盘)和采购时间(安装时间)。 - 内部链接:如果
web-app房间想用shared-ui房间里的一个定制花瓶(自己写的包),管家不会跑去外面的商店(npm仓库)买。他直接在内部创建一个“快捷方式”(符号链接),让web-app直接就能访问到shared-ui里最新的花瓶。你修改shared-ui的代码,web-app里立刻就能生效,无需发布和更新版本号。 - 统一命令执行:管家可以帮你一次性给所有房间打扫卫生(运行命令)。比如,你一声令下“测试”,管家就会跑到每个有测试任务的房间去执行测试。
接下来,我们就通过一个完整的例子,看看这位“管家”具体是怎么工作的。
技术栈声明:以下所有示例均基于 Node.js 技术栈,使用 Yarn 作为包管理器。
三、手把手实战:构建你的第一个Monorepo项目
让我们来创建一个模拟上述场景的项目。最终的项目结构会是这样:
my-monorepo/
├── package.json
├── yarn.lock
├── packages/
│ ├── shared-ui/
│ │ ├── package.json
│ │ └── index.js
│ ├── utils/
│ │ ├── package.json
│ │ └── index.js
│ ├── web-app/
│ │ ├── package.json
│ │ └── index.js
│ └── admin-panel/
│ ├── package.json
│ └── index.js
第一步:初始化根项目
首先,创建一个总文件夹,并初始化它。
mkdir my-monorepo && cd my-monorepo
yarn init -y
这会生成一个根目录的 package.json 文件。这是“管家”的配置文件。
第二步:启用Workspaces功能
修改根目录的 package.json,告诉Yarn:“我这个项目用的是workspaces模式,我所有的‘房间’都放在 packages 目录下”。
// 根目录 package.json
{
"name": "my-monorepo",
"private": true, // 通常Monorepo根目录是私有的,不发布
"workspaces": [
"packages/*" // 使用通配符指定所有工作区位于packages文件夹下
],
"version": "1.0.0",
"license": "MIT"
}
第三步:创建各个工作区(子项目)
在 packages 目录下创建我们的四个“房间”。
创建共享UI包 (
shared-ui)mkdir -p packages/shared-ui cd packages/shared-ui yarn init -y编辑它的
package.json,给它起个名字,这是我们内部引用它的依据。// packages/shared-ui/package.json { "name": "@my-monorepo/shared-ui", // 使用scope(@组织名/包名)是常见做法,避免命名冲突 "version": "1.0.0", "main": "index.js" }创建一个简单的组件函数:
// packages/shared-ui/index.js // 一个简单的“按钮”组件 function MyButton(props) { return `我是按钮:${props.text}`; } module.exports = { MyButton };创建工具函数包 (
utils)# 在根目录执行 mkdir -p packages/utils cd packages/utils yarn init -y// packages/utils/package.json { "name": "@my-monorepo/utils", "version": "1.0.0", "main": "index.js" }// packages/utils/index.js // 一个工具函数,将字符串转为大写 function toUpperCase(str) { return str.toUpperCase(); } module.exports = { toUpperCase };创建Web应用 (
web-app)mkdir -p packages/web-app cd packages/web-app yarn init -y编辑它的
package.json。关键来了:我们如何引用内部的shared-ui和utils?就像引用外部包一样,但版本号写*或workspace:*,表示总是使用本地工作区版本。// packages/web-app/package.json { "name": "web-app", "version": "1.0.0", "main": "index.js", "dependencies": { "lodash": "^4.17.21", // 外部依赖 "react": "^18.2.0", // 外部依赖 "@my-monorepo/shared-ui": "*", // 内部依赖!使用通配符*指向本地workspace "@my-monorepo/utils": "workspace:*" // 另一种写法,效果相同,更清晰 } }// packages/web-app/index.js const { MyButton } = require('@my-monorepo/shared-ui'); const { toUpperCase } = require('@my-monorepo/utils'); const _ = require('lodash'); console.log('=== Web App ==='); console.log(MyButton({ text: '点击我' })); console.log('转换大写:', toUpperCase('hello from web-app')); console.log('Lodash 示例:', _.camelCase('foo bar'));创建管理后台 (
admin-panel) 过程类似,我们让它也依赖shared-ui和utils。// packages/admin-panel/package.json { "name": "admin-panel", "version": "1.0.0", "dependencies": { "react": "^18.2.0", "@my-monorepo/shared-ui": "*", "@my-monorepo/utils": "*" } }// packages/admin-panel/index.js const { MyButton } = require('@my-monorepo/shared-ui'); const { toUpperCase } = require('@my-monorepo/utils'); console.log('=== Admin Panel ==='); console.log(MyButton({ text: '管理按钮' })); console.log('转换大写:', toUpperCase('hello from admin'));
第四步:让管家上岗——安装所有依赖
现在,所有“房间”都准备好了。我们回到大宅子的门口(根目录),让管家(Yarn)一次性采购并布置好所有家具(安装依赖)。
cd ../../.. # 回到根目录 my-monorepo
yarn install
运行这个命令后,你会看到:
- 根目录下生成了一个
node_modules文件夹和一个yarn.lock文件。 - 根目录的
node_modules里安装了react、lodash这些被多个工作区共享的依赖。 - 根目录的
node_modules里还有@my-monorepo/shared-ui和@my-monorepo/utils的符号链接,直接指向packages/下的真实目录。 - 各个子包(如
web-app)自己的目录下,没有node_modules文件夹(或者只有极少数特例)。它们共享根目录的依赖。
这就是“依赖提升”和“内部链接”的直观体现。
第五步:运行与验证
现在,我们可以轻松地运行任何一个工作区的代码。
运行Web应用:
# 在根目录下,使用 yarn workspace <工作区名称> <命令> yarn workspace web-app node index.js输出:
=== Web App === 我是按钮:点击我 转换大写: HELLO FROM WEB-APP Lodash 示例: fooBar成功!
web-app调用了shared-ui和utils的代码。运行管理后台:
yarn workspace admin-panel node index.js为所有工作区运行相同命令(比如安装某个公共开发依赖):
# 在根目录添加一个开发依赖,所有工作区可用(如测试框架Jest) yarn add -W jest-W标志代表安装到根工作区(workspace root)。在所有工作区运行脚本(如果它们定义了相同的脚本名,如
test):yarn workspaces run test这条命令会遍历所有工作区,如果该工作区的
package.json里有scripts.test,就执行它。
四、深入理解:场景、优缺点与注意事项
应用场景
- 多包项目库:比如你正在开发一个像
Babel、React、Vue 3这样的大型开源库,它由许多独立但关联的包(@babel/core,@babel/parser等)组成。Monorepo是事实标准。 - 全栈应用:前端(React/Vue)、后端(Node.js)、移动端(React Native)代码放在一起,共享业务模型和工具代码。
- 微前端架构:多个相对独立的前端应用(微前端)共存于一个仓库,便于协调和共享基础组件。
- 公司内部项目群:多个中后台项目,共用一套组件库、工具函数和构建配置。
技术优缺点
优点:
- 代码共享极其方便:内部包直接源码引用,修改立即生效,极大提升开发效率和重构安全性。
- 依赖管理高效:依赖提升减少重复安装,节省磁盘空间和安装时间。一个
yarn install搞定所有。 - 版本一致性:所有子项目共享第三方库的版本(由根
yarn.lock锁定),避免因版本不同导致的诡异bug。 - 原子化提交:一次提交可以跨多个项目,便于进行关联更改和回溯。
- 统一工具链:可以方便地在根目录配置统一的代码格式化(Prettier)、代码检查(ESLint)、构建工具等。
缺点与挑战:
- 仓库体积增长快:所有代码历史在一起,仓库克隆时间变长,对Git工具有一定压力(但可通过
git sparse-checkout等缓解)。 - 权限粒度变粗:很难精细控制每个人对每个子目录的访问权限(如果使用Git)。
- 构建复杂度增加:需要工具来识别哪些包因代码改动需要重新构建/测试,而不是每次都全量构建。这通常需要引入如
Turborepo、Nx或Lerna(与Yarn Workspaces结合使用)等更高级的构建系统。 - 对IDE的挑战:需要IDE很好地支持Monorepo结构,才能正确进行代码跳转、引用和提示。
核心注意事项
- 私有根项目:根目录的
package.json通常应设置"private": true,因为它本身不作为一个可发布的包。 - 使用符号链接:理解内部依赖是通过符号链接实现的。某些深度依赖文件路径解析的库或工具(如Webpack的某些旧插件)可能会有问题,但现代工具链基本都已支持。
- 谨慎选择依赖安装位置:使用
yarn add时注意:- 在子包目录下:
yarn add lodash会安装到根node_modules(提升),但依赖项会写入该子包的package.json。 - 在根目录下:
yarn add lodash会报错(因为根项目不是包)。必须用yarn add -W lodash安装到根。 - 给特定工作区安装:
yarn workspace web-app add axios。
- 在子包目录下:
- 与Lerna等工具配合:Yarn Workspaces解决了依赖安装和链接问题,而 Lerna 更擅长版本发布(统一或独立升版)、生成变更日志(CHANGELOG)和运行跨包复杂脚本。两者常结合使用,取长补短。
五、总结
Yarn Workspaces 为我们管理Monorepo项目提供了一套优雅而强大的原生解决方案。它通过“依赖提升”和“内部链接”两大核心机制,解决了多包项目依赖混乱、安装冗余、内部协作低效的痛点。虽然它并非银弹,在仓库规模和构建优化上会带来新的挑战,但对于有一定规模、且包之间存在紧密协作的前端/Node.js项目来说,它能带来的开发体验和协作效率的提升是巨大的。
上手Yarn Workspaces并不复杂,从一个小型的、包含两三个内部包的项目开始实践,你很快就能体会到这位“智能管家”带来的便利。随着项目增长,再考虑引入像 Turborepo 这样的构建加速工具,就能驾驭更复杂的Monorepo场景了。
评论