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. 控制台子應用頁面的顯示,使用<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的端口改為與主應用配置的一致,且加上跨域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. 子應用的加載兼容


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM