改造的原因
产品由多个团队进行开发
包含:数据模型板块,数据治理板块,数据看板,持续集成等多个模块。
痛点
- Git 冲突频繁
- 发布互相阻塞(要等某个模块发布完成后,才能继续发布)
- 技术栈无法升级(怕影响别人)
好处
- 各团队独立开发、测试、部署,互不干扰。
- 多产品共享同一门户(统一入口)
- 统一主题,导航,登陆
- 各产品独立部署,按路由进行加载
改造后的问题
一、首屏加载变慢
问题表现:
- 用户打开页面时,白屏时间变长
- Network 面板显示:主应用加载完后,才开始加载子应用资源
原因分析:
- 串行加载 :主应用启动 → 路由匹配 → 动态
fetch子应用 HTML → 解析 JS/CSS → 执行 - 未预加载:子应用资源在用户点击后才开始请求
- 重复依赖:多个子应用各自打包 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 类名冲突,导致样式错乱
原因分析:
- CSS 加载时机晚于 DOM 渲染
- 未启用样式隔离,或隔离后选择器权重不足
- 动态插入
<style>标签被延迟
优化方案:
| 方案 | 说明 |
|---|---|
| 启用 experimentalStyleIsolation | qiankun 自动为子应用 CSS 添加前缀 ts start({ sandbox: { experimentalStyleIsolation: true } }) |
| Critical CSS 内联 | 将首屏关键样式内联到 HTML 中(需构建支持) |
| 统一 CSS 命名规范 | 使用 CSS Modules / BEM,避免全局类名 |
三、资源重复下载(带宽浪费)
问题表现:
- 多个子应用都包含
lodash、moment、antd.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 注入样式的"作用域"内
- qiankun 创建一个 div(带
[data-qiankun="myReactDemo"])并插入到#container - 你的子应用
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 { ... }
```**