引子
上一章聊了下微前端的概念,这一章就来整点干货,全是实践遇到的坑:
为了防止篇幅过长,这里就先只写路由相关的东西。
下面的内容全部是基于主子应用同时使用hash路由实现的
history模式可以使用base参数来实现,在导航及匹配过程中可以不需要那么多处理
路由
qiankun支持基于路由匹配和手动加载两种加载子应用的模式。手动加载适合本身不带有路由,只具有单个页面的简单子应用,而带有路由的子应用更适合通过路由匹配的方式来激活。
手动加载不需要考虑路由处理,这里就略过了。
而通过路由匹配来加载,其中有三个核心问题:
- 如何判断当前路由进入了子应用
- 如何解析属于子应用的路由
- 如何渲染属于子应用的路由
判断当前路由进入了子应用
在qiankun提供的子应用注册api中,需要提供匹配规则activeRule:
registerMicroApps([
{
name: 'react app', // app name registered
entry: '//localhost:7100',
container: '#yourContainer',
activeRule: '/yourActiveRule',
}
])
当微应用信息注册完之后,一旦浏览器的url发生变化,便会自动触发qiankun的匹配逻辑,所有activeRule 规则匹配上的微应用就会被插入到指定的container中,同时依次调用微应用暴露出的生命周期钩子。
前缀约定
为了使qiankun能正常匹配到子应用的路由,子应用所有的路由都应该以/yourActiveRule开头,例如子应用的首页路由应设置为/yourActiveRule/home。
当需要同时注册多个子应用时,activeRule不应该互为子串。例如不应该将两个子应用的activeRule分为设置为/yourActiveRule和/yourActiveRule2(除非你想同时激活这两个子应用)。
为了方便起见,我们可以将所有子应用的activeRule统一格式为/sub_app_xx,xx为子应用的名称。
解析属于子应用的路由
要想正确渲染出主应用和子应用的组件,我们先来看路由解析的过程:
graph TD
A[路径跳转/sub_app_test/home] --> A1[主应用根据路径匹配路由<br/>通过component属性渲染组件]
A1 --> B{主应用匹配<br/>activeRule}
B --> |Yes: 路由属于子应用|C[交给子应用处理]
B --> |No: 路由属于主应用|D[结束]
C --> E[子应用根据路径匹配路由匹配<br/>通过component属性渲染组件]
观察流程图,我们可以发现:
- 主应用需要拥有子应用的路由,否则第二步会直接报错
- /sub_app_test/home对应的路由组件被渲染了两次
比较合理的解决方法是,在主应用中添加一条不含component的通配路由:
{
path: '/sub_app_test/*' //通配路由,不含component
}
渲染属于子应用的路由
方法一:容器切换
主应用作为一个控制台项目时,较为常见的是这种展示结构:
渲染子应用组件需要对main部分进行一些改造:判断路由属于主应用时,main直接作为<router-view>渲染主应用本身的路由,而判断路由属于子应用时,main作为qiankun的渲染容器。
需要注意的是,为了防止
qiankun加载时找不到容器DOM,该DOM需要使用v-show代替v-if控制显隐。
<template>
<section class="main-container">
<!-- 非子应用路由时使用router-view -->
<transition v-if="!isSubApp" name="fade-transform" mode="out-in">
<router-view />
</transition>
<!-- 子应用路由时展示容器Dom -->
<div v-show="isSubApp" id="yourContainer"/>
</section>
</template>
<script>
computed: {
//判断路由属于子应用还是主应用
isSubApp() {
return this.$route.path.indexOf('/sub_app') > -1
}
}
</script>
DOM需要根据id对应注册子应用时填写的容器节点container。例如这里的id为yourContainer,对应前面注册时填写的#yourContainer,更详细的匹配规则可以参考qiankun官网。
方法二:路由通配添加特殊组件
为通配路由增加特殊组件,该组件为qiankun的渲染容器
{
path: '/sub_app_test/*',
component: () => import('../Microapp.vue'),
}
//Microapp.vue
<template>
<div id="yourContainer"/>
</template>
其他问题
自动改写子应用路由前缀
按照前缀约定,名为test的子应用的所有路由都应该以/sub_app_test开头。但在实际开发时,子应用可能是由一个已经存在的项目改造而来。这种项目在改造前已包含了大量的路由声明,修改成本较大,且若后续要变更子应用的名称,还需要再次大量改动路由相关代码。手动修改显然是不合适的。
我们可以利用vue-router提供的路由异名属性alias,为所有原生路由添加一个带有前缀的异名:
const prefix = window.__POWERED_BY_QIANKUN__ ? '/sub_app_test' : ''
export function addAlias(routes) {
if(!window.__POWERED_BY_QIANKUN__) return
for (let route of routes) {
let path = route.path
//当路径为/xxx时,添加alias: /sub_app_test/xxx
if (path && path.startsWith('/') && !path.startsWith(prefix)){
route.alias = prefix + path
}
if (route.children) {
addAlias(route.children)
}
}
}
路径为/home的原生路由:
{
path: '/home',
name: 'home',
component: () => import('@/modules/home'),
},
处理后得到如下路由,该路由可以匹配路径/sub_app_test/home:
{
path: '/home',
name: 'home',
component: () => import('@/modules/home'),
alias:'/sub_app_test/home' //处理后新增的异名属性
},
改写子应用路由导航
除了路由声明,子应用内部可能还存在大量的路由跳转逻辑。
举个例子,在子应用的首页/sub_app_test/home点击按钮后可以进入表单中心:
onClick(){
this.$router.push('/formCenter')
}
根据qiankun匹配规则和我们的前缀约定,只有以/sub_app_xx开头的路径可以匹配子应用,这时定向到的路径为/formCenter已经跳出了子应用。事实上,我们期望的路径是/sub_app_test/formCenter。
为此,我们可以通过重写vue-router的原生跳转方法使路径重定向:
import Router from 'vue-router'
const prefix = window.__POWERED_BY_QIANKUN__ ? '/sub_app_test' : ''
if (window.__POWERED_BY_QIANKUN__) {
//重写Push方法
const originalPush = Router.prototype.push
Router.prototype.push = function push(location, onResolve, onReject) {
if ((typeof location) === 'string') {
if (!location.includes(prefix))
location = prefix + location
}
if ((typeof location) === 'object' && !location.name) {
if (!location.path.includes(prefix))
location.path = prefix + location.path
}
if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
return originalPush.call(this, location).catch(err => err)
}
//重写Replace方法
const originalReplace = Router.prototype.replace
Router.prototype.replace = function replace(location, onResolve, onReject) {
if ((typeof location) === 'string') {
if (!location.includes(prefix))
location = prefix + location
}
if ((typeof location) === 'object' && !location.name) {
if (!location.path.includes(prefix))
location.path = prefix + location.path
}
if (onResolve || onReject) return originalReplace.call(this, location, onResolve, onReject)
return originalReplace.call(this, location).catch(err => err)
}
}
并在全局前置路由守卫中进行导航重定向:
const prefix = window.__POWERED_BY_QIANKUN__ ? '/sub_app_test' : ''
router.beforeEach(async (to, from, next) => {
if(!window.__POWERED_BY_QIANKUN__){
next()
return
}
if(to.path && !to.path.startsWith(prefix)){
next({ path: prefix + to.path, query: to.query, params: to.params, replace: true })
return
}
next()
}
动态路由的子应用,反复进出后路由异常
与vue-router版本有关,建议使用v3.0.7+。