问题

  • Qiankun + Vite 作为子应用时,样式隔离无效, 即使配置了 experimentalStyleIsolation: true 也无效

  • Ant Design 组件库样式隔离无效

解决方案

配置插件样式隔离

  • 在子应用 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
    import prefixer from 'postcss-prefix-selector';

    export default defineConfig({
    css: {
    postcss: {
    plugins: [
    prefixer({
    prefix: '[data-qiankun-app]', // 这里的值要和 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;
    },
    }),
    ],
    },
    },
    });
  • 在子应用 main.tsx 中为根节点添加 data-qiankun-app 属性

    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
    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);

    /**
    * 添加属性,用于样式隔离
    * 注意:这里的属性名要和 postcss-prefix-selector 插件中的 prefix 保持一致
    */
    app.setAttribute('data-qiankun-app', 'true');

    createRoot(app).render(<App />);
    };

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

    if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun();
    else render();
  • 下面是一个示例,可以在控制台中看到样式被添加了前缀,实现了样式隔离

    1
    2
    3
    4
    5
    6
    7
    8
    import React from 'react';
    import './App.css';

    const App = () => {
    return <div className="app">Hello, Qiankun!</div>;
    };

    export default App;
    1
    2
    3
    4
    5
    6
    7
    8
    .app {
    color: red;
    }

    // 在控制台中被转换成
    [data-qiankun-app] .app {
    color: red;
    }
  • 实现跟 qiankun experimentalStyleIsolation 类似的效果

配置组件库样式隔离

  • 上面样式对自己写的类名生效,但是对于引入的组件库样式隔离可能无效

    1
    2
    3
    4
    5
    6
    import { ConfigProvider } from 'antd';
    import App from './App';

    <ConfigProvider prefixCls="app">
    <App />
    </ConfigProvider>;
    1
    2
    3
    .app-btn {
    color: red;
    }

项目地址

项目实践(可选)

包版本

  • 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
├── 根目录
├── packages # 项目目录, 名称要跟 package.json 中 workspaces 以及 lerna.json 中 packages 一致
├── app # 主应用app
├── src
├── 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 配置文件
├── 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,
    },
    });

启动项目

在根目录下执行 npm install 安装依赖

  • 方式一:进入各个应用目录, 分别执行 npm run dev

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 进入 packages/app 目录
    cd ./packages/app
    # 启动主应用
    npm run dev

    # 进入 packages/app1 目录
    cd ./packages/app1
    # 启动子应用
    npm run dev

    # 进入 packages/app2 目录
    cd ./packages/app2
    # 启动子应用
    npm run dev
  • 方式二:进入根目录, 执行 npm run dev -w=xxx, -w 就是 --workspace, xxx 对应各个应用 package.jsonname, 详见:npm workspace

    1
    2
    3
    4
    5
    6
    7
    8
    # 根目录下启动主应用
    npm run dev -w=app

    # 根目录下启动子应用
    npm run dev -w=app1

    # 根目录下启动子应用
    npm run dev -w=app2

安装依赖项

  • 全局安装 antdreact-router-dom

    1
    2
    # 在根目录下执行
    npm install antd react-router-dom -w=app -w=app1 -w=app2
  • 主应用 app 安装 qiankun

    1
    2
    # 在根目录下执行
    npm install qiankun -w=app
  • 子应用 app1app2 安装 vite-plugin-qiankun

    1
    2
    # 在根目录下执行
    npm install vite-plugin-qiankun -D -w=app1 -w=app2
  • 子应用 app2 安装 postcss-prefix-selector

    1
    2
    # 在根目录下执行
    npm install postcss-prefix-selector -D -w=app2

链接主/子应用

  • 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
    import { createRoot } from 'react-dom/client';
    import './index.css';
    import App from './App.tsx';
    import { registerMicroApps, start } from 'qiankun';

    /** 注册子应用 */
    registerMicroApps(
    [
    {
    name: 'app1', // 子应用名称(全局唯一)
    entry: 'http://localhost:8001', // 这里的端口号要和子应用的端口号一致
    container: '#subapp1', // 子应用挂载点
    activeRule: '/app1', // 这里的路径要和子应用的路由路径一致
    },
    {
    name: 'app2', // 子应用名称(全局唯一)
    entry: 'http://localhost:8002', // 这里的端口号要和子应用的端口号一致
    container: '#subapp2', // 子应用挂载点
    activeRule: '/app2', // 这里的路径要和子应用的路由路径一致
    },
    ],
    {
    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');
    },
    }
    );

    /** 启动子应用 */
    start();

    createRoot(document.getElementById('root')!).render(<App />);
  • packages/app/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
    58
    59
    60
    61
    import {
    createBrowserRouter,
    Outlet,
    RouterProvider,
    useNavigate,
    } from 'react-router-dom';

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

    const handleClick = (name: string) => {
    navigate(`/${name}`);
    };

    return (
    <div className="home">
    <h1>主应用</h1>

    <div className="btn-group">
    <button onClick={() => handleClick('')}>主应用 app</button>
    <button onClick={() => handleClick('app1')}>子应用 app1</button>
    <button onClick={() => handleClick('app2')}>子应用 app2</button>
    </div>

    <div className="children">
    <Outlet />
    </div>
    </div>
    );
    };

    /** 创建路由 */
    const routes = createBrowserRouter(
    [
    {
    path: '/',
    element: <BaseLayout />,
    children: [
    {
    path: '/',
    element: <h2>app</h2>,
    },
    {
    path: 'app1/*', // 通配符 * 表示匹配所有子路由
    element: <div id="subapp1"></div>, // 子应用挂载点 对应 main.tsx 注册子应用的 container
    },
    {
    path: 'app2/*', // 通配符 * 表示匹配所有子路由
    element: <div id="subapp2"></div>, // 子应用挂载点 对应 main.tsx 注册子应用的 container
    },
    ],
    },
    ],
    { basename: '/' }
    );

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

    export default App;
  • packages/app/src/index.css 中配置样式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    .home {
    display: flex;
    align-items: center;
    flex-direction: column;
    }

    .btn-group {
    width: 300px;
    display: flex;
    justify-content: space-around;
    }

    .children {
    width: 100%;
    height: 400px;
    margin-top: 40px;
    border: 2px solid #000;
    }
  • 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 中设置是否以子应用的方式启动

    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
    import { createBrowserRouter, RouterProvider } from 'react-router-dom';

    /** 创建路由 */
    const routes = createBrowserRouter(
    [
    {
    path: '/',
    element: <h2>app1</h2>,
    },
    ],
    { basename: '/app1' } // 设置路由前缀
    );

    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 中设置是否以子应用的方式启动

    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
    import { createBrowserRouter, RouterProvider } from 'react-router-dom';

    /** 创建路由 */
    const routes = createBrowserRouter(
    [
    {
    path: '/',
    element: <h2>app2</h2>,
    },
    ],
    { basename: '/app2' } // 设置路由前缀
    );

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

    export default App;

配置样式隔离

  • packages/app/src/App.tsx 中添加测试 divButton

    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 { Button } from 'antd';
    import {
    createBrowserRouter,
    Outlet,
    RouterProvider,
    useNavigate,
    } from 'react-router-dom';

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

    const handleClick = (name: string) => {
    navigate(`/${name}`);
    };

    return (
    <div className="home">
    <h1>主应用</h1>

    <div className="btn-group">
    <button onClick={() => handleClick('')}>主应用 app</button>
    <button onClick={() => handleClick('app1')}>子应用 app1</button>
    <button onClick={() => handleClick('app2')}>子应用 app2</button>
    </div>

    <div className="children">
    <Outlet />
    </div>
    </div>
    );
    };

    /** 创建路由 */
    const routes = createBrowserRouter(
    [
    {
    path: '/',
    element: <BaseLayout />,
    children: [
    {
    path: '/',
    element: (
    <>
    {/* 测试样式 */}
    <h2>app</h2>
    <div className="test">测试样式隔离</div>
    <Button>测试组件库样式</Button>
    </>
    ),
    },
    {
    path: 'app1/*', // 通配符 * 表示匹配所有子路由
    element: <div id="subapp1"></div>, // 子应用挂载点 对应 main.tsx 注册子应用的 container
    },
    {
    path: 'app2/*', // 通配符 * 表示匹配所有子路由
    element: <div id="subapp2"></div>, // 子应用挂载点 对应 main.tsx 注册子应用的 container
    },
    ],
    },
    ],
    { basename: '/' }
    );

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

    export default App;
  • packages/app/src/index.css 添加测试 样式

    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
    .home {
    display: flex;
    align-items: center;
    flex-direction: column;
    }

    .btn-group {
    width: 300px;
    display: flex;
    justify-content: space-around;
    }

    .children {
    width: 100%;
    height: 400px;
    margin-top: 40px;
    border: 2px solid #000;
    }

    /* 测试样式 */

    .test {
    color: red;
    }

    .ant-btn {
    height: 60px;
    }
  • packages/app1/src/App.tsx 中添加测试 divButton, 并设置 组件库样式前缀

    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
    import { Button, ConfigProvider } from 'antd';
    import { createBrowserRouter, RouterProvider } from 'react-router-dom';

    /** 创建路由 */
    const routes = createBrowserRouter(
    [
    {
    path: '/',
    element: (
    <>
    {/* 测试样式 */}
    <h2>app1</h2>
    <div className="test">测试样式隔离</div>
    <Button>测试组件库样式</Button>
    </>
    ),
    },
    ],
    { basename: '/app1' } // 设置路由前缀
    );

    function App() {
    return (
    // 设置组件库样式前缀
    <ConfigProvider prefixCls="app1">
    <RouterProvider router={routes} />
    </ConfigProvider>
    );
    }

    export default App;
  • packages/app1/src/index.css 添加测试 样式

    1
    2
    3
    4
    5
    6
    7
    .test {
    color: blue;
    }

    .ant-btn {
    height: 60px;
    }
  • packages/app2/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 = 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 }),
    ],
    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/app2/src/main.tsx 中给 根节点 添加 data-qiankun-app2 属性

    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);

    /**
    * 添加属性,用于样式隔离
    * 注意:这里的属性名要和 postcss-prefix-selector 插件中的 prefix 保持一致
    */
    app.setAttribute('data-qiankun-app2', 'true');

    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 中添加测试 divButton

    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
    import { Button } from 'antd';
    import { createBrowserRouter, RouterProvider } from 'react-router-dom';

    /** 创建路由 */
    const routes = createBrowserRouter(
    [
    {
    path: '/',
    element: (
    <>
    {/* 测试样式 */}
    <h2>app2</h2>
    <div className="test">测试样式隔离</div>
    <Button>测试组件库样式</Button>
    </>
    ),
    },
    ],
    { basename: '/app2' } // 设置路由前缀
    );

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

    export default App;
  • packages/app2/src/index.css 添加测试 样式

    1
    2
    3
    4
    5
    6
    7
    .test {
    color: green;
    }

    .ant-btn {
    height: 60px;
    }

效果

  • 主应用 app 和 子应用 app1 样式没有隔离(出现样式混乱的情况), 但是组件库样式隔离生效

  • 主应用 app 和 子应用 app2 样式隔离, 但是组件库样式没有隔离