手把手带你在微前端(qiankun)中实现多页签功能(路由keepalive)

4,955 阅读6分钟

效果展示

00.gif

前言

在上篇文章中已经实现了基本的多页签功能,接下来在微前端中实现多页签功能。建议没看过上一篇的朋友,可以先看下上一篇。

手把手带你基于ant design pro 5实现多tab页(路由keepalive) juejin.cn/post/722476…

实现思路

实现起来很简单,还记得上篇文章中获取路由组件实例用的是useOutlet这个hooks,本篇我们只需要自己实现一下这个方法就能实现微前端的多页签了。

具体实现

初始化微前端项目

创建pnpm workspace项目

不了解pnpm workspace的可以从网上先了解一下,首先我们先建一个目录,并进入

mkdir antd-pro-qiankun-keepalive-tabs && cd antd-pro-qiankun-keepalive-tabs 

执行下面命令,初始化项目

pnpm init

在根目录新建pnpm-workspace.yaml文件,并写入以下代码

packages:
  - "modules/*"

在根目录下新建modules目录,我们微前端模块放在里面

mkdir modules

进入modules文件夹,使用antd pro脚手架初始化微前端主项目,选umi4

cd modules && pro create main

在modules文件夹下创建app1文件夹,然后执行以下命令

cd app1 && pnpm create umi

选择Simple App

image.png

使用pnpm

image.png 初始化后,会自动安装依赖

我们在app1根目录下创建.env文件,更改启动端口 image.png

再回到项目根目录下,更改package.json文件,增加一个start命令

 "start": "pnpm -C ./modules/main start & pnpm -C ./modules/app1 start"

image.png 然后执行启动命令

npm start

启动成功后,访问http://localhost:8000http://localhost:8001, 此时应该能正常访问。

使用qiankun微前端插件

在主项目modules/main/config/config.ts配置文件中使用qiankun插件,具体qiankun用法请参考官网

qiankun: {
    master: {
      apps: [{
        name: 'app1',
        entry: '//localhost:8001'
      }],
    }
  },

image.png

更改app1目录下package.json中name字段,改成app1

image.png

给app1模块安装umi插件,执行以下命令

pnpm --filter "app1" add @umijs/plugins

安装完依赖后,在modules/app1/.umirc.ts配置文件中使用qiankun插件

plugins: ['@umijs/plugins/dist/qiankun'],
qiankun: {
  slave: {},
},

image.png 然后再更改主项目modules/main/config/routes.ts路由文件,添加子模块app1路由

 {
    name: 'app1-index',
    icon: 'table',
    path: '/app1/*',
    microApp: 'app1'
  },

image.png

回到根目录执行npm start命令,启动项目。启动成功后,访问http:localhost:8000 登录成功后,点击app1-index菜单,就能看到app1模块 src/pages/index.tsx里的内容了。

image.png 到此微前端项目搭建成功了

实现多页签功能

把上一篇文章中src/layouts文件夹和文件一起复制到当前主项目src目录下

image.png 上文中提到我们只需要自己实现useOutlet这个hooks就行了,那么我们就来实现这个hooks,在layouts目录下新建useOutlet.tsx文件

import { MicroAppWithMemoHistory, useLocation } from '@umijs/max'

export function useOutlet() {
  const { pathname } = useLocation();

  // 获取模块名
  const [, moduleName] = pathname.split('/');

  // 加载微前端模块另外一种方式
  return (
    <MicroAppWithMemoHistory name={moduleName} url={pathname} />
  )
}

把路由中的microApp属性移除,因为使用了MicroAppWithMemoHistory这种加载方式,具体请参考qiankun官网。再添加一个app1 docs路由测试,更改重定向路由到/app1/

image.png

修改主项目qiankun配置,关闭微前端单例模式,如果开启单例,就没办法保留其他tab的状态了。把singular设置为false

qiankun: {
    master: {
      apps: [{
        name: 'app1',
        entry: '//localhost:8001'
      }],
      singular: false,
    },
  },

移除app1模块/src/layouts文件夹,并在路由中移除,移除后有可能会报错,重启一下服务就行了。

更改app1模块中src/pages/index.tsx文件,添加一个input组件,测试切换路由是否能保持状态,并且给div设置一个很大的高度,测试是否能保留滚动条位置。

image.png 效果展示

09.gif

支持主项目路由

现在是不支持主项目路由的,切换到“欢迎”菜单会报错,我们兼容一下主项目路由。

image.png 实现思路:在useOutlet这个hooks中,判断一下路由是否是子模块,如果不是子模块,就使用原始的useOutlet的返回值。

把主项目qiankun配置抽到一个单独的文件中导出,方便我们在代码中获取有哪些子模块。然后再主项目app.ts文件中引入qiankun配置,然后再导出。

  • 删除config文件中的qiankun apps和singular配置 image.png

  • 在主项目src目录下新建qiankun-config.ts文件,代码如下

    export const qiankunConfig = {
     master: {
       apps: [{
         name: 'app1',
         entry: '//localhost:8001'
       }],
       singular: false,
     },
    };
    
  • 在src/app.ts文件中赋值给qiankun,并导出qiankun。 image.png

  • 更改useOutlet.tsx文件

    import { MicroAppWithMemoHistory, useLocation, useOutlet as useChildren } from '@umijs/max'
    import { qiankunConfig } from '@/qiankun-config';
    
    export function useOutlet() {
      const { pathname } = useLocation();
    
      const children = useChildren();
    
      const [, moduleName] = pathname.split('/');
    
      // 如果没有匹配到子模块,就使用主模块的useOutlet
      if (!qiankunConfig.apps.some(app => app.name === moduleName)) {
        return children;
      }
    
      return (
        <MicroAppWithMemoHistory name={moduleName} url={pathname} />
      )
    }
    

主模块的路由也支持了

image.png

在子模块中调用刷新、关闭、关闭其他功能,并监听onShow和onHidden事件

实现思路:通过父子模块通信,把这几个方法传到子模块中

  • 改造/src/layouts/indes.tsx文件,把这几个方法传给子模块。

image.png

  • 子模块中使用useModel获取主模块传过来的数据,首先在modules/app1/.umirc.ts中引入@umijs/plugins/dist/model插件

image.png

  • 使用const { keepAliveHandle } = useModel('@@qiankunStateFromMaster');获取主项目传过来的数据,在app1/src/pages/index中测试refreshTab、closeTab、closeOhterTab、onShow、onHidden方法

code.png

image.png

解决bug

我以为上面东西搞完,基本就结束了,谁知道在测试的时候发现了个大bug。bug流程是这样的,a切换到b,关掉a,再打开a就会一直空白。然后报这个错:dex.js:1 single-spa minified message #31: See https://single-spa.js.org/error/?code=31&arg=mount&arg=parcel&arg=app1_1&arg=3000我到qiankun仓库issue找相关解决方案,发现也有人遇到这个问题,但是没有明确的解决方案,然后自己扒qiankun源码看,最后发现了bug的原因。原来qiankun在加载子模块生命周期的时候,会做缓存,缓存的key用的微前端容器的xpath路径,我先打开一个,然后在打开一个,然后关掉第一个,然后再打开第一个,这时候新加的xpath和前面一个一样了,然后qiankun发现当前xpath已经加载了,就不再去加载。这个bug大致是这样触发的,我们只需要把缓存的key改成当前url + xpath就行了,这个我后面会单独出一篇文章去讲,这块逻辑有点复杂。

bug演示

11.gif

然后我就把umi-qiankun插件引入到当前主项目中,改了点源码,后面会给umi提个pr,作者可能不会合并,因为这种使用场景不多。

image.png 改了这里,如果组件传id就用组件传的,如果没传,还用以前的name。

image.png 改造useOutlet hooks,组件id属性设置为pathname

10.gif

添加新模块app2

复制app1整个文件夹内容到app2文件夹下,改.env的启动端口为8002,然后主项目配置一下路由和微前端配置。

image.png

效果展示

12.gif

总结

我这个实现方案应该会有性能问题,最好的方案应该在子模块中实现缓存,同一个模块不用每次都重新加载,性能应该会好一点。大家有更好的方案可以在评论区提醒我一下。

后面我会把这个功能做成umi插件,方便大家使用,接下来应该会出一篇umi插件开发的文章教程,对这个感兴趣的可以关注我一下。

文中的代码我已经上传到github了,地址是:github.com/dbfu/antd-p…