116- 微前端架构

190 阅读16分钟

参考网址:qiankun.umijs.org/zh

一、基本概念

微前端架构借鉴了微服务架构思想。将一个庞大的前端工程拆分为很多个模块,独立为一个应用的。一个项目可以拆分为多个项目开发。每个人可以负责自己的项目,独立开发、测试、部署。然后在通过架构设计将这些小的应用整合在一起。实现通信,并将所有业务结合起来。对于用户来说。访问的时候就是一个项目。

场景:

  1. 项目非常大,会将业务优先级列出来。优先级高的业务先开发,优先级低的业务延后开发。一个庞大的项目遇到需要进行拆分,可以分为很多个版本。一个版本一个版本迭代。

    一个项目要拆分为多个业务,并且每个业务采用不同技术栈来开发。可以利用微前端架构来进行设计。

    拆分开的项目,不限制技术、不限制语言。最终你可以整合在一起。

  2. 你们公司做产品开发,开发了一套工业管理系统。其实我们可以在项目设计之初,就将业务拆分开来开发。将项目每个业务板块,都独立开发(每个业务都是一个独立项目)。在你进行项目整合的时候,可以利用微前端架构选择性整合项目。

单体架构,一个项目搞定所有业务。开发起来更加简单,遇到庞大业务。后去不好拆分和维护

微前端架构:开发设计会比较麻烦,还要解决样式隔离、通信问题。但是拆分多个板块,独立开发、部署。整合在一起运行,这就是灵活地方

特点:

  1. 技术栈无关:主框架不限制引入技术,子应用也不限制技术。
  2. 独立开发/部署:每一端都可以独立开发并部署,还可以整合起来一起运行
  3. 增量升级:当一个应用庞大之后,技术升级累加业务,实际上很麻烦。可能会影响之前业务,微前端只需要继续叠加项目。
  4. 独立运行:每个子应用独立运行,相互间不影响
  5. 效率提升:维护效率

二、微前端架构主流方案

(1)方案一

基于iframe这个框架来实现应用拆分并加载运行。

核心思想,这个标签在HTML代码中引入过后,可以加载第三方的网站。这样就可以一个项目中整合多个项目的页面。可以把每个iframe标签链接的资源当成一个独立的项目

特点:

  1. 用法简单,一个标签就可以解决项目整合的问题。
  2. 完美隔离,每个iframe标签里面的内容,完全独立,在内存中独立的空间。html、css、js代码相互影响。
  3. 不限制使用,你可以用多个iframe来实现业务加载

缺点:

  1. 无法保持路由状态,刷新后页面重置了。
  2. 完全隔离,导致当前应用和子应用之间通信非常麻烦。
  3. 整个应用加载会比较慢。iframe加载比较耗时。内存消耗比较大。

(2)single-spa路由劫持方案

single-spa单页面开发,推荐动态加载模块来实现微前端。

提供一个基础HTML页面,在这个页面中通过ES6的组件化思想动态加载模块。达到我们项目整合目的。

(3)micro-app

京东提供的一个微前端架构方案。

京东内部在single-spa基础上在次封装的一套架构。

(4)qiankun

蚂蚁开源微前端架构。真正意义上上单页面开发微前端架构。

目前来说,这个架构设计比较完善。解决了vite、webpack这种打包工具兼容问题。

三、主应用项目设计

每个微前端项目都需要一个主应用(有且仅有一个),负责启动项目并开始加载(组织)所有微应用。

项目中可以有多个微应用。每个微应用可以独立成一个业务。接入主应用中。

启动主应用,就可以访问对应微应用。

主应用和微应用无限制技术栈。

在我的项目中: Vue3作为主应用:

  1. 登录、注册、权限、基础功能

React作为微应用:

  1. 商品板块(把商品业务全部涵盖)

Vue2作为微应用:

  1. 商铺、商户管理作为独立业务

Vue3作为微应用:

  1. 财务统计

四、主应用环境搭建

要将Vue3作为主应用,需要在这个项目中下载qiankun依赖

(1)下载依赖

npm i qiankun

(2)配置qiankun的内容

src/qiankun/index.js(index.ts)这个文件就是qiankun核心配置文件

/**
 * 用于存放微应用,每增加一个微应用,需要在这个数组配置一下
 */
const apps: any = [
]
import {
    registerMicroApps,
    addGlobalUncaughtErrorHandler,
    start
} from "qiankun"
/**
 * 进行微应用的注册。
 * qiankun才能识别这些微应用
 */
registerMicroApps(apps, {
    //微应用加载之前,会执行这个生命周期
    beforeLoad: (app) => {
        console.log("beforeLoad", app.name);
        return Promise.resolve()
    },
    //微应用挂载成功后执行的生命周期
    afterMount: (app) => {
        console.log("afterMount", app.name);
        return Promise.resolve()
    }
})
/**
 * 添加全局异常捕获处理器
 */
addGlobalUncaughtErrorHandler((event) => {
    console.error(event);
    const { message: msg } = event
    //加载失败的时候,判断提示
    if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
        alert("微应用加载失败!!!!")
    }
})
export default start

(3)加载qiankun配置

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from "@/router"
import i18n from "@/lang"
import {createPinia} from "pinia"
import piniaPersist from "pinia-plugin-persist"
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import startQiankun from "@/qiankun"
//app实例上面挂载插件,以后每个组件都可以使用router这个插件
app.use(router)
app.use(i18n)
app.mount('#app')
// 启动qiankun架构开始加载微应用
startQiankun()

app.mount代表进行Vue3项目挂载。

startQiankun代表启动qiankun的环境,加载子应用

(4)配置路由出口

给主应用中配置的路由添加name属性

{path:"/login",name:"Login",component:Login,meta:{cache:false}},

在路由渲染出口的位置,进行判断

<router-view v-show="route.name"></router-view>
<div v-show="!route.name" id="container"></div>
import {useRoute} from "vue-router"
const route = useRoute()

div#container这个容器就是用于存放你的子应用页面。

当我们子应用配置成功后,主应用访问加载子应用,以后默认会将子应用放在当前这个容器中显示

五、React子应用

React是一个独立的项目。可以自己独立启动运行。也可以实现在主应用加载这个子应用。

就行需要配置React项目的基础环境。并配置React加载到qianlun实现微前端环境

(1)下载对应插件

npm i @craco/craco
npm i craco-less

配置项目启动环境。

修改启动命令

"scripts": {
      "start": "craco start",
      "build": "craco build",
      "test": "craco test",
      "eject": "craco eject"
    },

配置craco.config.js

const CracoLessPlugin = require("craco-less")
module.exports = {
    plugins:[
        {plugin:CracoLessPlugin}
    ]
}

(2)配置好React路由

npm i react-router-dom

下载好路由后,需要在App.js中配置路由

import React from 'react'
import { BrowserRouter, Routes, Route } from "react-router-dom"
import List from './pages/List'
import Category from './pages/Category'
//进行判断,当前React应用是独立运行,还是通过主应用加载
const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react":""
//react/category
export default function App() {
  return (
    <BrowserRouter basename={BASE_NAME}>
      <Routes>
        <Route path='/' element={<List/>}></Route>
        <Route path='/category' element={<Category/>}></Route>
      </Routes>
    </BrowserRouter>
  )
}

当前这个React可以独立运行,默认访问路径/

但是这个项目通过主应用来加载,默认访问这个项目的时候,路由会增加前缀/react

所以我们在进行路由设计的时候,window.__POWERED_BY_QIANKUN__判断到底这个项目独立运行还是通过qiankun来加载项目

(3)进入index.js文件

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
let root = null;
if (window.__POWERED_BY_QIANKUN__) {
  // 动态设置 webpack publicPath,防止资源加载出错
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
/**
 * 渲染函数
 * 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
 */
function render() {
  root = ReactDOM.createRoot(document.getElementById('root'));
  root.render(<App />);
}
// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}
/**
 * 生命周期函数:由qiankun产生的
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log("ReactApp bootstraped");
}
/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log("ReactApp mount", props);
  render(props);
}
/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  console.log("ReactApp unmount");
  // ReactDOM.unmountComponentAtNode(document.getElementById("root"));
  if (root) {
    root.unmount();
    root = null;
  }
}

总结:

  1. 当前这个子应用打包运行是由子应用自己完成(独立部署运行)还是通过qiankun来调用加载
  2. render函数封装过后,调用会不一样。
  3. 如果qiankun调用你这个子项目,我们会产生三个生命周期函数。

(4)配置webpack打包

在craco.config.js文件中配置打包规则

const CracoLessPlugin = require("craco-less")
module.exports = {
    plugins: [
        { plugin: CracoLessPlugin }
    ],
    webpack: {
        configure: (webpackConfig, { env, paths }) => {
            webpackConfig.output = {
                library: `ReactApp`, //打包后项目名字ReactApp
                libraryTarget: 'umd', //打包格式,必须采用umd,qiankun才能加载
                // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
                chunkLoadingGlobal: `webpackJsonp_ReactApp`,
                globalObject: 'window'
            }
            return webpackConfig
        }
    },
    devServer: {
        //允许跨域问题
        headers: {
            'Access-Control-Allow-Origin': '*',
        },
        historyApiFallback: true,
        hot: false,
        // watchContentBase : false,
        liveReload: false
    }
}

启动项目,保证能够独立允许项目

(5)父应用加载子应用

需要在父应用中的App.vue中设计如下代码

<template>
  <div>
    <router-view v-show="route.name"></router-view>
    <div v-show="!route.name" id="container"></div>
    <!-- <router-view></router-view> -->
  </div>
</template>
<script lang='ts' setup>
import {useRoute} from "vue-router"
const route = useRoute()
</script>
<style lang='scss' scoped>
</style>

对于父级应用来说,/react这个路由属于一级路由。需要早App.vue中寻找container容器

六、子应用页面显示到二级路由

(1)在主应用中配置路由

/react这个路由作为主应用的一个二级路由页面

{path:"/",name:"Home",component:Home,meta:{cache:false},
        children:[
            {path:"home",name:"Main",component:Main,meta:{cache:false}},
            {path:"system/user",name:"User",component:User,meta:{cache:true}},
            {path:"system/role",name:"Role",component:Role,meta:{cache:false}},
            {path:"react/:pathMatch(.*)",name:"React",component:ReactVue,meta:{cache:false}},
        ],
    },

设置的路由路径为react/:pathMatch(.*)代表你访问/react或者/react/xxx都会映射到ReactVue组件

/react属于主应用中的一个路由页面。

(2)创建React.vue文件

在views目录下面创建React.vue文件,作为我们子应用要渲染的页面

<template>
    <!-- 渲染容器,要实现子应用内容填放 -->
  <div id="container"></div>
</template>
<script lang='ts' setup>
</script>
<style lang='scss' scoped>
</style>

相当于我们访问/react的时候,会在页面中先渲染这个React.vue页面。

然后我们子应用匹配路由也是/react,同时也会调用子应用。并拿到对应加载数据。

就会默认显示在当前这个页面的container容器中

(3)注册微应用代码

  {
        name: 'ReactApp', // app name registered
        entry: '//localhost:8000',
        container: '#container',
        activeRule: '/react',
    }
]

activeRule代表我们加载子应用的路径。

总结:

主应用加载子应用,只有你访问子应用路由。一般才会默认加载子应用。为了性能考虑

当切出子应用的时候,不会销毁。现在主应用卸载这个子应用。

等你下次切入到子应用中,在进行mount一次,在渲染

七、Vue2子应用

Vue2可以作为主应用,也可以作为子应用。

在Vue2中配置项目也要满足两个规范:

  1. 独立打包运行这个项目不受影响。
  2. 通过qiankun来加载这个vue2子项目,要能够实现通信

(1)改造路由

import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('../views/AboutView.vue')
  }
]
export default routes

将创建VueRouter的代码删除,只需要在index.js文件中配置路由映射规则就可以了

是因为我们后续在main.js中根据判断来决定new VueRouter是否需要添加base属性

(2)main.js配置加载环境

import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import routes from "./router"
Vue.config.productionTip = false
Vue.use(VueRouter);
if (window.__POWERED_BY_QIANKUN__) {
  // 动态设置 webpack publicPath,防止资源加载出错
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
let instance = null;
let router = null;
/**
 * 渲染函数
 * 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
 */
function render() {
  // 在 render 中创建 VueRouter,可以保证在卸载微应用时,移除 location 事件监听,防止事件污染
  router = new VueRouter({
    // 运行在主应用中时,添加路由命名空间 /vue
    base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",
    mode: "history",
    routes,
  });
  // 挂载应用
  instance = new Vue({
    router,
    render: (h) => h(App),
  }).$mount("#app");
}
// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}
/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发          bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log("VueApp bootstraped");
}
/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log("VueApp mount", props);
  render(props);
}
/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  console.log("VueApp unmount");
  instance.$destroy();
  instance = null;
  router = null;
}

核心思想也是一样:

根据项目执行情况来决定是否添加base基础路由路径

主要qiankun来加载子应用,我们都要产生三个生命周期函数

配置完成后,你们可以独立打包运行vue2项目测试一下能否浏览器正常进行访问

(3)配置webpack

我们需要找到项目vue.config.js文件

有可能你创建的项目中没有这个文件。你自己在项目根目录创建一个同名文件

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
    transpileDependencies: true,
    devServer: {
        port: 8001,
        headers: {
            'Access-Control-Allow-Origin': '*',
        },
    },
    configureWebpack: {
        output: {
            library: `Vue2App`,
            libraryTarget: 'umd', // 把微应用打包成 umd 库格式
            // jsonpFunction: `webpackJsonp_vue2-project`, // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
        },
    },
})

完成上述的配置过后,vue2子应用配置成功了。

(4)主应用额外添加一个路由

在Vue3主应用路由映射中添加如下代码

{path:"/",name:"Home",component:Home,meta:{cache:false},
        children:[
            {path:"home",name:"Main",component:Main,meta:{cache:false}},
            {path:"system/user",name:"User",component:User,meta:{cache:true}},
            {path:"system/role",name:"Role",component:Role,meta:{cache:false}},
            {path:"react/:pathMatch(.*)",name:"React",component:ReactVue,meta:{cache:false}},
            {path:"vue/:pathMatch(.*)",name:"React",component:VuePageVue,meta:{cache:false}}
        ],
    },

在views文件夹中创建VuePageVue我呢家

<template>
  <div id="container"></div>
</template>
<script lang='ts' setup>
</script>
<style lang='scss' scoped>
</style>

(5)主应用添加子应用

src/qiankun/index.ts文件中

const apps: any = [
    {
        name: 'ReactApp', // app name registered
        entry: '//localhost:8000',
        container: '#container',
        activeRule: '/react',
    },
    {
        name: 'Vue2App',
        entry: '//localhost:8001',
        container: '#container',
        activeRule: '/vue',
    }
]

在主应用添加了一个子应用。

以后主应用中访问/vue默认进入子应用

(6)问题出现

image-20240105121213141

如果在Vue的主应用中,加载Vue子应用。

一定要注意,子应用$mount挂载的时候。指定位置不要和主应用挂载位置一样。

不然就会出现子应用页面直接挂载主应用html文件中,覆盖主应用代码

子应用中

// 挂载应用
  instance = new Vue({
    router,
    render: (h) => h(App),
  }).$mount("#app2");
index.html
<body>
    <div id="app2"></div>vue
    <!-- built files will be auto injected -->
  </body>

才能实现我们Vue项目和Vue项目通信。并减少冲突

八、服务通信

(1)流程分析

父应用登录过后获取用户信息。如果在子应用中使用,应该如何进行跨应用通信。

父子应用通信我们一般直接提供全局共享的环境。父应用可以往里面存储数据,子应用取出来使用。

子应用修改全局共享环境的数据,父应用触发页面更新

子应用和子应用之间通信,也是通过把数据传递父应用全局共享,在实现多个子应用修改

qiankuan这个框架中,通信流程如下:

20230324173522

这个通信流程总结:

  1. 需要提供一个全局状态池,一般会放在主应用中。
  2. 任何一个应用想要实现数据共享,必须注册一个观察者。放在观察者池里面。
  3. 任何一个应用修改了全局状态池里面的数据,通知观察者池里面的应用进行页面更新

采用了观察者模式来实现通信

(2)主应用代码搭建

在主应用中找到Home组件。并加入下面代码

import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化全局状态池
const actions: MicroAppStateActions = initGlobalState({username:"bobo"});
// 创建一个观察者,并加入到观察者池里面
actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});

总结:

  1. initGlobalState这个api可以实现初始化一个全局状态。
  2. onGlobalStateChange可以进行actions状态池新增一个观察者,一旦全局状态数据发送变化,通知我们当前这个应用获取更新结果

(3)React子应用中开发代码

在src/qiankun/actions.js

封装Action工具目的是为了得到父应用传递过来的参数,并封装起来。让页面能够调用

class Actions {
    // 默认值为空 Action
    actions = {
        onGlobalStateChange: null,
        setGlobalState: null,
    };
    /**
     * 设置 actions
     */
    setActions(actions) {
        this.actions = actions;
    }
    /**
     * 映射
     */
    onGlobalStateChange() {
        return this.actions.onGlobalStateChange(...arguments);
    }
    /**
     * 映射
     */
    setGlobalState() {
        return this.actions.setGlobalState(...arguments);
    }
}
const actions = new Actions();
export default actions;

在React子应用index.js文件中引入actions.js

export async function mount(props) {
  console.log("ReactApp mount", props);
  actions.setActions(props)
  render(props);
}

接受到父组件的actions对象,并保存自己封装actions工具中。

下一步就可以在页面中,引入封装actions来实现对数据修改

List组件中代码

import React, { useEffect, useState } from 'react'
import styles from "../assets/styles/list.module.less"
import actions from "../qiankun/actions"
export default function List() {
  const [myname,setMyname] = useState("默认名字")
  //组件挂载成功
  useEffect(()=>{
    actions.onGlobalStateChange((state,prev)=>{
      console.log("更新过后数据",state);
      console.log("更新之前数据",prev);
      setMyname(state.username)
    })
  },[])
  const changeState = ()=>{
    actions.setGlobalState({username:"吃饭睡觉"})
  }
  return (
    <div>
      <h2 className={styles.title}>List</h2>
      <p>{myname}</p>
      <button onClick={changeState}>修改名字</button>
    </div>
  )
}

子应用要更新全局状态的数据,可以使用setGlobalState来进行更新。

父应用接受到数据变化,也会进行页面渲染。

总结:

实现应用通信,有两种方案:

  1. 本地存储的方案:缺点在于数据大小由限制,持久化操作,效率更慢
  2. 通过全局状态管理,父子应用可以共享这个状态。提供了更大的状态机。效率更高。开发更麻烦

九、样式隔离

样式隔离主要解决父应用和子应用之间的样式冲突、子应用和子应用之间样式是否相互影响

核心原理:在父应用指定的容器中,将子应用的打包后代码。加载运行。

解决问题:

  1. 父子应用的样式隔离问题
  2. 子应用和子应用之间样式隔离

总结:

  1. 默认情况下,父应用设置的样式,会影响子应用(父应用样式要么公共的,要么子应用有一样样式名字)
  2. 默认情况下,子应用和子应用之间样式不会产生影响

子应用之间样式隔离问题

如果你的项目启动开启了沙箱环境。那子应用之间样式隔离默认情况下九不用在处理。

默认情况下沙箱可以确保单实例场景子应用之间的样式隔离。

配置代码

// 启动qiankun架构开始加载微应用
startQiankun({prefetch:"all",sandbox:{strictStyleIsolation:true}})

strictStyleIsolation:这个模式下面默认设置值为true

微应用可以是框架来搭建,也可以是H5项目,如果是H5项目。样式隔离采用默认沙箱更加方便。

子应用和父应用样式隔离

虽然开启了沙箱环境,但是无法解决父应用和子应用之间的样式冲突问题。

strictStyleIsolation只能解决单应用实例之间的样式隔离问题。无法解决父子应用样式隔离问题

父子样式隔离问题需要让每个应用开启自己的模块样式

Vue项目:

<style scoped>

意味着当前的css样式,只能作用于本项目中指定页面,不会影响其他页面,更不可能影响子应用或者父应用

React项目:

import styles from "...xxx.less"
styles.box

来设计样式

如果你的项目是Vue或者React获取其他脚手架项目。默认都会自带样式的模块化。可以隔绝自身样式对其他模块影响。

但是如果以后你的微前端架构中,有普通的H5项目,或者JQuery项目。默认没有样式模块化(样式隔离)

默认采用官方推出的沙箱环境来解决样式隔离问题