效果展示
前言
在上篇文章中已经实现了基本的多页签功能,接下来在微前端中实现多页签功能。建议没看过上一篇的朋友,可以先看下上一篇。
手把手带你基于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
使用pnpm
初始化后,会自动安装依赖
我们在app1根目录下创建.env
文件,更改启动端口
再回到项目根目录下,更改package.json文件,增加一个start命令
"start": "pnpm -C ./modules/main start & pnpm -C ./modules/app1 start"
然后执行启动命令
npm start
启动成功后,访问http://localhost:8000 和 http://localhost:8001, 此时应该能正常访问。
使用qiankun
微前端插件
在主项目modules/main/config/config.ts
配置文件中使用qiankun插件,具体qiankun用法请参考官网
qiankun: {
master: {
apps: [{
name: 'app1',
entry: '//localhost:8001'
}],
}
},
更改app1目录下package.json中name字段,改成app1
给app1模块安装umi插件,执行以下命令
pnpm --filter "app1" add @umijs/plugins
安装完依赖后,在modules/app1/.umirc.ts
配置文件中使用qiankun插件
plugins: ['@umijs/plugins/dist/qiankun'],
qiankun: {
slave: {},
},
然后再更改主项目
modules/main/config/routes.ts
路由文件,添加子模块app1路由
{
name: 'app1-index',
icon: 'table',
path: '/app1/*',
microApp: 'app1'
},
回到根目录执行npm start
命令,启动项目。启动成功后,访问http:localhost:8000 登录成功后,点击app1-index菜单,就能看到app1模块 src/pages/index.tsx里的内容了。
到此微前端项目搭建成功了
实现多页签功能
把上一篇文章中src/layouts
文件夹和文件一起复制到当前主项目src目录下
上文中提到我们只需要自己实现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/
修改主项目qiankun配置,关闭微前端单例模式,如果开启单例,就没办法保留其他tab的状态了。把singular
设置为false
qiankun: {
master: {
apps: [{
name: 'app1',
entry: '//localhost:8001'
}],
singular: false,
},
},
移除app1模块/src/layouts文件夹,并在路由中移除,移除后有可能会报错,重启一下服务就行了。
更改app1模块中src/pages/index.tsx文件,添加一个input组件,测试切换路由是否能保持状态,并且给div设置一个很大的高度,测试是否能保留滚动条位置。
效果展示
支持主项目路由
现在是不支持主项目路由的,切换到“欢迎”菜单会报错,我们兼容一下主项目路由。
实现思路:在
useOutlet
这个hooks中,判断一下路由是否是子模块,如果不是子模块,就使用原始的useOutlet的返回值。
把主项目qiankun配置抽到一个单独的文件中导出,方便我们在代码中获取有哪些子模块。然后再主项目app.ts文件中引入qiankun配置,然后再导出。
-
删除config文件中的qiankun apps和singular配置
-
在主项目src目录下新建qiankun-config.ts文件,代码如下
export const qiankunConfig = { master: { apps: [{ name: 'app1', entry: '//localhost:8001' }], singular: false, }, };
-
在src/app.ts文件中赋值给qiankun,并导出qiankun。
-
更改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} /> ) }
主模块的路由也支持了
在子模块中调用刷新、关闭、关闭其他功能,并监听onShow和onHidden事件
实现思路:通过父子模块通信,把这几个方法传到子模块中
- 改造/src/layouts/indes.tsx文件,把这几个方法传给子模块。
- 子模块中使用useModel获取主模块传过来的数据,首先在
modules/app1/.umirc.ts
中引入@umijs/plugins/dist/model
插件
- 使用
const { keepAliveHandle } = useModel('@@qiankunStateFromMaster');
获取主项目传过来的数据,在app1/src/pages/index中测试refreshTab、closeTab、closeOhterTab、onShow、onHidden方法
解决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演示
然后我就把umi-qiankun插件引入到当前主项目中,改了点源码,后面会给umi提个pr,作者可能不会合并,因为这种使用场景不多。
改了这里,如果组件传id就用组件传的,如果没传,还用以前的name。
改造useOutlet hooks,组件id属性设置为pathname
添加新模块app2
复制app1整个文件夹内容到app2文件夹下,改.env的启动端口为8002,然后主项目配置一下路由和微前端配置。
效果展示
总结
我这个实现方案应该会有性能问题,最好的方案应该在子模块中实现缓存,同一个模块不用每次都重新加载,性能应该会好一点。大家有更好的方案可以在评论区提醒我一下。
后面我会把这个功能做成umi插件,方便大家使用,接下来应该会出一篇umi插件开发的文章教程,对这个感兴趣的可以关注我一下。
文中的代码我已经上传到github了,地址是:github.com/dbfu/antd-p…