qiankun拆分组合微应用

686 阅读4分钟

红猪.webp

背景

当前公司为某云计算下的多云管理平台,从零到一开发迭代两年时间。项目一致处于快速迭代期,最近因为版本规划的放缓,决定解决一下历次迭代的一些顽疾。其中一个比较泛性的顽疾就是之前以iframe形式的微应用体系。

调研

通过对网上广泛的微应用文档阅读、组内成员的建议、GitHub,Gitee上Demo实践。最终选择了qiankun微应用体系。以目前采用的Vue2 + ElementUI + qiankun + vite来整体搭建一种快捷、精简、向下兼容的新开发模式。口号有多响,活就有多多 ̄へ ̄。

前期准备

前期准备为啥单独拎出来讲?主要准备的比较多,所谓“磨刀不误砍柴工”。

xx-components-boxs v1.0.8

将之前两年积累的组件库、工具函数、公共方法、过滤器、枚举等去业务化重新整理,以namespace的形式集成精简,以npm包的形式发布到私有仓库Nexus中。确保每个子应用以最小的成本集成、更新依赖,整个系统统一规划输出能力。

// 子应用依赖安装
npm install xx-components-boxs --save

xx-template

构建一个子应用模板,有些类似vue-cli生成出来的开发模板。只是自身模板是带业务规划的,对新应用的扩展更便利。同时里面集成了项目的整体介绍、使用手册,支持第三个开发人员使用。

xx-cli

既然开发模板都出来了,脚手架工具xx-cli也应用而生。

// 子应用基础模板生成
xx-cli init app

xx-deploy

为啥又会有个xx-deploy,它的诞生主要还是因为部署太慢了,部署为啥会慢?“对,肯定是部署方案有问题”。如图所示,pulling、waiting、retrying、verifying、downloading、building,Use Time 1x min xx s

image.png

开发环境依赖Jenkins部署,部署用时过长并且采用聚合形式部署,不适合拆分后微应用的部署。xx-deploy以本地部署为主的部署方式,以极快的方式支撑开发环境部署。Use Time xx ms

// 只部署子应用 app1
npm run deploy app1
// 并行部署配置中心所有子应用
npm run deploy
// npm run build + npm run deploy
npm run build-deploy app1
// 同时部署子应用 app1、app2
npm run deploy app1 app2

image.png

webpack切换成vite

主要是为了构建最精简化的xx-template,提高启动、打包速度

应用拆分

  1. 将登陆注册认证单独拆分出来作为一个应用,支撑主应用和众多子应用认证。
  2. 构建主应用支撑导航、面包屑、等框架业务
  3. 根据业务拆解多个子应用作为业务子应用
  4. ....

具体的拆分以产品业务规划为主,不再赘言

qiankun集成问题

集成的代码片段千篇一律,首先来过一下遇到的问题。

主子应用样式冲突问题

遇到的主要问题是之前改造的子应用#app以上的样式未起作用,原因是因为主子应用合并为一块Dom。在沙箱隔离情况下,html,body上的样式未加载。最终采用了去除沙箱隔离,主子应用共用公共样式@/style/index.scss,定制化样式以命名空间隔离方式。

面包屑更新问题

因为之前面包屑采用了拼接的方式组装而成,拼接逻辑由各个iframe中访问路由页面时自己拼接组装,业务下放后容易遗漏出错。最终采用子应用在路由变化时主动向主应用推送标识,主应用根据标识递归检索出匹配的面包屑。

路由页面局部刷新问题

因为集成qiankun后路由行为分为自身路由router-view和子应用容器加载,所以刷新页面行为不一致。最终点击刷新时区分两种场景。自身路由页面刷新采用销毁重建router-view形式,子应用容器刷新采用主应用发送刷新标识给子应用,子应用获取刷新标识后采用销毁重建router-view形式,刷新行为实现统一。

reload(){
    this.reloadRotate += 360;
    if(this.isSub){
        //通知当前活跃子应用刷新路由页面
        qiankunActions.setGlobalState({ reload: true });
    }
    else{
        this.$store.dispatch('keepAlive/mainReload', '');
    }
}

一级路由问题

一级路由的问题指的是这类页面之前是和框架在一层的router-view,现在以子应用的形式集成进来屈居在框架下面。结果出现了嵌套的导航。最终采用的方式还是监听路由,如果是这类路由。手动挂载loadMicroApp微应用与框架router-view平级。

import { loadMicroApp } from 'qiankun';
// 微应用实例
let microApp = null; // 微应用实例
/**
 * 加载app
 */
export const loadApp = () => {
    microApp = loadMicroApp({
        name: 'micro-public',
        entry: '//xx/fm1p/',
        container: '#microFullApp'
    });
};

/**
 * 卸载app
 */
export const unloadApp = () => {
    if (microApp) {
        microApp.unmount(); // 卸载微应用
    }
};

性能问题

个别子应用业务体量比较大,在第一次打开时加载超过5s。一方面精简子应用,删除冗余业务代码;用xx-components-boxs替换众多依赖包。另一方面启用qiankun的预加载特性。最终最重的子应用第一次加载也控制在3s以内。

部分代码片段

主应用安装qiankun npm包,构建应用注册中心。

import { initGlobalState, setDefaultMountApp } from "qiankun";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
const initialState = {
    //这里写初始化数据
    breadcrumbList: [],
    routePath: null,
    routeName: null,
    reload: false
};
const microApps = [
    {
        name: 'micro-public', // 子应用名称
        entry: microConf['micro-public'].url, // 默认会加载这个html解析里面的js动态的执行,因为请求了资源子应用需要支持跨域
        activeRule: '/micro-public' // 访问某个URL的时候将这个端口号挂在到这个上去
    },
    {
        name: 'micro-cloud', // 子应用名称
        entry: microConf['micro-cloud'].url, // 默认会加载这个html解析里面的js动态的执行,因为请求了资源子应用需要支持跨域
        activeRule: '/micro-cloud' // 访问某个URL的时候将这个端口号挂在到这个上去
    },
    {
        name: 'micro-app', // 子应用名称
        entry: microConf['micro-app'].url, // 默认会加载这个html解析里面的js动态的执行,因为请求了资源子应用需要支持跨域
        activeRule: '/micro-app' // 访问某个URL的时候将这个端口号挂在到这个上去
    }
];
// 初始化全局下发的数据
export const qiankunActions = initGlobalState(initialState);

/* 重构apps */
export const filterApps = () => {
    microApps.forEach((item) => {
        // 必选,微应用的容器节点的选择器或者 Element 实例。
        item.container = item.container || "#microApp";
        // 可选,主应用需要传递给微应用的数据。
        item.props = {
            routerBase: item.activeRule, // 下发基础路由
            initialState: initialState, // 下发全局数据方法
        };
    });
    return microApps;
};

let startTime = 0;

/* qiankun全局声明周期钩子 */
export const microConfig = {
    beforeLoad: [
        (app) => {
            // 加载子应用前,加载进度条
            NProgress.start();
            startTime = (new Date()).valueOf();
            console.log(`%c [${app.name}] before load`, "background:#3a5ab0 ; padding: 1px; border-radius: 3px;  color: #fff");
            return Promise.resolve();
        },
    ], // 预加载
    beforeMount: [
        (app) => { },
    ], // 挂载前回调
    afterMount: [
        (app) => {
            // 加载子应用前,进度条加载完成
            NProgress.done();
            console.log(`%c [${app.name}] after mount,${(new Date()).valueOf() - startTime} ms`, "background:#7d7453 ; padding: 1px; border-radius: 3px;  color: #fff");
            return Promise.resolve();
        },
    ], // 挂载后回调
    beforeUnmount: [
        (app) => { },
    ], // 卸载前回调
    afterUnmount: [
        (app) => { },
    ], // 卸载后回调
};
import { registerMicroApps, start, addGlobalUncaughtErrorHandler } from "qiankun";

import { filterApps, microConfig } from "./registerApp";


// 添加全局的未捕获异常处理器
addGlobalUncaughtErrorHandler((event) => {
  const { message: msg } = event;
  if (msg) console.error(msg);
});

// 微应用注册
export const registerApps = () => {
  const _apps = filterApps();
  registerMicroApps(_apps, microConfig);
  start({
    prefetch: "all", // 可选,是否开启预加载,默认为 true。
    sandbox: { strictStyleIsolation: false }, // 可选,是否开启沙箱,默认为 true。// 从而确保微应用的样式不会对全局造成影响。
    singular: true, // 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 true。
  });
};