前端团队想提效?Monorepo 实战方案:从工具链选型到多项目插件化复用(含代码示例)

202 阅读12分钟

前端做 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,会弹出交互界面:

    1. 选这次改了哪个包(比如选 @my-org/plugin-user);
    1. 选变更类型:修 bug(patch,比如 1.0.0→1.0.1)、加新功能(minor,1.0.0→1.1.0)、不兼容大改(major,1.0.0→2.0.0);
    1. 写一句变更描述(比如 “修复登录按钮不显示的问题”);

完了会在.changeset 里生成一个 md 文件,不用管它。

  • 第二步:更版本号 + 生成 CHANGELOG

执行npx changeset version,它会自动:

    1. 改对应包的 package.json 版本号(比如 user 插件从 1.0.0→1.0.1);
    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 步,跟着来就行:

  1. 用 pnpm workspace 搭架子,分清楚 packages(插件)和 apps(项目);
  1. 按 “基础层 + 业务层” 拆包,每个包只干一件事,别瞎依赖;
  1. 在 core 包里定插件接口和管理器,所有插件按规矩来;
  1. 用 changesets 管版本,自动记变更、更版本、发包;
  1. 项目里装插件、用插件,升级的时候直接更依赖就行。

这种模式比微服务省心多了 —— 不用管多个服务的部署运维,代码集中管,复用和升级都方便,中大型团队用起来特别香。