一、从一个常见的烦恼说起

想象一下,你正在开发一个大型前端应用,这个应用由好几个部分组成:一个用户看到的主网站(web-app)、一个给管理员用的后台(admin-panel),还有好几个被它们共同使用的、自己写的工具包(比如 shared-ui 组件库、 utils 通用函数库)。

传统的做法,是把每个部分都当成一个独立的项目,各自有一个自己的代码仓库。这下麻烦就来了:当你在 shared-ui 里改了一个按钮的样式,为了在 web-app 里用上这个新样式,你需要:

  1. shared-ui 里发布一个新版本(比如 1.0.1)。
  2. 跑到 web-app 的项目里,更新 package.json,把 shared-ui 的版本号改成 1.0.1
  3. 运行 npm installyarn install 来安装新版本。

如果 admin-panel 也用到了这个组件,你还得把第2、3步再重复一遍。这还只是两个项目,如果项目再多一些,这种重复劳动和版本同步会让人非常头疼。而且,web-appadmin-panel 可能都安装了 ReactLodash 这些相同的库,这意味着你的电脑里会存在多份一模一样的 React 代码,既占空间,安装也慢。

有没有一种办法,能把所有相关的项目放在一个“大仓库”里统一管理,让它们能轻松地互相引用,并且共享依赖呢?这就是 Monorepo(单一仓库) 的概念。而 Yarn Workspaces,就是 Yarn 这个包管理工具为我们提供的、用来高效管理 Monorepo 的“利器”。

二、初识Yarn Workspaces:它到底是什么?

你可以把 Yarn Workspaces 理解为一个“智能的管家”。这个管家管理着一个大宅子(Monorepo 仓库),宅子里有好几个房间(每个子项目,我们称之为 workspace工作区)。

这个管家的核心本领有三点:

  1. 依赖提升与共享:如果好几个房间都需要同样的家具(比如 React 这个库),管家不会给每个房间都买一套。他会买一套放在公共的客厅(仓库根目录的 node_modules)里,所有房间共用。这大大节省了空间(磁盘)和采购时间(安装时间)。
  2. 内部链接:如果 web-app 房间想用 shared-ui 房间里的一个定制花瓶(自己写的包),管家不会跑去外面的商店(npm仓库)买。他直接在内部创建一个“快捷方式”(符号链接),让 web-app 直接就能访问到 shared-ui 里最新的花瓶。你修改 shared-ui 的代码,web-app 里立刻就能生效,无需发布和更新版本号。
  3. 统一命令执行:管家可以帮你一次性给所有房间打扫卫生(运行命令)。比如,你一声令下“测试”,管家就会跑到每个有测试任务的房间去执行测试。

接下来,我们就通过一个完整的例子,看看这位“管家”具体是怎么工作的。

技术栈声明:以下所有示例均基于 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 目录下创建我们的四个“房间”。

  1. 创建共享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 };
    
  2. 创建工具函数包 (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 };
    
  3. 创建Web应用 (web-app)

    mkdir -p packages/web-app
    cd packages/web-app
    yarn init -y
    

    编辑它的 package.json关键来了:我们如何引用内部的 shared-uiutils?就像引用外部包一样,但版本号写 *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'));
    
  4. 创建管理后台 (admin-panel) 过程类似,我们让它也依赖 shared-uiutils

    // 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 里安装了 reactlodash 这些被多个工作区共享的依赖。
  • 根目录的 node_modules 里还有 @my-monorepo/shared-ui@my-monorepo/utils符号链接,直接指向 packages/ 下的真实目录。
  • 各个子包(如 web-app)自己的目录下,没有 node_modules 文件夹(或者只有极少数特例)。它们共享根目录的依赖。

这就是“依赖提升”和“内部链接”的直观体现。

第五步:运行与验证

现在,我们可以轻松地运行任何一个工作区的代码。

  1. 运行Web应用

    # 在根目录下,使用 yarn workspace <工作区名称> <命令>
    yarn workspace web-app node index.js
    

    输出:

    === Web App ===
    我是按钮:点击我
    转换大写: HELLO FROM WEB-APP
    Lodash 示例: fooBar
    

    成功!web-app 调用了 shared-uiutils 的代码。

  2. 运行管理后台

    yarn workspace admin-panel node index.js
    
  3. 为所有工作区运行相同命令(比如安装某个公共开发依赖):

    # 在根目录添加一个开发依赖,所有工作区可用(如测试框架Jest)
    yarn add -W jest
    

    -W 标志代表安装到根工作区(workspace root)。

  4. 在所有工作区运行脚本(如果它们定义了相同的脚本名,如 test):

    yarn workspaces run test
    

    这条命令会遍历所有工作区,如果该工作区的 package.json 里有 scripts.test,就执行它。

四、深入理解:场景、优缺点与注意事项

应用场景

  • 多包项目库:比如你正在开发一个像 BabelReactVue 3 这样的大型开源库,它由许多独立但关联的包(@babel/core@babel/parser等)组成。Monorepo是事实标准。
  • 全栈应用:前端(React/Vue)、后端(Node.js)、移动端(React Native)代码放在一起,共享业务模型和工具代码。
  • 微前端架构:多个相对独立的前端应用(微前端)共存于一个仓库,便于协调和共享基础组件。
  • 公司内部项目群:多个中后台项目,共用一套组件库、工具函数和构建配置。

技术优缺点

优点:

  1. 代码共享极其方便:内部包直接源码引用,修改立即生效,极大提升开发效率和重构安全性。
  2. 依赖管理高效:依赖提升减少重复安装,节省磁盘空间和安装时间。一个 yarn install 搞定所有。
  3. 版本一致性:所有子项目共享第三方库的版本(由根 yarn.lock 锁定),避免因版本不同导致的诡异bug。
  4. 原子化提交:一次提交可以跨多个项目,便于进行关联更改和回溯。
  5. 统一工具链:可以方便地在根目录配置统一的代码格式化(Prettier)、代码检查(ESLint)、构建工具等。

缺点与挑战:

  1. 仓库体积增长快:所有代码历史在一起,仓库克隆时间变长,对Git工具有一定压力(但可通过 git sparse-checkout 等缓解)。
  2. 权限粒度变粗:很难精细控制每个人对每个子目录的访问权限(如果使用Git)。
  3. 构建复杂度增加:需要工具来识别哪些包因代码改动需要重新构建/测试,而不是每次都全量构建。这通常需要引入如 TurborepoNxLerna(与Yarn Workspaces结合使用)等更高级的构建系统。
  4. 对IDE的挑战:需要IDE很好地支持Monorepo结构,才能正确进行代码跳转、引用和提示。

核心注意事项

  1. 私有根项目:根目录的 package.json 通常应设置 "private": true,因为它本身不作为一个可发布的包。
  2. 使用符号链接:理解内部依赖是通过符号链接实现的。某些深度依赖文件路径解析的库或工具(如Webpack的某些旧插件)可能会有问题,但现代工具链基本都已支持。
  3. 谨慎选择依赖安装位置:使用 yarn add 时注意:
    • 在子包目录下:yarn add lodash 会安装到根 node_modules(提升),但依赖项会写入该子包的 package.json
    • 在根目录下:yarn add lodash 会报错(因为根项目不是包)。必须用 yarn add -W lodash 安装到根。
    • 给特定工作区安装:yarn workspace web-app add axios
  4. 与Lerna等工具配合:Yarn Workspaces解决了依赖安装和链接问题,而 Lerna 更擅长版本发布(统一或独立升版)、生成变更日志(CHANGELOG)和运行跨包复杂脚本。两者常结合使用,取长补短。

五、总结

Yarn Workspaces 为我们管理Monorepo项目提供了一套优雅而强大的原生解决方案。它通过“依赖提升”和“内部链接”两大核心机制,解决了多包项目依赖混乱、安装冗余、内部协作低效的痛点。虽然它并非银弹,在仓库规模和构建优化上会带来新的挑战,但对于有一定规模、且包之间存在紧密协作的前端/Node.js项目来说,它能带来的开发体验和协作效率的提升是巨大的。

上手Yarn Workspaces并不复杂,从一个小型的、包含两三个内部包的项目开始实践,你很快就能体会到这位“智能管家”带来的便利。随着项目增长,再考虑引入像 Turborepo 这样的构建加速工具,就能驾驭更复杂的Monorepo场景了。