Qiankun + Vite 实现微前端
介绍
Lerna 是一个快速、领先的构建系统,用于管理和发布来自同一源码仓库的多个
JavaScript/TypeScript
软件包Qiankun 是一个基于
single-spa
的微前端实现库,在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统Vite 是一个超快的前端构建工具,赋能下一代
Web
应用的发展
思路
主应用
app
配置四个菜单App
、App1
、App2
、App3
菜单
App
链接主应用app
,可以调用子应用app1
、app2
、app3
菜单
App1
链接子应用app1
,样式未隔离,组件库样式未隔离菜单
App2
链接子应用app2
,样式未隔离,组件库样式隔离菜单
App3
链接子应用app3
,样式隔离,组件库样式未隔离
项目地址
实践
包版本
node
:18.20.7
npm
:10.8.2
lerna
:8.1.8
vite
:6.1.0
qiankun
:2.10.16
项目目录
1 | ├── 根目录 |
搭建 monorepo
项目 (可选)
通过
npm install lerna -g
全局安装lerna
命令行执行
npx lerna init
初始化项目配置
lerna.json
文件如下1
2
3
4
5{
"npmClient": "npm", // 使用 npm
"packages": ["packages/*"], // 指定包目录
... // 其他配置
}配置
package.json
文件如下
1 | { |
创建主/子应用
依次执行下面命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 从根目录进入 packages 目录
cd ./packages
# 创建主应用 app
npm create vite@latest
# Project name
app
# Select a framework
React
# Select a variant
TypeScript + SWC在
packages/app/vite.config.ts
中配置启动端口
和应用名称
1
2
3
4
5
6export default defineConfig({
plugins: [react()],
server: {
port: 8000, // 启动端口
},
});
依次执行下面命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 从根目录进入 packages 目录
cd ./packages
# 创建子应用 app1
npm create vite@latest
# Project name
app1
# Select a framework
React
# Select a variant
TypeScript + SWC在
packages/app1/vite.config.ts
中配置启动端口
和应用名称
1
2
3
4
5
6export default defineConfig({
plugins: [react()],
server: {
port: 8001, // 启动端口
},
});
依次执行下面命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 从根目录进入 packages 目录
cd ./packages
# 创建子应用 app2
npm create vite@latest
# Project name
app2
# Select a framework
React
# Select a variant
TypeScript + SWC在
packages/app2/vite.config.ts
中配置启动端口
和应用名称
1
2
3
4
5
6export default defineConfig({
plugins: [react()],
server: {
port: 8002, // 启动端口
},
});
依次执行下面命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 从根目录进入 packages 目录
cd ./packages
# 创建子应用 app3
npm create vite@latest
# Project name
app3
# Select a framework
React
# Select a variant
TypeScript + SWC在
packages/app3/vite.config.ts
中配置启动端口
和应用名称
1
2
3
4
5
6export default defineConfig({
plugins: [react()],
server: {
port: 8003, // 启动端口
},
});
启动项目
在根目录下执行 npm install
安装依赖
方式一:进入各个应用目录,分别执行
npm run dev
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# 进入 packages/app 目录
cd ./packages/app
# 启动主应用
npm run dev
# 进入 packages/app1 目录
cd ./packages/app1
# 启动子应用
npm run dev
# 进入 packages/app2 目录
cd ./packages/app2
# 启动子应用
npm run dev
# 进入 packages/app3 目录
cd ./packages/app3
# 启动子应用
npm run dev方式二:进入根目录,执行
npm run dev -w=xxx
,-w
就是--workspace
,xxx
对应各个应用package.json
的name
,详见:npm workspace1
2
3
4
5
6
7
8
9
10
11# 根目录下启动主应用
npm run dev -w=app
# 根目录下启动子应用
npm run dev -w=app1
# 根目录下启动子应用
npm run dev -w=app2
# 根目录下启动子应用
npm run dev -w=app3
安装依赖
所有应用安装 路由 React-Router-Dom、组件库 Ant Design
1
npm i react-router-dom antd -w=app -w=app1 -w=app2 -w=app3
主应用
app
安装 微前端架构 Qiankun1
npm i qiankun -w=app
子应用
app1
、app2
、app3
安装Qiankun
插件 Vite-Plugin-Qiankun1
npm i vite-plugin-qiankun -D -w=app1 -w=app2 -w=app3
子应用
app3
安装样式隔离
插件 Postcss-Prefix-Selector1
npm i postcss-prefix-selector -D -w=app3
链接主/子应用
在
packages/app/src/main.tsx
中注册所有子应用
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
52import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.tsx';
import { registerMicroApps, RegistrableApp, start } from 'qiankun';
export const microApps: Array<RegistrableApp<any>> = [
{
name: 'app1', // 子应用名称(全局唯一)
entry: 'http://localhost:8001', // 这里的端口号要和子应用的端口号一致
container: '#subapp1', // 子应用挂载点
activeRule: '/app1', // 这里的路径要和子应用的路由前缀一致
props: { routeType: 'browser' }, // 传递给子应用的数据
},
{
name: 'app2', // 子应用名称(全局唯一)
entry: 'http://localhost:8002', // 这里的端口号要和子应用的端口号一致
container: '#subapp2', // 子应用挂载点
activeRule: '/app2', // 这里的路径要和子应用的路由前缀一致
props: { routeType: 'browser' }, // 传递给子应用的数据
},
{
name: 'app3', // 子应用名称(全局唯一)
entry: 'http://localhost:8003', // 这里的端口号要和子应用的端口号一致
container: '#subapp3', // 子应用挂载点
activeRule: '/app3', // 这里的路径要和子应用的路由前缀一致
props: { routeType: 'browser' }, // 传递给子应用的数据
},
];
/** 注册子应用 */
registerMicroApps(microApps, {
beforeLoad: async (app) => {
console.log(`%c before load: ${app.name}`, 'color: green');
},
beforeMount: async (app) => {
console.log(`%c before mount: ${app.name}`, 'color: green');
},
afterMount: async (app) => {
console.log(`%c after mount: ${app.name}`, 'color: yellow');
},
beforeUnmount: async (app) => {
console.log(`%c before unmount: ${app.name}`, 'color: red');
},
afterUnmount: async (app) => {
console.log(`%c after unmount: ${app.name}`, 'color: red');
},
});
/** 启动子应用, 防止子应用刷新刷新dom找不到问题 */
start();
createRoot(document.getElementById('root')!).render(<App />);在
packages/app/src/App.tsx
中配置路由
和子应用挂载的容器
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
52import {
createBrowserRouter,
Navigate,
RouterProvider,
} from 'react-router-dom';
import Home from './pages/Home';
import BaseLayout from './pages/Layout';
/** 路由配置 */
const routes = createBrowserRouter(
[
{
path: '/',
element: <Navigate to="/app" replace />, // 重定向
},
{
path: '/',
element: <BaseLayout />, // 布局
children: [
{
path: '/app',
element: <Home />, // 首页
},
{
path: '/app1/*', // 通配符 * 表示匹配所有子路由
element: <div id="subapp1"></div>, // 子应用挂载点 对应 main.tsx 注册子应用的 container
},
{
path: '/app2/*', // 通配符 * 表示匹配所有子路由
element: <div id="subapp2"></div>, // 子应用挂载点 对应 main.tsx 注册子应用的 container
},
{
path: '/app3/*', // 通配符 * 表示匹配所有子路由
element: <div id="subapp3"></div>, // 子应用挂载点 对应 main.tsx 注册子应用的 container
},
],
},
{
path: '*',
element: <div>404</div>,
},
],
{
basename: '/',
}
);
function App() {
return <RouterProvider router={routes} />;
}
export default App;在
packages/app/src/pages/Layout.tsx
中创建页面布局
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
69import { Layout, Menu, theme } from 'antd';
import { useEffect, useState } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
const { Header, Content, Footer } = Layout;
const menuItems = [
{ key: 'app', label: 'App' },
{ key: 'app1', label: 'App1' },
{ key: 'app2', label: 'App2' },
{ key: 'app3', label: 'App3' },
];
/** 布局组件 */
const BaseLayout = () => {
const location = useLocation();
const navigate = useNavigate();
const {
token: { colorBgContainer, borderRadiusLG },
} = theme.useToken();
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
/** 菜单点击事件 */
const handleMenuClick = ({ key }) => {
navigate(`/${key}`);
};
useEffect(() => {
const key = location.pathname.split('/')[1];
setSelectedKeys([key]);
}, [location]);
return (
<Layout>
<Header style={{ display: 'flex', alignItems: 'center' }}>
<div className="demo-logo" />
<Menu
theme="dark"
mode="horizontal"
items={menuItems}
selectedKeys={selectedKeys}
style={{ flex: 1, minWidth: 0 }}
onClick={handleMenuClick}
/>
</Header>
<Content style={{ padding: '16px 48px' }}>
<div
style={{
background: colorBgContainer,
minHeight: 400,
padding: 24,
borderRadius: borderRadiusLG,
}}>
<Outlet />
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>
Demo ©{new Date().getFullYear()} Created by Zxiaosi
</Footer>
</Layout>
);
};
export default BaseLayout;在
packages/app/src/pages/Home.tsx
中创建首页
1
2
3
4
5const Home = () => {
return <h2>App</h2>;
};
export default Home;
在
packages/app1/vite.config.ts
中配置Vite-Plugin-Qiankun
插件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
29import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import qiankun from 'vite-plugin-qiankun';
// https://vite.dev/config/
export default ({ mode }) => {
const useDevMode = mode === 'development';
const host = '127.0.0.1';
const port = 8001;
const subAppName = 'app1';
const base = useDevMode
? `http://${host}:${port}/${subAppName}`
: `/${subAppName}`; // 这里 subAppName 对应 createBrowserRouter 的 basename
return defineConfig({
base,
server: {
port,
cors: true, // 作为子应用时,如果不配置,则会引起跨域问题
origin: `http://${host}:${port}`, // 必须配置,否则无法访问静态资源
},
plugins: [
// 在开发模式下需要把react()关掉
// https://github.com/tengmaoqing/vite-plugin-qiankun?tab=readme-ov-file#3dev%E4%B8%8B%E4%BD%9C%E4%B8%BA%E5%AD%90%E5%BA%94%E7%94%A8%E8%B0%83%E8%AF%95
...[useDevMode ? [] : [react()]],
qiankun(subAppName, { useDevMode }),
],
});
};在
packages/app1/src/main.tsx
中配置Qiankun生命周期钩子函数
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
33import { createRoot } from 'react-dom/client';
import {
QiankunProps,
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
import App from './App.tsx';
import './index.css';
/** 渲染函数 */
const render = (container?: HTMLElement) => {
const app =
container || (document.getElementById('root') as HTMLDivElement);
createRoot(app).render(<App />);
};
/** Qiankun 生命周期钩子 */
const qiankun = () => {
renderWithQiankun({
bootstrap() {},
async mount(props: QiankunProps) {
render(props.container);
},
update: () => {},
unmount: () => {},
});
};
// 检查是否在 Qiankun 环境中
console.log('qiankunWindow', qiankunWindow.__POWERED_BY_QIANKUN__);
if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun(); // 以子应用的方式启动
else render();在
packages/app1/src/App.tsx
中配置路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import { createBrowserRouter, RouterProvider } from 'react-router-dom';
/** 路由前缀 */
const basename = '/app1';
/** 创建路由 */
const routes = createBrowserRouter(
[
{
path: '/',
element: <h2>app1</h2>,
},
],
{
basename,
}
);
function App() {
return <RouterProvider router={routes} />;
}
export default App;
在
packages/app2/vite.config.ts
中配置Vite-Plugin-Qiankun
插件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
29import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import qiankun from 'vite-plugin-qiankun';
// https://vite.dev/config/
export default ({ mode }) => {
const useDevMode = mode === 'development';
const host = '127.0.0.1';
const port = 8002;
const subAppName = 'app2';
const base = useDevMode
? `http://${host}:${port}/${subAppName}`
: `/${subAppName}`; // 这里 subAppName 对应 createBrowserRouter 的 basename
return defineConfig({
base,
server: {
port,
cors: true, // 作为子应用时,如果不配置,则会引起跨域问题
origin: `http://${host}:${port}`, // 必须配置,否则无法访问静态资源
},
plugins: [
// 在开发模式下需要把react()关掉
// https://github.com/tengmaoqing/vite-plugin-qiankun?tab=readme-ov-file#3dev%E4%B8%8B%E4%BD%9C%E4%B8%BA%E5%AD%90%E5%BA%94%E7%94%A8%E8%B0%83%E8%AF%95
...[useDevMode ? [] : [react()]],
qiankun(subAppName, { useDevMode }),
],
});
};在
packages/app2/src/main.tsx
中配置Qiankun生命周期钩子函数
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
33import { createRoot } from 'react-dom/client';
import {
QiankunProps,
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
import App from './App.tsx';
import './index.css';
/** 渲染函数 */
const render = (container?: HTMLElement) => {
const app =
container || (document.getElementById('root') as HTMLDivElement);
createRoot(app).render(<App />);
};
/** Qiankun 生命周期钩子 */
const qiankun = () => {
renderWithQiankun({
bootstrap() {},
async mount(props: QiankunProps) {
render(props.container);
},
update: () => {},
unmount: () => {},
});
};
// 检查是否在 Qiankun 环境中
console.log('qiankunWindow', qiankunWindow.__POWERED_BY_QIANKUN__);
if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun(); // 以子应用的方式启动
else render();在
packages/app2/src/App.tsx
中配置路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import { createBrowserRouter, RouterProvider } from 'react-router-dom';
/** 路由前缀 */
const basename = '/app2';
/** 创建路由 */
const routes = createBrowserRouter(
[
{
path: '/',
element: <h2>app2</h2>,
},
],
{
basename,
}
);
function App() {
return <RouterProvider router={routes} />;
}
export default App;
在
packages/app3/vite.config.ts
中配置Vite-Plugin-Qiankun
插件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
29import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import qiankun from 'vite-plugin-qiankun';
// https://vite.dev/config/
export default ({ mode }) => {
const useDevMode = mode === 'development';
const host = '127.0.0.1';
const port = 8003;
const subAppName = 'app3';
const base = useDevMode
? `http://${host}:${port}/${subAppName}`
: `/${subAppName}`; // 这里 subAppName 对应 createBrowserRouter 的 basename
return defineConfig({
base,
server: {
port,
cors: true, // 作为子应用时,如果不配置,则会引起跨域问题
origin: `http://${host}:${port}`, // 必须配置,否则无法访问静态资源
},
plugins: [
// 在开发模式下需要把react()关掉
// https://github.com/tengmaoqing/vite-plugin-qiankun?tab=readme-ov-file#3dev%E4%B8%8B%E4%BD%9C%E4%B8%BA%E5%AD%90%E5%BA%94%E7%94%A8%E8%B0%83%E8%AF%95
...[useDevMode ? [] : [react()]],
qiankun(subAppName, { useDevMode }),
],
});
};在
packages/app3/src/main.tsx
中配置Qiankun生命周期钩子函数
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
33import { createRoot } from 'react-dom/client';
import {
QiankunProps,
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
import App from './App.tsx';
import './index.css';
/** 渲染函数 */
const render = (container?: HTMLElement) => {
const app =
container || (document.getElementById('root') as HTMLDivElement);
createRoot(app).render(<App />);
};
/** Qiankun 生命周期钩子 */
const qiankun = () => {
renderWithQiankun({
bootstrap() {},
async mount(props: QiankunProps) {
render(props.container);
},
update: () => {},
unmount: () => {},
});
};
// 检查是否在 Qiankun 环境中
console.log('qiankunWindow', qiankunWindow.__POWERED_BY_QIANKUN__);
if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun(); // 以子应用的方式启动
else render();在
packages/app3/src/App.tsx
中配置路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import { createBrowserRouter, RouterProvider } from 'react-router-dom';
/** 路由前缀 */
const basename = '/app3';
/** 创建路由 */
const routes = createBrowserRouter(
[
{
path: '/',
element: <h2>app3</h2>,
},
],
{
basename,
}
);
function App() {
return <RouterProvider router={routes} />;
}
export default App;
多个子应用共存
在
packages/app/src/pages/Home.tsx
中配置加载子应用方法
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
57import { Divider } from 'antd';
import { microApps } from '../main';
import { useEffect, useRef } from 'react';
import { loadMicroApp, MicroApp } from 'qiankun';
/** 子应用样式 */
const subStyle = {
width: 'calc((100% - 20px * 2) / 3)',
height: 400,
border: '1px solid red',
};
const Home = () => {
const microInstanceRef = useRef<MicroApp>(undefined); // 子应用实例
const handleClick = (appName: string) => {
microInstanceRef.current?.unmount();
const microApp = microApps.find((item) => item.name === appName);
console.log('microApps', microApp);
const { activeRule, ...rest } = microApp;
microInstanceRef.current = loadMicroApp({
...rest,
props: { routeType: 'memory' }, // 设置路由类型为 memory
});
};
useEffect(() => {
return () => {
microInstanceRef.current?.unmount();
};
}, []);
return (
<div>
<h2>App</h2>
<Divider />
<button onClick={() => handleClick('app1')}>加载 app1</button>
<button onClick={() => handleClick('app2')}>加载 app2</button>
<button onClick={() => handleClick('app3')}>加载 app3</button>
<Divider>子应用</Divider>
<div style={{ display: 'flex', gap: '0 20px' }}>
<div id="subapp1" style={subStyle}></div>
<div id="subapp2" style={subStyle}></div>
<div id="subapp3" style={subStyle}></div>
</div>
</div>
);
};
export default Home;
在
packages/app1/src/main.tsx
中传递主应用的props
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
33import { createRoot } from 'react-dom/client';
import {
QiankunProps,
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
import App from './App.tsx';
import './index.css';
/** 渲染函数 */
const render = (container?: HTMLElement, microProps = {}) => {
const app =
container || (document.getElementById('root') as HTMLDivElement);
createRoot(app).render(<App microProps={microProps} />);
};
/** Qiankun 生命周期钩子 */
const qiankun = () => {
renderWithQiankun({
bootstrap() {},
async mount(props: QiankunProps) {
render(props.container, props);
},
update: () => {},
unmount: () => {},
});
};
// 检查是否在 Qiankun 环境中
console.log('qiankunWindow', qiankunWindow.__POWERED_BY_QIANKUN__);
if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun(); // 以子应用的方式启动
else render();在
packages/app1/src/App.tsx
中配置多种路由模式
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
42import { useMemo } from 'react';
import {
createBrowserRouter,
createHashRouter,
createMemoryRouter,
RouteObject,
RouterProvider,
} from 'react-router-dom';
/** 路由前缀 */
const basename = '/app1';
/** 创建路由 */
const router: RouteObject[] = [
{
path: '/',
element: <h2>app1</h2>,
},
];
function App({ microProps }: any) {
const { routeType } = microProps;
const routes = useMemo(() => {
switch (routeType) {
case 'hash':
return createHashRouter(router, { basename });
case 'memory':
return createMemoryRouter(router, {
basename,
initialEntries: ['/app1'], // 初始化时指定初始路径, 用户可以通过浏览器前进后退操作
initialIndex: 0, // 初始化时指定初始索引
});
default:
return createBrowserRouter(router, { basename });
}
}, [routeType]);
return <RouterProvider router={routes} />;
}
export default App;
在
packages/app2/src/main.tsx
中传递主应用的props
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
33import { createRoot } from 'react-dom/client';
import {
QiankunProps,
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
import App from './App.tsx';
import './index.css';
/** 渲染函数 */
const render = (container?: HTMLElement, microProps = {}) => {
const app =
container || (document.getElementById('root') as HTMLDivElement);
createRoot(app).render(<App microProps={microProps} />);
};
/** Qiankun 生命周期钩子 */
const qiankun = () => {
renderWithQiankun({
bootstrap() {},
async mount(props: QiankunProps) {
render(props.container, props);
},
update: () => {},
unmount: () => {},
});
};
// 检查是否在 Qiankun 环境中
console.log('qiankunWindow', qiankunWindow.__POWERED_BY_QIANKUN__);
if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun(); // 以子应用的方式启动
else render();在
packages/app2/src/App.tsx
中配置多种路由模式
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
42import { useMemo } from 'react';
import {
createBrowserRouter,
createHashRouter,
createMemoryRouter,
RouteObject,
RouterProvider,
} from 'react-router-dom';
/** 路由前缀 */
const basename = '/app2';
/** 创建路由 */
const router: RouteObject[] = [
{
path: '/',
element: <h2>app2</h2>,
},
];
function App({ microProps }: any) {
const { routeType } = microProps;
const routes = useMemo(() => {
switch (routeType) {
case 'hash':
return createHashRouter(router, { basename });
case 'memory':
return createMemoryRouter(router, {
basename,
initialEntries: ['/app2'], // 初始化时指定初始路径, 用户可以通过浏览器前进后退操作
initialIndex: 0, // 初始化时指定初始索引
});
default:
return createBrowserRouter(router, { basename });
}
}, [routeType]);
return <RouterProvider router={routes} />;
}
export default App;
在
packages/app3/src/main.tsx
中传递主应用的props
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
33import { createRoot } from 'react-dom/client';
import {
QiankunProps,
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
import App from './App.tsx';
import './index.css';
/** 渲染函数 */
const render = (container?: HTMLElement, microProps = {}) => {
const app =
container || (document.getElementById('root') as HTMLDivElement);
createRoot(app).render(<App microProps={microProps} />);
};
/** Qiankun 生命周期钩子 */
const qiankun = () => {
renderWithQiankun({
bootstrap() {},
async mount(props: QiankunProps) {
render(props.container, props);
},
update: () => {},
unmount: () => {},
});
};
// 检查是否在 Qiankun 环境中
console.log('qiankunWindow', qiankunWindow.__POWERED_BY_QIANKUN__);
if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun(); // 以子应用的方式启动
else render();在
packages/app3/src/App.tsx
中配置多种路由模式
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
42import { useMemo } from 'react';
import {
createBrowserRouter,
createHashRouter,
createMemoryRouter,
RouteObject,
RouterProvider,
} from 'react-router-dom';
/** 路由前缀 */
const basename = '/app3';
/** 创建路由 */
const router: RouteObject[] = [
{
path: '/',
element: <h2>app3</h2>,
},
];
function App({ microProps }: any) {
const { routeType } = microProps;
const routes = useMemo(() => {
switch (routeType) {
case 'hash':
return createHashRouter(router, { basename });
case 'memory':
return createMemoryRouter(router, {
basename,
initialEntries: ['/app3'], // 初始化时指定初始路径, 用户可以通过浏览器前进后退操作
initialIndex: 0, // 初始化时指定初始索引
});
default:
return createBrowserRouter(router, { basename });
}
}, [routeType]);
return <RouterProvider router={routes} />;
}
export default App;
样式隔离
在
packages/app/src/pages/Home.tsx
中配置测试样式dom
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
62import { Button, Divider } from 'antd';
import { microApps } from '../main';
import { useEffect, useRef } from 'react';
import { loadMicroApp, MicroApp } from 'qiankun';
/** 子应用样式 */
const subStyle = {
width: 'calc((100% - 20px * 2) / 3)',
height: 400,
border: '1px solid red',
};
const Home = () => {
const microInstanceRef = useRef<MicroApp>(undefined); // 子应用实例
const handleClick = (appName: string) => {
microInstanceRef.current?.unmount();
const microApp = microApps.find((item) => item.name === appName);
console.log('microApps', microApp);
const { activeRule, ...rest } = microApp;
microInstanceRef.current = loadMicroApp({
...rest,
props: { routeType: 'memory' }, // 设置路由类型为 memory
});
};
useEffect(() => {
return () => {
microInstanceRef.current?.unmount();
};
}, []);
return (
<div>
<h2>App</h2>
<Divider />
<button onClick={() => handleClick('app1')}>加载 app1</button>
<button onClick={() => handleClick('app2')}>加载 app2</button>
<button onClick={() => handleClick('app3')}>加载 app3</button>
<Divider>样式</Divider>
<div className="color">测试样式文字</div>
<Button type="primary">测试组件库样式按钮</Button>
<Divider>子应用</Divider>
<div style={{ display: 'flex', gap: '0 20px' }}>
<div id="subapp1" style={subStyle}></div>
<div id="subapp2" style={subStyle}></div>
<div id="subapp3" style={subStyle}></div>
</div>
</div>
);
};
export default Home;在
packages/app/src/index.css
中设置样式
1
2
3
4
5
6
7
8
9/* 测试样式文字 */
.color {
color: red;
}
/* 测试组件库样式按钮 */
.ant-btn {
height: 60px;
}
在
packages/app1/src/App.tsx
中测试样式dom
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
54import { Button, Divider } from 'antd';
import { useMemo } from 'react';
import {
createBrowserRouter,
createHashRouter,
createMemoryRouter,
RouteObject,
RouterProvider,
} from 'react-router-dom';
/** 路由前缀 */
const basename = '/app1';
/** 创建路由 */
const router: RouteObject[] = [
{
path: '/',
element: (
<div>
<h2>app1</h2>
<Divider>样式未隔离, 组件库样式未隔离</Divider>
<div className="color">测试样式文字</div>
<Button type="primary" className="btn">
测试组件库样式按钮
</Button>
</div>
),
},
];
function App({ microProps }: any) {
const { routeType } = microProps;
const routes = useMemo(() => {
switch (routeType) {
case 'hash':
return createHashRouter(router, { basename });
case 'memory':
return createMemoryRouter(router, {
basename,
initialEntries: ['/app1'], // 初始化时指定初始路径, 用户可以通过浏览器前进后退操作
initialIndex: 0, // 初始化时指定初始索引
});
default:
return createBrowserRouter(router, { basename });
}
}, [routeType]);
return <RouterProvider router={routes} />;
}
export default App;在
packages/app1/src/index.css
中设置样式
1
2
3
4
5
6
7
8/* 测试样式文字 */
.color {
color: green;
}
/* 测试组件库样式按钮 */
.ant-btn {
}
在
packages/app2/src/App.tsx
中配置测试样式dom
以及组件库样式前缀
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
57import { Button, ConfigProvider, Divider } from 'antd';
import { useMemo } from 'react';
import {
createBrowserRouter,
createHashRouter,
createMemoryRouter,
RouteObject,
RouterProvider,
} from 'react-router-dom';
/** 路由前缀 */
const basename = '/app2';
/** 创建路由 */
const router: RouteObject[] = [
{
path: '/',
element: (
<div>
<h2>app2</h2>
<Divider>样式未隔离, 组件库样式隔离</Divider>
<div className="color">测试样式文字</div>
<Button type="primary">测试组件库样式按钮</Button>
</div>
),
},
];
function App({ microProps }: any) {
const { routeType } = microProps;
const routes = useMemo(() => {
switch (routeType) {
case 'hash':
return createHashRouter(router, { basename });
case 'memory':
return createMemoryRouter(router, {
basename,
initialEntries: ['/app2'], // 初始化时指定初始路径, 用户可以通过浏览器前进后退操作
initialIndex: 0, // 初始化时指定初始索引
});
default:
return createBrowserRouter(router, { basename });
}
}, [routeType]);
return (
// 配置组件库样式前缀
<ConfigProvider prefixCls="app2">
<RouterProvider router={routes} />
</ConfigProvider>
);
}
export default App;在
packages/app2/src/index.css
中设置样式
1
2
3
4
5
6
7
8
9/* 测试样式文字 */
.color {
color: blue;
}
/* 测试组件库样式按钮 */
.app2-btn {
/* height: 60px; */
}
在
packages/app3/vite.config.ts
中配置postcss-prefix-selector
样式隔离插件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
58import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import qiankun from 'vite-plugin-qiankun';
import prefixer from 'postcss-prefix-selector';
// https://vite.dev/config/
export default ({ mode }) => {
const useDevMode = mode === 'development';
const host = '127.0.0.1';
const port = 8003;
const subAppName = 'app3';
const base = useDevMode
? `http://${host}:${port}/${subAppName}`
: `/${subAppName}`; // 这里 subAppName 对应 createBrowserRouter 的 basename
return defineConfig({
base,
server: {
port,
cors: true, // 作为子应用时,如果不配置,则会引起跨域问题
origin: `http://${host}:${port}`, // 必须配置,否则无法访问静态资源
},
plugins: [
// 在开发模式下需要把react()关掉
// https://github.com/tengmaoqing/vite-plugin-qiankun?tab=readme-ov-file#3dev%E4%B8%8B%E4%BD%9C%E4%B8%BA%E5%AD%90%E5%BA%94%E7%94%A8%E8%B0%83%E8%AF%95
...[useDevMode ? [] : [react()]],
qiankun(subAppName, { useDevMode }),
],
css: {
postcss: {
plugins: [
prefixer({
prefix: `[data-qiankun-${subAppName}]`, // 这里的值要和 main.tsx 中的属性名保持一致
transform(prefix, selector, prefixedSelector, filePath, rule) {
if (selector.match(/^(html|body)/)) {
return selector.replace(/^([^\s]*)/, `$1 ${prefix}`);
}
if (filePath.match(/node_modules/)) {
return selector; // Do not prefix styles imported from node_modules
}
const annotation = rule.prev();
if (
annotation?.type === 'comment' &&
annotation.text.trim() === 'no-prefix'
) {
return selector; // Do not prefix style rules that are preceded by: /* no-prefix */
}
return prefixedSelector;
},
}),
],
},
},
});
};在
packages/app3/src/main.tsx
中添加根节点属性,用于样式隔离
(重要)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
40import { createRoot } from 'react-dom/client';
import {
QiankunProps,
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
import App from './App.tsx';
import './index.css';
/** 渲染函数 */
const render = (container?: HTMLElement, microProps = {}) => {
const app =
container || (document.getElementById('root') as HTMLDivElement);
/**
* 添加属性,用于样式隔离
* 注意:这里的属性名要和 postcss-prefix-selector 插件中的 prefix 保持一致
*/
app.setAttribute('data-qiankun-app3', 'true');
createRoot(app).render(<App microProps={microProps} />);
};
/** Qiankun 生命周期钩子 */
const qiankun = () => {
renderWithQiankun({
bootstrap() {},
async mount(props: QiankunProps) {
render(props.container, props);
},
update: () => {},
unmount: () => {},
});
};
// 检查是否在 Qiankun 环境中
console.log('qiankunWindow', qiankunWindow.__POWERED_BY_QIANKUN__);
if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun(); // 以子应用的方式启动
else render();在
packages/app3/src/App.tsx
中测试样式dom
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
52import { Button, Divider } from 'antd';
import { useMemo } from 'react';
import {
createBrowserRouter,
createHashRouter,
createMemoryRouter,
RouteObject,
RouterProvider,
} from 'react-router-dom';
/** 路由前缀 */
const basename = '/app3';
/** 创建路由 */
const router: RouteObject[] = [
{
path: '/',
element: (
<div>
<h2>app2</h2>
<Divider>样式隔离, 组件库样式未隔离</Divider>
<div className="color">测试样式文字</div>
<Button type="primary">测试组件库样式按钮</Button>
</div>
),
},
];
function App({ microProps }: any) {
const { routeType } = microProps;
const routes = useMemo(() => {
switch (routeType) {
case 'hash':
return createHashRouter(router, { basename });
case 'memory':
return createMemoryRouter(router, {
basename,
initialEntries: ['/app3'], // 初始化时指定初始路径, 用户可以通过浏览器前进后退操作
initialIndex: 0, // 初始化时指定初始索引
});
default:
return createBrowserRouter(router, { basename });
}
}, [routeType]);
return <RouterProvider router={routes} />;
}
export default App;在
packages/app3/src/index.css
中设置样式
1
2
3
4
5
6
7
8/* 测试样式文字 */
.color {
color: fuchsia;
}
/* 测试组件库样式按钮 */
.ant-btn {
}