前端做 Monorepo 分包 + 插件化 + 版本管理,核心思路其实很简单:把代码集中管起来,拆成一个个独立模块,按标准流程发布。这样既能解决多项目复用、升级难的问题,又不用像微服务那样,每个服务都要单独运维部署(毕竟 Monorepo 是管代码的,微服务是管运行时的,俩事儿能结合但得分清边界)。下面结合实际落地步骤,一步步说清楚怎么干:
一、先想明白:我们到底要解决啥问题?
落地前别上来就搭架子,先搞清楚目标,不然容易走偏:
- Monorepo 不是为了 “赶时髦” :主要是为了集中管理多个相关的包(比如 UI 组件、业务模块),不用再维护一堆小仓库。像配置(eslint、tsconfig)、依赖(比如 vue、axios)、工具链(vite、vitest)都能共享,跨包开发也方便 —— 比如改个 utils 里的函数,所有依赖它的包能实时生效,不用来回发版。
- 插件化是为了 “灵活复用” :把业务或功能拆成独立插件(比如用户登录模块、订单模块),定个统一接口,不管是 A 项目还是 B 项目,想用就 “插” 进去,后续升级插件也不用改主项目代码。
- 别跟微服务搞混:Monorepo 是 “代码层面集中管”,所有包都在一个仓库里;微服务是 “运行时独立部署”,每个服务单独跑。如果项目大了,也能结合用(比如用 Monorepo 管微服务的代码,再分别部署),但咱们先聚焦 “用 Monorepo 做插件化,降低多项目维护成本” 这个核心。
二、搭架子:选对工具链,起步不踩坑
核心要解决 4 个问题:包怎么管、依赖怎么隔、任务怎么跑、版本怎么发。主流工具各有侧重,按团队规模选就行:
| 工具 | 实际用下来的优势 | 适合谁用 |
|---|---|---|
| pnpm workspace | 轻得很,不用学太多新东西,依赖用软链管理,不会有重复安装的问题,速度快 | 中小团队、刚起步,不想在工具上花太多精力 |
| Turborepo | 能缓存任务(比如上次测过的包,没改就不重新测),还能分析跨包依赖,大型项目构建 / 测试能省不少时间 | 中大型项目,构建慢、测试耗时的团队 |
| Nx | 功能全,能自动生成代码、集成测试、CI 流程,还支持 vue、react 多框架 | 大型团队,需要标准化全流程,避免每个人搞一套 |
新手推荐先上 pnpm workspace + changesets:足够用,学习成本低,后续想升级 Turborepo 也能无缝衔接。
1. 手把手初始化 Monorepo 仓库
以 pnpm 为例,步骤很简单,跟着敲就行:
# 1. 建个仓库文件夹,进去初始化
mkdir frontend-monorepo && cd frontend-monorepo
pnpm init # 生成package.json,随便填点信息就行
# 2. 配workspace:告诉pnpm哪些地方是包/项目
# 新建pnpm-workspace.yaml文件,复制下面内容
cat > pnpm-workspace.yaml << EOF
packages:
- 'packages/*' # 放所有插件、工具包(核心)
- 'apps/*' # 放具体项目(比如项目A、项目B)
- 'examples/*' # 可选:放示例项目,方便新人看怎么用插件
EOF
# 3. 建对应的文件夹,结构就清晰了
mkdir -p packages apps examples
# 解释下:
# packages:比如UI组件库、用户插件、订单插件、通用工具,都放这
# apps:比如公司的官网项目、管理后台项目,依赖packages里的东西
三、分包是关键:别瞎拆,也别不拆
分包的核心是 “每个包只干一件事,互相别瞎依赖”(高内聚、低耦合),拆太细会导致包太多不好管,拆太粗又没法单独复用。建议按 “基础功能 + 业务领域” 两层拆:
1. 先拆基础层:所有项目都能用的通用包
这些包跟具体业务没关系,谁用都不违和:
- @my-org/utils:纯工具函数,比如处理日期(格式化、算相差天数)、加密(md5、base64)、表单校验(手机号、邮箱),别放业务逻辑。
- @my-org/components:UI 组件二次封装,比如基于 Element Plus 封装个带权限的按钮、带搜索的下拉框,所有项目的 UI 风格能统一。
- @my-org/hooks:通用 hooks,比如 useRequest(处理接口请求 loading / 错误)、useAuth(判断用户权限),不用每个项目都写一遍。
- @my-org/core:最核心的 “框架包”,定插件的接口、注册方式、生命周期 —— 比如插件怎么装、怎么卸,主应用怎么调用插件,都在这定规矩。
2. 再拆业务层:跟具体业务绑定的插件
业务插件必须依赖 core 包(按 core 定的规矩来),而且不能互相依赖(比如用户插件别直接调用订单插件的方法):
- @my-org/plugin-user:用户相关功能,比如登录组件、个人中心页面、获取用户信息的接口封装。
- @my-org/plugin-order:订单相关,比如订单列表、详情页、支付回调处理。
- @my-org/plugin-stat:统计分析,比如销量图表、用户活跃度报表。
3. 拆包的 3 个小原则(避坑用)
- 别贪多:一个包只干一件事,比如 plugin-user 里别塞订单相关的逻辑。
- 别瞎依赖:业务插件只能依赖 core 或基础层包(utils、components),跨业务插件绝对不能互相依赖(不然改一个插件,其他插件全崩)。
- 大小适中:别搞个只有 10 行代码的包(没必要),也别搞个几 MB 的大包(发布、安装都慢)。
四、插件化:定好规矩,才能灵活复用
插件化的关键是 “统一接口”—— 不然主应用接插件的时候,有的插件叫 install,有的叫 init,乱套了。这个接口必须在 core 包里定死:
1. 先定插件的 “规矩”(接口)
用 TypeScript 写个接口,强制所有插件都按这个格式来(不用 TS 的话,也得在文档里写清楚):
// packages/core/src/types.ts
import { App } from 'vue';
// 所有插件都得符合这个结构
export interface Plugin {
name: string; // 插件唯一名字(比如"@my-org/plugin-user",不能重)
version: string; // 版本号(按语义化来,比如1.0.0)
dependencies?: string[]; // 依赖的其他插件(比如这个插件需要core包1.0以上)
install: (app: App, options?: any) => void; // 安装方法(主应用装插件时调用)
uninstall?: () => void; // 可选:卸载方法(不用这个插件时清理资源)
}
2. 写个 “插件管理器”(主应用用)
主应用不用自己一个个装插件,用 core 包里的管理器统一装,还能查已装的插件:
// packages/core/src/index.ts
import { App } from 'vue';
import { Plugin } from './types';
export class PluginManager {
private plugins = new Map<string, Plugin>(); // 存已装的插件
private app!: App; // 主应用的实例
// 初始化时传主应用实例
init(app: App) {
this.app = app;
}
// 装单个插件,支持传配置(比如给user插件传默认角色)
register(plugin: Plugin, options?: any) {
if (this.plugins.has(plugin.name)) {
console.warn(`插件${plugin.name}已经装过了,会被覆盖`);
}
this.plugins.set(plugin.name, plugin);
plugin.install(this.app, options); // 调用插件的安装方法
}
// 批量装插件(一次装多个,方便)
registerPlugins(plugins: { plugin: Plugin; options?: any }[]) {
plugins.forEach(item => this.register(item.plugin, item.options));
}
// 查已装的插件(比如想判断user插件有没有装)
getPlugin(name: string) {
return this.plugins.get(name);
}
}
// 导出单例,主应用直接用
export const pluginManager = new PluginManager();
3. 实际写个插件(以 user 插件为例)
按 core 定的规矩来写,不用管主应用怎么用,专注自己的功能:
// packages/plugin-user/src/index.ts
import { Plugin } from '@my-org/core';
import { App } from 'vue';
import UserLogin from './components/UserLogin.vue'; // 登录组件
import { useUserStore } from './stores/user'; // 用户状态管理
// 按Plugin接口写插件
const userPlugin: Plugin = {
name: '@my-org/plugin-user',
version: '1.0.0',
dependencies: [], // 暂时不依赖其他插件
// 主应用装插件时会调用这个方法
install(app: App, options?: { defaultRole: string }) {
// 1. 注册组件(主应用能直接用<UserLogin />)
app.component('UserLogin', UserLogin);
// 2. 提供状态管理(主应用用inject能拿到userStore)
app.provide('userStore', useUserStore());
// 3. 处理传进来的配置(比如设置默认角色)
if (options?.defaultRole) {
useUserStore().setDefaultRole(options.defaultRole);
}
}
};
export default userPlugin;
五、版本管理:别手动改版本号,容易乱
插件要独立升级,比如 A 项目用 user 插件 1.0,B 项目想用 2.0,就得管好版本。推荐用 changesets,能自动记变更、更版本、生成 CHANGELOG,不用手动改 package.json。
1. 先装 changesets
在仓库根目录装,作为开发依赖:
pnpm add -Dw @changesets/cli # -Dw表示在根目录装开发依赖,所有包共享
npx changeset init # 初始化,会生成.changeset文件夹(存变更记录)
2. 版本管理的 3 个步骤(日常开发用)
比如改了 user 插件,想发个新版本:
- 第一步:记变更
执行npx changeset,会弹出交互界面:
-
- 选这次改了哪个包(比如选 @my-org/plugin-user);
-
- 选变更类型:修 bug(patch,比如 1.0.0→1.0.1)、加新功能(minor,1.0.0→1.1.0)、不兼容大改(major,1.0.0→2.0.0);
-
- 写一句变更描述(比如 “修复登录按钮不显示的问题”);
完了会在.changeset 里生成一个 md 文件,不用管它。
- 第二步:更版本号 + 生成 CHANGELOG
执行npx changeset version,它会自动:
-
- 改对应包的 package.json 版本号(比如 user 插件从 1.0.0→1.0.1);
-
- 生成 / 更新 CHANGELOG.md(把刚才的变更描述写进去,不用手动写文档)。
- 第三步:发布到仓库
先在插件的 package.json 里配发布地址(比如公司私有仓库,或者 npm):
// packages/plugin-user/package.json
{
"name": "@my-org/plugin-user",
"version": "1.0.1",
"publishConfig": {
"registry": "https://你们公司的私有仓库地址"
}
}
然后执行发布命令:
pnpm publish -r # -r表示“发布所有有变更的包”,不用一个个发
3. 版本兼容的小技巧
- 按语义化版本来:patch(兼容修 bug)、minor(兼容加功能)、major(不兼容),这样项目知道能不能直接升。
- 跨包依赖用^:比如 core 包依赖 utils 包^1.0.0,表示能兼容 1.x.x 的所有版本,不用每次 utils 小升级都改 core 的依赖。
六、多项目复用:怎么在不同项目里用插件?
比如有两个项目:apps/project-a(管理后台)、apps/project-b(官网),想用同一个 user 插件,步骤跟平时用 npm 包差不多:
1. 项目里加依赖
在 project-a 的 package.json 里写清楚要依赖的插件,跟装其他包一样:
// apps/project-a/package.json
{
"dependencies": {
"@my-org/core": "^1.0.0", // 必须装core,不然插件用不了
"@my-org/plugin-user": "^1.0.1", // 装user插件1.0.1版本
"@my-org/plugin-order": "^2.0.0" // 再装个订单插件
}
}
然后执行pnpm install,就能把这些插件装到项目里。
2. 主应用里装插件
在 project-a 的入口文件(main.ts)里,用 core 的插件管理器装插件:
// apps/project-a/src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { pluginManager } from '@my-org/core'; // 引入管理器
import userPlugin from '@my-org/plugin-user'; // 引入user插件
import orderPlugin from '@my-org/plugin-order'; // 引入订单插件
const app = createApp(App);
// 先初始化管理器(传主应用实例)
pluginManager.init(app);
// 批量装插件,还能给user插件传配置(默认角色是admin)
pluginManager.registerPlugins([
{ plugin: userPlugin, options: { defaultRole: 'admin' } },
{ plugin: orderPlugin } // 订单插件不用传配置,就空着
]);
app.mount('#app');
之后在 project-a 里,就能直接用 user 插件的组件(比如)、状态(比如 inject ('userStore'))了。
3. 插件升级:想更到新版本怎么办?
比如 user 插件出了 1.2.0 版本,project-a 想升级:
# 进project-a目录,执行更新命令
cd apps/project-a
pnpm update @my-org/plugin-user@1.2.0 # 指定更到1.2.0版本
如果只是想更到最新的兼容版本(比如 1.x.x 的最新版),直接pnpm update @my-org/plugin-user就行。
七、维护成本怎么降?比微服务省心多了
Monorepo 比微服务好维护,核心是 “集中管 + 有规矩”,不用每个包 / 项目都搞一套配置、测试、CI:
1. 配置共享:一次配置,所有包复用
比如 eslint、prettier、tsconfig 这些配置,在根目录写一次,其他包直接继承:
- 根目录建个tsconfig.base.json,写通用配置:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "NodeNext",
"strict": true // 严格模式,所有包都按这个来
}
}
- 其他包(比如 plugin-user)的tsconfig.json直接继承:
{ "extends": "../../tsconfig.base.json" }
这样不用每个包都写一遍 tsconfig,改配置也只改根目录的就行。
2. 测试和 CI:集中管,省时间
- 测试:用 Turborepo 或 Nx 能实现 “增量测试”—— 比如只改了 user 插件,就只测 user 插件,不用全量跑所有包的测试,省时间。
- CI:比如 GitHub Actions,在根目录配一个 CI 文件,所有包的测试、构建、发布都用这一套流程,不用每个包都写 CI:
# .github/workflows/ci.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # 拉代码
- uses: pnpm/action-setup@v2 # 装pnpm
- run: pnpm install # 装依赖
- run: pnpm test # 跑所有测试(用turbo的话是turbo run test)
3. 插件迭代:文档和测试别少
- 文档:每个插件的 README.md 必须写清楚 —— 怎么装、怎么用、版本变更(可以用 typedoc 自动生成 API 文档),不然别人不知道怎么用。
- 测试:每个插件至少写单元测试(比如用 vitest 测 utils 函数),复杂的插件加 E2E 测试(比如用 playwright 测登录流程),不然升级的时候容易把旧功能搞崩。
- 废弃预警:如果要删某个接口,先在 CHANGELOG 里写清楚 “v2.0.0 会删掉 xxx 方法”,给项目留出升级时间,别突然删。
4. 想结合微服务?也能搞
如果项目太大,想把前端拆成独立部署的子应用(比如管理后台、官网分开部署),可以这么干:
- 用 Monorepo 管所有子应用(apps/app1、apps/app2)和插件(packages/*);
- 子应用用微前端框架(比如 qiankun)独立部署;
- 子应用需要的插件,要么在构建时打包进去,要么从 CDN 加载(比如把 plugin-user 发成 UMD 包,子应用用 script 标签引入)。
八、看别人怎么干:大厂都这么用
- Babel:用 Monorepo 管着 @babel/core 和一堆插件,比如 @babel/plugin-transform-arrow-functions,想加新语法支持就加个插件,特别灵活。
- Vue3:源码就是 Monorepo 结构,packages 里分 vue、@vue/compiler-sfc、@vue/runtime-core 这些包,版本用 changesets 管理,升级起来很规范。
- 阿里 / 字节:内部很多前端团队都用 Monorepo + 插件化,比如把电商的 “商品”“购物车”“支付” 拆成插件,不同业务线(淘宝、天猫)按需复用,升级也不用改业务线代码。
总结:落地其实不难,关键是别想复杂
核心步骤就 5 步,跟着来就行:
- 用 pnpm workspace 搭架子,分清楚 packages(插件)和 apps(项目);
- 按 “基础层 + 业务层” 拆包,每个包只干一件事,别瞎依赖;
- 在 core 包里定插件接口和管理器,所有插件按规矩来;
- 用 changesets 管版本,自动记变更、更版本、发包;
- 项目里装插件、用插件,升级的时候直接更依赖就行。
这种模式比微服务省心多了 —— 不用管多个服务的部署运维,代码集中管,复用和升级都方便,中大型团队用起来特别香。