随着技术的飞速发展,我们公司内部一个基于“上古神器” jQuery + PHP 构建的十年历史老项目已显力不从心,技术非常老旧且维护成本高昂,其实已经无数次想要重构,但是苦于历史遗留原因以及业务的稳定性而一直难以下手,而且一时半会又不能全部重构。本次新页面较多且后续将持续迭代新模块,而老页面的改动较少且代码库错综复杂,牵一发而动全身。

经过几番思考,我们发现微前端是一种非常实用的去实施渐进式重构的架构,很适合用微前端技术来完成本次需求,最终决定利用 Vue3 + Vite 搭建一个全新的基座(主应用),作为新旧系统融合的桥梁,将原来的老项目接入到基座,后面的新需求都在新项目里面开发就行,不用再动老项目。此举不仅实现了新页面用 Vue3 开发,而且老项目也能和新项目融合在一起,既保持了旧系统的稳定运行,又引入了新技术栈的活力。
同时,鉴于我们另一个 Vue2 + webpack 项目也同样面临技术过时和项目规模庞大的问题,每次开发时运行起来非常卡顿,打包很慢,后期难以维护,也需要用微前端来进行一些拆分,不可能一直往该项目上堆代码。
所以,我们决定一步到位,设计了一套微前端项目模板,将微前端的核心配置抽象为可复用的插件,并结合自研组件库、HTTP请求、权限控制等插件,构建了一个全面的项目脚手架,旨在简化未来项目的搭建流程,提升开发效率,确保技术栈的先进性与可持续性。

从对⽐图可以看出,⽬前开源的微前端框架中有的功能 Micro App都有,并提供了⼀些它们不具备的功能,⽐如静态资源地址补全,元素隔离,插件系统等。

我们本次项目使用的是 Vue3+Vite+TypeScript 的技术栈,在综合对比了各个框架之后,认为MicroApp是最适合我们当前现实情况的。原因有下:
使用简单,将所有功能都封装到一个类WebComponent组件内,从而实现在基座应用中嵌入一行代码即可渲染一个微前端应用。
不需要像 single-spa 和 qiankun 一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改webpack配置,是目前市面上接入微前端成本最低的方案。
功能丰富,提供了 js沙箱、样式隔离、元素隔离、预加载、数据通信、静态资源补全等一系列完善的功能。
零依赖,这赋予它小巧的体积和更高的扩展性。
兼容所有框架,为了保证各个业务之间独立开发、独立部署的能力,micro-app做了诸多兼容,在任何技术框架中都可以正常运行。
侵入性低:对原代码几乎没有影响。
组件化:基于webComponents思想实现微前端。
使用 pnpm 和 monorepo 管理项目依赖和代码结构,确保所有子应用和基座应用都位于同一仓库的不同目录下,便于集中管理和版本控制。
- /root
- |-- /packages
- |-- /main-app # 基座应用
- |-- /old-app-wrapper # 老项目接入容器
- |-- /new-module-a # 新应用A
- |-- /new-module-b # 新应用B
- ...
- |-- /pnpm-workspace.yaml
- |-- /package.json
基座应用(Main App)
使用 Vue 3 + Vite 搭建的基座应用将成为整个系统的核心,负责路由管理、权限验证、资源加载等基础设施功能。基座应用应保持轻量级,避免过度耦合,并提供必要的API和事件系统供子应用使用。
子应用(Micro Apps)
老项目接入(旧应用容器):将老项目(PHP+jQuery)作为一个子应用,确保与基座应用的隔离和独立运行。新建一个项目,用于专门展示老项目页面,先在路由表中给所有路由都添加一个 iframeUrl参数(存的是旧页面的地址),并封装一个 iframe 组件,在组件中监听路由变化,动态更新Iframe的src,每一次切换路由,就将页面的地址传入 iframe 组件,从而加载出对应的老页面。
新模块开发(新应用容器):新页面和模块直接在Vue 3项目中开发,利用Vue 3的Composition API、响应式系统等优势,提高开发效率和代码质量。当旧系统中有某个部分要重构时,则将旧项目中的路由下线,并将重构后的模块进行上线,实现无缝替换。
通信机制
建立基座应用与子应用之间的有效通信机制,如使用自定义事件、全局状态管理(如Vuex或Zustand)或专门的通信库。
自定义事件:基座应用可以监听来自子应用的自定义事件,并作出相应处理。子应用同样可以监听基座应用的事件。
全局状态管理:使用Vuex或Zustand等状态管理库,在基座应用中维护全局状态,子应用可以通过API访问或修改这些状态(如果允许)。
专门的通信库:如使用single-spa、qiankun等微前端框架提供的API进行通信。
模板设计:
核心配置插件化:将微前端的核心配置(如子应用注册、加载策略、生命周期管理等)封装成可复用的插件,便于在不同项目中快速集成。
自研组件库:整合并封装常用的UI组件,提高开发效率和界面一致性。
HTTP请求插件:封装统一的HTTP请求处理逻辑,包括请求拦截、响应处理、错误重试等,简化API调用。
权限控制插件:基于角色或权限的动态路由控制,确保系统的安全性。
脚手架构建:
使用Vite作为构建工具,利用其快速冷启动和热模块替换特性,提升开发体验。
集成ESLint、Prettier等代码质量工具,确保代码风格统一和减少错误。
提供一键生成项目结构的脚本,包括基础目录、配置文件、基础路由和页面模板等。
CICD:
统一的CICD流程为各个子应用和主应用提供统一的构建/部署流程
拆分功能模块:首先,我们需要将整个后台管理系统拆分为多个独立的功能模块,如用户管理模块、专项管理模块、订单管理模块等。每个模块都可以作为一个独立的微应用进行开发和维护。
设计通信协议:为了实现各个微应用之间的通信和资源共享,我们需要设计一套统一的通信协议和API。例如,我们可以定义一个emit方法来触发自定义事件,以及一个on方法来监听自定义事件;我们还可以使用Webpack的CommonsChunkPlugin插件来实现公共资源的提取和共享。
开发主应用:主应用是整个后台管理系统的入口,它负责加载和管理各个微应用。主应用需要提供一个容器元素来承载各个微应用的内容,并提供一些基础设施服务,如路由管理、状态管理等。此外,主应用还需要实现与各个微应用的通信和资源共享。
开发微应用:每个微应用都是一个独立的功能模块,它可以独立开发、部署和运行。每个微应用都需要提供一个容器元素来承载该应用的内容,并提供一些与主应用交互的接口,如共享资源、通信等。此外,微应用还需要实现自身的业务逻辑和界面展示。
集成测试:在完成各个微应用的开发后,我们需要对整个系统进行集成测试,确保各个微应用之间的通信和资源共享正常工作。此外,我们还需要对整个系统的性能、稳定性等进行测试和优化。
本文中的微前端项目,使用的是 v-micro-app-plugin插件 ,它是一款基于京东MicroApp框架的微前端插件,旨在帮助开发者快速地将微应用集成到不同的系统中,实现高效、灵活的前端模块化开发。以下是详细的使用指南,希望能够帮助你快速上手。
本文中的案例-资源地址
- 微前端插件 v-micro-app-plugin 源码地址:https://github.com/yoguoer/v-micro-app-plugin.git
- 用该插件搭建的的示例项目 vMicroVerseHub 源码地址:https://github.com/yoguoer/vMicroVerseHub.git
- npm 包地址:v-micro-app-plugin - npm (npmjs.com)
技术栈:
主应用:Vue3+Vite+TypeScript
子应用1(老项目):用 iframe 挨个嵌入
子应用2(新模块):react / Vue3 ...
本次重构的是一个后台管理系统,最外层是基座,基座不仅是微前端应用集成的一个关键平台,还承载着维护公共资源、管理依赖项以及确立开发规范的重要使命。具体而言,其职责可概括为以下几点:
子应用集成,给子应用提供渲染容器
权限管理
会话管理
路由、菜单管理
主题管理
共享依赖
多语言管理(important)

因为micro-app对主应用和子应用的技术栈没有任何要求,所以,我们新建三个项目,my-app(Vue3)、my-app1(React)、my-app2(Vue2)。my-app是整体项目的主应用,也就是基座,my-app1和my-app2都是平级的子应用。
1、创建一个项目作为主应用,这个步骤就不赘述了。
笔者创建了一个主应用叫 main-app,提供一个框架给子应用。
2、安装 v-micro-app-plugin 微前端插件
pnpm i v-micro-app-plugin --save
3、配置并使用
为了便于后续复用该配置信息(配置路由、菜单、名称等等),我们将 options 参数独立出来,放在 settings 文件夹下的 microAppSetting.ts 文件中。
microAppSetting.ts:
- const env = import.meta.env.MODE
-
- const microAppUrl = {
- appFirst: {
- development: 'http://localhost:3000/#/',
- test: 'https://test.example.com/vivien/appFirst/',
- production: 'https://www.example.com/vivien/appFirst/'
- },
- appSecond: {
- development: 'http://localhost:4000/#/',
- test: 'https://test.example.com/vivien/appSecond/',
- production: 'https://www.example.com/vivien/appSecond/'
- },
- };
-
- const microAppSetting = {
- projectName: 'mainApp',
- subAppConfigs: {
- 'appFirst': {
- name: 'appFirst',
- url: microAppUrl['appFirst'][env]
- },
- 'appSecond': {
- name: 'appSecond',
- url: microAppUrl['appSecond'][env]
- }
- },
- isBaseApp: true, // 标记当前应用为主应用
- basePath: '/', // 打包路径或其他基础路径
- disableSandbox: false, // 是否禁用沙箱
- iframe: true, // 是否使用 iframe
- }
-
- export default microAppSetting
- export { microAppUrl }
main.js:
- import microAppSetting from '@/settings/microAppSetting'
-
- const options = microAppSetting
-
- // 初始化微前端插件
- await initMyMicroApp(app, options, router, store);
⚠注意:一定要在 router 和 store 初始化后,才可以使用 initMyMicroApp 进行初始化!!!举个简单的例子:
import { createApp } from 'vue' import './style.css' import App from './App.vue'y import { createPinia } from 'pinia' import router from './router/index' import ElementPlus from 'element-plus' import 'element-plus/theme-chalk/index.css' import initMyMicroApp from 'v-micro-app-plugin' import microAppSetting from '@/settings/microAppSetting' const app = createApp(App) const store = createPinia() app.use(router).use(ElementPlus).use(store) // 初始化微前端插件 const options = microAppSetting await initMyMicroApp(app, options, router, store); app.mount('#app')
1、创建任意多个项目作为子应用,这个步骤就不赘述了。
笔者创建了两个子应用,一个叫 sub-app-first,一个叫 sub-app-second。
2、安装 v-micro-app-plugin 微前端插件
pnpm i v-micro-app-plugin --save
3、配置并使用
sub-app-first:
- const options = {
- projectName: 'appFirst',
- subAppConfigs: {},
- isBaseApp: false, // 标记当前应用不为主应用
- basePath: '/', // 打包路径或其他基础路径
- disableSandbox: false, // 是否禁用沙箱
- iframe: true, // 是否使用 iframe
- }
-
- // 初始化微前端插件
- await initMyMicroApp(app, options, router, store);
sub-app-second:
- const options = {
- projectName: 'appSecond',
- subAppConfigs: {},
- isBaseApp: false, // 标记当前应用不为主应用
- basePath: '/', // 打包路径或其他基础路径
- disableSandbox: false, // 是否禁用沙箱
- iframe: true, // 是否使用 iframe
- }
-
- // 初始化微前端插件
- await initMyMicroApp(app, options, router, store);
有了主子应用之后,我们就需要在主应用中给子应用配置路由信息,这里一共有 2 个子应用,我们为它们分别进行配置。
appFirst:
- import microAppSetting from '@/settings/microAppSetting'
-
- export default {
- path: '/appFirst',
- name: 'appFirst',
- component: Layout,
- order: 1,
- hidden: false,
- meta: {
- title: 'appFirst',
- hideBreadcrumb: false,
- icon: Document,
- microAppOptions: microAppSetting.subAppConfigs!['appFirst']
- }
- }
appSecond:
- import microAppSetting from '@/settings/microAppSetting'
-
- export default {
- path: '/appSecond',
- name: 'appSecond',
- component: Layout,
- order: 2,
- hidden: false,
-
- meta: {
- title: 'appSecond',
- hideBreadcrumb: false,
- icon: Document,
- microAppOptions: microAppSetting.subAppConfigs!['appSecond'],
- }
- }
众所周知,路由切换时,可以给填充上对应路径的内容,同理,microApp中的也有同样的功能。我们可以对其进行二次封装,结合 v-if,以便于根据是路由指向的是子应用,还是本系统自由模块,来判断究竟是渲染微应用视图,还是渲染普通视图。
为了达到这个目的,我们可以新建一个 MicroAppContainer 文件夹,在其中创建一个index.vue,然后键入以下内容:
- <div :class="[`${prefixCls}-container`]">
-
- <micro-app v-bind="options" :name="options.name" keep-alive>micro-app>
- div>
- <script setup lang="ts">
- import { watch } from "vue";
-
- const props = defineProps<{
- options: {
- [key: string]: any;
- };
- }>();
-
- let prefixCls = props.options.name
-
- watch(
- () => props.options,
- (newValue) => {
- prefixCls = newValue.name
- },
- { immediate: true, deep: true }
- );
- script>
- <style>style>
⚠注意:
keep-alive 属性可根据需要决定是否设置。
![]()
在你需要加载子应用页面的地方:
- <div :class="[`${prefixCls}-viewer-microapp`]" v-if="isMicroAppView">
- <MicroAppContainer :options="microAppOptions" />
- div>
- <div v-else>
- <router-view />
- div>
一些必要的逻辑语句:
- import { watchEffect, ref } from 'vue'
- import { useRoute } from 'vue-router'
- const route = useRoute()
-
- let isMicroAppView: Ref = ref(false)
- let microAppOptions: Ref = ref({})
- watchEffect(async () => {
- microAppOptions.value = route.meta.microAppOptions
- isMicroAppView.value = !isNullOrUnDef(microAppOptions.value) && !isEmpty(microAppOptions.value)
- })
我们可以看到,sub-app-first 和 sub-app-second 均能独立作为一个系统去运行,并且在 main-app 下也能作为一个模块存在。
sub-app-first:


sub-app-second:


main-app:


控制台输出信息:


前文已经提到,老页面不需要做任何修改,且牵一发而动全身,只适合直接用 iframe 搬过来,相当于换个皮肤展示就好。但又因页面数量庞大,所以我们选择直接封装一个 iframe 组件,配合路由动态设置其 src 值,实现页面的动态切换。
在这里,我们专门创建了一个子应用,用于独立地展示该老系统,起到新旧隔离的作用,具体操作步骤如下:
1、首先,在 views 中新建一个 iframeViews 文件夹,然后创建 index.vue
- <div ref="iframeContainers" v-if="loading">
- <iframe
- :key="name"
- :src="url"
- height="100%"
- width="100%"
- sandbox="allow-scripts allow-same-origin"
- frameborder="0"
- >iframe>
- div>
-
- <script setup>
- import { ref, onMounted, watch, onUnmounted } from 'vue'
- import { useRoute } from 'vue-router'
-
- let route = useRoute() // 获取当前路由信息
-
- const iframeContainers = ref(null)
- let url = ref(null)
- let name = ref(null)
- let loading = ref(false)
-
- onMounted(() => {
- })
-
- onUnmounted(() => {
- })
-
- watch(
- () => route,
- (newRoute) => {
- loading.value = true
- url.value = newRoute.meta.iframeUrl
- name.value = newRoute.name
- // console.log('🚀 ~ watch ~ newPath:', newRoute, url.value, name.value)
- },
- { immediate: true, deep: true }
- )
- script>
-
- <style scoped>
- style>
2、配置路由表
- // 用户管理
- import { Layout } from '@/router/layout'
- import { $t } from "@/plugins/locales/setupLocale";
- import { User } from "@element-plus/icons-vue";
- import { getIframeUrl } from "@/settings/iframeUrlSetting";
-
- export default {
- path: "/user",
- name: 'user',
- component: Layout,
- order: 1,
- hidden: false,
- redirect: "userList",
- meta: {
- title: $t('用户管理'),
- hideBreadcrumb: false,
- icon: User
- },
- children: [
- {
- path: '/userList',
- component: () => import("@/views/iframeViews/index.vue"),
- name: 'userList',
- hidden: false,
- meta: {
- title: $t('用户列表'),
- iframeUrl: getIframeUrl('userList'),
- }
- },
- {
- path: '/auth',
- component: () => import("@/views/iframeViews/index.vue"),
- name: 'auth',
- hidden: false,
- meta: {
- title: $t('权限列表'),
- iframeUrl: getIframeUrl('auth'),
- }
- }
- ]
- }
3、为了能够在开发、测试、部署环境下都能正常运行,避免跨域问题,我们还需要通过灵活的方式来动态获取 iframeUrl
- const env = import.meta.env.VITE_NODE_ENV
- const url = {
- development: "https://example.com/vivien_test/",
- production: "https://example.com/vivien_prod/",
- test: "https://example.tcl.com/vivien_test/",
- }
- const iframeUrl = {
- development: {
- userList: '/vivien/user/index.html',
- auth: '/vivien/auth/index.html'
- },
- production: {
- userList: '/prod/user/index.html',
- auth: '/prod/auth/index.html'
- },
- test: {
- userList: '/test/user/index.html',
- auth: '/test/auth/index.html'
- }
- }
- // 获取iframeUrl
- export function getIframeUrl(name: string): string {
- return url[env] + iframeUrl[env][name]
- }
经过这番操作,我们的旧系统就全部都嵌入进来啦!至于新系统,我们就和平常的开发一样,常规操作就可以了。主应用打开的视图如下:

不管我们拆分成了多少个项目来开发然后拼接成一个页面,对于用户来说,这完完全全就是一个系统,只是对于开发者来说有区别而已。
完成了基础功能之后,我们还需要确保应用之间能够相互通信,由于主应用和子应用的通信 API 有一点差别,用的时候容易混淆,不够简便,所以我们对其进行了二次封装,提供了统一的通信 API。
对于具体的使用方法,我们通过几个简单的例子来说明:
准备工作
首先,要引入我们的 getMicroAppMessage() 方法,获取一个通信对象
import { getMicroAppMessage } from "v-micro-app-plugin"; const microAppMessage = getMicroAppMessage();
发出全局信息:用法一致
- microAppMessage.sendGlobal({
- data: { fun: "sendGlobal", text: "给全局发送数据~sendGlobal" },
- callback: () => {
- console.log("使用sendGlobal发送数据成功,执行回调!");
- },
- });
子应用给主应用发出信息:无需 appName 参数
- microAppMessage.sendMessage({
- data: { app: "appSecond", value: "子应用给主应用发送数据~sendMessage" },
- callback: () => {
- console.log("子应用使用sendMessage发送数据成功,执行回调!");
- },
- });
主应用给子应用发出信息:需要 appName 参数
- microAppMessage.sendMessage({
- data: { app: "mainApp", value: "主应用给appFirst发送数据~sendMessage" },
- appName: "appFirst",
- callback: () => {
- console.log("主应用使用sendMessage发送数据成功,执行回调!");
- },
- });
接收全局信息: 用法一致
- setTimeout(() => {
- console.log("接收到的全局信息getGlobalMessage:", microAppMessage.getGlobalMessage());
- }, 1000);
子应用接收主应用发来的信息:无需 appName 参数
- setTimeout(() => {
- console.log(
- "子应用接收到主应用发来的非全局信息getMessage:",
- microAppMessage.getMessage()
- );
- }, 1000);
主应用接收子应用发来的信息:需要 appName 参数
- setTimeout(() => {
- console.log(
- "主应用收到appFirst发来的信息getMessage:", microAppMessage.getMessage('appFirst'),
- "主应用收到appSecond发来的信息getMessage:", microAppMessage.getMessage('appSecond')
- );
- }, 1000);
控制台信息:

