介绍

  • Lerna 是一个快速、领先的构建系统, 用于管理和发布来自同一源码仓库的多个 JavaScript/TypeScript 软件包

  • Qiankun 是一个基于 single-spa 的微前端实现库, 在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统

  • Vite 是一个超快的前端构建工具, 赋能下一代 Web 应用的发展

思路

  • 主应用 app 配置四个菜单 AppApp1App2App3

  • 菜单 App 链接主应用 app, 可以调用子应用 app1app2app3

  • 菜单 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

node 版本小于 18, 安装 vite-plugin-qiankun 后启动会报错 ReferenceError: ReadableStream is not defined

解决方案:更改 cheerio 的版本, 详见:cheerio upgrade problem

packages/app/package.json 下添加下面内容 注意不要带 ^ 符号

1
2
3
4
5
6
7
8
{
... // 其他配置
"devDependencies": {
... // 其他依赖
"cheerio": "1.0.0-rc.12",
"vite": "5.4.11"
}
}

最后删除 node_modules 文件夹与 package-lock.json 文件,重新安装依赖 (重要!!!)

项目目录

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
├── 根目录
├── packages # 项目目录, 名称要跟 package.json 中 workspaces 以及 lerna.json 中 packages 一致
├── app # 主应用app
├── src
├── pages # 页面
├── Home.tsx # 首页
├── Layout.tsx # 布局
├── App.tsx # 路由配置文件
├── index.css # 全局样式
├── main.tsx # 项目入口文件
├── vite.config.ts # vite 配置文件
├── app1 # 子应用app1
├── src
├── App.tsx # 路由配置文件
├── index.css # 全局样式
├── main.tsx # 项目入口文件
├── vite.config.ts # vite 配置文件
├── app2 # 子应用app2
├── src
├── App.tsx # 路由配置文件
├── index.css # 全局样式
├── main.tsx # 项目入口文件
├── vite.config.ts # vite 配置文件
├── app3 # 子应用app3
├── src
├── App.tsx # 路由配置文件
├── index.css # 全局样式
├── main.tsx # 项目入口文件
├── vite.config.ts # vite 配置文件
├── lerna.json # lerna 配置文件
├── package.json # 全局配置文件

搭建 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
    6
    export 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
    6
    export 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
    6
    export 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
    6
    export 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.jsonname, 详见:npm workspace

    1
    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 安装 微前端架构 Qiankun

    1
    npm i qiankun -w=app
  • 子应用 app1app2app3 安装 Qiankun插件 Vite-Plugin-Qiankun

    1
    npm i vite-plugin-qiankun -D -w=app1 -w=app2 -w=app3
  • 子应用 app3 安装 样式隔离插件 Postcss-Prefix-Selector

    1
    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
    52
    import { 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
    52
    import {
    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
    69
    import { 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
    5
    const 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
    29
    import { 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
    33
    import { 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
    23
    import { 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
    29
    import { 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
    33
    import { 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
    23
    import { 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
    29
    import { 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
    33
    import { 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
    23
    import { 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;

多个子应用共存

参考: Qiankun+Vite多个子应用共存

  • 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
    57
    import { 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
    33
    import { 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
    42
    import { 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
    33
    import { 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
    42
    import { 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
    33
    import { 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
    42
    import { 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;

样式隔离

参考: Qiankun+Vite样式隔离解决方案

  • 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
    62
    import { 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
    54
    import { 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
    57
    import { 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
    58
    import { 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
    40
    import { 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
    52
    import { 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 {
    }

效果