本篇文章将介绍一个多页签模式的后台管理系统使用无界搭建微前端的过程以及遇到的一些问题。
父应用是vue2+webpack 子应用是vue3+vite。
请结合无界的官方文档阅读本篇博客,基础内容这里就不做废话浪费诸位大佬的时间了。
无界的基础引入
我们先建一个组件来引入无界。将来可以将所有对无界的操作都放在这个组件内。这里为了多页签模式页面缓存的记录,我们使用了保活模式。大致是这样的
js
<template>
<div class="height-block width-block">
<WujieVue
v-show="name === 'app1'"
width="100%"
height="100%"
name="app1"
:url="app1[mode]"
:alive="true"
:after-mount="afterMount"
/>
<WujieVue
v-show="name === 'app2'"
width="100%"
height="100%"
name="app2"
:url="app2[mode]"
:alive="true"
:after-mount="afterMount"
/>
</div>
</template>
<script>
import WujieVue from "wujie-vue2"
export default {
name: `WuJie`,
components: { WujieVue },
props: {
name: {
type: String,
default: `app1`,
},
},
data () {
return {
mode: process.env.NODE_ENV,
app1: {
development: `http://localhost:8010/`,
production: `${window.location.origin}/mrpv3/#/`,
},
app2: {
development: `http://localhost:8011/`,
production: `${window.location.origin}/mesh/#/`,
},
}
},
}
</script>
<style scoped lang="less">
</style>
然后我们将这个组件放在我们项目的路由根组件旁边。根据路由信息来判断 我们是渲染当前项目的路由。还是通过无界渲染子应用内容。
router-view 没有做隐藏处理,我这边是如果是子应用页面挂载了一个空组件。
然后通过路由中的meta属性中的childApp是否有值来判断当前页面是否是子应用的页面。
js
<template>
<router-view
v-if="!multiTab"
:key="$route.matched[1].name"
/>
<keep-alive v-else>
<router-view
v-if="!(src && src.includes(xinyiBaseUrl))"
:key="$route.fullPath"
/>
</keep-alive>
</template>
<template v-if="mode !== `development`">
<xin-yi v-show="src && src.includes(xinyiBaseUrl)" />
</template>
<WuJie
v-show="$route.meta.childApp"
:name="$route.meta.childApp"
/>
这样 我们子项目的嵌入就基本完成了!
ps:可以看到上面代码用到的显隐方式都是v-show。我们这样做的目的就是可以让子应用预加载。这是一种比较粗糙的方式。我们还可以使用用v-if 结合 preloadApp函数的方式。例如这样
js
<WujieVue
v-show="name === 'app1'" ==》 改为 v-if
width="100%"
height="100%"
name="app1"
:url="app1[mode]"
:alive="true"
:after-mount="afterMount"
/>
import { preloadApp } from "wujie"
mounted(){
preloadApp({ name: `app1`, url: this.mrpv3[this.mode], alive: true, exec: true, afterMount: this.afterMount })
preloadApp({ name: `app2`, url: this.mesh[this.mode], alive: true, exec: true afterMount: this.afterMount })
}
这样做的好处是无界会内部使用requestIdleCallback内容帮你分片加载。不会造成资源加载阻塞的问题。
父子应用的路由切换同步
因为我们使用的是保活模式。所以父子应用路由的切换需要用event bus来手动实现。代码大致如下
我们先将上一步父应用中的wujie组件添加如下内容。
js
import { bus } from "wujie"
watch: {
'$route': {
async handler (route, oldRoute) {
if (route.meta.childApp) {
bus.$emit(`routeChange`, this.$route)
}
},
},
},
methods: {
afterMount () {
if (this.$route.meta.childApp) {
bus.$emit(`routeChange`, this.$route)
}
bus.$on(`childrenRouteChange`, this.childrenRouteChange)
},
childrenRouteChange (route) {
this.$router.push(route)
},
},
wacth中监听父级路由的变化,如果父级路由发生变化 ,并且新进入的路由是子应用的页面。那么就使用 bus. <math xmlns="http://www.w3.org/1998/Math/MathML"> e m i t ( ' r o u t e C h a n g e ' , t h i s . emit(`routeChange`, this. </math>emit('routeChange',this.route) 来通知子应用的项目 路由也发生变化。
监听子应用的afterMount生命周期。当afterMount生命周期执行完毕后 我们根据当前路由是否是子应用路由来觉得是否切换子应用的路由是否发生切换。(看到这你是否有这样的疑问,为什么不在watch中使用初始化监听?非要子应用的afterMount生命周期完毕才行呢?因为如果你在watch中使用了初始化监听,在watch执行的时候子应用可能还没挂载完毕,关于routeChange的监听还没开启,这样就会导致bus. <math xmlns="http://www.w3.org/1998/Math/MathML"> e m i t ( ' r o u t e C h a n g e ' , t h i s . emit(`routeChange`, this. </math>emit('routeChange',this.route) 的通知成为无效通知。)
上面两步是完成了 父应用路由修改改变子应用的路由。也是同样的原理。我们在父级应用的无界组件中开启一个childrenRouteChange的事件监听。等待子应用给父级应用发送bus. <math xmlns="http://www.w3.org/1998/Math/MathML"> e m i t ( ' c h i l d r e n R o u t e C h a n g e ' , t h i s . emit(`childrenRouteChange`, this. </math>emit('childrenRouteChange',this.route) 的事件,来实现子应用路由修改改变父应用的路由。
子应用的代码改造如下:
main.js文件中的核心代码如下。我们在这个文件中引入对无界子应用改造的函数并执行。
js
import { createApp } from 'vue'
import App from './App.vue'
import wujieSetting from '@/plugins/wujie'
const app = createApp(App)
app.mount(`#app`)
//无界微前端子应用配置
wujieSetting()
@/plugins/wujie 目录下的wujie.js代码如下。我们在执行的函数中监听父应用路由的改变
js
import router from '@/router'
export default function () {
if (window.__POWERED_BY_WUJIE__) {
//路径改变
window.$wujie?.bus.$on(`routeChange`, (route: any) => {
router.push(route)
})
//无界生命周期挂载
window.__WUJIE_MOUNT = () => {}
window.__WUJIE_UNMOUNT = () => {}
}
}
然后我们还要改造子应用路由的拦截器。判断是否是无界的子应用。通过判断from.name是否有值 来判断出是否是第一次进入应用。第一次进入 是不需要通知父应用路由改变的。
js
router.beforeEach((to, from) => {
if (window.__POWERED_BY_WUJIE__ && from.name) {
window.$wujie?.bus.$emit(`childrenRouteChange`, to)
}
})
这样子应用就实现了。监听父应用路由改变
与 通知父应用路由改变
。
父子应用的信息传递
我们的子应用在开发的过程中可能会用到父应用的一些信息。例如 需要子应用的菜单最好要和父应用保持一致。子应用可能需要父应用记录的 用户信息 角色信息 平台配置等信息。
实现的方式也很简单。也是用event bus 和刚才将的路由通讯大同小异。
依旧是父应用的wujie组件中在afterMount生命周期执行后添加emit通知
js
watch: {
'$store.state.Setting': {
async handler (setting) {
bus.$emit(`settingChange`, {
...this.$store.state.Setting,
...this.$store.state.Role,
})
},
deep: true,
},
'$store.state.Role': {
async handler (role) {
bus.$emit(`settingChange`, {
...this.$store.state.Setting,
...this.$store.state.Role,
})
},
deep: true,
},
},
afterMount(){
bus.$emit(`settingChange`, {
...this.$store.state.Setting,
...this.$store.state.Role,
})
bus.$emit(`allMenusChange`, this.$store.state.Route.routes)
},
值得一提的是 不要忘了加上watch的监听。父应用的传递给子应用的信息如果发生了改变 要重新发送。
子应用的监听就是这样子了。
js
import router from '@/router'
import { useMainStore } from '@/store'
import { useRouteStore } from '@/store/route'
export default function () {
if (window.__POWERED_BY_WUJIE__) {
const mainStore = useMainStore()
const routeStore = useRouteStore()
//获取全部菜单
window.$wujie?.bus.$on(`allMenusChange`, (allMenus: any) => {
routeStore.setAllMenus(allMenus)
})
//路径改变
window.$wujie?.bus.$on(`routeChange`, (route: any) => {
router.push(route)
})
//获取公共配置数据
window.$wujie?.bus.$on(`settingChange`, (setting: any) => {
mainStore.platformConfig = setting.platformConfig
mainStore.deptInfo = setting.deptInfo
mainStore.userInfo = setting.userInfo
mainStore.rolePermissionList = setting.rolePermissions
})
//刷新路由
window.$wujie?.bus.$on(`refRoute`, useRouteStore().refRoute)
//关闭路由
window.$wujie?.bus.$on(`removeCachedRoute`, useRouteStore().removeCachedRoute)
//无界生命周期挂载
window.__WUJIE_MOUNT = () => {}
window.__WUJIE_UNMOUNT = () => {}
}
}
细心的朋友可能会发现 这些代码中有两行和我们之前讲的是毫无关系的。
js
//刷新路由
window.$wujie?.bus.$on(`refRoute`, useRouteStore().refRoute)
//关闭路由
window.$wujie?.bus.$on(`removeCachedRoute`, useRouteStore().removeCachedRoute)
这就是给我们开头说的多页签模式准备的。
多页签模式与无界的结合
实现方式相对来说是比较简单的。但是有个很重要的前提,那就是 父子应用都要支持多页签的功能。有了这个前提实现起来就很容易了。
多页签上面的功能无非就是 新增页签 关闭当前页签 关闭左侧页签 关闭右侧页签 关闭其他页签 刷新当前页签。看似很多其实不然。实际工作的就三个函数 新增 删除 与刷新。新增相当于页面跳转。支持多页签的系统都会有内部处理 因此 我们只需要在删除与刷新的时候做通讯就可以了。
父应用的wujie组件添加如下代码:
javascript
mounted(){
event.$on(`refOpenRoute`, (fullPath) => {
const route = this.openRoutes.find(route => route.fullPath === fullPath)
bus.$emit(`refRoute`, route)
})
event.$on(`removeRouteCache`, (fullPath) => {
bus.$emit(`removeCachedRoute`, [fullPath])
})
}
我在父应用中建了一套自己的event bus通讯机制(之前提到的全是wujie中带的,用于父子应用通讯的 和这个不是一个东西,切记!)。当多页签删除的时候触发removeRouteCache事件。多页签刷新的时候触发refOpenRoute事件。就如上述代码所示。当这两个事件触发的时候 我们用 bus. <math xmlns="http://www.w3.org/1998/Math/MathML"> e m i t ( ' r e f R o u t e ' , r o u t e ) 或 b u s . emit(`refRoute`, route) 或 bus. </math>emit('refRoute',route)或bus.emit(removeCachedRoute
, [fullPath]) 来通知子应用触发相同的事件。来实现多页签的刷新。子应用添加的代码就是我们上个模块最后提到的这个:
swift
//刷新路由
window.$wujie?.bus.$on(`refRoute`, useRouteStore().refRoute)
//关闭路由
window.$wujie?.bus.$on(`removeCachedRoute`, useRouteStore().removeCachedRoute)
完整代码
不包含多页签实现相关代码
父应用的wujie组件
js
<template>
<div class="height-block width-block">
<WujieVue
v-if="name === 'app1'"
width="100%"
height="100%"
name="app1"
:url="app1[mode]"
:alive="true"
:after-mount="afterMount"
/>
<WujieVue
v-if="name === 'app2'"
width="100%"
height="100%"
name="app2"
:url="app2[mode]"
:alive="true"
:after-mount="afterMount"
/>
</div>
</template>
<script>
import { mapState } from "vuex"
import { preloadApp, bus } from "wujie"
import WujieVue from "wujie-vue2"
import event from '@/plugins/event'
export default {
name: `WuJie`,
components: { WujieVue },
props: {
name: {
type: String,
default: `app1`,
},
},
data () {
return {
mode: process.env.NODE_ENV,
app1: {
development: `http://localhost:8010/`,
production: `${window.location.origin}/app1/#/`,
},
app2: {
development: `http://localhost:8011/`,
production: `${window.location.origin}/app2/#/`,
},
}
},
computed: { ...mapState(`Route`, [`openRoutes`]) },
mounted () {
preloadApp({ name: `app1`, url: this.app1[this.mode], alive: true, exec: true, afterMount: this.afterMount })
preloadApp({ name: `app2`, url: this.app2[this.mode], alive: true, exec: true, afterMount: this.afterMount })
event.$on(`refOpenRoute`, (fullPath) => {
const route = this.openRoutes.find(route => route.fullPath === fullPath)
bus.$emit(`refRoute`, route)
})
event.$on(`removeRouteCache`, (fullPath) => {
bus.$emit(`removeCachedRoute`, [fullPath])
})
},
watch: {
'$route': {
async handler (route, oldRoute) {
if (route.meta.childApp && oldRoute.name !== `Empty`) {
bus.$emit(`routeChange`, this.$route)
}
},
},
'$store.state.Setting': {
async handler (setting) {
bus.$emit(`settingChange`, {
...this.$store.state.Setting,
...this.$store.state.Role,
})
},
deep: true,
},
'$store.state.Role': {
async handler (role) {
bus.$emit(`settingChange`, {
...this.$store.state.Setting,
...this.$store.state.Role,
})
},
deep: true,
},
},
methods: {
afterMount () {
bus.$emit(`settingChange`, {
...this.$store.state.Setting,
...this.$store.state.Role,
})
bus.$emit(`allMenusChange`, this.$store.state.Route.routes)
if (this.$route.meta.childApp) {
bus.$emit(`routeChange`, this.$route)
}
bus.$on(`childrenRouteChange`, this.childrenRouteChange)
},
childrenRouteChange (route) {
this.$router.push(route)
},
},
}
</script>
<style scoped lang="less">
</style>
子应用wujie.js代码
typescript
import router from '@/router'
import { useMainStore } from '@/store'
import { useRouteStore } from '@/store/route'
export default function () {
if (window.__POWERED_BY_WUJIE__) {
const mainStore = useMainStore()
const routeStore = useRouteStore()
//获取全部菜单
window.$wujie?.bus.$on(`allMenusChange`, (allMenus: any) => {
routeStore.setAllMenus(allMenus)
})
//路径改变
window.$wujie?.bus.$on(`routeChange`, (route: any) => {
router.push(route)
})
//获取公共配置数据
window.$wujie?.bus.$on(`settingChange`, (setting: any) => {
mainStore.platformConfig = setting.platformConfig
mainStore.deptInfo = setting.deptInfo
mainStore.userInfo = setting.userInfo
mainStore.rolePermissionList = setting.rolePermissions
})
//刷新路由
window.$wujie?.bus.$on(`refRoute`, useRouteStore().refRoute)
//关闭路由
window.$wujie?.bus.$on(`removeCachedRoute`, useRouteStore().removeCachedRoute)
//无界生命周期挂载
window.__WUJIE_MOUNT = () => {}
window.__WUJIE_UNMOUNT = () => {}
}
}
无界子应用路由拦截
javascript
router.beforeEach((to, from) => {
if (window.__POWERED_BY_WUJIE__ && from.name) {
window.$wujie?.bus.$emit(`childrenRouteChange`, to)
}
})