qiankun微前端项目实践方案(基础框架篇)


一、前言

相信大家对于微前端的概念和思想都有了解过,在此我不再赘述。在我们的业务项目中,由于项目比较大,在日常的开发过程中也暴露出来了问题:项目启动慢,打包部署上线慢。这给我们开发和运维人员带来了很大的不便,有时候有紧急任务需要上线,也得打包半个钟才能交付到运维处。因此,我们打算使用微前端的方案,来解决我们目前的困境。下面我以一个简化版本的 demo,进行我们实践的介绍。 demo 源码放在 github 上:https://github.com/xiaohuiguo/qiankun-vue-ts-demo 。

二、项目简介

项目划分为几个模块系统:
主应用:【头部+侧栏+总览页+登录页】
系统A:应用1【首页+介绍页】
系统B : 应用2【首页+介绍页】

项目页面视图:

结构介绍:
我们根据业务情况划分了,主应用、子应用;
主应用主要是主框架结构,包含头部侧栏以及控制页面显示区域,另外对于一些常规页(登录/总览入口/注册)这类的页面直接放在主应用即可。
子应用则按业务情况,进行划分,这里我分成了 应用1 和 应用2。

系统操作演示:

三、技术选型

以下是目前比较流行的几种方案对比(参考了网上一些总结的不错的资料):

框架思考:考虑到业务以及团队技术水平情况,我们选择了qiankun(乾坤)作为我们的微服务接入框架,vue+ts作为项目主开发框架。主要是qiankun的接口封装的比较好,也比较容易上手,对于我们目前团队的能力,是可以接受的。

四、qiankun 框架构建

1.主框架应用
1.1 路由及视图设计
首先,一般项目都是有一个登录页的,在登录页不加载子应用,只有通过登录成功后,跳到控制台子应用的页面时,才进行加载子应用的。在本项目中,如果是打开主应用的页面都是不会去加载子应用的;
主应用的页面有登录页,总览页(属于控制台),路由如下:

/login
/gernal

在本项目中,子应用都是在控制台展示的,当打开子应用的路由时,就会触发子应用资源的加载,子应用路由如下:

/subone/**
/subtwo/**

针对以上情况,我们的视图区要做3种类型的视图区兼容

  1. 非控制台的页面显示区(如登录页),使用router-view
  2. 控制台主应用页面的显示,使用router-view
  3. 控制台子应用页面的显示,使用

当路由切换时,这里使用一个变量viewType来进行判断,切换视图区;另外,系统切换时我们头部系统显示以及侧栏也需要进行变化,这里使用一个变量menuType来进行判断:

// App.vue

// App.vue
private status: any = {
            viewType: 'control_main', // 页面视图类型 {String} --full:非控制台部分| control_main:控制台主应用|control_sub:控制台子应用;用于控制视图展示区切换 
            menuType: 'sysA' // 导航类型 {String} -- sysA:系统A| sysB:系统B;用于控制左侧菜单切换
        }
private getPageStatus(index: any) {
          console.log(index)
            if (['login'].indexOf(index) > -1) {
                this.status.viewType = "full";
            } else if ([ 'gernal'].indexOf(index) > -1) {
                this.status.viewType = "control_main"
            } else {
                this.status.viewType = "control_sub"
            }
            this.$forceUpdate();
        }
private filterMenu(route: any) {
            let menuType = route.path.split('/')[1];
            switch (menuType) {
                case 'subtwo':
                    this.status.menuType = 'sysB';
                    break;
                default:
                    this.status.menuType = 'sysA';
                    break;
            }
            this.navActive = this.nav[this.status.menuType];
        }
@Watch('$route') changeRoute(to: any, from: any) {
            this.navActive = this.nav[this.status.menuType];
            console.log(to, from)
            let menuType = to.path.split('/')[1];
            this.filterMenu(to);
            this.getPageStatus(menuType);
        }

1.2 子应用注册
子应用信息配置包括路由触发值,端口,以及视图区的容器

// main.ts
// 子应用端口
const MicroAppsPort: any = {
    VUE_APP_SUB_ONE: 8081,
    VUE_APP_SUB_TWO: 8082
}
function getEntry(name: any) {
    const entryUrl = '//' + environment['host'] + ':';
    return entryUrl + MicroAppsPort[name] + '/'
}
// 构建子应用, #subapp-viewport为子应用容器
const appsRouter: any = [
    {
        name: 'subone',
        entry: getEntry('VUE_APP_SUB_ONE'),
        activeRule: '/subone',
    },
    {
        name: 'subtwo',
        entry: getEntry('VUE_APP_SUB_TWO'),
        activeRule: '/subtwo',
    }
]
const microApps: any = appsRouter.map((item: any) => {
    return {
        ...item,
        container: '#subapp-viewport', // 子应用挂载的div
        props: {
            routerBase: item.activeRule, // 下发基础路由
            window: window // 保持父子公用同一个window
        }
    }
});

使用qiankun提供的api进行子应用的注册及微服务启动

// main.ts
// 注册子应用
registerMicroApps(microApps);
// 启动微服务
start();

1.3 mait.ts和App.vue完整代码

// mait.ts完整代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerMicroApps, start } from 'qiankun'

import {environment} from "@/environment/environment";

// 组件总的样式
import '@/assets/sass/index.scss';

// 渲染主应用, #app为主应用根元素
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
// 子应用端口
const MicroAppsPort: any = {
    VUE_APP_SUB_ONE: 8081,
    VUE_APP_SUB_TWO: 8082
}
function getEntry(name: any) {
    const entryUrl = '//' + environment['host'] + ':';
    return entryUrl + MicroAppsPort[name] + '/'
}
// 构建子应用, #subapp-viewport为子应用容器
const appsRouter: any = [
    {
        name: 'subone',
        entry: getEntry('VUE_APP_SUB_ONE'),
        activeRule: '/subone',
    },
    {
        name: 'subtwo',
        entry: getEntry('VUE_APP_SUB_TWO'),
        activeRule: '/subtwo',
    }
]
const microApps: any = appsRouter.map((item: any) => {
    return {
        ...item,
        container: '#subapp-viewport', // 子应用挂载的div
        props: {
            routerBase: item.activeRule, // 下发基础路由
            window: window // 保持父子公用同一个window
        }
    }
});
// 注册子应用
registerMicroApps(microApps);
// 启动微服务
start();
// App.vue完整代码



2. 系统A:子应用1(系统B同理)

2.1 main.ts修改
由于用的是history路由模式,子应用需要兼容qiankun框架嵌入时的应用base路径

// main.ts完整代码

import './public-path.ts'
import Vue from 'vue'
import VueRouter, { NavigationGuardNext, Route } from 'vue-router'
import App from './App.vue'
import routes from './router'

Vue.config.productionTip = false

let router = null
let instance: any = null
const _window: any = window

function render ({props, routerBase}: any = {}) {
  router = new VueRouter({
    // 子模块是history路由时,处理basi url
    base: _window.__POWERED_BY_QIANKUN__ ? routerBase : '/',
    mode: 'history',
    routes
  })
  instance = new Vue({
    router,
    render: h => h(App)
  }).$mount(props ? props.querySelector('#app') : '#app')
}

// 本地调试
if (!_window.__POWERED_BY_QIANKUN__) {
  render()
}

// 导出生命周期
export async function bootstrap () {
  console.log('应用1启动')
}

export async function mount (props: any) {
  console.log('应用1挂载', props)
  render(props)
}

export async function unmount () {
  instance.$destroy()
  instance.$el.innerHTML = ''
  instance = null
  router = null
}

2.2 path_public.ts修改,兼容qiankun加载情况下应用的端口,并且需要在上面main.ts中引入

const _window: any = window
if (_window.__POWERED_BY_QIANKUN__) {
  if (process.env.NODE_ENV === 'development') {
      // eslint-disable-next-line
      __webpack_public_path__ = `//localhost:${process.env.VUE_APP_PORT}${process.env.BASE_URL}`;
    } else {
      // eslint-disable-next-line
      __webpack_public_path__ = _window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
}

2.3 添加vue.webpack.js和端口配置
devServer的端口改为与主应用配置的一致,且加上跨域headersoutput配置

// vue.webpack.js
const { name } = require('./package.json') 
const webpack = require('webpack');
module.exports = {
  transpileDependencies: ['common'],
  chainWebpack: config => config.resolve.symlinks(false),
  configureWebpack: {
    output: {
      // 把子应用打包成 umd 库格式
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`
    },
    plugins: []
  },
  devServer: {
    port: process.env.VUE_APP_PORT, // 端口配置
    headers: {
      'Access-Control-Allow-Origin': '*'
    }
  }
}
// .env
VUE_APP_PORT=8081

五、小结

由此一个简单的微前端框架便完成了,需要注意的点是:

  1. 主应用如何注册子应用
  2. 系统切换时侧栏和可视区同步变化兼容
  3. 子应用的加载兼容