美文网首页
搭建qiankun Demo

搭建qiankun Demo

作者: 山上有桃子 | 来源:发表于2022-01-06 19:10 被阅读0次

    最近项目重构,leader想要前端应用微应用技术,作为前端基石,为今天新旧技术迭代做准备。so,今天开始尝试阿里 qiankun,并搭建一个demo。

    在尝试新技术之前,我们需要分别准备一个主应用test-qiankun-main-vue3,及一个子应用test-qiankun-son-app1
    公司现在前端技术栈 vue3 + typeScript ,所以使用 vue-cli 4脚手架来创建我们的父子应用

    $ vue create <projectName>
    
    创建主应用成功.png 主应用目录.png

    现在,为了更好的展示效果,我们现将主应用使用element-plus简单改造一下。

    // /userCenter/index.vue 页面
    
    <template>
        <el-container class="user-center-layout">
            <el-header class="user-center-layout__header">
                <div class="logo">XXXX系统</div>
                <div class="" style="flex: 1;"></div>
                <el-button @click="goHome" icon="Close">退出</el-button>
            </el-header>
            <el-container>
                <el-aside class="user-center-layout__aside">
                    <router-link :to="{name:'customerOrderList'}" class="test-menu-link" type="primary"><el-icon><location/></el-icon>客户订单</router-link>
                </el-aside>
                <el-container>
                    <el-main class="user-center-layout__main">
                        <router-view/>
                    </el-main>
                    <el-footer class="user-center-layout__footer">
                        版权所有:xxxxxxx 公司
                    </el-footer>
                </el-container>
            </el-container>
        </el-container>
    </template>
    
    简陋的菜单.png

    主菜单已经有了一个自己的模块【客户订单列表】,现在,假设我们有一个新的(或旧的)项目,里面有一个账单模块,我们需要将这个“账单模块”整合到主应用的后台页面中来。(请忽视简陋的页面)


    子应用app1-首页.png
    子应用app1-我的账单页.png
    子应用app1-我的费用页.png

    我们分别在主应用和子应用中都安装 qiankun

    $ npm i qiankun -S
    

    然后将主引用的 /userCenter/index 复制一份到/userCenter/childApp,略做修改,因为我们需要再该页面加载子应用,所以在该页面注册子应用

    <!-- /userCenter/childApp.vue -->
    <template>
        <el-container class="user-center-layout">
            <el-header class="user-center-layout__header">
                <div class="logo">XXXX系统</div>
                <div class="" style="flex: 1;"></div>
                <el-button @click="goHome" icon="Close">退出</el-button>
            </el-header>
            <el-container>
                <el-aside class="user-center-layout__aside">
                    <test-menu></test-menu>
                </el-aside>
                <el-container>
                    <el-main class="user-center-layout__main child-app-wrap" >
                        <div id="childAppContainer"></div>
                    </el-main>
                    <el-footer class="user-center-layout__footer">
                        版权所有:xxxxxxx 公司
                    </el-footer>
                </el-container>
            </el-container>
        </el-container>
    </template>
    
    <script>
        import {defineComponent, ref} from 'vue'
        import { registerMicroApps, start } from 'qiankun';
        import TestMenu from './TestMenu.vue'
        // 注册微(子)应用
        if (!window.qiankunStarted) registerMicroApps([
            {
                name: 'app1',
                entry: 'http://192.168.1.111:8090',
                container: '#childAppContainer',
                activeRule: '/childApp/app1',
            },
        ]);
        export default defineComponent({
            components:{TestMenu},
            methods:{
                goHome() {
                    this.$router.push({name:"Home"});
                },
                ......
            },
            mounted() {
                if (!window.qiankunStarted) {
                    // 启动
                    window.qiankunStarted = true;
                    start();
                }
            },
        })
    </script>
    

    左侧菜单组件

    <template>
        <div class="" v-for="item of menuList" :key="item.name">
            <el-divider content-position="left">{{item.name}}</el-divider>
            <router-link
                    :to="{path:son.path}"
                    v-for="son in item.children" :key="son.name"
                    class="test-menu-link"
                    type="primary"
            >
                <el-icon v-if="son.icon"><component :is="son.icon"/></el-icon>
                {{son.name}}
            </router-link>
        </div>
    </template>
    
    <script>
        export default {
            name: "testMenu",
            data(){
                return {
                    menuList:[
                        {
                            name:"订单模块",
                            children:[
                                {
                                    name:'客户订单',
                                    path:"/customerOrderList",
                                    icon:"location",
                                },
                            ]
                        },
                        {
                            name:"财务模块(app1)", // 此处是我们需要引入的微应用
                            children:[
                                {
                                    name:'Home',
                                    path:"/childApp/app1",
                                    icon:"document",
                                },
                                {
                                    name:'我的账单',
                                    path:"/childApp/app1/myBill",
                                    icon:"document",
                                },
                                {
                                    name:'我的费用',
                                    path:"/childApp/app1/myFee",
                                    icon:"toiletPaper",
                                },
    
                            ]
                        },
                    ],
                }
            }
        }
    </script>
    

    主应用路由增加对/childApp/app1路径的支持

    // /route/index.ts
    const routes = [
      {
        path: '/userCenter',
        name: 'userCenter',
        component: userCenter,
        children:[
          {
            path: '/customerOrderList',
            name: 'customerOrderList',
            component: () => import('../views/orderManagement/customerOrderList.vue'),
          }
        ]
      },
      {
        path: '/:childApp+',
        name: 'childApp',
        component: childApp,
      },
    ]
    

    然后是修改子应用

    // /router/index.ts
    // const router = createRouter({
    //   history: createWebHistory(process.env.BASE_URL),
    //   routes
    // })
    
    // export default router
    
    export default routes
    
    // main.ts
    import { createApp } from 'vue'
    import App from './App.vue'
    // import router from './router'
    import store from './store'
    
    import ElementPlus from 'element-plus'
    
    import '@/assets/style/init.scss'
    import '@/assets/style/initElement.scss'
    
    import { createRouter, createWebHistory} from 'vue-router'
    import routes from './router'
    
    const isQiankun = window.__POWERED_BY_QIANKUN__;
    
    if (isQiankun) {
        __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    let router = null;
    let instance:AnyObject|null = null;
    function render(props:any = {}) {
        const container:any = props.container;
        const base:string = isQiankun ? '/childApp/app1/' : process.env.BASE_URL; //如果检测到主应用,则使用在主应用中注册时匹配的baseUrl
        router = createRouter({
            history: createWebHistory(base),
            routes
        });
        instance = createApp(App);
        instance.use(ElementPlus);
        instance.use(store);
        instance.use(router);
        instance.mount(container ? container.querySelector('#app') : '#app');
    }
    
    if(!isQiankun) { // 如果不是在qiankun框架下,则单独运行,便于调试
        render();
    }
    
    // 返回的给qiankun主应用的子应用生命周期钩子
    export async function bootstrap() {
        console.log('[vue] vue app bootstraped');
    }
    export async function mount(props:any) {
        console.log('[vue] props from main framework', props);
        render(props);
    }
    export async function unmount() {
        // 卸载子应用实例的根组件
        console.log('[vue] vue app unmount');
        if(instance) instance.unmount();
        if(instance) instance._container.innerHTML = '';
        instance = null;
        router = null;
    }
    

    vue.config.js

    const { name } = require('./package');
    module.exports = {
        productionSourceMap: true,
        devServer: {
            port: 8090,
            headers: { 
                'Access-Control-Allow-Origin': '*', //允许跨域
            },
        },
        configureWebpack: {
            output: {
                library: `${name}-[name]`,
                libraryTarget: 'umd', // 把微应用打包成 umd 库格式
                jsonpFunction: `webpackJsonp_${name}`,
            },
        },
    };
    

    注意,主应用加载子应用,是通过请求方式获取的子应用程序相关资源(document、js、css、图片),所以会有跨域问题跨域问题,生产环境需要后台配置子应用允许跨域。
    而在开发环境,因为使用的时webpack.devServer服务,配置'Access-Control-Allow-Origin': '*'即可开启跨域,进行开发调试。

    现在,我们打开主应用看看效果


    主应用效果展示.gif

    现在我们已经成功在主应用上加载了一个子应用,那么,我们再加一个呢?
    假设一下, 我们现在有一个旧的历史项目 test-qiankun-app1,我们想讲这个项目也加载进主应用中,现在我们来改造一下。

    首先,修改一下子应用test-qiankun-app1 项目

    /router/index.ts

    import Vue from 'vue'
    import VueRouter, { RouteConfig} from 'vue-router'
    import Home from '../views/Home.vue'
    import {Route,NavigationGuardNext} from "vue-router";
    
    Vue.use(VueRouter)
    
    const routes: Array<RouteConfig> = [
      {
        path: '/',
        name: 'Home',
        component: Home
      },
      {
        path: '/about',
        name: 'About',
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
      }
    ]
    
    // const router = new VueRouter({
    //   mode: 'history',
    //   base: process.env.BASE_URL,
    //   routes
    // })
    
    export default routes
    

    main.ts

    import Vue from 'vue'
    import App from './App.vue'
    import VueRouter from 'vue-router'
    import routes from './router'
    import store from './store'
    
    Vue.config.productionTip = false
    
    const isQiankun = window.__POWERED_BY_QIANKUN__;
    const appName = "子应用 app1";
    
    if (isQiankun) {
      __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    let router = null;
    let instance:AnyObject|null = null;
    function render(props:any = {}) {
      const container:any = props.container;
      router = new VueRouter({
        base: isQiankun ? '/childApp/app2/' : '/',
        mode: 'history',
        routes,
      });
      router.beforeEach((to, from, next) => {
          console.log(`----- ${appName} beforeEach()`,`【${from.path}】 => 【${to.path}】`);
          next();
      });
      router.afterEach((to, from) => {
        console.log(`----- ${appName} afterEach()`,`【${from.path}】 => 【${to.path}】`);
      });
    
      instance = new Vue({
        router,
        store,
        render: (h) => h(App),
      }).$mount(container ? container.querySelector('#app') : '#app');
      console.log(instance)
    }
    
    if(!isQiankun) {
      render();
    }
    
    export async function bootstrap() {
      console.log('[vue] vue app bootstraped');
    }
    export async function mount(props:any) {
      console.log('[vue] props from main framework', props);
      render(props);
    }
    export async function unmount() {
      if(instance) instance.$destroy();
      if(instance) instance.$el.innerHTML = '';
      instance = null;
      router = null;
    }
    

    vue.config.js

    const { name } = require('./package');
    module.exports = {
        devServer: {
            port: 8089,
            headers: {
                'Access-Control-Allow-Origin': '*',
            },
        },
        configureWebpack: {
            output: {
                library: `${name}-[name]`,
                libraryTarget: 'umd', // 把微应用打包成 umd 库格式
                jsonpFunction: `webpackJsonp_${name}`,
            },
        },
    };
    

    然后,我们在主应用注册微应的配置项中加入test-qiankun-app1,将它在主应用中命名为app2

    主应用 childApp.vue

    <script>
        registerMicroApps([
            {
                name: 'app1',
                entry: 'http://192.168.1.111:8090',
                container: '#childAppContainer',
                activeRule: '/layWrap/app1',
            },
            // 新增子应用app2
            {
                name: 'app2',
                entry: 'http://192.168.1.111:8089',
                container: '#childAppContainer',
                activeRule: '/childApp/app2',
            },
        ]);
        export default defineComponent({
          ......
        })
    </script>
    

    TestMenu.vue

    <script>
        export default {
            name: "testMenu",
            data(){
                return {
                    menuList:[
                        {
                            name:"订单模块",
                            children:[
                               ......
                        },
                        {
                            name:"财务模块(app1)",
                            children:[
                              ......
                            ]
                        },
                        {
                            name:"测试模块(app2)",
                            children:[
                                {
                                    name:'Home',
                                    path:"/childApp/app2",
                                    icon:"document",
                                },
                                {
                                    name:'关于我们',
                                    path:"/childApp/app2/about",
                                    icon:"document",
                                },
                            ]
                        }
                    ],
                }
            }
        }
    </script>
    

    展示效果


    主应用加载了两个微应用.gif

    到这里,出现了一个问题,我们在主应用中打开app2后,点击app2内部的路由跳转后,再点击主应用的路由,主应用的路由无效,且控制台出现警告提示,而app1则没有该问题。


    微应用内部路由跳转后,主应用路由失效.gif
    警告提示.png

    具体错误信息

    [Vue Router warn]: Error with push/replace State DOMException: Failed to execute 'replaceState' on 'History': A history state object with URL 'http://192.168.1.111:8080undefined/' cannot be created in a document with origin 'http://192.168.1.111:8080' and URL 'http://192.168.1.111:8080/childApp/app2/'.
        at History.eval [as replaceState] (webpack-internal:///./node_modules/single-spa/lib/esm/single-spa.min.js:33:10677)
        at changeLocation (webpack-internal:///./node_modules/vue-router/dist/vue-router.esm-bundler.js:566:60)
        at Object.push (webpack-internal:///./node_modules/vue-router/dist/vue-router.esm-bundler.js:601:9)
        at finalizeNavigation (webpack-internal:///./node_modules/vue-router/dist/vue-router.esm-bundler.js:3202:31)
        at eval (webpack-internal:///./node_modules/vue-router/dist/vue-router.esm-bundler.js:3078:27)
    

    控制台警告输出中包含'http://192.168.1.111:8080undefined/',根据字面意思,简单推断应该是子应用内部路由变更时,主应用内部路由栈错误记录了一条undefined信息,那么预估错误可能与vue-router有关系。

    vue-router内某段相关代码.png

    且只有test-qiankun-app1出现这种问题,test-qiankun-son-app1没有该问题,那么,我们先从vue-router的版本入手。

    粗略比对一下 主应用、微引用test-qiankun-app1、微应用test-qiankun-son-app1 的各依赖版本

    应用 项目名 vue vue-router 是否存在undefined问题
    主应用 3.0.0 4.0.0-0
    app1 test-qiankun-son-app1 3.0.0 4.0.0-0
    app2 test-qiankun-app1 2.6.11 3.2.0

    可以看出,出现问题的微应用app2的vue-router版本与主应用和微应用app1明显不同,且横跨了一个大版本,进一步加深了是vue-router版本导致问题的怀疑。

    为什么验证我们的猜想,我们新建一个项目test-qiankun-son-app3,在主应用中将它注册为微应用app3,且app3的vue/vue-router版本与主应用一致。

    registerMicroApps([
            {
                name: 'app1',
                entry: 'http://192.168.1.111:8090',
                container: '#childAppContainer',
                activeRule: '/childApp/app1',
            },
            {
                name: 'app2',
                entry: 'http://192.168.1.111:8089',
                container: '#childAppContainer',
                activeRule: '/childApp/app2',
            },
            {
                name: 'app3',
                entry: 'http://192.168.1.111:8091',
                container: '#childAppContainer',
                activeRule: '/childApp/app3',
            },
        ])
    

    接下来,我们演示一下,没有出现路由undefined问题


    app3演示,没有出现问题.gif

    经过简单的对比后,我们发现,在阿里qiankun微框架下,主应用vue-router版本4.0时,微应用使用vue-router3.x版本时会存在路由undefined问题。

    当然,这种论证对比还不够严谨。

    正常情况下,因为app2的vue版本与主应用也不一致,我们还需要在app2项目中,将vue-router版本升级到4.0,进行对比,验证不同vue版本下,相同vue-router版本是否会产生该问题。更细致一些,还需要将主应用vue、vue-router版本降级,再使用不同依赖版本的子应用对该问题进行验证。

    当前任务紧迫,该事暂时搁置,后续抽时间进行。也欢迎有时间和兴趣验证的同仁与我分享一下结果。

    后记

    根据阿里qiankun文档对微前端核心价值的定义包括

    技术栈无关:主框架不限制接入应用的技术栈,微应用具备完全自主权

    而公司本次重构,选择微前端框架作为基石,也是希望几年后公司开发人员流失,亦或新技术迭代维护时,更长的维护老项目的生命。

    虽然抱着使用框架一劳永逸的想法,但是在demo阶段,我发现了vue-router版本将导致主、子应用路由undefined问题,才明白即使框架考虑的再周全,在后续的使用中,也无法避免其他技术迭代导致可能的差异。

    所以理想和现实还是会有偏差的,而比使用框架更重要的是,身为开发人员自身学习的心。

    相关文章

      网友评论

          本文标题:搭建qiankun Demo

          本文链接:https://www.haomeiwen.com/subject/igjhcrtx.html