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
初始化项目配置项目
1
2
3
4
5{
"npmClient": "npm", // 使用 npm
"packages": ["packages/*"], // 指定包目录
... // 其他配置
}1
2
3
4{
"workspaces": ["packages/*"],
... // 其他配置
}
创建主/子应用
依次执行下面命令
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 {
}