文章参考下面两个 issues提取公共依赖 docqiankun 如何复用公共依赖

思路

  • 方式一:通过 CDN 引入公共依赖,并在主子应用中排除这些公共依赖。(如果 CDN网站 挂了,网站也就挂了,有风险

  • 方式二:将 CDN 资源下载到 public 文件夹下,并在主子应用中排除这些公共依赖。防止每个应用都在 public 中引入依赖,这里将 public 文件指定为 libs,统一存放公共依赖。

项目地址

创建项目

初始化项目

  • 项目目录结构

    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
    |── 项目根目录
    │ ├── libs # 存放公共依赖(使用cdn方式不需要这个文件夹)
    │ │ ├── react@18.3.1
    │ │ │ ├── react.development.js
    │ │ │ ├── react.production.min.js
    │ │ ├── react-dom@18.3.1
    │ │ │ ├── react-dom.development.js
    │ │ │ ├── react-dom.production.min.js
    │ ├── packages # 包目录
    │ │ ├── app # 主应用 (应用根目录)
    │ | | ├── src
    │ | | | ├── App.tsx
    │ | | | ├── main.tsx
    │ | | ├── .env
    │ | | ├── .env.production
    │ | | ├── index.html
    │ | | ├── vite.config.ts
    │ │ ├── app1 # 子应用 (应用根目录)
    │ | | ├── src
    │ | | | ├── App.tsx
    │ | | | ├── main.tsx
    │ | | ├── .env
    │ | | ├── .env.production
    │ | | ├── index.html
    │ | | ├── vite.config.ts
    │ ├── package.json # 全局 package.json
    │ ├── pnpm-workspace.yaml # pnpm workspace 配置文件
  • 使用 npm create vite 命令在 packages 目录下创建两个应用

安装依赖

1
2
3
4
5
6
7
8
9
10
11
# 在项目根目录执行

# 主子应用安装 react-router、@types/node
pnpm -F app -F app1 install react-router
pnpm -F app -F app1 install @types/node -D

# 主应用安装 qiankun
pnpm -F app install qiankun

# 子应用安装 vite-plugin-qiankun-lite
pnpm -F app1 install vite-plugin-qiankun-lite

配置主应用 app

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

    const container = document.getElementById('root')!;
    const root = createRoot(container);

    const render = (props: any) => root.render(<App {...props} />);

    render({ loading: false });
    const loader = (loading: boolean) => render({ loading });

    /** 注册子应用 */
    registerMicroApps(
    [
    {
    name: 'app1',
    entry: 'http://localhost:8001',
    container: '#sub-app',
    activeRule: '/app1',
    props: {
    appName: 'app1',
    },
    loader,
    },
    ],
    {
    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
    );
    },
    ],
    }
    );

    start();
  • 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
    import {
    createBrowserRouter,
    Link,
    Outlet,
    RouterProvider,
    } from 'react-router';

    function App({ loading }: any) {
    const router = createBrowserRouter(
    [
    {
    path: '/',
    element: (
    <>
    <div style={{ fontSize: 24 }}>主应用app</div>

    <Link to={'/'}>回首页</Link>

    <Link to={'/app1'} style={{ margin: '0 20px' }}>
    子应用app
    </Link>

    <Link to={'/test'}>子模块test</Link>

    {loading && <div>loading...</div>}
    <Outlet />
    <main id="sub-app"></main>
    </>
    ),
    children: [
    {
    path: 'app1/*',
    element: <></>,
    },
    {
    path: 'test',
    element: <div>test</div>,
    },
    ],
    },
    ],
    {
    basename: '/',
    }
    );

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

    export default App;
  • packages/app/vite.config.ts 中读取 .env 文件并设置端口号

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

    // https://vite.dev/config/
    export default ({ mode }) => {
    // 环境变量文件夹
    const envDir = resolve(__dirname, './');
    // 加载环境变量
    const env = loadEnv(mode, envDir);

    return defineConfig({
    server: {
    port: Number(env.VITE_PORT),
    },
    preview: {
    port: Number(env.VITE_PORT),
    },
    plugins: [react()],
    });
    };
  • packages/app/.env 中配置环境变量

    1
    VITE_PORT=8000

配置子应用 app1

  • packages/app1/src/main.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
    import { createRoot, type Root } from 'react-dom/client';
    import { name } from '../package.json';
    import App from './App.tsx';
    import './index.css';

    let root: Root;
    const render = (props: any = {}) => {
    const container = props?.container
    ? props.container.querySelector('#root')
    : document.getElementById('root');

    root = createRoot(container);

    root.render(<App appName={props.appName} />);
    };

    if (!window.__POWERED_BY_QIANKUN__) {
    render();
    }

    export async function bootstrap() {
    console.log(`${name} bootstrap`);
    }

    // biome-ignore lint/suspicious/noExplicitAny: <explanation>
    export async function mount(props: any) {
    console.log(`${name} mount`, props);
    render(props);
    }

    // biome-ignore lint/suspicious/noExplicitAny: <explanation>
    export async function unmount(props: any) {
    console.log(`${name} unmount`, props);
    root.unmount();
    }

    // biome-ignore lint/suspicious/noExplicitAny: <explanation>
    export async function update(props: any) {
    console.log(`${name} update`, props);
    }
  • packages/app1/src/App.tsx 中配置路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import { createBrowserRouter, RouterProvider } from 'react-router';

    function App({ appName = '' }) {
    const router = createBrowserRouter(
    [
    {
    path: '/',
    element: <>子应用app</>,
    },
    ],
    {
    basename: `/${appName}`,
    }
    );

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

    export default App;
  • packages/app1/vite.config.ts 中配置 qiankun 插件并设置端口号

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import react from '@vitejs/plugin-react';
    import { resolve } from 'path';
    import { defineConfig, loadEnv } from 'vite';
    import qiankun from 'vite-plugin-qiankun-lite';
    import { name } from './package.json';

    // https://vite.dev/config/
    export default ({ mode }) => {
    // 环境变量文件夹
    const envDir = resolve(__dirname, './');
    // 加载环境变量
    const env = loadEnv(mode, envDir);

    return defineConfig({
    server: {
    port: Number(env.VITE_PORT),
    },
    preview: {
    port: Number(env.VITE_PORT),
    },
    plugins: [react(), qiankun({ name: name, sandbox: true })],
    });
    };
  • packages/app1/.env 中配置环境变量

    1
    VITE_PORT=8001

未优化启动

打包项目

1
2
# 在项目根目录执行
pnpm -F app -F app1 run build

项目启动

1
2
# 在项目根目录执行
pnpm -F app -F app1 run preview

只有在第一次加载子应用的时候,才会请求所有资源,后续加载会走缓存。如果想看子应用不走缓存的资源大小,可以 禁用缓存

优化配置

CDN 网站

安装依赖

  • 安装 vite-plugin-externals

    1
    2
    # 在项目根目录执行
    pnpm -F app -F app1 i vite-plugin-externals -D
  • 使用 vite-plugin-externals 之后,就不用再配置 rollup.external 了,而且 开发环境 也会自动排除依赖

配置主应用 app

  • 修改 packages/app/vite.config.ts 文件如下,添加 vite-plugin-externals 插件。

    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
    import react from '@vitejs/plugin-react';
    import { resolve } from 'path';
    import { defineConfig, loadEnv } from 'vite';
    import { viteExternalsPlugin } from 'vite-plugin-externals';

    // https://vite.dev/config/
    export default ({ mode }) => {
    // 环境变量文件夹
    const envDir = resolve(__dirname, './');
    // 静态资源服务的文件夹
    const publicDir = resolve(__dirname, '../../libs');
    // 加载环境变量
    const env = loadEnv(mode, envDir);

    return defineConfig({
    publicDir: publicDir, // 指定依赖包位置
    server: {
    port: Number(env.VITE_PORT),
    },
    preview: {
    port: Number(env.VITE_PORT),
    },
    plugins: [
    react(),
    /**
    * 排除 react react-dom, 使用 cdn/本地文件 加载
    * - https://github.com/umijs/qiankun/issues/581
    * - https://github.com/umijs/qiankun/issues/627
    */
    viteExternalsPlugin({
    react: 'React',
    'react-dom': 'ReactDOM',
    'react-dom/client': 'ReactDOM',
    }),
    ],
    });
    };
  • packages/app/.env 中设置 开发环境CDN链接 或者 本地文件

    1
    2
    3
    4
    5
    VITE_PORT=8000
    # VITE_REACT_FILE_PATH=https://unpkg.com/react@18.3.1/umd/react.development.js
    # VITE_REACT_DOM_FILE_PATH=https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js
    VITE_REACT_FILE_PATH=/react@18.3.1/react.development.js
    VITE_REACT_DOM_FILE_PATH=/react-dom@18.3.1/react-dom.development.js

配置子应用 app1

  • 修改 packages/app1/vite.config.ts 文件如下,添加 vite-plugin-externals 插件。
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 react from '@vitejs/plugin-react';
import { resolve } from 'path';
import { defineConfig, loadEnv } from 'vite';
import { viteExternalsPlugin } from 'vite-plugin-externals';
import qiankun from 'vite-plugin-qiankun-lite';
import { name } from './package.json';

// https://vite.dev/config/
export default ({ mode }) => {
// 环境变量文件夹
const envDir = resolve(__dirname, './');
// 静态资源服务的文件夹
const publicDir = resolve(__dirname, '../../libs');
// 加载环境变量
const env = loadEnv(mode, envDir);

return defineConfig({
publicDir: publicDir, // 指定依赖包位置
server: {
port: Number(env.VITE_PORT),
},
preview: {
port: Number(env.VITE_PORT),
},
plugins: [
react(),
qiankun({ name: name, sandbox: true }),
/**
* 排除 react react-dom, 使用 cdn/本地文件 加载
* - https://github.com/umijs/qiankun/issues/581
* - https://github.com/umijs/qiankun/issues/627
*/
viteExternalsPlugin({
react: 'React',
'react-dom': 'ReactDOM',
'react-dom/client': 'ReactDOM',
}),
],
});
};
  • packages/app1/.env 中设置 开发环境CDN链接 或者 本地文件

    1
    2
    3
    4
    5
    VITE_PORT=8001
    # VITE_REACT_FILE_PATH=https://unpkg.com/react@18.3.1/umd/react.development.js
    # VITE_REACT_DOM_FILE_PATH=https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js
    VITE_REACT_FILE_PATH=/react@18.3.1/react.development.js
    VITE_REACT_DOM_FILE_PATH=/react-dom@18.3.1/react-dom.development.js

配置所有应用

  • packages/app/.env.productionpackages/app1/.env.production 中配置 生产环境CDN链接 或者 本地文件

    1
    2
    3
    4
    # VITE_REACT_FILE_PATH=https://unpkg.com/react@18.3.1/umd/react.production.min.js
    # VITE_REACT_DOM_FILE_PATH=https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js
    VITE_REACT_FILE_PATH=/react@18.3.1/react.production.min.js
    VITE_REACT_DOM_FILE_PATH=/react-dom@18.3.1/react-dom.production.min.js
  • 最后在 packages/app/index.htmlpackages/app1/index.html 中引入环境变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>

    <!-- 注意这里的 ignore 关键字 -->
    <script ignore src="%VITE_REACT_FILE_PATH%"></script>
    <script ignore src="%VITE_REACT_DOM_FILE_PATH%"></script>
    </head>
    <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
    </body>
    </html>

优化启动(引入 CDN 链接)

打包项目

1
2
# 在项目根目录执行
pnpm -F app -F app1 run build

打包的文件中会引入 libs 中的文件,CICD移除 或者 不拷贝 即可

  • CDN 方式打包

  • 本地文件 方式打包

项目启动

1
2
# 在项目根目录执行
pnpm -F app -F app1 run preview

只有在第一次加载子应用的时候,才会请求所有资源,后续加载会走缓存。如果想看子应用不走缓存的资源大小,可以 禁用缓存

总资源加载大小和不优化时大小是差不多的

  • CDN 方式启动

  • 本地文件 方式启动

ignore 关键字

使用

不使用