10 年前端,我把踩过的所有坑熬成了一套"不会腐化"的 Vue3 Monorepo 底座

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 下来就是一个能直接长出业务的园区。

它的内部长这样:

flowchart BT subgraph pkgs [packages 共享层] P1[shared] P2[services] P3[ui] P4[build-config] end subgraph apps [apps 应用层] A1['example-vite SPA'] A2['example-rsbuild SPA'] A3['example-rsbuild-mpa MPA'] end A1 --> P1 & P2 & P3 & P4 A2 --> P1 & P2 & P3 & P4 A3 --> P1 & P2 & P3 & P4

应用在上面长,公共能力在下面沉淀。改一次 @repo/shared 里的工具函数,三个应用同时生效------这就是"代码共享"四个字落到实处的样子。


三、先说清楚:LYStack 讲的是架构思路,不是 Vue3

往下读之前我想先把一件事讲明白,免得你因为"不用 Vue"就划走。

LYStack 真正的资产不是那几个 @repo/* 包,而是它落地的那五根支柱:

  • 构建工具可插拔 ------AppBuildOptions 中立契约,业务代码不认识构建工具
  • 服务层依赖反转 ------configureServiceAuth 把认证能力注入服务层,而不是写死
  • env 收口 ------getEnv fail 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 要解的题,也是下面"构建可插拔"这根支柱的由来。


五、为什么我自己又造一个底座

市面上脚手架那么多,我为什么还要自己造一个?因为我反复踩中它们的"三宗罪":

  1. 构建锁死:工具配置和业务代码缠在一起,想换构建器等于重构。
  2. 服务层耦合:axios 单例 + 全局拦截器,token 从哪来、401 跳哪里全写死在请求层,换个项目就得动手术。
  3. 文档脱节:README 和真实代码各说各话,新人照着文档干一脚踩空。

LYStack 是从一个长期维护、对接过 3 套异构后端的真实生产项目里蒸馏出来的。我把业务剥掉,只留下那些被时间反复验证过的架构骨架,并立了三条不肯妥协的设计原则:

  • 构建可插拔------构建工具是可替换的实现细节,不是地基。
  • 依赖方向受控------底层永远不知道上层的存在,依赖只能从外往里注入。
  • 显式优于隐式------配置错了就当场报错,绝不静默兜底留个坑给你半夜排查。

下面五根支柱,就是这三条原则落到代码里的样子。

那你可能会问:市面上现成方案不少,为什么不直接拿来用?我都认真看过,它们各自解决的是另一类问题:

现成方案 为什么没直接用
Nx 功能很全但偏重,配置心智成本高,且对构建工具有较强假设,不符合我"构建可插拔"的诉求
Turborepo 官方 examples 解决的是任务编排(缓存、并行),不解决业务分层与依赖方向
antfu/starter 等个人 starter 个人风格鲜明、上手快,但不是面向企业级分层与多应用协作设计的
Element Plus 等官方模板 绑定了具体 UI 库,且不覆盖 MPA 与多后端场景

LYStack 不和它们竞争------Turborepo 我自己也在用(就是上面的任务编排层)。它补的是这些方案普遍不管的那部分:业务分层、依赖反转、构建工具中立、文档与代码不脱节。

在进入细节之前,先给你一张全景图------五根支柱不是平行罗列,它们都从"三宗罪"长出来,又共同立在前面那三条设计原则之上:

flowchart TD SIN[三宗罪 - 构建锁死 服务层耦合 文档脱节] SIN --> P1[支柱一 构建可插拔] SIN --> P2[支柱二 依赖反转] SIN --> P3[支柱三 env收口+规范] SIN --> P4[支柱四 MPA契约] SIN --> P5[支柱五 AI友好] R1[显式优于隐式] -. 支撑 .-> P3 R2[依赖方向受控] -. 支撑 .-> P2 R2 -. 支撑 .-> P4 R3[构建工具中立] -. 支撑 .-> P1 R3 -. 支撑 .-> P5

读懂这张图,下面五节就是在逐根拆解它。


六、支柱一:构建工具可插拔

踩坑前史:上一个项目用 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 这一层里:

flowchart LR O[AppBuildOptions 中立契约] O --> VA[vite-adapter] O --> RA[rsbuild-adapter] VA --> V[Vite 构建] RA --> R[Rsbuild 构建]

coreapp 永远不会直接 import viteimport 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] 认证失效,请接入路由跳转逻辑');
  },
});
flowchart LR APP[app 层 bootstrap] -->|注入认证能力| SVC[services 层 不感知来源] SVC -->|发请求时回调| APP

依赖方向被反转了:不是服务层去 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.jstsconfig.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 每次都吃得到。

整条链路是这样串起来的:

flowchart LR A[AGENTS.md 编码DNA] --> C[CLAUDE.md 引用AGENTS] A --> D[docs/rules 10份规范] C --> AI[AI Agent 读规范] D --> AI AI --> OUT[规范化代码]

一句话总结这根支柱:别让 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 年踩坑的沉淀,但它还很年轻。如果它戳中了你的痛点,欢迎来项目里逛逛:

项目地址:github.com/liangy0323/...

给个 star 是对我最大的鼓励;如果你对架构有不同看法,更欢迎来提 issue 一起聊------架构这件事,从来不是一个人能想全的。

LYStack · 不是脚手架,是架构地基。

相关推荐
Cerrda2 小时前
开发体验升级:UnoCSS 自定义 SVG 图标热更新方案
架构·前端框架
禅思院7 小时前
路由性能高可用架构实战方案
前端·架构·前端框架
JouYY1 天前
简单聊一下Harness层中的人机协同(HITL)
前端框架·llm·agent
星栈1 天前
Dioxus 多页面怎么做:`dioxus-router`、嵌套路由、`Outlet` 和页面组织,一篇给你讲顺
前端·rust·前端框架
怕浪猫2 天前
哪些软件对 Chrome DevTools Protocol 频繁使用
人工智能·架构·前端框架
星栈3 天前
Dioxus 的响应式系统:`Signal`、`Memo`、`Effect` 和异步状态到底该怎么分工
前端·前端框架
禅思院4 天前
路由性能优化终极指南:从懒加载漏洞到边缘渲染的架构跃迁
前端·架构·前端框架
怕浪猫4 天前
Electron 系列文章封面图
算法·架构·前端框架
星栈4 天前
Dioxus 的 `rsx!` 语法:如果你会 React,上手确实特别快
前端·前端框架