一、前言
相信大家對於微前端的概念和思想都有了解過,在此我不再贅述。在我們的業務項目中,由於項目比較大,在日常的開發過程中也暴露出來了問題:項目啟動慢,打包部署上線慢。這給我們開發和運維人員帶來了很大的不便,有時候有緊急任務需要上線,也得打包半個鍾才能交付到運維處。因此,我們打算使用微前端的方案,來解決我們目前的困境。下面我以一個簡化版本的 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種類型的視圖區兼容
- 非控制台的頁面顯示區(如登錄頁),使用
router-view - 控制台主應用頁面的顯示,使用
router-view - 控制台子應用頁面的顯示,使用
<div id="subapp-viewport"></div>
當路由切換時,這里使用一個變量viewType來進行判斷,切換視圖區;另外,系統切換時我們頭部系統顯示以及側欄也需要進行變化,這里使用一個變量menuType來進行判斷:
// App.vue
<template>
<div class="home-container">
<!--主應用非控制台頁面展示區:比如登錄-->
<router-view v-if="status.viewType === 'full'"></router-view>
<!--控制台頁面展示區-->
<div style="width: 100%;height: 100%;" v-show="status.viewType !== 'full'">
<div class="home-header box">
<header-nav :menuType=status.menuType></header-nav>
</div>
<div class="home-content box">
<div class="home-nav">
<ul class="nav-menu-admin">
<li class="nav-menu-item" v-for="(item, i) in navActive" @click="skip(item.path)">
<span slot="title">{{item.name}}</span>
</li>
</ul>
</div>
<!--主應用頁面展示區-->
<router-view v-show="status.viewType === 'control_main'"></router-view>
<!--子應用頁面展示區-->
<div id="subapp-viewport" class="flex" v-show="status.viewType === 'control_sub'"></div>
</div>
</div>
</div>
</template>
// 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完整代碼
<template>
<div class="home-container">
<!--主應用非控制台頁面展示區:比如登錄-->
<router-view v-if="status.viewType === 'full'"></router-view>
<!--控制台頁面展示區-->
<div style="width: 100%;height: 100%;" v-show="status.viewType !== 'full'">
<div class="home-header box">
<header-nav :menuType=status.menuType></header-nav>
</div>
<div class="home-content box">
<div class="home-nav">
<ul class="nav-menu-admin">
<li class="nav-menu-item" v-for="(item, i) in navActive" @click="skip(item.path)">
<span slot="title">{{item.name}}</span>
</li>
</ul>
</div>
<!--主應用頁面展示區-->
<router-view v-show="status.viewType === 'control_main'"></router-view>
<!--子應用頁面展示區-->
<div id="subapp-viewport" class="flex" v-show="status.viewType === 'control_sub'"></div>
</div>
</div>
</div>
</template>
<script lang="ts">
import {
Component,
Vue,
Watch
} from 'vue-property-decorator';
import HeaderNav from "@/components/header-nav/header-nav.vue";
@Component({
components: {
HeaderNav
}
})
export default class App extends Vue {
$router: any;
private $route: any;
private isLoading: boolean = true;
private $window: any;
private user: any = {
email: 'admin'
};
private status: any = {
viewType: 'control_main', // 頁面視圖類型 {String} --full:非控制台部分| control_main:控制台主應用|control_sub:控制台子應用;用於控制試圖展示區切換
menuType: 'sysA' // 導航類型 {String} -- sysA:系統A| sysB:系統B;用於控制左側菜單切換
}
private nav: any = {
sysA: [
{
name:'總覽頁',
path:'/gernal'
},
{
name:'子應用1首頁',
path:'/subone/home'
},
{
name:'子應用1介紹頁',
path:'/subone/about'
},
],
sysB: [
{
name:'子應用2首頁',
path:'/subtwo/home'
},
{
name:'子應用2介紹頁',
path:'/subtwo/about'
}
]
};
private navActive:any = [];
@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);
}
/**重置頭部導航/側欄菜單顯示 */
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];
}
/*重置容器顯示情況*/
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 skip(url:any) {
this.$router.push(url);
}
private mounted() {
/**整理頁面 */
let menuType = this.$route.path.split('/')[1];
this.getPageStatus(menuType);
/**整理導航 */
this.filterMenu(this.$route);
}
}
</script>
<style lang="scss">
....
</style>
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的端口改為與主應用配置的一致,且加上跨域headers和output配置
// 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
五、小結
由此一個簡單的微前端框架便完成了,需要注意的點是:
- 主應用如何注冊子應用
- 系統切換時側欄和可視區同步變化兼容
- 子應用的加載兼容
