10 年前端,我把踩过的所有坑熬成了一套"不会腐化"的 Vue3 Monorepo 底座
LYStack · 不是脚手架,是架构地基
做了 10 年前端,我前后用过不下 7 个脚手架。每一个刚上手都香,用一年就开始腐化:想换构建工具发现业务代码全绑死了,服务层和登录态耦得死死的,README 写的和代码早就对不上。
最后我把一个跑了多年的真实生产项目,一点点提纯成了一套干净的底座,叫 LYStack 。它不替你写业务,它给你的业务一个不会随时间腐化的地基。
这篇文章我想从最基础的痛点讲起------哪怕你只工作了两三年、没听过 monorepo,也能看懂为什么需要它。然后再带你一层层看清楚这套底座的设计。
先把它能干什么摆出来:
| 构建工具 | SPA(单页) | MPA(多页) | 本地启动 / 构建速度 |
|---|---|---|---|
| Vite 8 | ✅ | 🚧 Roadmap | 小项目快,项目大了变慢 |
| Rsbuild 2(Rspack) | ✅ | ✅ | 项目越大越快 |
关键不是"支持哪个工具",而是:业务代码一行不改,就能在它们之间切换。
一、你是不是也这样?
先不上术语,讲个你大概率经历过的场景。
你们公司有一个产品,它有三个端:内嵌在 App 里的 H5、PC 网页端、还有给运营用的管理后台。技术栈一模一样,都是 Vue3。但在代码层面,它们是三五个互相独立的小工程,各自一个仓库,各自一套配置。
于是这些事每天都在发生:
- 改一个公共的工具函数、一个接口封装、一个日期格式化方法,你得在每个工程里复制粘贴一遍。改漏一个,那个端就出线上 bug。
- 每个小工程一套自己的脚手架配置、一条自己的 Jenkins 流水线。工程越多,要维护的流水线越多,越堆越乱。
- 版本各搞各的:A 工程用
axios@1.6,B 工程还停在axios@1.3。哪天出了个诡异的请求 bug,你都不知道是不是版本差异导致的。
你能感觉到这很别扭、很累,但一时说不清到底哪里错了。
我来帮你说清楚:这些代码本来应该是共享的,却被物理隔离在了不同的工程里。 你每天在做的"复制粘贴同步",本质是在用人力,弥补架构上的缺失。
二、那 Monorepo 到底是什么?为什么能治这病
Monorepo 这个词听着唬人,其实一句话就能说明白:
把多个原本分散的工程,放进同一个仓库 里统一管理,让它们能直接共享代码、共用一套规范和版本。
打个比方。以前是"每家各开一个小作坊",水电自己拉、规章自己定、工具自己买,作坊之间要协作只能靠人来回跑腿。Monorepo 就是把这些作坊搬进同一个园区:水电(依赖)、规章(规范)、共享车间(公共代码包)统一供给。
对照前面那些痛点,它是这么逐条开药的:
- 公共逻辑抽成一个包,所有应用直接引用。 改一处,处处生效,彻底告别复制粘贴。
- 版本由一处统一管理(pnpm 的 catalog 机制做"单一真相源"),不再 A 工程一个版本、B 工程一个版本。
- 任务编排工具统一构建、测试(Turborepo),不用每个工程单独养一条 Jenkins 流水线。
LYStack 就是一套已经把这些都搭好的 monorepo 底座------你不用从零研究 pnpm workspace、catalog、Turborepo 怎么配,clone 下来就是一个能直接长出业务的园区。
它的内部长这样:
应用在上面长,公共能力在下面沉淀。改一次 @repo/shared 里的工具函数,三个应用同时生效------这就是"代码共享"四个字落到实处的样子。
三、先说清楚:LYStack 讲的是架构思路,不是 Vue3
往下读之前我想先把一件事讲明白,免得你因为"不用 Vue"就划走。
LYStack 真正的资产不是那几个 @repo/* 包,而是它落地的那五根支柱:
- 构建工具可插拔 ------
AppBuildOptions中立契约,业务代码不认识构建工具 - 服务层依赖反转 ------
configureServiceAuth把认证能力注入服务层,而不是写死 - env 收口 ------
getEnvfail loudly + ESLint 强制,环境变量不再散落 - MPA js-first 契约 ------
PageEntry作单一真相源 - AI 友好 ------
AGENTS.md+ 规范文档,让 AI 产出自动对齐团队约定
这五条搬到 React、Angular、Svelte 项目里照样成立。 React 项目用 Vite / Webpack / Rsbuild 也要解决构建可插拔;React 的 fetch 层也要解决认证注入;React 项目同样要收口 env、同样要给 AI 喂规范。
我用 Vue3 做参考实现------就像《设计模式》用 C++ 和 Smalltalk 写示例,但没人说"设计模式是 C++ 的"。框架决定了组件模型、响应式、生态,它当然重要;我说的是这套架构思路与框架无关,不是说框架本身无所谓。 所以如果你用 React,把 @repo/ui 换成你自己的组件库、把 <script setup> 换成函数组件就行------但别动那五根支柱,那才是 LYStack 的核心。
说明:我只提供了 Vue3 实现,没有 React / Angular 版本。这里讲的是"方法论可迁移",不是"开箱支持多框架",别误会。
四、为什么不能被构建工具绑死:Vite 与 Rsbuild 的真实取舍
讲下一个支柱之前,我得先铺垫一个很现实的问题,否则你不会明白底座为什么要在"构建工具"上下这么大功夫。
现在大家起项目,默认都用 Vite。小项目里它体验确实好,秒启动、热更新飞快。
但项目一大就开始有可感知的代价。 拿 Vite + Element Plus 这种大型组件库的组合来说,项目越做越大之后,你会发现:首次启动、首次编译看着快,实际体验越来越拖------首次预构建(dep optimization)耗时,加上浏览器原生 ESM 加载海量模块变慢,HMR 也开始变迟钝。这不是配置问题,是这套机制在大型项目下的固有代价。
而 Rsbuild(底层是 Rspack,Rust 实现)在大型项目下的编译表现更稳定 :打包性能和本地编译速度都是第一梯队,项目越大优势越明显。这不是跑分数据,是多个生产项目实战验证过 的结论。除了速度,它在多页应用(MPA)场景下还有构建机制上的天然契合(js-first),这点后面专门讲。
所以结论不是"Vite 不好",而是:
不同阶段、不同规模,需要不同的工具。 今天用 Vite 起步快,明天项目大了想切 Rsbuild 提速------问题来了:你能不能"业务代码一行不改"就切过去?
绝大多数项目做不到。因为 vite.config 和业务代码早就互相渗透了,换工具 ≈ 重构。这正是 LYStack 要解的题,也是下面"构建可插拔"这根支柱的由来。
五、为什么我自己又造一个底座
市面上脚手架那么多,我为什么还要自己造一个?因为我反复踩中它们的"三宗罪":
- 构建锁死:工具配置和业务代码缠在一起,想换构建器等于重构。
- 服务层耦合:axios 单例 + 全局拦截器,token 从哪来、401 跳哪里全写死在请求层,换个项目就得动手术。
- 文档脱节:README 和真实代码各说各话,新人照着文档干一脚踩空。
LYStack 是从一个长期维护、对接过 3 套异构后端的真实生产项目里蒸馏出来的。我把业务剥掉,只留下那些被时间反复验证过的架构骨架,并立了三条不肯妥协的设计原则:
- 构建可插拔------构建工具是可替换的实现细节,不是地基。
- 依赖方向受控------底层永远不知道上层的存在,依赖只能从外往里注入。
- 显式优于隐式------配置错了就当场报错,绝不静默兜底留个坑给你半夜排查。
下面五根支柱,就是这三条原则落到代码里的样子。
那你可能会问:市面上现成方案不少,为什么不直接拿来用?我都认真看过,它们各自解决的是另一类问题:
| 现成方案 | 为什么没直接用 |
|---|---|
| Nx | 功能很全但偏重,配置心智成本高,且对构建工具有较强假设,不符合我"构建可插拔"的诉求 |
| Turborepo 官方 examples | 解决的是任务编排(缓存、并行),不解决业务分层与依赖方向 |
| antfu/starter 等个人 starter | 个人风格鲜明、上手快,但不是面向企业级分层与多应用协作设计的 |
| Element Plus 等官方模板 | 绑定了具体 UI 库,且不覆盖 MPA 与多后端场景 |
LYStack 不和它们竞争------Turborepo 我自己也在用(就是上面的任务编排层)。它补的是这些方案普遍不管的那部分:业务分层、依赖反转、构建工具中立、文档与代码不脱节。
在进入细节之前,先给你一张全景图------五根支柱不是平行罗列,它们都从"三宗罪"长出来,又共同立在前面那三条设计原则之上:
读懂这张图,下面五节就是在逐根拆解它。
六、支柱一:构建工具可插拔
踩坑前史:上一个项目用 Webpack 配到一半,团队想换更快的构建工具,结果发现 loader、plugin、别名、环境注入全和 Webpack API 绑死,业务代码里还散落着一堆构建工具专有写法。最后评估下来"迁移成本 ≈ 重写配置",只能硬着头皮留在原地。那次之后我就认定:业务代码绝不能认识构建工具的脸。
别以为换成 Vite 就没事了------同样的故事在 Vite ⇄ Rsbuild 之间照样会重演:只要业务代码认识了工具的脸,换工具就是重写。回到第四节的问题:怎么才能"业务代码一行不改"地在 Vite 和 Rsbuild 之间切?
LYStack 的答案是:让应用只描述"我要构建什么",而不关心"用什么工具构建"。
核心是一份构建工具中立的契约 AppBuildOptions:
ts
// packages/build-config/src/types.ts
export interface AppBuildOptions {
kind: AppKind; // 'spa' | 'mpa'
appName: string;
root: string;
entry?: string; // SPA 单入口
pages?: PageEntry[]; // MPA 多入口
// ......全是中立字段,没有一个 vite/rsbuild 专有概念
}
应用的构建配置文件因此变得极薄------它只填这份契约,完全不碰构建工具的 API:
ts
// apps/example-vite/vite.config.ts
import { defineViteConfig, resolveRoot } from '@repo/build-config/vite';
export default defineViteConfig({
kind: 'spa',
appName: 'example-vite',
root: resolveRoot(import.meta.url),
entry: 'index.html',
});
想切到 Rsbuild?把 defineViteConfig 换成 defineRsbuildConfig,其余一字不改。真正的工具差异,全被关在 adapter 这一层里:
core 和 app 永远不会直接 import vite 或 import rsbuild------构建工具被彻底关进 adapter。这就是"今天 Vite、明天 Rsbuild,业务代码一行不改"的底气。
七、支柱二:服务层依赖反转
踩坑前史:有个项目从自研登录换成统一 SSO,本以为是后端的事,结果改起来发现------拦截器写死了 token 的读取、401 的判断、登录页的跳转,服务层"知道"的东西太多。一处认证逻辑变更,硬是牵动了一堆请求相关文件。从此我坚持:请求层只管发请求,别的一概不许知道。
第二宗罪是服务层耦合。你一定见过这种代码:一个全局的 axios 单例,拦截器里写死 token = localStorage.getItem('token'),401 判断靠 if (msg.includes('登录失效')),跳转直接 router.push('/login')。
它的问题是:请求层"知道"了太多它不该知道的事------token 存哪、用什么路由、登录失效怎么处理。换个项目这些全变,于是请求层跟着重写。
LYStack 反过来做。服务层提供一个 AxiosFactory,每个后端服务建自己的实例(应对"一个前端对接多个异构后端"),但它不知道 token 从哪来、失效了怎么办------这些由应用层在启动时"注入"进去:
ts
// 应用启动时(组合根)注入,而不是服务层写死
// bootstrap 是组合根,允许直接接触 localStorage 这类底层 API;
// 业务代码则不行,必须走 @repo/shared 的抽象层
configureServiceAuth({
getToken: () => localStorage.getItem(STORAGE_TOKEN_KEY) ?? '',
onAuthenticationFailure: () => {
// 真实项目里跳登录页:router.push('/login')
console.warn('[bootstrap] 认证失效,请接入路由跳转逻辑');
},
});
依赖方向被反转了:不是服务层去 import 应用的东西,而是应用把能力注入给服务层。服务层从头到尾不感知 token 从哪来、UI 用什么框架------它只负责发请求。这样它才能真正被复用,而不是每个项目重写一遍。
八、支柱三:分层、env 收口与工程规范
前面 @repo/* 那张分层图,落到目录上是四个包,各管一摊、职责清晰:
| 包 | 职责 |
|---|---|
@repo/build-config |
构建契约 + Vite/Rsbuild adapter |
@repo/shared |
env 读取、常量、工具函数、共享类型 |
@repo/services |
Axios 工厂与拦截器 |
@repo/ui |
组件与样式 |
这里有个细节特别能体现"显式优于隐式"的原则------环境变量收口。
踩坑前史:曾经一个
getEnv(key)在读不到变量时静默返回?? '',结果某次环境配置漏了,baseURL 变成空字符串,请求全打到了当前域名下。线上报错一片,排查了快两天才定位到那个空串。从那以后我的原则是:必填变量读不到,就当场炸,别给我兜底。
Vite 读 env 走 import.meta.env,Rsbuild 走 process.env,两套机制并不通用。如果业务代码到处直接读,等于又被构建工具绑死了。所以 LYStack 把它收敛到 @repo/shared 的一个 getEnv 里,对外只暴露构建工具无关的读取方式,而且读不到必填变量就当场抛错,不给你一个空字符串让你上线后才发现:
ts
// 业务层只 import 这里,永远不碰 import.meta / process.env
const apiBase = getEnv('PUBLIC_API_BASE_URL'); // 缺失即 fail loudly
光靠自觉不够,所以还有一条 ESLint 规则从规则层面禁止 业务代码直接出现 import.meta.env / process.env。配套的工程规范也都立好了:catalog 统一版本(升级依赖只改一处)、husky + commitlint + lint-staged 守住提交质量、TypeScript strict 全开。
规范不是写在 wiki 上让人遵守,而是写进工具链里强制收口------这才是不会腐化的关键。
九、支柱四:MPA 由 Rsbuild 承载的 js-first 契约
第四节说过 Rsbuild 在多页应用上有天然优势,这里补上原因。
Vite 是 html-first 的:每个页面要先有一个 html 文件当入口,做 MPA 时一堆 html 散落各处,配置起来很拧巴。Rspack 则是 js-first,用一份配置就能驱动多入口,天生适合多页场景。
LYStack 把"多页"也抽象成了中立契约 PageEntry,由它作为唯一真相源驱动所有入口:
ts
export interface PageEntry {
name: string; // 页面名(同时是产物 html 名 / 输出子目录名)
entry: string; // 入口 TS/JS 文件
title?: string;
env?: EnvMode[]; // 仅在指定环境构建(如 mock 页只在 development)
}
目前 MPA 由 Rsbuild adapter 实现;Vite adapter 会显式抛出"暂不支持"的错误,并在 roadmap 里标明。
我特意没有假装它"什么都支持"。这正好呼应第四宗罪------文档不脱节 。能做什么、不能做什么,代码和文档说的是同一件事。底座可以有边界,但边界必须诚实。
十、支柱五:AI 友好------让 AI 替你写"规范"代码,而不是写一堆乱码
这是我觉得当下最值得讲的一根支柱。
现在大家或多或少都在用 AI 写代码(Copilot、Cursor、Claude Code)。这些工具会读你的 eslint.config.js 和 tsconfig.json,工具链能管的部分它们大多能对齐。但你八成也体会过那种痛:AI 不懂你项目的"非工具链约束"------比如组件要薄、逻辑要抽 hook、import 分组顺序、命名风格、该用 barrel 导出还是直引。这些 ESLint 管不到的约定,AI 全凭猜,review 它的产出比自己写还累。
根因是:AI 不知道你的"规矩"。那就把规矩明明白白写给它看。
LYStack 内置了一整套"给 AI 看的规范":
AGENTS.md(同时兼容 Claude Code 的CLAUDE.md):项目的"编码 DNA"------编程思想、核心原则、技术栈、规范索引全在里面。AI 一进项目先读它,立刻知道这个项目"怎么思考、怎么动手"。这部分核心思想,参照了社区里把成熟项目的 agent 实践沉淀成 skills 的做法。docs/rules/一整套规范文档(注释 / 命名 / 目录 / Vue3 / TypeScript / Pinia / Axios / 样式 / 错误处理 / 编码自查)。这些规则来自我 10 年的经验,加上与 AI 反复讨论打磨,还参照了 antfu(Vue / Vite 核心团队成员)的 skills 实践。
怎么用?写组件、写业务之前,让 AI 先参照这套 rules,它产出的代码就会自动对齐规范 :薄组件厚逻辑、barrel 统一导出、类型安全链、不用 any、不用 Options API......你不用再一条条 review 去纠正。
关于让规范"始终生效",antfu 有个做法值得借鉴:
想让某些 skills/规范始终生效 ,就直接在
AGENTS.md里引用它们。
LYStack 正是这么做的------CLAUDE.md 用一行 @AGENTS.md 把整套规范引进来,AI 每次都吃得到。
整条链路是这样串起来的:
一句话总结这根支柱:别让 AI 帮倒忙。把规范喂给它,它就是你的规范化代码生产力。
十一、跑给你看
光说不练没意思。底座里放了三个示例应用,clone 下来三条命令就能同时跑起来,三个端口各演示一种形态:
bash
git clone https://github.com/liangy0323/LYStack.git
cd LYStack
pnpm install
pnpm dev
| 示例 | 形态 | 构建工具 | 端口 |
|---|---|---|---|
| example-vite | SPA | Vite | 5173 |
| example-rsbuild | SPA | Rsbuild | 5273 |
| example-rsbuild-mpa | MPA | Rsbuild | 5373 |
三个应用都在消费同一套 @repo/* 共享包:调 @repo/services 发请求、用 @repo/shared 读环境变量和工具函数、引 @repo/ui 的组件。它们的启动装配都收敛在各自的"组合根" bootstrap() 里------前面那段注入认证能力的代码,就是从这里来的:
ts
// apps/example-vite/src/bootstrap/index.ts
export function bootstrap(): void {
configureServiceAuth({ /* 注入 getToken / onAuthenticationFailure */ });
setErrorMessenger((msg) => console.error(`[LYStack] ${msg}`));
}
启动流程显式、可读、可测;业务组件不再各自做初始化,一切装配收敛于一处。打开浏览器 Console,你能看到完整的装配日志和请求链路,大致长这样:
text
[LYStack] bootstrap start
[LYStack] service auth configured
[LYStack] error messenger ready
[LYStack] bootstrap done
[bootstrap] 认证失效,请接入路由跳转逻辑 // 触发一次未授权请求时
装配顺序、认证注入、错误兜底,每一步都显式打点------出问题时照着日志就能定位到是哪一环,不用在一堆全局初始化里翻。
这里特意不放 UI 截图------底座的价值在架构,不在界面好不好看。
@repo/ui只放了演示组件,真实项目里替换成你自己的设计系统即可。
补一句和第三节呼应的诚实标注:@repo/ui 目前只提供 Vue3 参考实现。因为框架无关的是那五根支柱(架构思路),不是 UI 组件------UI 组件天然绑定框架的组件模型,没法做到框架无关。架构思路可迁移、UI 组件随框架走,这两件事分开看就不矛盾了。
十二、它适合谁,又不适合谁
架构没有银弹,诚实标注边界比夸它全能更重要。
适合:
- 中大型、要长期维护的 Vue3 项目,尤其是一个前端要对接多个异构后端的场景。
- 一个团队维护多个应用 :多个 app 共享同一套
@repo/*基础设施,规范统一、版本统一。 - 对构建工具未来走向没把握,想给自己留一条"以后能换"的退路。
- 重度用 AI 写代码,希望产出自动对齐团队规范,而不是每次 review 纠风格。
不适合:
- 一次性 demo、活动页、纯静态官网 ------Monorepo 那套分层和工程约束对它们是纯负担,直接
npm create vite更省事。 - 单人、单一应用、没有多应用共享需求------Monorepo 的核心价值是多应用共享一套基础设施;如果就一个 app、也不对接多后端,那套分层的收益你享受不到,反而多一层心智负担。
- 想开箱即用一整套 UI 组件库------这里 UI 是留白的,要你自己接设计系统;如果你需要开箱即用的完整 UI 组件库,建议直接使用 Element Plus / Ant Design Vue 的官方模板。
一句话:它解决的是"项目会长大、会长期维护、会被多人和 AI 反复改"带来的腐化问题。如果你的项目没有这些前提,它就是过度设计。
十三、上手、Roadmap 与一点请求
如果你也受够了脚手架腐化,欢迎试一试:
bash
git clone https://github.com/liangy0323/LYStack.git && cd LYStack
pnpm install
pnpm dev # 同时跑起三个示例
pnpm new:app # 生成一个新应用(plop 脚手架)
pnpm new:page # 给 MPA 生成一个新页面
还没做完的部分我也一并列出来:
- 测试体系:Vitest 还没接入,这是下一步重点。
- CI 流水线:尚未配置。
- Vite 的 MPA 支持 :契约已留好(
PageEntry),adapter 待补,目前会显式报"暂不支持"。
这套底座是我 10 年踩坑的沉淀,但它还很年轻。如果它戳中了你的痛点,欢迎来项目里逛逛:
给个 star 是对我最大的鼓励;如果你对架构有不同看法,更欢迎来提 issue 一起聊------架构这件事,从来不是一个人能想全的。
LYStack · 不是脚手架,是架构地基。