qiankun+vue2多页签构建项目

568 阅读4分钟

项目是2024年3月19日开始构建,vue2从2023年12月31日之后官方就不再进行维护,但是项目还是用vue2是基于历史原因,可能会有政府相关单位使用进行本地化部署。也许未来会考虑升级到vue3+ts。

qiankun本身官方文档能够提供的说明,其实比较少,虽然社区很大,但是总体来说会很多坑,之前一直久仰大名,但是真正在使用的时候才知道,真的就是一路缝缝补补过来的。

1.基座

这是整个项目的顶级,使用vue2+ant-design+pinia+qiankun搭建。

  • 路由监听 使用vue2构建一个基本的vue项目,除了需要在vue-router上进行一些特殊的处理,其他和正常的构建一个vue2项目没有区别

image.png

这一个router配置项是因为子应用会有自己的路由,基座不需要知道这些具体的路由内容是什么,直接放行通过,并把这个路由匹配到具体的菜单之后,新增一个页签或者跳转到对应的页签即可。

qiankun本身会监听路由加载对应的子应用,并在对应的container中载入。所以本身基座会有两个路由监听,一个是vue2的vue-router,另外一个是qiankun的activeRule。

  • 公共内容
  1. 所有子应用会使用到一些通用的东西,都会集成到基座来统一引入。例如本项目采用的ant-design,子应用只会引入ant-design,相应的样式antd.less则由基座来引入,避免重复引入。统一在基座进行通用样式管理,为后期换肤功能提供可能。
  2. 用户信息和用户角色权限的获取和存储(以前的旧项目是采用iframe来使用的,每个业务模块的开发都必须后台封装一个中转接口去调用基础部门的接口来获取角色的信息和用户的权限)
  3. axios的请求头,接口地址的配置文件和一些公共的接口(例如上传接口)
  • qiankun配置的构造
  1. 子应用是根据菜单来进行划分 image.png

每个文件里面都是一个子应用(vue2项目),props的path是基座添加一个页签时传递给对应的子应用需要跳转的路由地址。

  1. 一个子应用可能会出现多个页签,例如案例1和案例2就是可能同时存在的两个页签,但他们归属于同一个子应用。
  2. 根据qiankun的registrableApp所需要传递的参数的格式,将subapplication文件夹下面对应的所有子应用构建成符合格式的对象
    /**
     * 读取subapplication文件下面的所有js文件
     * 该文件夹下面只允许放相关的子应用配置
     * 每个文件对应一个子应用,子应用相关配置如下
     * {
     *      manual:是否为手动加载,默认为false
     *      isClose:是否可以关闭,默认为true
     *      container:子应用放置的dom(不指定,则自动生成)
     *      activeRule:加载规则(自动生成)
     *      name:子应用名称(自动生成)
     *      entry:子应用的入口地址
     *      props:{    传递给子应用的数据(可不填,不填写则默认传递下面的内容,如果填写,则合并之后传递)
     *          globalConfig:全局配置
     *          communication:子应用通信通道
     *          mainServerCommun:基座通信通道
     *          changeMainRouter:路由变更传递
     *          appStore:基座的仓库
     *          cachedInstance:{
     *              app:子应用
     *              version:框架版本
     *              qkObj:如果手动加载,则会有值,乾坤的加载对象
     *          }
     *      }
     * }
     */
    const subapplicationFile = require.context("../subapplication",false,/\.js$/),
    apps = subapplicationFile
    .keys()
    .map(val => {
        let subapplication = subapplicationFile(val).default,
        fileName = val.replace(/.+\/(.+)?\..+/,"$1");

        tileMenu(subapplication.children).flat(Infinity).forEach(app => {
            let container = app.container || util.getRandomName(fileName);

            Object.assign(app,{
                manual:app.manual == void 0?false:app.manual,
                isClose:app.isClose == void 0?true:app.isClose,
                name:container,
                container:`#sp-${container}`,
                /**
                 * 匹配规则
                 * 如果当前子应用已存在 || 当前的地址的hash和子应用的hash匹配
                 * @param {*} location [本地url相关]
                 * @returns boolean
                 */
                activeRule:location => {
                    if(app.props.cachedInstance.app) {
                        return true;
                    } else if(location.hash.startsWith(`#${app.props.path}`)) {
                        $api.appendElement({
                            name:container
                        },() => {
                            $api.$eventBus.emit("tabOperation",{
                                type:'add',
                                app
                            });
                        });
                        return true;
                    }

                    return false;
                }
            });
            // 如果props未填写,则初始化
            app.props == void 0 && (app['props'] = {});

            Object.assign(app.props,{
                globalConfig,
                resizeOb,
                appStore:null,
                cachedInstance:{
                    app:null,   // 子应用
                    version:'',  // 版本
                    qkObj:null
                },
                /**
                 * 和子应用通信(给一个空函数,避免调用报错)
                 * loseActive:失活
                 * getActive:激活
                 * changeRouter:切换路由
                 */
                communication:function() {},
                /**
                 * 和基座通信
                 * @param {string} type [操作类型]
                 * @param {*} record [数据]
                 */
                mainServerCommun:function(type,record) {
                    switch(type) {
                        case "saveApp": // 缓存app
                            Object.assign(app.props.cachedInstance,record);
                        break;
                        case "replaceCommunication":    // 替换子的通信
                            app.props.communication = record;
                        break;
                        case "jumpPage":    // 页面跳转
                            $api.$eventBus.emit("tabOperation",{
                                type:'jumpPage',
                                params:record
                            });
                        break;
                    }
                }
            });
        });

        return subapplication;
    });
  1. 基座的框架页,监听路由变化,判断当前路由是否在已经存在的页签中,如果存在则跳转到对应的页签

image.png

样式规范

样式的编写应该遵循相应的规范,尽量避免使用scoped来编写。遵循对应的前端规范有助于快速定位到对应的子应用以及相应的模块。例如mLayout-layoutIndex(m模块,Layout框架,layoutIndex框架首页)

子应用

  1. 每一个在基座中配置的子应用对应的路由,都会在新增一个页签之后实例化一个vue对象,所以在main.js中不能引入样式,否则会在相同子应用的每一个页签中反复的引入相同的样式。
if (window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
} else {
    // 独立运行
    render();
}

let instance = null,
mainService;

export async function bootstrap() {
}

export async function mount(props) {
    const { container,cachedInstance } = props;

    if(!cachedInstance.app) {
        mainService = props;
        Vue.prototype.$mainService = mainService;

        instance = new Vue({
            router:router.router,
            pinia,
            render: (h) => h(App),
        }).$mount(container ? container.querySelector('#app') : '#app');

        // 在基座缓存子应用
        mainService.mainServerCommun("saveApp",{
            app:instance,
            version:'vue2'
        });
        // 默认界面
        router.router.push(`${props.path}/index`);
        // 初始化基座相关事件
        configMainServe();
    }
}

function configMainServe() {
    /**
     * 通信
     * @param {string} type [操作类型]
     * @param {*} record [数据]
     */
    mainService.mainServerCommun("replaceCommunication",function(type,record) {
        switch(type) {
        }
    });
}

export async function unmount() {
}
  1. 子应用的路由都应该使用懒加载的方式进行加载

image.png

  1. 子应用的路由都应该采用abstract进行监听。使用qiankun多页签的方式,本身基座和qiankun都会对浏览器的地址栏进行监听,如果每一个子应用也对地址栏进行监听,则会造成极大的性能问题和混乱。
  2. 子应用不使用keep-alive进行缓存,vue通过keep-alive缓存,会保存虚拟dom,在规则匹配的时候重新渲染对应的dom,但是多页签的切换很频繁,通过keep-alive反复的保存和渲染会使页面出现极大的性能损耗。

第一次写技术文章,如有不足之处,请见谅,有更好的建议欢迎交流,谢谢