搭建qiankun Demo

2022-01-06  本文已影响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问题,才明白即使框架考虑的再周全,在后续的使用中,也无法避免其他技术迭代导致可能的差异。

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

上一篇下一篇

猜你喜欢

热点阅读