Qiankun-Vite 插件

Node 版本问题

  • Node 16 使用 vite-plugin-qiankun 插件启动时会报错 ReferenceError: ReadableStream is not defined,需要更改 cheerio 的版本(注意不要带 ^ 符号),详见:cheerio upgrade problem

    1
    2
    3
    "devDependencies": {
    "cheerio": "1.0.0-rc.12"
    }
  • Node 16 使用 Vite 时,也需要更改版本(其他版本也可自行尝试

    1
    2
    3
    "devDependencies": {
    "vite": "5.4.11"
    }
  • 最后删除 node_modules 文件夹与 package-lock.json 文件,重新安装依赖 (重要!!!)

Qiankun 样式问题

Qiankun 多个子应用共存

Qiankun 应用间跳转

  • 官网示例:微应用之间如何跳转?

  • 主应用Layout 组件中往 window 上挂载 navigate 属性,子应用中通过 window.navigate 实现跳转
    加载子应用时,Layout 组件往往已经加载完成,此时 window.navigate 属性已经存在,可以直接使用,否则需要等待 Layout 组件加载完成后,再使用 window.navigate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 主应用
    const Layout = () => {
    const navigate = useNavigate();

    useEffect(() => {
    window.mainNavigate = navigate;
    }, []);

    return (
    <>
    <Outlet />
    </>
    );
    };
    1
    2
    // 子应用
    window.mainNavigate('/home');

Qiankun 打包优化

  • 官网示例:如何提取出公共的依赖库

  • 通过 CDN 的方式加载 ReactReactDomReactRouter 等依赖库,并使用 externals 配置排除这些依赖,实现多个应用共享一份通用依赖,进而实现打包体积的减小,详见:Qiankun 打包优化

Qiankun 部署问题

  • 官网示例:如何部署

  • 参考 微前端 qiankun 从搭建到部署的实践 中的方式,这样子应用配置只需要写一份,以后新增子应用也不需要改 nginx 配置

    1
    2
    3
    4
    5
    6
    7
    8
    ├── main
    │   └── index.html
    └── subapp
    ├── sub-react
    │   └── index.html
    └── sub-vue
    └── index.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    server {
    listen 80;
    server_name qiankun.fengxianqi.com;
    location / {
    root /data/web/qiankun/main; # 主应用所在的目录
    index index.html;
    try_files $uri $uri/ /index.html;
    }
    location /subapp {
    alias /data/web/qiankun/subapp;
    try_files $uri $uri/ /index.html;
    }
    }

Qiankun 动态路由

  • 在路由数据中添加 routerAttr 属性,参考下面数据格式(主应用页面子应用一级页面子应用二级页面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    const routes = [
    {
    path: '/dashboard',
    component: 'Dashboard',
    meta: {
    title: '主应用App', // 主应用页面
    icon: 'DashboardOutlined',
    },
    },
    {
    path: '/app1',
    component: 'Microapp',
    meta: {
    title: '子应用App1', // 子应用页面
    icon: 'ControlOutlined',
    },
    routerAttr:
    '{"name": "app1", "entry": "http://localhost:8001", "activeRule": "/app1/*", "rootId": "sub-app"}',
    },
    {
    path: '/app2',
    component: 'Outlet',
    meta: {
    title: '子应用App2',
    icon: 'DatabaseOutlined',
    },
    children: [
    {
    path: '/app2/home',
    component: 'Microapp',
    meta: {
    title: '子应用App2-Home', // 子应用二级页面
    icon: 'ControlOutlined',
    },
    routerAttr:
    '{"name": "app2", "entry": "http://localhost:8002", "activeRule": "/app2/*", "rootId": "sub-app"}',
    },
    {
    path: '/app2/test',
    component: 'Microapp',
    meta: {
    title: '子应用App2-Test', // 子应用二级页面
    icon: 'ControlOutlined',
    },
    routerAttr:
    '{"name": "app2", "entry": "http://localhost:8002", "activeRule": "/app2/*", "rootId": "sub-app"}',
    },
    ],
    },
    ];
  • 展示出来的菜单结构如下:

    1
    2
    3
    4
    5
    ├── 主应用App
    ├── 子应用App1
    |── 子应用App2
    │   └── 子应用App2-Home
    │   └── 子应用App2-Test
  • 最后在 App 组件中实现逻辑

    • 主应用页面使用懒加载 lazy(modules[`./pages/${item.component}/index.tsx`])

    • 子应用页面使用 Microapp 组件去加载

    • 二级子应用页面出口使用 Outlet 组件去加载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    // App.tsx

    /** 导入指定文件下的路由模块 */
    const modules = import.meta.glob('./pages/**/*.tsx');

    // 所有路由
    const defaultRoutes: RouteObject[] = [
    { path: '/login', Component: <Login /> },
    { path: '/', element: <Navigate to="/dashboard" replace /> },
    {
    id: 'layout', // 唯一标识
    path: '/',
    Component: <Layout />, // 不要使用懒加载
    children: [],
    errorElement: <>找不到页面</>,
    },
    { path: '*', Component: <NotFound /> },
    ];

    /** 根组件 */
    const App = () => {
    const [router, setRouter] =
    useState<RouterProviderProps['router']>(undefined);

    /** 获取路由数据 */
    const getRoutes = async () => {
    try {
    // 微应用信息
    const microApps = [];

    // 处理路由数据
    const handleRoutes = (routes: any) => {
    return (
    routes?.map((item) => {
    let children = [];

    if (item.children) {
    children = handleRoutes(item.children);
    }

    // 是否是微应用
    if (item.routerAttr) {
    const routerAttr = JSON.parse(item.routerAttr) || {};
    const id = routerAttr.rootId || 'sub-app';

    // 添加微应用信息(防止重复)
    if (micorApps.find((_) => _.name !== routerAttr.name)) {
    microApps.push({ ...routerAttr, container: `#${id}` });
    }

    // Microapp 不能使用懒加载(否则qiankun拿不到节点信息)
    return { ...item, element: <Microapp rootId={id} />, children };
    } else {
    // 路由出口
    if (item.component === 'Outlet') {
    return { ...item, element: <Outlet />, children };
    }

    // 懒加载组件
    const Component =
    lazy(modules[`./pages/${item.component}/index.tsx`] as any) ||
    null;
    return { ...item, Component, children };
    }
    }) || []
    );
    };

    // 返回上面的数据结构
    const data = getRoutesApi() || [];

    // 子路由处理
    const subRoutes = handleRoutes(data);

    // 合并路由
    const allRoutes = defaultRoutes.map((item) => {
    if (item.id === 'layout') return { ...item, children: subRoutes };
    return item;
    });

    // 注册微应用
    registerMicroApps(microApps, {
    beforeLoad: [
    async (app) => {
    console.log(
    '[LifeCycle] before load %c%s',
    'color: green;',
    app.name
    );
    },
    ],
    beforeMount: [
    async (app) => {
    console.log(
    '[LifeCycle] before mount %c%s',
    'color: green;',
    app.name
    );
    },
    ],
    afterUnmount: [
    async (app) => {
    console.log(
    '[LifeCycle] after unmount %c%s',
    'color: green;',
    app.name
    );
    },
    ],
    });

    // 启动 qiankun
    start({ sandbox: { experimentalStyleIsolation: true } });

    // 设置路由
    setRouter(createBrowserRouter(allRoutes, { basename: '/' }));
    } catch (error) {
    console.error('获取路由信息错误, 请配置路由接口:', error);
    }
    };

    useEffect(() => {
    getRoutes();
    }, []);

    if (!router) return <>Loading...</>;

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

    export default App;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // Microapp.tsx
    import { memo } from 'react';

    /** 渲染微应用 */
    const Microapp = ({ rootId }) => {
    return <main id={rootId}></main>;
    };

    export default memo(Microapp);

Qiankun 子应用 loader 状态

  • 动态路由 的基础上实现

  • 方案 1:使用 createContext + useContext

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // store.ts
    import { createContext, useContext, useState } from 'react';

    interface IMicroApp {
    loading: boolean;
    setLoading: (loading: boolean) => void;
    }

    const MicroappState = createContext<MicroappState>({
    loading: false,
    });

    export const MicroappStateProvider = ({ children }: any) => {
    const [loading, setLoading] = useState(false);

    return (
    <MicroappState.Provider value={{ loading, setLoading }}>
    {children}
    </MicroappState.Provider>
    );
    };

    export const useMicroappState = () => useContext(MicroappState);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // App.tsx
    import { useMicroappState, MicroappStateProvider } from './store';

    const App = () => {
    const { setLoading } = useMicroappState();

    useEffect(() => {
    // 微应用信息
    const microApps = [];

    microApps.push({
    name: 'app1',
    entry: '//localhost:3001/',
    container: '#app',
    activeRule: '/app1',
    props: {},
    loader: (loading) => {
    setLoading(loading);
    },
    });
    }, []);

    return <></>;
    };

    const AppProvider = () => {
    return (
    <MicroappStateProvider>
    <App />
    </MicroappStateProvider>
    );
    };

    export default AppProvider;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // Microapp.tsx
    import { memo } from 'react';
    import { useMicroappState } from './store';

    /** 渲染微应用 */
    const Microapp = ({ rootId }) => {
    const { loading } = useMicroappState();

    if (loading) return <div>Loading...</div>;
    return <main id={rootId}></main>;
    };

    export default memo(Microapp);
  • 方案 2:使用 zustand

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // store.ts
    import { create } from 'zustand';

    interface IMicroApp {
    loading: boolean;
    setLoading: (loading: boolean) => void;
    }

    export const useMicroappState = create((set) => ({
    loading: false,
    setLoading: (loading: boolean) => set({ loading }),
    }));
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // App.tsx
    import { useMicroappState } from './store';

    const App = () => {
    useEffect(() => {
    // 微应用信息
    const microApps = [];

    microApps.push({
    name: 'app1',
    entry: '//localhost:3001/',
    container: '#app',
    activeRule: '/app1',
    props: {},
    loader: (loading) => {
    useMicroappState.getInitialState().setLoading(loading);
    },
    });
    }, []);

    return <></>;
    };
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // Microapp.tsx
    import { memo } from 'react';
    import { useMicroappState } from './store';

    /** 渲染微应用 */
    const Microapp = ({ rootId }) => {
    const loading = useMicroappState((state) => state.loading);

    if (loading) return <div>Loading...</div>;
    return <main id={rootId}></main>;
    };

    export default memo(Microapp);

React-Router 版本问题

  • React Router 在新版本中加入了 startTransition 属性,会导致路由在切换一次显示 子应用dom 挂载不上问题

  • 解决方案 1:关闭 startTransition 属性

    1
    2
    3
    4
    // react-router-dom v6 版本
    import { RouterProvider } from "react-router-dom";

    <RouterProvider future={{ v7_startTransition: false }}>
    1
    2
    3
    4
    // react-route v7 版本 (这里爆红也能正常使用)
    import { RouterProvider } from 'react-router/dom';

    <RouterProvider />;
  • 解决方案 2:使用 navigate 时,加上 { flushSync: true } 属性

    1
    2
    3
    4
    5
    // 默认 RouterProvider 中是开启 startTransition 属性
    import { useNavigate } from 'react-router-dom';

    const navigate = useNavigate();
    navigate('/', { flushSync: true }); // 注意子应用挂载dom不要使用懒加载导入