使用MicroApp重构旧项目

前言

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

经过几番思考,我们发现微前端是一种非常实用的去实施渐进式重构的架构 ,很适合用微前端技术来完成本次需求,最终决定**利用 Vue3 + Vite 搭建一个全新的基座(主应用),作为新旧系统融合的桥梁,将原来的老项目接入到基座,后面的新需求都在新项目里面开发就行,不用再动老项目。**此举不仅实现了新页面用 Vue3 开发,而且老项目也能和新项目融合在一起,既保持了旧系统的稳定运行,又引入了新技术栈的活力。

同时,鉴于我们另一个 Vue2 + webpack 项目也同样面临技术过时和项目规模庞大的问题,每次开发时运行起来非常卡顿,打包很慢,后期难以维护,也需要用微前端来进行一些拆分,不可能一直往该项目上堆代码。

所以,我们决定一步到位,设计了一套微前端项目模板,将微前端的核心配置抽象为可复用的插件,并结合自研组件库、HTTP请求、权限控制等插件,构建了一个全面的项目脚手架,旨在简化未来项目的搭建流程,提升开发效率,确保技术栈的先进性与可持续性。

微前端框架选型(MicroApp)

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

我们本次项目使用的是 Vue3+Vite+TypeScript 的技术栈,在综合对比了各个框架之后,认为MicroApp是最适合我们当前现实情况的。原因有下:

  1. 使用简单,将所有功能都封装到一个类WebComponent组件内,从而实现在基座应用中嵌入一行代码即可渲染一个微前端应用。

  2. 不需要像 single-spa 和 qiankun 一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改webpack配置,是目前市面上接入微前端成本最低的方案。

  3. 功能丰富,提供了 js沙箱、样式隔离、元素隔离、预加载、数据通信、静态资源补全等一系列完善的功能。

  4. 零依赖,这赋予它小巧的体积和更高的扩展性。

  5. 兼容所有框架,为了保证各个业务之间独立开发、独立部署的能力,micro-app做了诸多兼容,在任何技术框架中都可以正常运行。

  6. 侵入性低:对原代码几乎没有影响。

  7. 组件化:基于webComponents思想实现微前端。

微前端架构设计(pnpm+monorepo)

使用 pnpm 和 monorepo 管理项目依赖和代码结构,确保所有子应用和基座应用都位于同一仓库的不同目录下,便于集中管理和版本控制。

javascript 复制代码
/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流程为各个子应用和主应用提供统一的构建/部署流程

微前端设计思路

  1. 拆分功能模块:首先,我们需要将整个后台管理系统拆分为多个独立的功能模块,如用户管理模块、专项管理模块、订单管理模块等。每个模块都可以作为一个独立的微应用进行开发和维护。

  2. 设计通信协议:为了实现各个微应用之间的通信和资源共享,我们需要设计一套统一的通信协议和API。例如,我们可以定义一个emit方法来触发自定义事件,以及一个on方法来监听自定义事件;我们还可以使用Webpack的CommonsChunkPlugin插件来实现公共资源的提取和共享。

  3. 开发主应用:主应用是整个后台管理系统的入口,它负责加载和管理各个微应用。主应用需要提供一个容器元素来承载各个微应用的内容,并提供一些基础设施服务,如路由管理、状态管理等。此外,主应用还需要实现与各个微应用的通信和资源共享。

  4. 开发微应用:每个微应用都是一个独立的功能模块,它可以独立开发、部署和运行。每个微应用都需要提供一个容器元素来承载该应用的内容,并提供一些与主应用交互的接口,如共享资源、通信等。此外,微应用还需要实现自身的业务逻辑和界面展示。

  5. 集成测试:在完成各个微应用的开发后,我们需要对整个系统进行集成测试,确保各个微应用之间的通信和资源共享正常工作。此外,我们还需要对整个系统的性能、稳定性等进行测试和优化。

v-micro-app-plugin

本文中的微前端项目,使用的是 v-micro-app-plugin插件 ,它是一款基于京东MicroApp框架的微前端插件,旨在帮助开发者快速地将微应用集成到不同的系统中,实现高效、灵活的前端模块化开发。以下是详细的使用指南,希望能够帮助你快速上手。

本文中的案例-资源地址

该插件暂时放在私有 npm 包中,外部无法获取,但是可以直接将项目中打包后生成的dist文件夹复制下来,放到需要使用的项目中的 node_modules 文件夹下即可。(虽然这并不是一个推荐的做法,因为它绕过了 npm 的包管理和版本控制功能,可能会导致一系列问题,包括版本冲突、难以维护和更新等。)当然,也可以私信我,我直接把包分享给你啦!~~

项目实践

技术栈:

  • 主应用:Vue3+Vite+TypeScript

  • 子应用1(老项目):用 iframe 挨个嵌入

  • 子应用2(新模块):react / Vue3 ...

本次重构的是一个后台管理系统,最外层是基座,基座不仅是微前端应用集成的一个关键平台,还承载着维护公共资源、管理依赖项以及确立开发规范的重要使命。具体而言,其职责可概括为以下几点:

  1. 子应用集成,给子应用提供渲染容器

  2. 权限管理

  3. 会话管理

  4. 路由、菜单管理

  5. 主题管理

  6. 共享依赖

  7. 多语言管理(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 微前端插件

bash 复制代码
pnpm i v-micro-app-plugin --save

3、配置并使用

为了便于后续复用该配置信息(配置路由、菜单、名称等等),我们将 options 参数独立出来,放在 settings 文件夹下的 microAppSetting.ts 文件中。

  • microAppSetting.ts:
javascript 复制代码
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:
javascript 复制代码
import microAppSetting from '@/settings/microAppSetting'
​
const options = microAppSetting
​
// 初始化微前端插件  
await initMyMicroApp(app, options, router, store);

**⚠注意:**一定要在 router 和 store 初始化后,才可以使用 initMyMicroApp 进行初始化!!!举个简单的例子:

javascript 复制代码
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 微前端插件

bash 复制代码
pnpm i v-micro-app-plugin --save

3、配置并使用

  • sub-app-first:
javascript 复制代码
const options = {
  projectName: 'appFirst',
  subAppConfigs: {},
  isBaseApp: false, // 标记当前应用不为主应用
  basePath: '/', // 打包路径或其他基础路径 
  disableSandbox: false, // 是否禁用沙箱
  iframe: true, // 是否使用 iframe
}
​
// 初始化微前端插件  
await initMyMicroApp(app, options, router, store);
  • sub-app-second:
javascript 复制代码
const options = {
  projectName: 'appSecond',
  subAppConfigs: {},
  isBaseApp: false, // 标记当前应用不为主应用
  basePath: '/', // 打包路径或其他基础路径 
  disableSandbox: false, // 是否禁用沙箱
  iframe: true, // 是否使用 iframe
}
​
// 初始化微前端插件  
await initMyMicroApp(app, options, router, store);

配置路由信息

有了主子应用之后,我们就需要在主应用中给子应用配置路由信息,这里一共有 2 个子应用,我们为它们分别进行配置。

  • appFirst:
javascript 复制代码
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:
javascript 复制代码
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'],
  }
}

封装 MicroAppContainer

众所周知,路由切换时,可以给<router-view />填充上对应路径的内容,同理,microApp中的<micro-app></micro-app>也有同样的功能。我们可以对其进行二次封装,结合 v-if,以便于根据是路由指向的是子应用,还是本系统自由模块,来判断究竟是渲染微应用视图,还是渲染普通视图。

为了达到这个目的,我们可以新建一个 MicroAppContainer 文件夹,在其中创建一个index.vue,然后键入以下内容:

javascript 复制代码
<template>
  <div :class="[`${prefixCls}-container`]">
    <!-- name:应用名称, url:应用地址 -->
    <micro-app v-bind="options" :name="options.name" keep-alive></micro-app>
  </div>
</template>
<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 属性可根据需要决定是否设置。

区分是否微应用视图

  • 在你需要加载子应用页面的地方:
html 复制代码
        <div :class="[`${prefixCls}-viewer-microapp`]" v-if="isMicroAppView">
          <MicroAppContainer :options="microAppOptions" />
        </div>
        <div v-else>
          <router-view />
        </div>
  • 一些必要的逻辑语句:
javascript 复制代码
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 搬过来,相当于换个皮肤展示就好。但又因页面数量庞大,所以我们选择直接封装一个 iframe 组件,配合路由动态设置其 src 值,实现页面的动态切换。

在这里,我们专门创建了一个子应用,用于独立地展示该老系统,起到新旧隔离的作用,具体操作步骤如下:

1、首先,在 views 中新建一个 iframeViews 文件夹,然后创建 index.vue

javascript 复制代码
<template>
  <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>
</template>

<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、配置路由表

javascript 复制代码
// 用户管理
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

javascript 复制代码
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() 方法,获取一个通信对象

javascript 复制代码
import { getMicroAppMessage } from "v-micro-app-plugin";

const microAppMessage = getMicroAppMessage();
  • 发出全局信息:用法一致
javascript 复制代码
  microAppMessage.sendGlobal({
    data: { fun: "sendGlobal", text: "给全局发送数据~sendGlobal" },
    callback: () => {
      console.log("使用sendGlobal发送数据成功,执行回调!");
    },
  });
  • 子应用给主应用发出信息:无需 appName 参数
javascript 复制代码
  microAppMessage.sendMessage({
    data: { app: "appSecond", value: "子应用给主应用发送数据~sendMessage" },
    callback: () => {
      console.log("子应用使用sendMessage发送数据成功,执行回调!");
    },
  });
  • 主应用给子应用发出信息:需要 appName 参数
javascript 复制代码
  microAppMessage.sendMessage({
    data: { app: "mainApp", value: "主应用给appFirst发送数据~sendMessage" },
    appName: "appFirst",
    callback: () => {
      console.log("主应用使用sendMessage发送数据成功,执行回调!");
    },
  });
  • 接收全局信息: 用法一致
javascript 复制代码
  setTimeout(() => {
    console.log("接收到的全局信息getGlobalMessage:", microAppMessage.getGlobalMessage());
  }, 1000);
  • 子应用接收主应用发来的信息:无需 appName 参数
javascript 复制代码
  setTimeout(() => {
    console.log(
      "子应用接收到主应用发来的非全局信息getMessage:",
      microAppMessage.getMessage()
    );
  }, 1000);
  • 主应用接收子应用发来的信息:需要 appName 参数
javascript 复制代码
  setTimeout(() => {
    console.log(
      "主应用收到appFirst发来的信息getMessage:", microAppMessage.getMessage('appFirst'),
      "主应用收到appSecond发来的信息getMessage:", microAppMessage.getMessage('appSecond')
    );
  }, 1000);
  • 控制台信息:
相关推荐
喜欢吃燃面1 分钟前
C++算法竞赛:位运算
开发语言·c++·学习·算法
传奇开心果编程2 分钟前
【传奇开心果系列】Flet框架实现的家庭记账本示例自定义模板
python·学习·ui·前端框架·自动化
草莓熊Lotso4 分钟前
《详解 C++ Date 类的设计与实现:从运算符重载到功能测试》
开发语言·c++·经验分享·笔记·其他
阿珊和她的猫4 小时前
v-scale-scree: 根据屏幕尺寸缩放内容
开发语言·前端·javascript
_Kayo_6 小时前
node.js 学习笔记3 HTTP
笔记·学习
加班是不可能的,除非双倍日工资8 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi9 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
CCCC13101639 小时前
嵌入式学习(day 28)线程
jvm·学习
gnip9 小时前
vite和webpack打包结构控制
前端·javascript
excel10 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端