主题
基于京东MicroApp的微前端实现
一、前言
1、什么是微前端,为什么用微前端?
1)机构平台中的家庭床位模块复用社区大部分模块之后,机构平台将会变成巨石应用,有将近400个模块,项目变得不可运行
2)随着业务发展,平台功能越来越多,微前端搭配微服务是趋势,是更加科学合理的架构体系
3)当前养老部门机构、社区小组模块共用,未来跨部门、跨企业协作将会多了一种更简单高效的对接方式
4)「技术栈无关」是微前端的核心价值,我们可以通过他来升级团队技术栈
*其它自行了解
2、为什么选择MicroApp?
- 对Vue3的支持更好,微组件方案也更加友好
3、项目环境介绍
- 主应用:Vue2 + Webpack
- 子应用A:Vue2 + Webpack
- 子应用B:Vue3 + Vite
二、微前端集成
1、主应用管理
1)配置并启动微应用,通常在main.js中引用,详细配置见官网
javascript
import microApp from '@micro-zoe/micro-app'
microApp.start({
'router-mode': 'native',
'disable-memory-router': true
})
2)路由配置,用于将路由指向统一的页面
javascript
import micro from '@/components/common/micro.vue'
const router = new Router({
routes:[
...loginList, //其它路由
{
path: '/main',
name: 'main',
component: main,
redirect: '/index',
children: [
{
path: '/404',
name: '404',
component: nopage
},
...otherList, //其它路由
{
path: '/*',
name: 'micro',
component: micro
}
]
}
]
})
3)通用页面micro.vue
- getMicroApps() 所有子应用的配置
- checkVersion() 子应用版本校验,有更新时刷新页面
- doAppLoad()、doAppUpdate() 应用加载和更新的提示效果
javascript
<template>
<micro-app
v-if="name"
:key="name"
:name='name'
:url='url'
:data='data'
:iframe="iframe"
:baseroute="baseroute"
@created="created"
@beforemount="beforemount"
@mounted="mounted"
@unmount="unmount"
@error="error"
>
</micro-app>
</template>
<script>
import microApp from '@micro-zoe/micro-app'
import axios from 'axios'
import { getMicroApps } from '../../config/micro.js'
export default {
data() {
return {
name: '',
url: '',
iframe: false,
baseroute: '',
version: {}
}
},
computed: {
data() {
return {
store: this.$store
}
},
mainpage() {
return this.$store.state.MAINPAGE
}
},
watch: {
$route: {
handler(val) {
let active = getMicroApps().find(it => val.path.startsWith(it.baseroute+'/'))
if(!active) return
let { name, iframe = false, baseroute = '' } = active
if(name == this.name) {
microApp.setData(this.name, {
action: 'Route',
data: {
path: val.path,
query: val.query || {}
}
})
} else {
this.current = Date.now()
this.iframe = iframe
this.baseroute = baseroute
this.url = `${sessionStorage[name+'URL']}?timestamp=${Date.now()}`
this.name = name
}
this.checkVersion()
},
deep: true,
immediate: true
}
},
methods: {
created(e) {
this.mainpage.doAppLoad(true)
},
beforemount(e) {
this.mainpage.doAppLoad(true)
},
mounted(e) {
if(!e.detail.name) return
console.log(`<MicroBase/${e.detail.name} mounted success, use ${Date.now()-this.current}ms>`);
this.mainpage.doAppLoad(false)
},
unmount(e) {
if(!e.detail.name) return
console.log(`<MicroBase/${e.detail.name} unmount!>`);
},
error(e) {
console.log(`<MicroBase/${e.detail.name} error!>`);
this.mainpage.doAppLoad(false)
this.loadError()
},
checkVersion() {
let appname = this.name
let microurl = `${sessionStorage[appname+'URL']}`
axios.get(`${microurl}/static/web.version.json?timestamp=${Date.now()}`).then(({data}) => {
let { webVersion } = data
if(!this.version[appname]) {
this.version[appname] = webVersion
}
console.log("子应用"+appname+"最新版本:"+webVersion+",当前版本:"+this.version[appname]);
if(this.version[appname] != webVersion) {
this.mainpage.doAppUpdate()
}
})
},
async loadError() {
await this.$LonbonConfirm("应用加载失败,请刷新当前页面", true, '刷新')
location.reload()
}
}
}
</script>
2、子应用管理
- 子应用路由增加前缀
window.__MICRO_APP_BASE_ROUTE__
- main.js中注册子应用
javascript
//Vue2 (在子应用main.js中)
var app = null;
function render(props = {}) {
const { store } = props;
//动态注册子应用、并添加双向绑定
store.registerModule([name], ProStore)
Vue.observable(store)
app = new Vue({
router,
i18n,
store,
render: (h) => h(App),
}).$mount('#app');
}
window.mount = () => {
render(window.microApp.getData())
}
window.unmount = () => {
$('body').removeClass('theme-dark').removeClass('theme-action');
app.$store.unregisterModule([name])
app.$destroy();
app.$el.innerHTML = '';
app = null;
}
window.microApp.addDataListener(({action, data}) => {
if(action == 'Route') {
router.replace(data)
}
})
javascript
//Vue3 (在子应用main.js中)
let app = null
function render(props = {}) {
const { store } = props;
app = createApp(App)
app.config.warnHandler = () => null
app.config.globalProperties.$store = store
app.mount('#app');
}
window.mount = () => {
window._ = window.rawWindow._
window.$ = window.rawWindow.$
window.AMap = window.rawWindow.AMap
window._AMapSecurityConfig = window.rawWindow._AMapSecurityConfig
const props = window.microApp.getData()
render(props);
}
window.unmount = () => {
app.unmount()
app._container.innerHTML = ''
app = null
}
window.microApp.addDataListener(async ({action, data}) => {
if(action == 'Route') {
const router = app.config.globalProperties.$router
router.replace(data)
return
}
})
3、数据通信
- 固定数据,主应用通过micro.vue中的:data="data"将数据传给子应用
- 动态数据,主应用通过microApp.setData传值,详见micro.vue
- 方法调用,通常在主应用中将方法挂载到window对象或者store上,用于子应用调用
4、路由管理
- 该项目为旧项目,所以自己管理的路由。若是新项目,推荐使用官网提供的路由系统
- 统一在主应用进行管理,子应用使用router.replace进行路由切换
javascript
//1、子应用-调用主应用路由跳转方法(在子应用main.js中)
Vue.prototype.$routerPush = (val) => {
if(typeof val == 'string') val = { path: val }
if(!val.app) val.app = name
app.$store.state.MAINPAGE.routerPush(val)
}
javascript
//2、主应用-进行路由跳转(在主应用micro.js中)
function routerPush(val) {
if(!val.path) throw new Error('path不可为空')
let { baseroute } = getMicroApps().find(it => it.name == val.app)
val.path = baseroute + val.path
if(!val.title) {
this.$router.push(val)
} else {
this.tabAddOther(val)
}
}
javascript
//3、主应用-监听到路由变化后给子应用发送指令(在主应用micro.vue中)
microApp.setData(this.name, {
action: 'Route',
data: {
path: val.path,
query: val.query || {}
}
})
javascript
//4、子应用-收到指令后替换路由(在子应用main.js中)
window.microApp.addDataListener(({action, data}) => {
if(action == 'Route') {
router.replace(data)
}
})
三、微组件实现
1、主应用扩展微组件加载方法
javascript
//(在主应用micro.js中)
import microApp from '@micro-zoe/micro-app'
/**
* 加载微组件的函数
* @param {Object} options 配置对象
* @param {string} options.container 容器的选择器,不传默认会在micro-content下创建
* @param {string} options.name 组件的名称,默认为 'micropublic'
* @param {string} options.baseroute 基础路由,默认为 '/micro-component'
* @param {boolean} options.iframe 是否使用iframe加载,默认为 true
* @param {Object} options.data 传递给组件的数据对象
* @returns {Promise} 返回一个Promise,解析为组件实例
*/
export function loadMicroComponent({
container,
name = 'micropublic',
baseroute = '/micro-component',
iframe = true,
...data
}) {
// 创建一个新的Promise,通过microApp.renderApp异步渲染微应用
return new Promise(resolve => {
// 构建并配置微应用
const appName = `${name}${UUID.generate().substring(0, 8)}`
if(!container) {
container = `#${appName}`
$('.micro-content').append(`<div id="${appName}"></div>`)
}
microApp.renderApp({
name: appName, // 为应用生成一个唯一的名称
url: `${sessionStorage[name+'URL']}?timestamp=${Date.now()}`, // 从sessionStorage获取组件URL,并添加时间戳
container,
iframe,
baseroute,
destory: true, // 是否在卸载时销毁应用
fiber: true, // 是否启用fiber调度
data: {
store, // 传递全局store
...data, // 扩展额外的数据
success(instance) { // 成功加载组件后的回调
instance.destoryMicroComponent = () => {
microApp.unmountApp(appName)
//若是默认容器,则删除容器
if(container == `#${appName}`) $(container).remove()
}
resolve(instance) // 解析Promise
}
},
lifeCycles: { // 组件生命周期回调
mounted (e) { // 组件挂载成功的回调
console.log(`<MicroBase/${e.detail.name} mounted success>`);
},
unmount (e) { // 组件卸载的回调
console.log(`<MicroBase/${e.detail.name} unmount>`);
},
}
})
})
}
2、子应用路由配置中增加微组件处理
- 若是微组件模式则指向统一的页面lp-micro.vue
javascript
/**
* routePrefix 基础路由前缀,若为/micro-component,则表示微组件模式
* routes 项目路由
* micros 项目微组件
*/
const { routePrefix, routes, micros } = options
const isMicroComponent = routePrefix == '/micro-component'
let childList = []
if(isMicroComponent) {
childList = [{
path: "/:catchAll(.*)",
name: "microComponent",
meta: {
...micros()
},
component: () => import("./components/lp-micro.vue")
}]
}
- micros为项目微组件,如下:
javascript
export function useMicroComponents() {
return {
// 来邦养老管理平台-监护设置
careSetting: () => import("@/components/setting/index.base.vue"),
// 更多操作
seatMoreAction: () => import("@/components/seat/action/seat-more-action.vue"),
}
}
- 所有微组件路由都指向该页面lp-micro.vue,使用动态组件进行加载
javascript
<template>
<component ref="componentRef" :is="active" v-if="active" v-bind="props" v-on="on"/>
</template>
<script setup>
import { nextTick, ref, defineAsyncComponent } from 'vue';
import { useRoute } from 'vue-router'
const { meta: microComponents } = useRoute()
const componentRef = ref(null)
const active = ref(null)
const { component, props, on, success, error } = window.microApp.getData()
if(component) {
const _component = microComponents[component]
active.value = defineAsyncComponent(_component)
let st = setInterval(() => {
if(componentRef.value) {
clearInterval(st)
success && success(componentRef.value);
}
}, 100);
} else {
error && error();
}
</script>
- 微组件使用加载示例
javascript
<template>
<section v-loading="loading">
<el-row class="containerHeader bx">
<el-col class="headTitle" :span="6">{{ $store.state.pageTitle }}</el-col>
</el-row>
<el-row id="micro-system-care" class="containerBody container-tabs" style="overflow: hidden">
</el-row>
</section>
</template>
<script>
export default {
data() {
return {
loading: false
}
},
async mounted() {
this.loading = true
await this.$store.state.MAINPAGE.loadMicroComponent({
container: '#micro-system-care',
component: 'careSetting',
})
this.loading = false
}
}
</script>