基于Vue2 & qiankun的微前端实现(第1章- 路由)

3,778 阅读4分钟

引子

上一章聊了下微前端的概念,这一章就来整点干货,全是实践遇到的坑:

image.png

为了防止篇幅过长,这里就先只写路由相关的东西。

下面的内容全部是基于主子应用同时使用hash路由实现的

history模式可以使用base参数来实现,在导航及匹配过程中可以不需要那么多处理

路由

qiankun支持基于路由匹配和手动加载两种加载子应用的模式。手动加载适合本身不带有路由,只具有单个页面的简单子应用,而带有路由的子应用更适合通过路由匹配的方式来激活。

手动加载不需要考虑路由处理,这里就略过了。

而通过路由匹配来加载,其中有三个核心问题:

  1. 如何判断当前路由进入了子应用
  2. 如何解析属于子应用的路由
  3. 如何渲染属于子应用的路由

判断当前路由进入了子应用

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_xxxx为子应用的名称。

解析属于子应用的路由

要想正确渲染出主应用和子应用的组件,我们先来看路由解析的过程:

graph TD
A[路径跳转/sub_app_test/home] --> A1[主应用根据路径匹配路由<br/>通过component属性渲染组件]
A1 --> B{主应用匹配<br/>activeRule}
B --> |Yes: 路由属于子应用|C[交给子应用处理]
B --> |No: 路由属于主应用|D[结束]
C --> E[子应用根据路径匹配路由匹配<br/>通过component属性渲染组件]

观察流程图,我们可以发现:

  1. 主应用需要拥有子应用的路由,否则第二步会直接报错
  2. /sub_app_test/home对应的路由组件被渲染了两次

比较合理的解决方法是,在主应用中添加一条不含component的通配路由:

{ 
    path: '/sub_app_test/*' //通配路由,不含component
} 

渲染属于子应用的路由

方法一:容器切换

主应用作为一个控制台项目时,较为常见的是这种展示结构:

image.png

渲染子应用组件需要对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。例如这里的idyourContainer,对应前面注册时填写的#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+