Skip to content

基于京东MicroApp的微前端实现

一、前言

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>