微前端qiankun接入的问题

改造的原因

产品由多个团队进行开发

包含:数据模型板块,数据治理板块,数据看板,持续集成等多个模块。

痛点

  • Git 冲突频繁
  • 发布互相阻塞(要等某个模块发布完成后,才能继续发布)
  • 技术栈无法升级(怕影响别人)

好处

  • 各团队独立开发、测试、部署,互不干扰。
  • 多产品共享同一门户(统一入口)
  • 统一主题,导航,登陆
  • 各产品独立部署,按路由进行加载

改造后的问题

一、首屏加载变慢

问题表现:

  • 用户打开页面时,白屏时间变长
  • Network 面板显示:主应用加载完后,才开始加载子应用资源

原因分析:

  1. 串行加载 :主应用启动 → 路由匹配 → 动态 fetch 子应用 HTML → 解析 JS/CSS → 执行
  2. 未预加载:子应用资源在用户点击后才开始请求
  3. 重复依赖:多个子应用各自打包 React、Ant Design 等公共库

优化方案:

方案 说明
预加载(Prefetch) 在主应用空闲时提前加载子应用资源 ts start({ prefetch: true }) // qiankun 内置支持
公共资源 external + CDN 将 React、Vue 等通过 <script>全局引入,子应用不打包 需配合 publicPath和沙箱兼容处理
启用缓存 静态资源加 hash,配置强缓存(Cache-Control: max-age=31536000)
SSR / SSG(高级) 主应用服务端渲染骨架屏,提升感知速度

二、闪屏

问题表现:

  • 页面先显示无样式内容,再突然应用样式(FOUC)
  • 不同子应用的 CSS 类名冲突,导致样式错乱

原因分析:

  1. CSS 加载时机晚于 DOM 渲染
  2. 未启用样式隔离,或隔离后选择器权重不足
  3. 动态插入 <style> 标签被延迟

优化方案:

方案 说明
启用 experimentalStyleIsolation qiankun 自动为子应用 CSS 添加前缀 ts start({ sandbox: { experimentalStyleIsolation: true } })
Critical CSS 内联 将首屏关键样式内联到 HTML 中(需构建支持)
统一 CSS 命名规范 使用 CSS Modules / BEM,避免全局类名

三、资源重复下载(带宽浪费)

问题表现:

  • 多个子应用都包含 lodashmomentantd.css
  • 总包体积膨胀,加载慢

原因分析:

  • 各子应用独立构建,未共享依赖

优化方案:

方案 说明
主应用提供公共依赖 在主应用 index.html中通过 CDN 引入: html <script src="https://cdn.../react@18/umd/react.production.min.js"></script>子应用构建时 external 掉这些依赖
Module Federation(Webpack 5) 更先进的依赖共享方案,但需统一构建工具
自建共享包(Monorepo) 通过 workspace 引用公共组件/工具库

qiankun接入vue2

主应用(以react为例)

修改入口文件(index.js)

  • 自带样式隔离
    • sandbox: {

strictStyleIsolation: true, // 开启样式隔离

},

javascript 复制代码
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { registerMicroApps, start } from 'qiankun';
import App from './App';
import { microApps } from './microApps';
import './index.css';

// 注册微应用
registerMicroApps(microApps, {
  beforeLoad: (app) => {
    console.log('[主应用] before load', app.name);
    return Promise.resolve();
  },
  beforeMount: (app) => {
    console.log('[主应用] before mount', app.name);
    return Promise.resolve();
  },
  afterMount: (app) => {
    console.log('[主应用] after mount', app.name);
    return Promise.resolve();
  },
  beforeUnmount: (app) => {
    console.log('[主应用] before unmount', app.name);
    return Promise.resolve();
  },
  afterUnmount: (app) => {
    console.log('[主应用] after unmount', app.name);
    return Promise.resolve();
  },
});

// 启动 qiankun
start({
  sandbox: {
    strictStyleIsolation: true, // 开启样式隔离
  },
  prefetch: false, // 关闭预加载
});

// 设置默认进入的微应用(可选)
// setDefaultMountApp('/my-admin');

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

增加microApp.js文件

arduino 复制代码
// 微应用配置
export const microApps = [
  {
    name: 'my-admin', // 微应用名称
    entry: '//localhost:9527', // 微应用入口地址(Vue Element Admin 默认端口)
    container: '#subapp-viewport', // 微应用挂载的容器
    activeRule: '/my-admin', // 激活路由
    props: {
      // 传递给微应用的数据
      routerBase: '/my-admin',
    },
  },
  {
    name: 'my-pro-app',
    entry: '//localhost:8000', // my-pro-app 端口
    container: '#subapp-viewport',
    activeRule: '/my-pro-app',
    props: {
      routerBase: '/my-pro-app',
    },
    // 注意:qiankun 会从 HTML 中解析入口文件,确保入口文件导出了生命周期函数
  },
];

增加挂载容器

我目前是使用 antd 的 Layout 和 Menu 进行了切换访问子应用

ini 复制代码
import React, { useState, useEffect } from 'react';
import { Breadcrumb, Layout, Menu, theme } from 'antd';
import { useNavigate, useLocation } from 'react-router-dom';
const { Header, Content, Footer } = Layout;

const App = () => {
  const {
    token: { colorBgContainer, borderRadiusLG },
  } = theme.useToken();
  const navigate = useNavigate();
  const location = useLocation();
  
  // 菜单项配置
  const menuItems = [
    {
      key: '/',
      label: '主应用首页',
    },
    {
      key: '/my-admin',
      label: 'My Admin',
    },
    {
      key: '/my-pro-app',
      label: 'My Pro App',
    },
  ];

  const [selectedKeys, setSelectedKeys] = useState([location.pathname]);

  useEffect(() => {
    setSelectedKeys([location.pathname]);
  }, [location.pathname]);

  const handleMenuClick = ({ key }) => {
    setSelectedKeys([key]);
    navigate(key);
  };

  return (
    <Layout style={{ minHeight: '100vh' }}>
      <Header style={{ display: 'flex', alignItems: 'center', position: 'fixed', top: 0, zIndex: 1003, width: '100%' }}>
        <div className="demo-logo" style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold' }}>
          Qiankun 主应用
        </div>
        <Menu
          theme="dark"
          mode="horizontal"
          items={menuItems}
          selectedKeys={selectedKeys}
          onClick={handleMenuClick}
        />
      </Header>
      <Content style={{ padding: '0 48px' }}>
        <Breadcrumb
          style={{ margin: '16px 0' }}
          items={[
            { title: 'Home' },
            { title: location.pathname === '/' ? '主应用' : '微应用' },
          ]}
        />
        <div
          style={{
            background: colorBgContainer,
            minHeight: 'calc(100vh - 200px)',
            padding: 24,
            borderRadius: borderRadiusLG,
            zIndex: 1002,
          }}
        >
          {/* 主应用首页内容 */}
          {location.pathname === '/' && (
            <div>
              <h1>欢迎使用 Qiankun 微前端主应用</h1>
              <p>请从顶部菜单选择要访问的微应用</p>
            </div>
          )}
          {/* 微应用挂载容器 */}
          <div id="subapp-viewport" style={{ zIndex: 1001, position: 'absolute', top: 80, left: 0, right: 0, bottom: 0 }}></div>
        </div>
      </Content>
      <Footer style={{ textAlign: 'center' }}>
        Qiankun 微前端主应用 ©{new Date().getFullYear()}
      </Footer>
    </Layout>
  );
};
export default App;

修改webpack配置

javascript 复制代码
const path = require('path');

module.exports = {
    webpack: {
      alias: {
        '@': path.resolve(__dirname, 'src'),
      },
      plugins: [
        // 添加自定义 plugin
      ],
      configure: (webpackConfig, { env, paths }) => {
        // 修改 webpackConfig
        // 允许跨域
        webpackConfig.headers = {
          'Access-Control-Allow-Origin': '*',
        };
        return webpackConfig;
      },
    },
    devServer: {
      // 自定义开发服务器
      port: 3001,
      headers: {
        'Access-Control-Allow-Origin': '*',
      },
      historyApiFallback: true,
    },  
  };

子应用

增加micro.js文件

  • 导出qiankun必须的三个函数
    • bootstrap
      • 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap
    • mount
      • 应用每次进入都会调用 mount 方法,通常在这里触发应用的渲染方法
    • unmount
      • 应用每次 切出/卸载 会调用 mount 方法
    • update(可选)
      • 仅在使用 loadMicroApp 方式加载微应用时生效
javascript 复制代码
import Vue from 'vue';
import App from './App'
import store from './store'
import router from './router'
import { setToken } from '@/utils/auth'



// src/micro.js
let instance = null;

function render(props = {}) {
  const { container } = props;
  
  instance = new Vue({
    router,
    store,
    render: h => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app');
}

// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log('[vue-element-admin] bootstrap');
}

export async function mount(props) {
  console.log('[vue-element-admin] mount', props);
  
  // 在微前端环境下自动设置用户信息和权限
  if (window.__POWERED_BY_QIANKUN__) {
    // 设置 token
    const token = 'qiankun-token'
    setToken(token)
    store.commit('user/SET_TOKEN', token)
    
    // 设置用户角色和基本信息
    store.commit('user/SET_ROLES', ['admin'])
    store.commit('user/SET_NAME', '微前端用户')
    store.commit('user/SET_AVATAR', '')
    store.commit('user/SET_INTRODUCTION', '')
    
    // 生成路由
    const accessRoutes = await store.dispatch('permission/generateRoutes', ['admin'])
    router.addRoutes(accessRoutes)
  }
  
  render(props);
}

export async function unmount() {
  console.log('[vue-element-admin] unmount');
  if (instance) {
    instance.$destroy();
    instance = null;
  }
}

修改入口文件 main.js

把 micro 文件内容引入 并且导出,由于 qiankun 需要访问子应用的三个钩子函数,必须要导出

增加public-path.js文件

ini 复制代码
// src/public-path.js
if (window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  }

修改webpack配置

javascript 复制代码
const { name } = require('./package');
module.exports = {
  devServer: {
    headers: {
      // 可跨域
      'Access-Control-Allow-Origin': '*',
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`, // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
    },
  },
};

样式冲突解决

一般 qiankun 自带了样式隔离 strictStyleIsolation: true 开启样式隔离。剩下的还有冲突,需要各自应用单独处理,类名增加前缀。

子应用如果使用了 position 进行了定位处理,脱离文档流,会导致子应用覆盖基座的内容,需要给子应用做处理,子应用的top left 这些需要根据实际情况进行偏移处理

qiankun接入 react + vite 项目

注意:永远不要用 vite 开发服务器作为微前端子应用的 entry

微前端子应用必须是 构建后的静态资源(纯 HTML + JS + CSS)。

注意:vite.config.ts中的base路径必须是相对路径

遇到的问题1

错误信息:

xml 复制代码
error occurs while executing normal script 
<script type="module">
import { injectIntoGlobalHook } from "/@react-refresh";
...
</script>

错误原因:

vite 开发服务器(vite dev)会自动注入 HMR(热更新)脚本,如:

xml 复制代码
<script type="module">
  import { injectIntoGlobalHook } from "/@react-refresh";
  ...
</script>

但 qiankun 在加载子应用的 HTML 的时候,会把整个
**#### 最简单的解决方式
vite.config.ts中增加这个,不执行这个报错依赖

css 复制代码
plugins: [
react({
fastRefresh: false // 禁用 React Refresh
})
],

解决方案1:先构建,再部署静态资源

步骤1:构建 Vite 项目
bash 复制代码
cd your-vite-react-app
npm run build # 生成 dist/ 目录
步骤2:本地启动静态服务器(模拟生产环境)
yaml 复制代码
npx serve -s dist -l 3001

这样 http://localhost:3001 返回的是 纯静态 HTML/JS/CSS,无 HMR 脚本

步骤3:主应用注册构建后的地址
arduino 复制代码
{
name: 'react-vite-app',
entry: '//localhost:3001', // ← 指向静态服务器
container: '#container',
activeRule: '/vite'
}

解决方案2:坚持要在 「开发环境」调试微前端 + Vite 子应用

目前 qiankun 官方不支持直接加载 Vite dev server ,但有 workaround

手动提供一个「纯净入口JS」
步骤1:创建 src/micro-entry.js
javascript 复制代码
export { bootstrap, mount, unmount } from './micro-app';
步骤2:修改 vite.config.ts,增加一个构建目标
php 复制代码
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: 'src/micro-entry.ts', // ← 关键
name: 'ReactViteApp',
formats: ['umd'],
fileName: 'micro-app',
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
},
});
步骤3:添加构建脚本
json 复制代码
// package.json
"scripts": {
"build:micro": "vite build"
}
步骤4:运行构建并启动静态服务
arduino 复制代码
npm run build:micro
npx serve -s dist -l 3001
步骤5:主应用注册 JS 入口
arduino 复制代码
{
name: 'react-vite-app',
entry: '//localhost:3001/micro-app.umd.js', // ← 直接加载 JS
container: '#container',
activeRule: '/vite'
}

遇到的问题2

错误信息

vbnet 复制代码
Failed to load module
script: Expected a JavaScript-or-Wasm
module script but the server responded with a
MIME type of "text/html". Strict MIME type checking
is enforced for module
scripts per HTML spec.

错误原因

子应用react19 + vite5的项目。
由于这样的项目无法正常使用vite的开发服务器进行微应用开发与调试。

解决方案

使用打包,然后通过静态服务器的形式进行调试与开发

arduino 复制代码
pnpm build

// 启动静态服务器
npx serve -s dist -l 3001

遇到的问题3

错误信息

bash 复制代码
application 'my-react-demo' died in status LOADING_SOURCE_CODE: [qiankun]: You need to export lifecycle functions in my-react-demo entry
Error: [qiankun]: You need to export lifecycle functions in my-react-demo entry
at getLifecyclesFromExports (http://localhost:3000/static/js/bundle.js:31610:9)

错误原因

qiankun加载了子应用,但是没有找到生命周期函数。
你使用的是 标准 Vite React 应用构建模式 (非 UMD),而 qiankun 默认期望子应用 导出一个包含生命周期的对象
但在标准 HTML 入口模式下,qiankun 会尝试从全局变量或模块导出中查找生命周期函数。如果你没正确暴露它们,就会报这个错。

解决方案

使用全局window进行暴露生命周期函数

dart 复制代码
const root = createRoot(document.getElementById('root')!)
if(window.__POWERED_BY_QIANKUN__) {
window.myReactDemo = {
bootstrap: async () => {
console.log('react app bootstraped');
},
mount: async (props) => {
render(props);
},
unmount: async () => {
root.unmount();
},
}
}

遇到的问题4

错误信息

加载子应用成功后,子应用样式丢失,控制台 network 显示已经加载 css文件。

错误原因

子应用的DOM挂载位置错误:子应用的 React 渲染容器( #root )不在 qiankun 注入样式的"作用域"内

  1. qiankun 创建一个 div(带 [data-qiankun="myReactDemo"])并插入到 #container
  2. 你的子应用 mount 函数中
ini 复制代码
const domContainer = props.container.querySelector('#root')

→ 但 props.container 是 qiankun 创建的沙箱 div → 它内部 没有 #root 元素!
所以你实际渲染到了 主文档的 #root(如果存在),或者渲染失败。
而 css 规则被重写为

csharp 复制代码
[data-qiankun="myReactDemo"] .some-class { ... }

但你的 DOM 不在 [data-qiankun=...] 内部 → 样式不匹配 → 看起来没样式

解决方案

在子应用的 mount 函数中,重新创建节点,塞一个div#root的节点

ini 复制代码
function async mount(props) {
let container = props.container;

// 如果容器内没有 #root,就创建一个
if (!container.querySelector('#root')) {
const rootEl = document.createElement('div');
rootEl.id = 'root';
container.appendChild(rootEl);
}

const domContainer = container.querySelector('#root')!;
const root = ReactDOM.createRoot(domContainer);
root.render(<App />);
}

这样的DOM结构就被更改为

xml 复制代码
<div id="container">
<div data-qiankun="myReactDemo">
<div id="root"> <!-- 你的 App 在这里 --> </div>
</div>
</div>

而css被重写为

csharp 复制代码
[data-qiankun="myReactDemo"] .header { ... }
```**
相关推荐
CharlieWang1 小时前
AI Elements Vue,帮助你更快的构建 AI 应用程序
前端·人工智能·chatgpt
新晨4371 小时前
JavaScript map() 方法:从工具到编程哲学的升华
前端·javascript
醒了接着睡1 小时前
JS 对象深拷贝
javascript
少卿1 小时前
Webpack 构建流程全解:从源码到产物的“奇幻漂流”
前端·webpack
西瓜树枝1 小时前
前端必读:HTTP 协议核心知识全景图(三)—— 响应头详解
前端·http
码途进化论1 小时前
Vue3 + Vite 系统中 SVG 图标和 Element Plus 图标的整合实战
前端·javascript·vue.js
新晨4371 小时前
JavaScript Array map() 方法详解
前端·javascript
Nayana1 小时前
webWorker 初步体验
前端·javascript
吃饺子不吃馅1 小时前
【开源】create-web-app:多引擎可插拔的前端脚手架
前端·javascript·架构