🧱 从“为什么要用 Monorepo”到“亲手跑通一个 Demo”:前端 Monorepo 完全入门指南
作者:一名曾对 Monorepo 充满疑惑的普通前端开发者
前言:我曾经也觉得 Monorepo 是“过度设计”
几个月前,我在 GitHub 上看到 Vue 3、Babel、Turborepo 这些明星项目都用 Monorepo,心里直犯嘀咕:
“Vue 不就是一个框架吗?干嘛非要把代码拆成十几个包,还塞进一个仓库?
我直接import相对路径不就行了?npm/pnpm 不是已经能管理依赖了吗?”
直到我尝试开发一个包含 两个应用 + 共享组件库 的小项目,才真正体会到:Monorepo 不是炫技,而是解决真实痛点的工程方案。
今天,我就用一篇实战文,带你从“完全不懂”到“亲手跑通”,彻底搞懂 Monorepo。
一、先说结论:Monorepo 到底解决了什么问题?
❌ 传统多仓库(Multirepo)的痛:
- 改一行共享组件代码 → 要发版 → 升级依赖 → 测试 → 循环 5 分钟
- 多个项目配置重复(ESLint、TS、构建脚本)
- 跨项目调试困难,版本容易冲突
✅ Monorepo 的解法:
- 所有相关代码放在一个仓库
- 共享模块源码直连,修改立即生效
- 统一工具链,一次配置,处处生效
- 支持独立发布每个子包(可选)
💡 Monorepo 的核心价值:让“紧密耦合的模块”高效协同开发。
二、动手实验:5 分钟亲手复现“没有 Monorepo 的痛”
🎯 项目背景:
假设你正在开发一个公司内部的前端系统,包含:
- 商品管理页(web-a)
- 用户中心页(web-b)
- 两者都用到了同一个 共享按钮组件(SharedButton)
你希望这个按钮统一风格,所以把它抽成一个独立模块。
但问题来了:怎么让两个页面都能用它,而且改一次就能同时生效?
我们先试试“不用 Monorepo”的方式——也就是把共享组件当成一个普通 npm 包来用。
🔧 第一步:模拟“独立发布”的流程
# 1. 创建一个文件夹放共享组件
mkdir shared-button
cd shared-button
# 2. 初始化为一个 npm 包(就像你准备发布到 npm)
npm init -y
# 3. 写一个按钮组件(简化版)
echo 'export const SharedButton = () => "【旧版按钮】";' > index.js
# 4. 打包成 .tgz 文件(相当于“本地发布”)
npm pack
# 生成 shared-button-1.0.0.tgz
💡 这一步模拟的是:你把
SharedButton当成一个正式 npm 包,先“发布”出来。
🔧 第二步:在商品页(web-a)中使用它
# 回到上一级目录
cd ..
# 创建商品页项目
mkdir web-a
cd web-a
npm init -y
# 安装刚才打包的“共享按钮”
npm install ../shared-button/shared-button-1.0.0.tgz
# 写一个简单的页面脚本
echo 'const { SharedButton } = require("shared-button"); console.log(SharedButton());' > index.js
现在运行:
node index.js
# 输出:【旧版按钮】
✅ 看起来一切正常!
🔧 第三步:修改按钮,看会发生什么?
产品说:“按钮文字要改成‘新版按钮’!”
你回到 shared-button 目录修改:
cd ../shared-button
echo 'export const SharedButton = () => "【新版按钮】";' > index.js
# 重新打包
npm pack
# 生成新的 shared-button-1.0.0.tgz
然后回到 web-a,再次运行:
cd ../web-a
node index.js
# 你猜输出什么?
# → 依然是:【旧版按钮】!
😱 为什么?
因为node_modules/shared-button里还是旧代码!
虽然你改了源文件,但web-a并不知道要更新。
🔧 第四步:手动“升级依赖”(痛苦开始了)
你必须手动:
# 先卸载旧版本
npm uninstall shared-button
# 再安装新打包的版本
npm install ../shared-button/shared-button-1.0.0.tgz
# 现在再运行
node index.js
# 输出:【新版按钮】✅
⏳ 每次改一行代码,都要重复这 3 步!
如果还有web-b、mobile-app、admin-panel……
你得在每个项目里都做一遍!
这就是 Multirepo 开发的真实日常 —— 效率极低,极易出错。
三、Monorepo 如何解决?理解 workspace 的真正含义
现在,我们用 Monorepo + pnpm workspaces 来解决这个问题。
🔍 什么是 workspace?
你可以把 workspace 理解为:
“这个仓库里,哪些文件夹是‘合法的内部包’?”
比如你告诉 pnpm:
“
packages/下的所有文件夹,都是我们自己写的包,别去 npm 找,直接用本地源码!”
pnpm 就会:
- 在安装依赖时,自动扫描这些目录
- 如果发现某个依赖(如
@my/ui)正好在packages/ui里 - 就在
node_modules里创建一个快捷方式(符号链接) ,指向packages/ui
→ 结果:你的代码直接引用源码,修改保存,立即生效!
🔧 实操:用 workspace 实现“源码直连”
第一步:创建 Monorepo 根目录
mkdir my-monorepo
cd my-monorepo
pnpm init
第二步:告诉 pnpm 哪些是“内部包”
创建文件 pnpm-workspace.yaml(注意是 .yaml,不是 .json):
packages:
- 'packages/*' # 所有 packages/ 下的文件夹都是内部包
- 'apps/*' # apps/ 下的是应用(也可选)
✅ 这个文件就是 workspace 的“身份证” —— 没有它,pnpm 不知道你是 Monorepo!
第三步:创建共享组件包
mkdir -p packages/ui
cd packages/ui
pnpm init
编辑 package.json,必须指定 name(带 scope 更规范):
{
"name": "@my-monorepo/ui",
"version": "1.0.0"
}
写组件:
// packages/ui/index.js
export const SharedButton = () => "【Monorepo 按钮】";
第四步:创建商品页(web-a)
cd ../..
mkdir -p apps/web-a
cd apps/web-a
pnpm init
关键一步:声明依赖时加上 workspace:*
pnpm add @my-monorepo/ui@workspace:*
这会在 package.json 中生成:
{
"dependencies": {
"@my-monorepo/ui": "workspace:*"
}
}
🔑
workspace:*的意思是:
“这个包来自本地 workspace,不要去 npm 拉,直接用源码!”
写页面脚本:
// apps/web-a/index.mjs
import { SharedButton } from '@my-monorepo/ui';
console.log(SharedButton());
第五步:在根目录安装依赖
cd ../..
pnpm install
💡 必须在根目录运行! pnpm 会读取
pnpm-workspace.yaml,建立所有链接。
🔧 第六步:见证奇迹
# 运行商品页
node apps/web-a/index.mjs
# 输出:【Monorepo 按钮】
# 修改共享组件
echo 'export const SharedButton = () => "【Monorepo 按钮 - 已更新!】";' > packages/ui/index.js
# 再次运行(无需任何操作!)
node apps/web-a/index.mjs
# 输出:【Monorepo 按钮 - 已更新!】✅
✨ 改完保存,直接生效!没有 uninstall,没有 install,没有等待!
🔍 验证:看看 pnpm 做了什么
在 apps/web-a 目录下运行:
ls -la node_modules/@my-monorepo/
你会看到:
ui -> ../../../../packages/ui
这就是一个 符号链接(symlink) ——
它让node_modules/@my-monorepo/ui指向packages/ui,
所以 Vite、Node.js 都能直接读到最新源码!
四、为什么不能直接用相对路径?
你可能会问:“我直接 import ... from '../../../packages/ui' 不行吗?”
技术上可以,但会带来隐藏成本:
| 问题 | 相对路径 | 包名引用 |
|---|---|---|
| 文件移动后 | 路径全崩 | 导入语句不变 |
| 模块封装性 | 侵入内部结构 | 遵守公开接口 |
| 工具链支持 | 需额外配置 | 开箱即用 |
| 未来发布 | 几乎不可能 | 一行命令发布 |
✅
@scope/name不仅是名字,更是模块的“身份证” —— 它让工具链知道:“这是一个独立单元”。
五、Monorepo 中的包能单独发布到 npm 吗?
完全可以!而且这是 Monorepo 的一大优势。
比如你的 @my-monorepo/ui,只需:
# 1. 构建(如有需要)
pnpm run build --filter @my-monorepo/ui
# 2. 发布
cd packages/ui
npm publish --access public
之后,任何人可以:
npm install @my-monorepo/ui
🌟 对内:本地协同开发;对外:独立 npm 包。两者无缝切换。
Vue 3 的 @vue/reactivity、Babel 的 @babel/core 都是这样工作的。
六、推荐技术栈(2026 年轻量级方案)
| 工具 | 作用 |
|---|---|
| pnpm | 高效包管理器(硬链接 + 符号链接) |
| pnpm workspaces | 声明哪些目录是“内部包” |
| Turborepo | 智能任务调度(增量构建、缓存) |
| Vite | 应用构建(支持 Monorepo 开箱即用) |
💡 不需要 Lerna、Rush 等重型工具,pnpm + Turborepo 足够覆盖 90% 场景。
七、什么时候该用 Monorepo?
✅ 适合:
- 多个应用共享组件/工具库
- 框架/库的多模块开发(如 Vue、Babel)
- 需要统一工程规范的团队项目
❌ 不适合:
- 单一简单应用
- 完全无关的项目(如电商 + 游戏后台)
📌 判断标准:这些项目是否需要频繁协同变更?
结语:Monorepo 不是银弹,但值得掌握
我曾经以为 Monorepo 是大厂专属,离我很远。
但当我亲手跑通第一个 Demo,看到“改一行代码,两个应用同时更新”的那一刻,才明白:
Monorepo 的本质,不是把代码塞进一个仓库,而是让协作回归简单。
如果你也在维护多个前端项目,不妨试试 Monorepo —— 它可能比你想象的更轻量、更实用。
📦 附:完整 Demo 代码结构
my-monorepo/
├── packages/
│ └── ui/ # 共享组件库
├── apps/
│ ├── web-a/ # 应用 A
│ └── web-b/ # 应用 B
├── pnpm-workspace.yaml # 声明工作区
├── turbo.json # Turborepo 配置
└── package.json # 根脚本
✨ 代码已开源,欢迎 Star & Fork:github.com/Jin82155140…
作者简介:一线前端工程师,热爱工程化与效率提升。欢迎关注我的掘金主页,获取更多实战干货!
💬 互动话题:你在项目中用过 Monorepo 吗?遇到了哪些坑?欢迎评论区交流!