参考网址:qiankun.umijs.org/zh
一、基本概念
微前端架构借鉴了微服务架构思想。将一个庞大的前端工程拆分为很多个模块,独立为一个应用的。一个项目可以拆分为多个项目开发。每个人可以负责自己的项目,独立开发、测试、部署。然后在通过架构设计将这些小的应用整合在一起。实现通信,并将所有业务结合起来。对于用户来说。访问的时候就是一个项目。
场景:
-
项目非常大,会将业务优先级列出来。优先级高的业务先开发,优先级低的业务延后开发。一个庞大的项目遇到需要进行拆分,可以分为很多个版本。一个版本一个版本迭代。
一个项目要拆分为多个业务,并且每个业务采用不同技术栈来开发。可以利用微前端架构来进行设计。
拆分开的项目,不限制技术、不限制语言。最终你可以整合在一起。
-
你们公司做产品开发,开发了一套工业管理系统。其实我们可以在项目设计之初,就将业务拆分开来开发。将项目每个业务板块,都独立开发(每个业务都是一个独立项目)。在你进行项目整合的时候,可以利用微前端架构选择性整合项目。
单体架构,一个项目搞定所有业务。开发起来更加简单,遇到庞大业务。后去不好拆分和维护
微前端架构:开发设计会比较麻烦,还要解决样式隔离、通信问题。但是拆分多个板块,独立开发、部署。整合在一起运行,这就是灵活地方
特点:
- 技术栈无关:主框架不限制引入技术,子应用也不限制技术。
- 独立开发/部署:每一端都可以独立开发并部署,还可以整合起来一起运行
- 增量升级:当一个应用庞大之后,技术升级累加业务,实际上很麻烦。可能会影响之前业务,微前端只需要继续叠加项目。
- 独立运行:每个子应用独立运行,相互间不影响
- 效率提升:维护效率
二、微前端架构主流方案
(1)方案一
基于iframe这个框架来实现应用拆分并加载运行。
核心思想,这个标签在HTML代码中引入过后,可以加载第三方的网站。这样就可以一个项目中整合多个项目的页面。可以把每个iframe标签链接的资源当成一个独立的项目
特点:
- 用法简单,一个标签就可以解决项目整合的问题。
- 完美隔离,每个iframe标签里面的内容,完全独立,在内存中独立的空间。html、css、js代码相互影响。
- 不限制使用,你可以用多个iframe来实现业务加载
缺点:
- 无法保持路由状态,刷新后页面重置了。
- 完全隔离,导致当前应用和子应用之间通信非常麻烦。
- 整个应用加载会比较慢。iframe加载比较耗时。内存消耗比较大。
(2)single-spa路由劫持方案
single-spa单页面开发,推荐动态加载模块来实现微前端。
提供一个基础HTML页面,在这个页面中通过ES6的组件化思想动态加载模块。达到我们项目整合目的。
(3)micro-app
京东提供的一个微前端架构方案。
京东内部在single-spa基础上在次封装的一套架构。
(4)qiankun
蚂蚁开源微前端架构。真正意义上上单页面开发微前端架构。
目前来说,这个架构设计比较完善。解决了vite、webpack这种打包工具兼容问题。
三、主应用项目设计
每个微前端项目都需要一个主应用(有且仅有一个),负责启动项目并开始加载(组织)所有微应用。
项目中可以有多个微应用。每个微应用可以独立成一个业务。接入主应用中。
启动主应用,就可以访问对应微应用。
主应用和微应用无限制技术栈。
在我的项目中: Vue3作为主应用:
- 登录、注册、权限、基础功能
React作为微应用:
- 商品板块(把商品业务全部涵盖)
Vue2作为微应用:
- 商铺、商户管理作为独立业务
Vue3作为微应用:
- 财务统计
四、主应用环境搭建
要将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;
}
}
总结:
- 当前这个子应用打包运行是由子应用自己完成(独立部署运行)还是通过qiankun来调用加载
- render函数封装过后,调用会不一样。
- 如果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中配置项目也要满足两个规范:
- 独立打包运行这个项目不受影响。
- 通过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)问题出现
如果在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这个框架中,通信流程如下:
这个通信流程总结:
- 需要提供一个全局状态池,一般会放在主应用中。
- 任何一个应用想要实现数据共享,必须注册一个观察者。放在观察者池里面。
- 任何一个应用修改了全局状态池里面的数据,通知观察者池里面的应用进行页面更新
采用了观察者模式来实现通信
(2)主应用代码搭建
在主应用中找到Home组件。并加入下面代码
import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化全局状态池
const actions: MicroAppStateActions = initGlobalState({username:"bobo"});
// 创建一个观察者,并加入到观察者池里面
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
总结:
- initGlobalState这个api可以实现初始化一个全局状态。
- 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来进行更新。
父应用接受到数据变化,也会进行页面渲染。
总结:
实现应用通信,有两种方案:
- 本地存储的方案:缺点在于数据大小由限制,持久化操作,效率更慢
- 通过全局状态管理,父子应用可以共享这个状态。提供了更大的状态机。效率更高。开发更麻烦
九、样式隔离
样式隔离主要解决父应用和子应用之间的样式冲突、子应用和子应用之间样式是否相互影响
核心原理:在父应用指定的容器中,将子应用的打包后代码。加载运行。
解决问题:
- 父子应用的样式隔离问题
- 子应用和子应用之间样式隔离
总结:
- 默认情况下,父应用设置的样式,会影响子应用(父应用样式要么公共的,要么子应用有一样样式名字)
- 默认情况下,子应用和子应用之间样式不会产生影响
子应用之间样式隔离问题
如果你的项目启动开启了沙箱环境。那子应用之间样式隔离默认情况下九不用在处理。
默认情况下沙箱可以确保单实例场景子应用之间的样式隔离。
配置代码
// 启动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项目。默认没有样式模块化(样式隔离)
默认采用官方推出的沙箱环境来解决样式隔离问题