起因

  • 最初接触 umi 微前端 时, 感觉这个就是两种子应用的加载方式

  • 后来在网上看到大佬的发的这个文章 探索微前端的场景极限, 才发现这个实现不简单, 引用了大佬的话:

    • 在应用 A 中通过调用 loadMicroApp(B) 的方式唤起微应用 B,然后在微应用 B 中通过 loadMicroApp(C) 的方式唤起微应用 C,通过这样的调用链路即可很完美的完成产品上的诉求。

    • 但是现实情况往往没有那么简单,前面提到过,若想要 loadMicroApp API 能符合预期的运行,我们需要确保被加载的微应用是不含自己的路由系统,否则会出现多个应用间路由系统互相 抢占/冲突 的情况。

    • 这种场景下,我们其实只需要确保微应用的路由系统不会干扰到全局的 URL 系统即可。幸运的是 react-routermemory history 模式很好的解决了这一问题。

仓库地址

项目搭建

使用 lerna 创建一个 monorepo 项目。

  • 命令行执行 npx lerna init, 初始化项目

开启 npm workspaces

  • lerna.json 中添加下面配置

    1
    2
    3
    4
    5
    {
    "npmClient": "npm",
    "packages": ["packages/*"],
    ... // 其他配置
    }
  • 然后在根目录下创建 packages 目录 (注意:名称要与上面配置的 packages/* 目录名称一致)

全局安装包

  • 在 根目录 下安装 react-router

    1
    npm install react-router
  • 在 根目录 下安装 antd

    1
    npm install antd
  • 在 根目录 下安装 less

    1
    npm install less -D
  • 在 根目录 下安装 [格式化工具](可选), 详见 .prettierrc 文件

    1
    npm install prettier-plugin-organize-imports prettier-plugin-packagejson -D

进入 packages 创建主应用 app

  • 依次执行下面命令

    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
    7
    8
    9
    10
    11
    12
    13
    14
    export default defineConfig({
    server: {
    port: 8000,
    },
    css: {
    preprocessorOptions: {
    less: {
    math: 'always',
    javascriptEnabled: true,
    },
    },
    },
    ... // 其他配置
    });

进入 packages 创建子应用 app1

  • 依次执行下面命令

    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
    7
    8
    9
    10
    11
    12
    13
    14
    export default defineConfig({
    server: {
    port: 8001,
    },
    css: {
    preprocessorOptions: {
    less: {
    math: 'always',
    javascriptEnabled: true,
    },
    },
    },
    ... // 其他配置
    });

进入 packages 创建子应用 app2

  • 依次执行下面命令

    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
    7
    8
    9
    10
    11
    12
    13
    14
    export default defineConfig({
    server: {
    port: 8002,
    },
    css: {
    preprocessorOptions: {
    less: {
    math: 'always',
    javascriptEnabled: true,
    },
    },
    },
    ... // 其他配置
    });

进入 packages 创建子应用 app3

  • 依次执行下面命令

    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
    7
    8
    9
    10
    11
    12
    13
    14
    export default defineConfig({
    server: {
    port: 8003,
    },
    css: {
    preprocessorOptions: {
    less: {
    math: 'always',
    javascriptEnabled: true,
    },
    },
    },
    ... // 其他配置
    });

配置项目

配置主应用 app

  • app 项目中安装 qiankun

    1
    2
    # 在根目录下执行
    npm install qiankun -w=app
  • 文件目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ├── src
    ├── layout
    |── index.less
    ├── index.tsx
    ├── microApp.tsx
    ├── pages
    ├── home
    ├── index.tsx
    ├── 404
    ├── index.tsx
    ├── App.tsx
    ├── index.css
    ├── main.tsx
    ├── vite.config.ts
  • 修改 packages/app/src/layout/microApp.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
    import { LoadableApp, loadMicroApp, MicroApp } from 'qiankun';
    import { useEffect, useRef } from 'react';

    interface Props {
    /** 子应用参数 */
    microapp: LoadableApp<any>;
    }

    /**
    * 微应用
    */
    const MicroApp = (props: Props) => {
    const { microapp } = props;

    const mocroInstanceRef = useRef<MicroApp>(undefined);

    useEffect(() => {
    const container = `#${microapp.container}`;
    mocroInstanceRef.current = loadMicroApp({ ...microapp, container });

    return () => {
    mocroInstanceRef.current?.unmount();
    };
    }, [microapp]);

    return (
    <div
    id={microapp?.container || 'subapp'}
    style={{ width: '100%', height: '100%' }}></div>
    );
    };

    export default MicroApp;
  • 修改 packages/app/src/layout/index.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
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    import { Layout, Menu } from 'antd';
    import { MenuItemType } from 'antd/es/menu/interface';
    import { LoadableApp } from 'qiankun';
    import { useEffect, useMemo, useState } from 'react';
    import { Outlet, useLocation, useNavigate } from 'react-router';
    import './index.less';
    import MicroApp from './microApp';

    const { Header, Content, Footer } = Layout;

    type MenuItemProps = {
    /** 菜单路径 */
    path?: string;
    /** 子应用参数 */
    microapp?: LoadableApp<any>;
    } & MenuItemType;

    /** 菜单数据 */
    const menuItems: MenuItemProps[] = [
    {
    key: '1',
    label: 'Home',
    path: '/home',
    },
    {
    key: '2',
    label: 'app1',
    path: '/app1',
    microapp: {
    name: 'app1',
    container: 'app1',
    entry: 'http://localhost:8001',
    },
    },
    {
    key: '3',
    label: 'app2',
    path: '/app2',
    microapp: {
    name: 'app2',
    container: 'app2',
    entry: 'http://localhost:8002',
    },
    },
    {
    key: '4',
    label: 'app3',
    path: '/app3',
    microapp: {
    name: 'app3',
    container: 'app3',
    entry: 'http://localhost:8003',
    },
    },
    ];

    /** 基础布局 */
    const BaseLayout = () => {
    const nativate = useNavigate();
    const location = useLocation();
    const [selectedKeys, setSelectedKeys] = useState<string[]>([]);

    /** 菜单点击事件 */
    const handleMenuClick = (item: any) => {
    setSelectedKeys([item.key]);
    nativate(item.item?.props.path);
    };

    /** 菜单选中项 */
    const selectObj = useMemo(() => {
    return (
    menuItems.find((item) => item.key === selectedKeys[0]) || undefined
    );
    }, [selectedKeys]);

    useEffect(() => {
    const path = location.pathname;
    const selectItem = menuItems.find((item) => item.path === path);
    if (!selectItem?.key) return;
    setSelectedKeys([selectItem?.key + '' || '1']);
    }, [location]);

    return (
    <Layout>
    <Header className="header">
    <div className="logo">Qiankun</div>

    <Menu
    theme="dark"
    mode="horizontal"
    className="menu"
    items={menuItems}
    selectedKeys={selectedKeys}
    onClick={handleMenuClick}
    />
    </Header>

    <Content className="content">
    <div className="outlet">
    {selectObj?.microapp ? (
    <MicroApp microapp={selectObj.microapp} />
    ) : (
    <Outlet />
    )}
    </div>
    </Content>

    <Footer className="footer">
    Demo ©{new Date().getFullYear()} Created Mr.XiaoSi
    </Footer>
    </Layout>
    );
    };

    export default BaseLayout;
  • 修改 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
    import { lazy } from 'react';
    import { createBrowserRouter, Navigate, RouterProvider } from 'react-router';

    /** 路由配置 */
    const routes = createBrowserRouter([
    {
    path: '/',
    element: <Navigate to="/home" />,
    },
    {
    path: '/',
    Component: lazy(() => import('./layout')),
    children: [
    {
    path: '/home',
    Component: lazy(() => import('./pages/home')),
    },
    {
    path: '/app1/*',
    element: <></>,
    },
    {
    path: '/app2/*',
    element: <></>,
    },
    {
    path: '/app3/*',
    element: <></>,
    },
    ],
    },
    {
    path: '*',
    Component: lazy(() => import('./pages/404')),
    },
    ]);

    function App() {
    return <RouterProvider router={routes} />;
    }

    export default App;
  • 修改 packages/app/vite.config.ts 文件内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import react from '@vitejs/plugin-react-swc';
    import { defineConfig } from 'vite';

    // https://vite.dev/config/
    export default defineConfig({
    plugins: [react()],
    server: {
    port: 8000,
    },
    css: {
    preprocessorOptions: {
    less: {
    math: 'always',
    javascriptEnabled: true,
    },
    },
    },
    });

配置子应用 app1

  • app1 项目中安装 qiankunvite-plugin-qiankunpostcss-prefix-selector

    1
    2
    3
    4
    # 在根目录下执行
    npm install qiankun -w=app1
    npm install vite-plugin-qiankun -D -w=app1
    npm install postcss-prefix-selector -D -w=app1
  • 文件目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    ├── src
    ├── layout
    |── index.less
    ├── index.tsx
    ├── pages
    ├── dashboard
    ├── index.tsx
    ├── user
    ├── index.tsx
    ├── App.tsx
    ├── index.css
    ├── main.tsx
    ├── vite.config.ts
  • 修改 packages/app1/src/layout/index.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
    import { Layout, Menu } from 'antd';
    import { Outlet, useNavigate } from 'react-router';
    const { Sider, Content } = Layout;

    import { MenuItemType } from 'antd/es/menu/interface';
    import { useState } from 'react';
    import './index.less';

    /** 菜单数据 */
    const menuItems: (MenuItemType & { path: string })[] = [
    {
    key: 'dashboard',
    label: 'App1 Dashboard',
    path: '/',
    },
    {
    key: 'user',
    label: 'User',
    path: '/user',
    },
    ];

    const BaseLayout = () => {
    const nativate = useNavigate();

    const [activeKey, setActiveKey] = useState<any[]>([menuItems[0].key]);
    /** 菜单点击事件 */
    const handleMenuClick = (item: any) => {
    nativate(item.item?.props?.path);
    setActiveKey([item.key]);
    };

    return (
    <Layout>
    <Sider width={200}>
    <Menu
    mode="inline"
    items={menuItems}
    selectedKeys={activeKey}
    onClick={handleMenuClick}
    />
    </Sider>

    <Content>
    <Outlet />
    </Content>
    </Layout>
    );
    };

    export default BaseLayout;
  • 修改 packages/app1/src/pages/dashboard/index.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
    import { Button, Drawer } from 'antd';
    import { loadMicroApp, MicroApp } from 'qiankun';
    import { useRef, useState } from 'react';

    const Dashboard = () => {
    const microInstanceRef = useRef<MicroApp>(undefined);

    const [open, setOpen] = useState(false);
    /** 点击事件 */
    const handleClick = () => {
    setOpen(true);
    };

    /** 弹窗打开关闭事件回调 */
    const handleAfterOpenChange = (flag) => {
    if (flag) {
    microInstanceRef.current = loadMicroApp({
    name: 'app2',
    container: '#app2',
    entry: 'http://localhost:8002',
    props: { routerType: 'memory' },
    });
    } else {
    setOpen(false);
    microInstanceRef.current?.unmount();
    }
    };

    return (
    <>
    <div>
    App1 Dashboard
    <div className="test">使用样式隔离</div>
    <Button type="primary" onClick={handleClick}>
    打开 app2
    </Button>
    </div>

    <Drawer
    width={600}
    open={open}
    onClose={() => setOpen(false)}
    afterOpenChange={handleAfterOpenChange}>
    <div id="app2" style={{ height: '100%' }}></div>
    </Drawer>
    </>
    );
    };

    export default Dashboard;
  • 修改 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
    import { lazy } from 'react';
    import { createBrowserRouter, RouterProvider } from 'react-router';

    /** 路由配置 */
    const router = createBrowserRouter(
    [
    {
    path: '/',
    Component: lazy(() => import('./layout')),
    children: [
    {
    path: '/',
    Component: lazy(() => import('./pages/dashboard')),
    },
    {
    path: '/user',
    Component: lazy(() => import('./pages/user')),
    },
    ],
    },
    ],
    { basename: '/app1' }
    );

    function App() {
    return <RouterProvider router={router} />;
    }

    export default App;
  • 修改 packages/app1/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
    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);
    app.setAttribute('data-qiankun-app1', 'true');
    createRoot(app).render(<App />);
    };

    /** Qiankun 生命周期钩子 */
    const qiankun = () => {
    renderWithQiankun({
    bootstrap() {
    console.warn('App bootstrap');
    },
    async mount(props: QiankunProps) {
    console.warn('App mount');
    render(props.container);
    },
    update: (props: QiankunProps) => {
    console.warn('App update', props);
    },
    unmount: (props: QiankunProps) => {
    console.warn('App unmount', props);
    },
    });
    };

    // 检查是否在 Qiankun 环境中
    console.log('qiankunWindow', qiankunWindow.__POWERED_BY_QIANKUN__);

    if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun();
    else render();
  • 修改 packages/app1/vite.config.ts 文件内容如下

    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
    import react from '@vitejs/plugin-react-swc';
    import prefixer from 'postcss-prefix-selector';
    import { defineConfig } from 'vite';
    import qiankun from 'vite-plugin-qiankun';

    // https://vitejs.dev/config/
    // https://cloud.tencent.com/developer/article/2138139
    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}`;

    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: {
    preprocessorOptions: {
    less: {
    math: 'always',
    javascriptEnabled: true,
    },
    },
    postcss: {
    plugins: [
    prefixer({
    prefix: `[data-qiankun-${subAppName}]`,
    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;
    },
    }),
    ],
    },
    },
    });
    };

配置子应用 app2

  • app2 项目中安装 vite-plugin-qiankunpostcss-prefix-selector

    1
    2
    3
    # 在根目录下执行
    npm install vite-plugin-qiankun -D -w=app2
    npm install postcss-prefix-selector -D -w=app2
  • 文件目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ├── src
    ├── layout
    |── index.less
    ├── index.tsx
    ├── pages
    ├── dashboard
    |── index.less
    ├── index.tsx
    ├── user
    ├── index.tsx
    ├── App.tsx
    ├── index.css
    ├── main.tsx
    ├── vite.config.ts
  • 修改 packages/app2/src/layout/index.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
    import { Layout, Menu } from 'antd';
    import { Outlet, useNavigate } from 'react-router';
    const { Sider, Content } = Layout;

    import { MenuItemType } from 'antd/es/menu/interface';
    import { useState } from 'react';
    import './index.less';

    /** 菜单数据 */
    const menuItems: (MenuItemType & { path: string })[] = [
    {
    key: 'dashboard',
    label: 'App2 Dashboard',
    path: '/',
    },
    {
    key: 'user',
    label: 'User',
    path: '/user',
    },
    ];

    const BaseLayout = () => {
    const nativate = useNavigate();

    const [activeKey, setActiveKey] = useState<any[]>([menuItems[0].key]);
    /** 菜单点击事件 */
    const handleMenuClick = (item: any) => {
    nativate(item.item?.props?.path);
    setActiveKey([item.key]);
    };

    return (
    <Layout>
    <Sider width={200}>
    <Menu
    mode="inline"
    items={menuItems}
    selectedKeys={activeKey}
    onClick={handleMenuClick}
    />
    </Sider>

    <Content>
    <Outlet />
    </Content>
    </Layout>
    );
    };

    export default BaseLayout;
  • 修改 packages/app2/src/pages/dashboard/index.tsx 文件内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { Button } from 'antd';

    const Dashboard = () => {
    return (
    <div>
    App2 Dashboard
    <div className="test">使用样式隔离</div>
    <Button>测试组件库样式</Button>
    </div>
    );
    };

    export default Dashboard;
  • 修改 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
    43
    44
    45
    46
    47
    48
    49
    import { lazy, useMemo } from 'react';
    import {
    createBrowserRouter,
    createHashRouter,
    createMemoryRouter,
    RouteObject,
    RouterProvider,
    } from 'react-router';

    /** 路由配置 */
    const router: RouteObject[] = [
    {
    path: '/',
    Component: lazy(() => import('./layout')),
    children: [
    {
    path: '/',
    Component: lazy(() => import('./pages/dashboard')),
    },
    {
    path: '/user',
    Component: lazy(() => import('./pages/user')),
    },
    ],
    },
    ];

    function App({ args }: any) {
    const { routerType = 'brower' } = args;

    const mergeRouter = useMemo(() => {
    switch (routerType) {
    case 'hash':
    return createHashRouter(router, { basename: '/app2' });
    case 'memory':
    return createMemoryRouter(router, {
    basename: '/app2',
    initialEntries: ['/app2', '/app2/user'],
    initialIndex: 0,
    });
    default:
    return createBrowserRouter(router, { basename: '/app2' });
    }
    }, [routerType]);

    return <RouterProvider router={mergeRouter} />;
    }

    export default App;
  • 修改 packages/app2/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
    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, args = {}) => {
    const app =
    container || (document.getElementById('root') as HTMLDivElement);
    app.setAttribute('data-qiankun-app2', 'true');
    createRoot(app).render(<App args={args} />);
    };

    /** Qiankun 生命周期钩子 */
    const qiankun = () => {
    renderWithQiankun({
    bootstrap() {
    console.warn('App bootstrap');
    },
    async mount(props: QiankunProps) {
    console.warn('App mount', props);
    render(props.container, props);
    },
    update: (props: QiankunProps) => {
    console.warn('App update', props);
    },
    unmount: (props: QiankunProps) => {
    console.warn('App unmount', props);
    },
    });
    };

    // 检查是否在 Qiankun 环境中
    console.log('qiankunWindow', qiankunWindow.__POWERED_BY_QIANKUN__);

    if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun();
    else render();
  • 修改 packages/app2/vite.config.ts 文件内容如下

    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
    import react from '@vitejs/plugin-react-swc';
    import prefixer from 'postcss-prefix-selector';
    import { defineConfig } from 'vite';
    import qiankun from 'vite-plugin-qiankun';

    // https://vitejs.dev/config/
    // https://cloud.tencent.com/developer/article/2138139
    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}`;

    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: {
    preprocessorOptions: {
    less: {
    math: 'always',
    javascriptEnabled: true,
    },
    },
    postcss: {
    plugins: [
    prefixer({
    prefix: `[data-qiankun-${subAppName}]`,
    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;
    },
    }),
    ],
    },
    },
    });
    };

配置子应用 app3

  • app3 项目中安装 vite-plugin-qiankun

    1
    2
    # 在根目录下执行
    npm install vite-plugin-qiankun -D -w=app3
  • 文件目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ├── src
    ├── layout
    |── index.less
    ├── index.tsx
    ├── pages
    ├── dashboard
    |── index.less
    ├── index.tsx
    ├── user
    ├── index.tsx
    ├── App.tsx
    ├── index.css
    ├── main.tsx
    ├── vite.config.ts
  • 修改 packages/app3/src/layout/index.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
    import { Layout, Menu } from 'antd';
    import { Outlet, useNavigate } from 'react-router';
    const { Sider, Content } = Layout;

    import { MenuItemType } from 'antd/es/menu/interface';
    import { useState } from 'react';
    import './index.less';

    /** 菜单数据 */
    const menuItems: (MenuItemType & { path: string })[] = [
    {
    key: 'dashboard',
    label: 'App3 Dashboard',
    path: '/',
    },
    {
    key: 'user',
    label: 'User',
    path: '/user',
    },
    ];

    const BaseLayout = () => {
    const nativate = useNavigate();

    const [activeKey, setActiveKey] = useState<any[]>([menuItems[0].key]);
    /** 菜单点击事件 */
    const handleMenuClick = (item: any) => {
    nativate(item.item?.props?.path);
    setActiveKey([item.key]);
    };

    return (
    <Layout>
    <Sider width={200}>
    <Menu
    mode="inline"
    items={menuItems}
    selectedKeys={activeKey}
    onClick={handleMenuClick}
    />
    </Sider>

    <Content>
    <Outlet />
    </Content>
    </Layout>
    );
    };

    export default BaseLayout;
  • 修改 packages/app3/src/pages/dashboard/index.tsx 文件内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { Button } from 'antd';

    const Dashboard = () => {
    return (
    <div>
    App3 Dashboard
    <div className="test">使用样式隔离</div>
    <Button>测试组件库样式</Button>
    </div>
    );
    };

    export default Dashboard;
  • 修改 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
    import { ConfigProvider } from 'antd';
    import { lazy } from 'react';
    import { createBrowserRouter, RouterProvider } from 'react-router';

    /** 路由配置 */
    const router = createBrowserRouter(
    [
    {
    path: '/',
    Component: lazy(() => import('./layout')),
    children: [
    {
    path: '/',
    Component: lazy(() => import('./pages/dashboard')),
    },
    {
    path: '/user',
    Component: lazy(() => import('./pages/user')),
    },
    ],
    },
    ],
    { basename: '/app3' }
    );

    function App() {
    return (
    <ConfigProvider prefixCls="app3">
    <RouterProvider router={router} />
    </ConfigProvider>
    );
    }

    export default App;
  • 修改 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) => {
    const app =
    container || (document.getElementById('root') as HTMLDivElement);
    createRoot(app).render(<App />);
    };

    /** Qiankun 生命周期钩子 */
    const qiankun = () => {
    renderWithQiankun({
    bootstrap() {
    console.warn('App bootstrap');
    },
    async mount(props: QiankunProps) {
    console.warn('App mount');
    render(props.container);
    },
    update: (props: QiankunProps) => {
    console.warn('App update', props);
    },
    unmount: (props: QiankunProps) => {
    console.warn('App unmount', props);
    },
    });
    };

    // 检查是否在 Qiankun 环境中
    console.log('qiankunWindow', qiankunWindow.__POWERED_BY_QIANKUN__);

    if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun();
    else render();
  • 修改 packages/app3/vite.config.ts 文件内容如下

    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
    import react from '@vitejs/plugin-react-swc';
    import { defineConfig } from 'vite';
    import qiankun from 'vite-plugin-qiankun';

    // https://vitejs.dev/config/
    // https://cloud.tencent.com/developer/article/2138139
    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}`;

    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: {
    preprocessorOptions: {
    less: {
    math: 'always',
    javascriptEnabled: true,
    },
    },
    },
    });
    };

效果