实现一个简单的微前端应用-qiankun

1,265 阅读9分钟

qiankun

前言

我们公司之前一直使用的qiankun来做微应用的,开发的时候我有做过一小部分,但是对于整体qiankun的使用还是没有一个很全面的了解,刚好最近有空,就自己弄了两个微应用实验。实现框架语言为:qiankun+vue2,内容除了基本的qiankun使用还有其他遇到的坑的解决方案:

  1. qinakun的两者模式使用及差异
  2. 解决样式冲突
  3. 子应用过多导致的性能过慢爆炸问题(没有具体实现在代码里面但是有文字说明)
  4. 实际项目中遇到的其他的坑

直接看效果图

什么是微应用

image.png

微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为多个小型前端应用聚合为一的应用,具备以下几个核心价值:

  • 技术栈无关: 主框架不限制接入应用的技术栈,微应用具备完全自主权
  • 独立 开发 、独立部署: 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
  • 增量升级: 在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
  • 独立运行时: 每个微应用之间状态隔离,运行时状态不共享。

现在社区中比较成熟的技术框架主要是single-spa、qiankun,当然iframe是一个天然的沙箱,也可以当做一个微应用运行。后面会讲到什么时候用iframe。

qiankun是蚂蚁金服基于single-spa,增加了sandbox,globalSate, 资源预加载等核心功能,需要编译为umd方式,对于AMD,systemJs支持不友好。

为什么要用微应用?不用的话不可以吗?

拿我们公司来说,开发的系统(只针对后管系统)有30-50个左右,领导要求对用户来说在各个系统点击的时候域名使用的是同一个,这样交互友好一些。

按照这个需求来说,首先想到的是把所有的系统在放到一个项目维护。先不说后面随着系统逐渐增多之后项目打包会很大的问题,我们很多开发都要在一个git进行操作,由于每个人自身的需求迭代都是比较快的,到时候操作git的时候难免出错,很难管理的。而且打包那么大,每次构建耗时需要很长,首屏加载也会很慢,对用户来说就是用起来好像有点卡。再一点就是,前端技术都是很快就迭代更新了,这不项目最开始搭建的时候Vue3还没有出来,运行了一两年Vue3出来了,大家想尝试新的技术,如果都放在一个git管理,那不是所有系统都需要重构?由于项目重构,重构后的系统内部可能存在的问题是不可预估的,那所有的系统都再重新测一遍已有的功能吗?当然是不可能的老铁,想什么呢

总的来说考虑使用微前端主要有以下几点:

  1. git项目管理方便,每个人只需负责自己的那个git项目就行,独立运行,独立部署,不用跟别人一起管理git,美滋滋
  2. 扩展性较强,各个应用独立,后续新的项目可以使用其他技术升级,不影响之前的子应用
  3. 性能(主要是首屏加载),只需加载基座模块的打包代码即可,会快很多

项目搭建

这里我并没有记录搭建的详细过程,但是可以肯定的是,一定要npm i qiankun的,哈哈开个玩笑。如果想要从头搭建一个主应用+子应用的项目,可以参考这个链接或者直接clone我的[git地址](qiankun-1 (github.com))

基本使用

qiankun是有两种使用模式,一种是registerMicroApps + start,另一种是loadMicroApp

基于路由配置微应用

也就是registerMicroApps + start 两个api一起运作。

RegistrableApps

name - 必选,微应用的名称,微应用之间必须确保唯一
entry - 必选,微应用的入口
container - 必选,微应用挂载的容器节点。如container: '#root'
activeRule - 必选,微应用的激活规则
props - object - 可选,主应用需要传递给微应用的数据
LifeCycles

beforeLoad - 可选
beforeMount - 可选
afterMount - 可选
beforeUnmount - 可选
afterUnmount - 可选
start Options

prefetch - 可选,是否开启预加载,默认为 true
sandbox - 可选,是否开启沙箱,默认为 true
示例 在main.js里面

const apps = [
  {
    name: 'vue1',
    entry: '//localhost:8001',
    container: '#vue1',
    id:'vue1',
    activeRule:'/vue1-admin',
    props: {
      appId:'vue1',
      routes: [
        {
          path: '/firstPage1',
          name: 'FirstPage1',
          type:'c',
          component: 'vue1/firstPage/index.vue',
        },
        {
          path: '/secondPage1',
          name: 'SecondPage1',
          type:'c',
          component: 'vue1/secondPage/index.vue',
        },
      ]
    },
    
  },
  {
    name: 'vue2',
    entry: '//localhost:8002',
    container: '#vue2',
    id:'vue2',
    activeRule:'/vue2-admin',
    props: {
      appId:'vue2',
      routes: [
        {
          path: '/firstPage2',
          name: 'FirstPage2',
          type:'c',
          component: 'vue2/firstPage/index.vue',
        },
        {
          path: '/secondPage2',
          name: 'SecondPage2',
          type:'c',
          component: 'vue2/secondPage/index.vue',
        },
      ]
    },
   
  }
]
 
registerMicroApps(apps);//注册应用
start();//开启

手动加载微应用

loadMicroApp options 基座调用

app - 必选,微应用的基础信息
name - 必选,微应用的名称,微应用之间必须确保唯一
entry - 必选,微应用的入口
container - 必选,微应用挂载的容器节点。如container: '#root'
props - object - 可选,主应用需要传递给微应用的数据
configuration - 可选,微应用的配置信息
sandbox - 可选,是否开启沙箱,默认为 true
子应用链接基座的生命周期

mount(): Promise<null>;
unmount(): Promise<null>;
update(customProps: object): Promise<any>;
getStatus(): | "NOT_LOADED" | "LOADING_SOURCE_CODE" | "NOT_BOOTSTRAPPED" | "BOOTSTRAPPING" | "NOT_MOUNTED" | "MOUNTING" | "MOUNTED" | "UPDATING" | "UNMOUNTING" | "UNLOADING" | "SKIP_BECAUSE_BROKEN" | "LOAD_ERROR";
loadPromise: Promise<null>;
bootstrapPromise: Promise<null>;
mountPromise: Promise<null>;
unmountPromise: Promise<null>
示例 

    handleMenu(type){
       let appData = this.microApp.filter(item => item.name == type)[0]
       if(!appData) return 
       this.activeApp = appData.id
      if(!this.mountedMicropApps[type]) {
        this.mountedMicropApps[type] = loadMicroApp(appData)
        this.mountedMicropApps[type].mountPromise.then(() => {
          console.log('--->成功加载子系统',);
        })
      }
      else {
         this.mountedMicropApps[type].update({}).then(() => {
          console.log('--->更新子系统',this.activeApp);
        })
      }
    }

两个模式的特点

第一种模式:基于路由配置微应用

  1. 这个模式相对是比较自动的,当浏览器的url发生变化后,框架会自动加载相应的微应用,判断条件就是注册应用时的activeRule ,如当前浏览器url是基于vue1-admin ,就会主动加载vue1这个子应用。

  1. 这个模式只能加载一个微应用,其他应用会被主动卸载掉。也就是子应用之间切换是不能有缓存的,每次需要重新加载。

第二种模式:手动加载微应用

  1. 需要主动在基座进行子应用路由注册,在注册时由于子应用的页面代码不在基座,所以路由可以不需要component属性。

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from './components/Home.vue'
Vue.use(VueRouter);
const routes = [    
    {
        path: '/home',
        name: 'Home',
        component: Home,
    },
    // 子系统路由注册
    {
        path: '/vue1-admin',
        name: 'vue1-admin',
        alwaysShow:true,
        hidden: false,
        children:[
        {
            path: 'firstPage1',
            name: 'FirstPage1',
            alwaysShow:true,
            hidden: false,
            meta:{ label: 'vue1' },
            type:'c'
        },
        {
            path: 'secondPage1',
            name: 'SecondPage1',
            alwaysShow:true,
            hidden: false,
            meta:{ label: 'vue1' },
            type:'c'
        },
        ]
    },
    {
        path: '/vue2-admin',
        name: 'vue2-admin',
        alwaysShow:true,
        hidden: false,
        children:[
        {
            path: 'firstPage2',
            name: 'FirstPage2',
            alwaysShow:true,
            hidden: false,
            meta:{ label: 'vue2' },
            type:'c'
        },
        {
            path: 'secondPage2',
            name: 'SecondPage2',
            alwaysShow:true,
            hidden: false,
            meta:{ label: 'vue2' },
            type:'c'
        },
        ]
    },
]
const router = new VueRouter({
    mode: 'history',
    base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",//基础路径
    routes
})
 
export default router
  1. 这个模式子应用切换不会主动卸载,需要自己手动卸载。再次切换到之前加载的子应用时,需通过update方法来更新。

使用遇到的问题

样式冲突

首先公司的系统因为需要路由缓存,再次切换期望不刷新页面,所以采用的是第二种模式。前面也说了,第二种模式应用之间相互切换并不会将之前加载的子应用都杀掉,静态文件这些都保存了。那么问题就来了,如果子应用存在相同类名的样式,是否会进行覆盖操作?答案是会的。

所以我做了一个实验,在base,vue1,vue2三个项目都修改了el-link的样式,请注意,这里的样式是需要写在全局才会有冲突的。

vue1vue2,我添加了相同的class名称.test

经过上面的动图和图片,可以很清晰看出确实是存在着样式冲突的,那么这个样式覆盖有没有什么规律?

首先静态资源加载的时候也是有先后顺序的,其中vue2的静态资源最后加载,那么不管你是先点击vue1应用还是vue2应用,最终变成的颜色都是红色,因为最后加载的样式会覆盖前面的样式。

解决方案

注意:基座包括所有子应用都需要这样添加

  1. npm install postcss-loader postcss-selector-namespace -D--save
  2. 根目录添加文件postcss.config.js
module.exports = {
    plugins: {
      'postcss-selector-namespace': {
        namespace(css) {
          // 不需要添加命名空间的文件
          if (css.includes('unNamespace')) return '';
          return '.base-css'
        }
      }
    }
  }
  1. app.vue同步添加class

解决之后运行如下:

性能问题

使用qiankun之后,首屏只加载基座代码,所以首屏加载比较快。但是这只是体现性能的一个方面,具体的子应用在真正运行时也会快吗?答案是否定的。

由于qiankun内部使用了eval,所以整体的性能会下降很多,这里我们暂且不追究为什么时候eval。就我们公司而言,因为系统比较多,eval使用也比较多,子应用在加载时其实是很慢的。同一个页面在本地运行(脱离qiankun)和接入基座对比,性能至少慢了10倍左右。

这种时候就要触动性能优化大军了,什么懒加载啦,减少Dom渲染啦等等,但是在qiankun里面就算你优化得再好,也是有性能瓶颈的,毕竟他的eval摆在那里,这是我们无法改变的。

所以比较暴力的是实用iframe,当一个页面因为需求问题无法再继续性能优化,就直接把这个页面使用iframe嵌入

  1. 判断该页面是否有iframe标识,这个标识一般是菜单配置时设置
  2. 如果是iframe标识,则不走微应用,直接将该页面嵌入主应用,token可以通过主应用url传给子应用,子应用只需解析token并加载页面就可以。注意的是这个时候iframe中的应用其实就是本地的应用。