• 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=" 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=" 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. 子应用的加载兼容
  • 相关阅读:
    关于前后端跨域问题的解决
    vue+vuex+router+element ui
    winfrom导入excel文件
    winform 导出excel文件
    winfrom DataGridView 列表操作
    LaTex之CTex初体验一
    图像配准的步骤
    Phase Based Feature Detection and Phase Congruency(相位一致性)
    查找论文中的代码
    KullbackLeibler
  • 原文地址:https://www.cnblogs.com/huiguo/p/15670091.html
Copyright © 2020-2023  润新知