厌倦了重度耦合?我用 Vue3 撸了一个真·插件化中后台框架

做前端这些年,经手过大大小小几十个中后台项目。市面上的开源 Admin 模板很多,UI 一个比一个炫酷,但真用到实际业务里,往往有一个致命的痛点:强耦合

你兴冲冲地拉下代码,发现里面塞满了图表、富文本编辑器、甚至还有花里胡哨的 3D 演示页面。你想做减法?一删文件,路由报错了;再删,状态管理又抛异常了。整个项目跑都跑不起来。我之前经常要花一整个上午的时间,专门去清理这些所谓"开箱即用"的屎山雏形。

我不想要一个大而全的全家桶。我想要的是一个干净的底座,加上一堆可以随时拔插的 U 盘。

这也是我写 Vue-Bag-Admin 的初衷。在这个项目里,我彻底抛弃了传统的单体架构,尝试用前端微模块(插件化)的思路,结合最新的全栈技术,重新定义中后台的开发体验。

🚀 快速通道

为什么是这套技术栈?

技术选型不能跟风,得看解决什么实际问题。Vue-Bag-Admin 敲定的核心前端栈是:Vue3 + Vite5 + Naive-ui + Pinia

说说我这么选的理由。

前端这块,Vue3 和 Vite5 已经是标配,没啥好说的。UI 组件库我选了 Naive-ui,而不是受众更广的 Element Plus。原因很简单:Naive-ui 的 TypeScript 支持做得更彻底,而且它的主题定制完全基于 CSS 变量,对于我们要做的插件化架构来说,样式隔离和主题切换的成本最低。

至于后端技术栈,我给自己定了一个原则:前端架构必须与后端彻底解耦

市面上的很多模板经常和某个特定的后端(比如 NestJS 或 Spring Boot)强绑定。但在 Vue-Bag-Admin 里,前端仅仅负责视图渲染、插件调度和状态管理。不论你的团队是用 Java、Go、Python 还是 Node.js 写接口,或者是直接使用 Strapi 这样的 Headless CMS,它都能无缝对接。底层网络请求被统一封装在独立的 @bag/request 包中,业务逻辑层完全不需要关心后端用的是什么语言。

深度解剖:我的前端插件化架构

在设计这套前端架构时,我给自己定了个死规矩:主基座(宿主应用)绝对不写具体的业务逻辑。

先上一张架构图,让大家直观感受下:

%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '16px', 'fontFamily': 'sans-serif' }}}%% flowchart TB subgraph Host ["🏠 主基座 (apps/admin)"] direction LR A["Vue3 App Instance"] B["Vue Router"] C["Pinia Store"] D["AppLayout & TabBar"] end subgraph Core ["⚙️ 调度核心 (packages/core)"] direction TB E{{"Plugin Manager"}} end subgraph Plugins ["🧩 业务插件包 (packages/plugin-*)"] direction LR F["@bag/plugin-shop"] G["@bag/plugin-sys-setting"] H["@bag/plugin-article"] end subgraph Common ["🛠️ 公共依赖包 (packages/*)"] direction LR I["@bag/ui (PmProTable)"] J["@bag/request (Axios/WS)"] end %% 核心调度关系 A ==>|初始化| E E -.->|动态注入路由| B E -.->|动态注入状态| C E -.->|动态渲染菜单| D %% 插件注册关系 F & G & H == "注册 (Routes/Menus/Locales)" ==> E %% 底层依赖关系 F & G & H -.->|复用组件与请求| I & J %% 样式美化 classDef host fill:#e3f2fd,stroke:#2196f3,stroke-width:2px,color:#0d47a1 classDef core fill:#fff3e0,stroke:#1976d2,stroke-width:2px,color:#1565c0,stroke-dasharray: 5 5 classDef plugin fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#1b5e20 classDef common fill:#f3e5f5,stroke:#388e3c,stroke-width:2px,color:#2e7d32 class A,B,C,D host class E core class F,G,H plugin class I,J common

整个仓库跑在 pnpm Workspace 上,代码按功能严格划分为 apps(应用)和 packages(依赖包),内部统一使用 @bag 作为命名空间。

  • apps/admin:这是前端的主基座。它是一具非常轻量的"空壳",只负责最基础的登录鉴权、全局状态初始化,以及提供一个 AppLayout(包含侧边栏、顶栏、标签页)。
  • packages/core:核心的插件调度器(Plugin Manager)。负责统筹所有插件的路由、菜单和状态注入。
  • packages/ui:业务无关的纯 UI 组件封装(比如我们基于 Naive 封装的高级表格组件 PmProTable)。
  • packages/request:统一处理 HTTP 和 WebSocket 请求的底层通信库。
  • packages/plugin-*:各种独立的业务插件包,比如电商模块、系统设置模块。

传统的业务模块是一堆散落在 views 文件夹里的组件。而在我们的架构里,业务模块是独立的 npm 包。基座需要什么功能,就引入对应的包并注册;不需要,直接在 package.json 里移除依赖。基座毫无感知,真正做到了按需拔插。

❓ 进阶思考:插件之间如何通信? 看到这里,有经验的开发者肯定会问:既然商品模块和订单模块是独立的包,如果订单需要调用商品组件或共享数据怎么办? 在 Vue-Bag-Admin 中,我们推崇"高内聚、低耦合"。插件之间极力避免直接的物理依赖。如果必须跨业务模块交互,我们通过 @bag/core 提供的全局 EventBus 进行解耦通信,或者借助 Pinia 的共用 Store(如 UserStore、AppStore)进行状态流转,绝对不让两个业务包产生直接的代码纠缠。

从老版本到新版本:我到底升级了什么?

熟悉我的朋友可能知道,我之前维护过一版旧的前端框架(文档地址:vite.itnavs.com/doc/)。

老版本是个典型的单体应用(Monolith),所有代码都塞在一个庞大的 src 目录下。虽然老版本在当时够用,大家也觉得"开箱即用"很方便,但随着业务线膨胀,几个痛点越来越让我难受。这次全新升级 Vue-Bag-Admin,我主要解决了以下几个核心问题:

1. 告别"牵一发而动全身"的强耦合 在老架构里,如果你想删掉"系统设置"功能,你需要去 views 删页面,去 router 删路由配置,去 store 删状态。漏掉任何一个地方,整个应用直接白屏报错。 而在新架构里,由于采用了插件化机制,你只需要在基座的 package.json 里把 @bag/plugin-sys-setting 这个依赖删掉,世界就清静了。路由和菜单会自动解绑。

2. 根除依赖污染(幽灵依赖) 老版本的 package.json 塞满了各种乱七八糟的库:图表、富文本、Excel 导出、拖拽组件......谁也说不清哪个页面用到了哪个包。 新架构下,富文本依赖只存在于特定的插件包(比如文章管理插件)里,基座的依赖极其干净。这让 Vite 的依赖预构建(Pre-bundling)更精准,冷启动速度肉眼可见地变快。

3. 跨项目复用从"复制粘贴"变成"搭积木" 以前接新外包,只能把整个仓库 Copy 一份,然后痛苦地删减不需要的代码。 现在?建个新空壳基座,按需 pnpm install 几个已有的 @bag/plugin-* 包,半天就能搭出一个定制版后台。

4. UI 层彻底解耦 旧版经常把业务代码和具体的 UI 组件库绑死。这次我抽离了 packages/ui 层,把常用的数据驱动组件封装在里面。就算以后想换掉底层组件库,也只需要在这个包里重构,上层的业务插件层根本不用动。

快速上手:跑起来只需要三步

跑起这个项目不需要折腾复杂的配置。确保你本地有 Node.js 20+ 和 pnpm 9+ 即可。

第一步:拉取代码与安装依赖

bash 复制代码
git clone https://github.com/hangjob/vue-bag-admin.git
cd pm-web-admin-next
pnpm install

第二步:启动前端基座与注册业务插件

为了保证前端基座(Host)的纯粹性,它自己不写任何业务逻辑,而是通过一个调度中心(Plugin Manager)在运行时把独立的业务插件"拼接"进来。

来看一下我们核心的插件调度逻辑,代码非常清晰直观:

typescript 复制代码
// apps/admin/src/core/plugin-manager.ts
export async function bootstrapPlugins(app: App, plugins: AdminPlugin[]) {
  const globalMenus: any[] = []

  for (const plugin of plugins) {
    // 1. 注册路由:基座动态挂载插件提供的路由
    if (plugin.routes) {
      plugin.routes.forEach(route => router.addRoute(route))
    }
    
    // 2. 收集菜单:汇总各插件的菜单配置,交给左侧边栏渲染
    if (plugin.menus) {
      globalMenus.push(...plugin.menus)
    }
    
    // 3. 动态合并语言包:实现插件级别的 i18n 隔离
    if (plugin.locales) {
      ;(Object.keys(plugin.locales) as Array<'zh-CN' | 'en'>).forEach((lang) => {
        const messages = plugin.locales?.[lang]
        if (messages) i18n.global.mergeLocaleMessage(lang, messages)
      })
    }

    // 4. 执行插件安装钩子:插件可以在这里注入自己的全局组件或状态
    if (plugin.install) {
      await plugin.install(app, { router })
    }
  }

  // 更新全局菜单状态并挂载路由
  useMenuStore().setMenus(globalMenus)
  app.use(router)
}

有了这个底座,当我们要接入一个系统设置模块(@bag/plugin-sys-setting)时,在基座入口文件里只需要两行代码:

typescript 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import { bootstrapPlugins } from './core/plugin-manager'
import sysSettingPlugin from '@bag/plugin-sys-setting'

const app = createApp(App)

// 调度器接管,动态装载系统设置模块
bootstrapPlugins(app, [sysSettingPlugin])

app.mount('#app')

插件内部是怎么工作的呢?在 plugin-sys-setting 的入口文件里,你只需要导出一个符合规范的对象,把对应的路由、菜单配置交出去:

typescript 复制代码
// packages/plugin-sys-setting/src/index.ts
import type { BagPlugin } from '@bag/core/types'
import SysSetting from './views/SysSetting.vue'

const plugin: BagPlugin = {
  name: 'sys-setting',
  routes: [
    {
      path: '/setting/base',
      component: SysSetting,
      meta: { title: '系统基础设置' }
    }
  ],
  menus: [
    {
      label: '系统设置',
      key: 'sys-setting',
      icon: 'settings-outline'
    }
  ]
}

export default plugin

就这么简单。插件自己管理自己的视图(views/SysSetting.vue 等),基座会自动把它们拼装到侧边栏和路由树里。

性能实测:到底有多快?

得益于 Vite 5 和 pnpm 的硬链接机制,加上 Turbo 的缓存加持,项目的响应速度相当生猛。

在我们的 M2 Mac 测试机上:

  • 前端 apps/admin 冷启动时间稳定在 300ms 左右。
  • 修改插件代码后,热更新(HMR)基本在 50ms 内完成,几乎感觉不到刷新。
  • 得益于按需加载的插件机制,即使你挂载了十几个业务模块,基座的初始 chunk 体积依然能控制在 300KB 以内。

实战检验:如何像拼乐高一样搭建复杂业务系统

纸上谈兵没意思。框架雏形出来后,我直接用它重构了一个现有的中大型电商后台管理系统

以前用传统单体 Vue 写类似项目,每次加新功能都要重新发版整个系统,不同外包团队的代码全搅在一起。

这次我们换了玩法。我们把复杂的电商业务拆分成了几个完全独立的 @bag/plugin-* 包:

  1. 商品模块@bag/plugin-shop-product(包含商品列表、规格发布、库存管理)
  2. 订单模块@bag/plugin-shop-order(包含订单中心、发货调度、退款售后)
  3. 营销模块@bag/plugin-shop-market(包含优惠券、秒杀活动、拼团配置)
  4. 财务模块@bag/plugin-shop-finance(包含资金对账、提现审批)

效果展示:

这些业务包互相独立,配合 @bag/ui 里的 PmProTable 组件,我们直接通过 JSON Schema 渲染出了 80% 的列表页和筛选表单。

最爽的是权限与交付环节。由于业务模块是纯粹的 npm 包,我们可以随时根据客户需求决定基座要加载哪些模块。

  • 客服团队打包部署时,只注入售后和订单插件;
  • 财务团队打包部署时,只注入对账单插件。 代码层面的物理隔离极其干净,再也不用担心改营销模块的 Bug 把订单模块弄崩了。

工程化加持:自动化构建与部署(CI/CD)

有了极其清晰的包结构边界,自动化部署也就变得顺理成章。在我们的重构实践中,结合 GitHub Actions(或 GitLab CI)和 Docker,整个发布流程非常丝滑:

  1. 精准构建:利用 pnpm 结合 Turbo 的增量构建能力,只有代码发生变更的业务包才会重新触发构建。
  2. 自动化发布:每次打 Tag 都会触发 CI,将构建好的静态资源推送到云存储或通过 Docker 部署到 Nginx 容器中。
  3. 版本回滚无痛:因为每个业务线都被抽离成了插件,哪个模块上了线有问题,只需要回退到包含该稳定版插件的镜像即可,不需要因为一个小 Bug 让整个大系统停机维护。

代码写出来就是为了用的。如果你也受够了臃肿的后台模板,想要一个清爽、高内聚低耦合的开发底座,不妨试试 Vue-Bag-Admin。

如果你觉得这套架构思路对你有启发,去 GitHub 给我点个关注吧,这是对我熬夜写代码最大的鼓励。

相关推荐
我是Superman丶1 小时前
Antigravity Retry 自动重试脚本
前端·javascript·vue.js
明月_清风2 小时前
前端工程化七连问:从紧急修复到版本控制,一文打通工程化任督二脉
前端·前端工程化
账号已注销free2 小时前
Vue3 defineProps使用指南
vue.js
LIO3 小时前
Pinia 极简指南:Vue 3 官方状态管理库
前端·vue.js
朝阳393 小时前
react【实战】首页 -- 响应式导航栏(含带联动动画的搜索框)
前端·react.js·前端框架
Ruihong3 小时前
手写 React 对比 VuReact 编译:真正省下来的是维护成本
vue.js·react.js·面试
俺不会敲代码啊啊啊4 小时前
el-table实现行拖拽(包含展开项)
前端·vue.js·typescript
架构源启4 小时前
2026 进阶篇:Spring Boot响应式编程 + Spring AI 1.1.4 流式实战 + Vue前端完整实现(避坑指南)
java·前端·vue.js·人工智能·spring boot·spring·ai编程
OpenTiny社区4 小时前
还在手写 AI 聊天页?这款 Vue3 气泡组件,直接搞定流式对话!
前端·vue.js·ai编程