搭建qiankun Demo

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

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

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

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

// /userCenter/index.vue 页面


简陋的菜单.png

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


子应用app1-首页.png

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

子应用app1-我的费用页.png

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

$ npm i qiankun -S

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





左侧菜单组件




主应用路由增加对/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 = [
  {
    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


TestMenu.vue


展示效果


主应用加载了两个微应用.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)