Qiankun 常见问题
Qiankun-Vite 插件
Qiankun 默认不能接入 Vite ,需要使用 vite-plugin-qiankun 插件,但是这个插件目前不支持
Qiankun的样式隔离
和React的热更新
推荐使用 vite-plugin-qiankun-lite 插件,解决了上面的问题,并且简化了配置
Node 版本问题
Node 16
使用vite-plugin-qiankun
插件启动时会报错ReferenceError: ReadableStream is not defined
,需要更改cheerio
的版本(注意不要带 ^ 符号),详见:cheerio upgrade problem1
2
3"devDependencies": {
"cheerio": "1.0.0-rc.12"
}Node 16
使用Vite
时,也需要更改版本(其他版本也可自行尝试)1
2
3"devDependencies": {
"vite": "5.4.11"
}最后删除
node_modules
文件夹与package-lock.json
文件,重新安装依赖 (重要!!!)
Qiankun 样式问题
官网示例:如何确保主应用跟微应用之间的样式隔离
组件库 Ant Design 添加前缀,或者使用 postcss-prefix-selector 插件,详见:Qiankun + Vite 样式隔离解决方案
使用
vite-plugin-qiankun-lite
插件,配置{ sandbox: { experimentalStyleIsolation: true } }
启用Qiankun的样式隔离
Qiankun 多个子应用共存
官网示例 1:如何同时激活两个微应用
官网示例 2:同时存在多个微应用时
使用
loadMicroApp
+momery
路由即可实现,更多参考:Qiankun + Vite 多个子应用共存
Qiankun 应用间跳转
官网示例:微应用之间如何跳转?
在
主应用
的Layout
组件中往window
上挂载navigate
属性,子应用中通过window.navigate
实现跳转
加载子应用时,Layout
组件往往已经加载完成,此时window.navigate
属性已经存在,可以直接使用,否则需要等待Layout
组件加载完成后,再使用window.navigate
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 主应用
const Layout = () => {
const navigate = useNavigate();
useEffect(() => {
window.mainNavigate = navigate;
}, []);
return (
<>
<Outlet />
</>
);
};1
2// 子应用
window.mainNavigate('/home');
Qiankun 打包优化
官网示例:如何提取出公共的依赖库
通过
CDN
的方式加载React
、ReactDom
、ReactRouter
等依赖库,并使用externals
配置排除这些依赖,实现多个应用共享一份通用依赖,进而实现打包体积的减小,详见:Qiankun 打包优化
Qiankun 部署问题
官网示例:如何部署
参考 微前端 qiankun 从搭建到部署的实践 中的方式,这样子应用配置只需要写一份,以后新增子应用也不需要改 nginx 配置
1
2
3
4
5
6
7
8├── main
│ └── index.html
└── subapp
├── sub-react
│ └── index.html
└── sub-vue
└── index.html1
2
3
4
5
6
7
8
9
10
11
12
13server {
listen 80;
server_name qiankun.fengxianqi.com;
location / {
root /data/web/qiankun/main; # 主应用所在的目录
index index.html;
try_files $uri $uri/ /index.html;
}
location /subapp {
alias /data/web/qiankun/subapp;
try_files $uri $uri/ /index.html;
}
}
Qiankun 动态路由
在路由数据中添加
routerAttr
属性,参考下面数据格式(主应用页面
、子应用一级页面
、子应用二级页面
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50const routes = [
{
path: '/dashboard',
component: 'Dashboard',
meta: {
title: '主应用App', // 主应用页面
icon: 'DashboardOutlined',
},
},
{
path: '/app1',
component: 'Microapp',
meta: {
title: '子应用App1', // 子应用页面
icon: 'ControlOutlined',
},
routerAttr:
'{"name": "app1", "entry": "http://localhost:8001", "activeRule": "/app1/*", "rootId": "sub-app"}',
},
{
path: '/app2',
component: 'Outlet',
meta: {
title: '子应用App2',
icon: 'DatabaseOutlined',
},
children: [
{
path: '/app2/home',
component: 'Microapp',
meta: {
title: '子应用App2-Home', // 子应用二级页面
icon: 'ControlOutlined',
},
routerAttr:
'{"name": "app2", "entry": "http://localhost:8002", "activeRule": "/app2/*", "rootId": "sub-app"}',
},
{
path: '/app2/test',
component: 'Microapp',
meta: {
title: '子应用App2-Test', // 子应用二级页面
icon: 'ControlOutlined',
},
routerAttr:
'{"name": "app2", "entry": "http://localhost:8002", "activeRule": "/app2/*", "rootId": "sub-app"}',
},
],
},
];展示出来的菜单结构如下:
1
2
3
4
5├── 主应用App
├── 子应用App1
|── 子应用App2
│ └── 子应用App2-Home
│ └── 子应用App2-Test最后在
App
组件中实现逻辑主应用页面使用懒加载
lazy(modules[`./pages/${item.component}/index.tsx`])
子应用页面使用
Microapp
组件去加载二级子应用页面出口使用
Outlet
组件去加载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131// App.tsx
/** 导入指定文件下的路由模块 */
const modules = import.meta.glob('./pages/**/*.tsx');
// 所有路由
const defaultRoutes: RouteObject[] = [
{ path: '/login', Component: <Login /> },
{ path: '/', element: <Navigate to="/dashboard" replace /> },
{
id: 'layout', // 唯一标识
path: '/',
Component: <Layout />, // 不要使用懒加载
children: [],
errorElement: <>找不到页面</>,
},
{ path: '*', Component: <NotFound /> },
];
/** 根组件 */
const App = () => {
const [router, setRouter] =
useState<RouterProviderProps['router']>(undefined);
/** 获取路由数据 */
const getRoutes = async () => {
try {
// 微应用信息
const microApps = [];
// 处理路由数据
const handleRoutes = (routes: any) => {
return (
routes?.map((item) => {
let children = [];
if (item.children) {
children = handleRoutes(item.children);
}
// 是否是微应用
if (item.routerAttr) {
const routerAttr = JSON.parse(item.routerAttr) || {};
const id = routerAttr.rootId || 'sub-app';
// 添加微应用信息(防止重复)
if (micorApps.find((_) => _.name !== routerAttr.name)) {
microApps.push({ ...routerAttr, container: `#${id}` });
}
// Microapp 不能使用懒加载(否则qiankun拿不到节点信息)
return { ...item, element: <Microapp rootId={id} />, children };
} else {
// 路由出口
if (item.component === 'Outlet') {
return { ...item, element: <Outlet />, children };
}
// 懒加载组件
const Component =
lazy(modules[`./pages/${item.component}/index.tsx`] as any) ||
null;
return { ...item, Component, children };
}
}) || []
);
};
// 返回上面的数据结构
const data = getRoutesApi() || [];
// 子路由处理
const subRoutes = handleRoutes(data);
// 合并路由
const allRoutes = defaultRoutes.map((item) => {
if (item.id === 'layout') return { ...item, children: subRoutes };
return item;
});
// 注册微应用
registerMicroApps(microApps, {
beforeLoad: [
async (app) => {
console.log(
'[LifeCycle] before load %c%s',
'color: green;',
app.name
);
},
],
beforeMount: [
async (app) => {
console.log(
'[LifeCycle] before mount %c%s',
'color: green;',
app.name
);
},
],
afterUnmount: [
async (app) => {
console.log(
'[LifeCycle] after unmount %c%s',
'color: green;',
app.name
);
},
],
});
// 启动 qiankun
start({ sandbox: { experimentalStyleIsolation: true } });
// 设置路由
setRouter(createBrowserRouter(allRoutes, { basename: '/' }));
} catch (error) {
console.error('获取路由信息错误, 请配置路由接口:', error);
}
};
useEffect(() => {
getRoutes();
}, []);
if (!router) return <>Loading...</>;
return <RouterProvider router={router} />;
};
export default App;1
2
3
4
5
6
7
8
9// Microapp.tsx
import { memo } from 'react';
/** 渲染微应用 */
const Microapp = ({ rootId }) => {
return <main id={rootId}></main>;
};
export default memo(Microapp);
Qiankun 子应用 loader 状态
在
动态路由
的基础上实现方案 1:使用
createContext
+useContext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// store.ts
import { createContext, useContext, useState } from 'react';
interface IMicroApp {
loading: boolean;
setLoading: (loading: boolean) => void;
}
const MicroappState = createContext<MicroappState>({
loading: false,
});
export const MicroappStateProvider = ({ children }: any) => {
const [loading, setLoading] = useState(false);
return (
<MicroappState.Provider value={{ loading, setLoading }}>
{children}
</MicroappState.Provider>
);
};
export const useMicroappState = () => useContext(MicroappState);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34// App.tsx
import { useMicroappState, MicroappStateProvider } from './store';
const App = () => {
const { setLoading } = useMicroappState();
useEffect(() => {
// 微应用信息
const microApps = [];
microApps.push({
name: 'app1',
entry: '//localhost:3001/',
container: '#app',
activeRule: '/app1',
props: {},
loader: (loading) => {
setLoading(loading);
},
});
}, []);
return <></>;
};
const AppProvider = () => {
return (
<MicroappStateProvider>
<App />
</MicroappStateProvider>
);
};
export default AppProvider;1
2
3
4
5
6
7
8
9
10
11
12
13// Microapp.tsx
import { memo } from 'react';
import { useMicroappState } from './store';
/** 渲染微应用 */
const Microapp = ({ rootId }) => {
const { loading } = useMicroappState();
if (loading) return <div>Loading...</div>;
return <main id={rootId}></main>;
};
export default memo(Microapp);方案 2:使用 zustand
1
2
3
4
5
6
7
8
9
10
11
12// store.ts
import { create } from 'zustand';
interface IMicroApp {
loading: boolean;
setLoading: (loading: boolean) => void;
}
export const useMicroappState = create((set) => ({
loading: false,
setLoading: (loading: boolean) => set({ loading }),
}));1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// App.tsx
import { useMicroappState } from './store';
const App = () => {
useEffect(() => {
// 微应用信息
const microApps = [];
microApps.push({
name: 'app1',
entry: '//localhost:3001/',
container: '#app',
activeRule: '/app1',
props: {},
loader: (loading) => {
useMicroappState.getInitialState().setLoading(loading);
},
});
}, []);
return <></>;
};1
2
3
4
5
6
7
8
9
10
11
12
13// Microapp.tsx
import { memo } from 'react';
import { useMicroappState } from './store';
/** 渲染微应用 */
const Microapp = ({ rootId }) => {
const loading = useMicroappState((state) => state.loading);
if (loading) return <div>Loading...</div>;
return <main id={rootId}></main>;
};
export default memo(Microapp);
React-Router 版本问题
React Router 在新版本中加入了
startTransition
属性,会导致路由在切换一次显示子应用dom
挂载不上问题解决方案 1:关闭
startTransition
属性1
2
3
4// react-router-dom v6 版本
import { RouterProvider } from "react-router-dom";
<RouterProvider future={{ v7_startTransition: false }}>1
2
3
4// react-route v7 版本 (这里爆红也能正常使用)
import { RouterProvider } from 'react-router/dom';
<RouterProvider />;解决方案 2:使用
navigate
时,加上{ flushSync: true }
属性1
2
3
4
5// 默认 RouterProvider 中是开启 startTransition 属性
import { useNavigate } from 'react-router-dom';
const navigate = useNavigate();
navigate('/', { flushSync: true }); // 注意子应用挂载dom不要使用懒加载导入