微前端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 { ... }
```**
相关推荐
吃杠碰小鸡9 小时前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone9 小时前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_090110 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农10 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king10 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳10 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵11 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星11 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_11 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝11 小时前
RBAC前端架构-01:项目初始化
前端·架构