Skip to main content

微前端技术 & 应用接入

一、关于微前端

1. 什么是微前端

微前端(Micro Frontends)是一种将前端应用拆分成多个模块,每个模块可单独开发和部署的技术。

适用场景

  • 多团队协作的大型项目
  • 旧项目渐进式重构
  • 多产品线需要统一入口
  • 核心业务模块需独立迭代

2. 微前端核心架构原则

2.1 独立开发与部署

  • 技术栈自由:允许 React/Vue/Angular 混用;
  • 独立仓库:每个子应用拥有独立代码库与 CI/CD 流程;

2.2 应用隔离

  • CSS 隔离:Shadow DOM / 命名空间等方案;
  • JS 隔离:沙箱机制防止全局污染;
  • 数据隔离:独立的状态管理;

2.3 动态加载与运行时集成

主应用通过动态加载(按需加载)子应用资源(HTML/JS/CSS),并在运行时组合。

3. 微前端的实现方案

方案优点缺点
路由分发式简单易用切换白屏/环境一致性差
iframe 方案天然隔离通信复杂/性能差
Web Components原生支持生态不完善
通过主流框架实现开箱即用/沙箱完善学习成本中等

3.1 路由分发式

通过 Nginx 反向代理路由到不同子应用

# Nginx 配置示例
location /app1 { proxy_pass http://app1-domain.com; }
location /app2 { proxy_pass http://app2-domain.com; }

3.2 iframe 方案

<iframe src="//child-app.com" class="micro-app"></iframe>

优点:天然隔离 缺点:通信复杂、性能差、SEO不友好

3.3 Web Components

浏览器原生组件化方案

class MyElement extends HTMLElement {
connectedCallback() {
this.innerHTML = `<h1>微前端组件</h1>`;
}
}
customElements.define('micro-app', MyElement);

3.4 通过主流框架实现

框架qiankunmicro-appwujie-micro
发布时间2019-08-012021-07-092022-07-05
原理基于 single-spa类 WebComponentWebComponent + iframe
数据通信机制propsaddDataListenerprops、window、eventBus
IE 兼容✅ 自动切换成 iframe
JS 沙箱✅ 通过 iframe 实现 JS 沙箱
样式隔离✅ 通过 webcomponent 实现页面样式元素隔离
元素隔离
静态资源地址补全
预加载
keep-alive
应用共享同一个资源
应用嵌套
插件系统
子应用不改造接入✅ 满足跨域可以不改
内置降级兼容处理✅ 通过 babel 来添加 polyfill
接入成本
社区成熟及活跃度★★★★★

选型建议

  1. 考虑系统需要兼容 ie 浏览器场景
    wujie > qiankun
  2. 接入便捷度考虑
    wujie > micro-app > qiankun
  3. 框架稳定性 (框架成熟度)
    qiankun > micro-app > wujie

二、基于 qiankun 的微前端搭建

qiankun 是一个基于 single-spa 的微前端实现库,只需调用几个 qiankun 的 API 即可完成应用的微前端改造,同时由于 qiankun 的 HTML entry 及沙箱的设计,使得微应用的接入像使用 iframe 一样简单。

下面以此目录结构为例,接入 qiankun:

main-app/
├── packages/
│ └── apps/
│ ├── app1-react/
│ └── app2-vue/
├── src/
│ ├── index.js
...
└── ...

1. 主应用接入

Create React App 生成的主应用为例:

安装依赖

cd main-app
yarn add qiankun

App.js 中添加子应用挂载容器:

main-app/src/App.js
import "./App.css";

function App() {
return (
<div className="App">
<h1>主应用</h1>
{/* 子应用挂载点 */}
<div id="subapp-container"></div>
</div>
);
}

export default App;

在入口文件注册子应用:

main-app/src/index.js
import React from "react";
import ReactDOM from "react-dom/client";
import { registerMicroApps, start } from "qiankun";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";

const root = ReactDOM.createRoot(document.getElementById("root"));
// 主应用渲染函数
function renderMainApp() {
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
}

// 注册微应用配置
const microApps = [
{
name: "app1-react", // 子应用名称(需与子应用 package.json 中的 name 一致)
entry: "//localhost:7101", // 子应用入口(开发环境地址需与子应用服务端口一致)
container: "#subapp-container", // 子应用挂载容器(需要存在于主应用 DOM 中)
activeRule: "/app1-react", // 子应用路由激活规则
},
{
name: "app2-vue",
entry: "//localhost:7102",
container: "#subapp-container",
activeRule: "/app2-vue",
},
];

// 启动配置
function initQiankun() {
// 注册微应用
registerMicroApps(microApps, {
beforeLoad: [(app) => console.log("Before load:", app.name)],
beforeMount: [(app) => console.log("Before mount:", app.name)],
afterMount: [(app) => console.log("After mount:", app.name)],
beforeUnmount: [(app) => console.log("Before unmount:", app.name)],
afterUnmount: [(app) => console.log("After unmount:", app.name)],
});

// 启动 qiankun
start({
prefetch: "all", // 预加载所有子应用
sandbox: { experimentalStyleIsolation: true }, // 开启样式隔离
});
}

// 初始化流程
function init() {
renderMainApp(); // 先渲染主应用
initQiankun(); // 再初始化 Qiankun
}

// 启动应用
init();

2. 子应用接入

2.1 React 子应用接入

Create React App 生成的子应用为例:

2.1.1 在配置文件适配 qiankun

如果是 webpack 搭建或已经 eject 的 React 项目,则在子应用的 webpack.config.js 中添加以下配置:

app1-react/webpack.config.js
module.exports = {
output: {
library: 'app1-react', // 必须与主应用注册的 name 一致
libraryTarget: 'umd', // 必须为 UMD 格式
publicPath: process.env.NODE_ENV === 'development'
? '//localhost:7101/' // 开发环境地址(与主应用 entry 一致)
: '/app1-react/', // 生产环境地址
},
devServer: {
port: 7101, // 端口必须与主应用 entry 一致
headers: {
'Access-Control-Allow-Origin': '*' // 允许主应用跨域加载
}
}
};

如果子应用是基于 create-react-app(CRA)生成但未暴露 Webpack 配置(即没有 eject 出 webpack.config.js),可用以下方式配置:

1、安装依赖

cd app1-react
yarn add react-app-rewired cross-env -D

2、创建配置文件

在子应用根目录创建 config-overrides.js:

app1-react/config-overrides.js
module.exports = {
webpack: (config) => {
// 配置 UMD 格式输出
config.output.library = "app1-react";
config.output.libraryTarget = "umd";
config.output.publicPath =
process.env.NODE_ENV === "development"
? "//localhost:7101/" // 开发环境地址(与主应用 entry 一致)
: "/app1-react/"; // 生产环境地址

// 允许主应用跨域加载
config.devServer = {
...config.devServer,
port: 7101, // 与主应用 entry 端口一致
headers: {
"Access-Control-Allow-Origin": "*",
},
};

return config;
},
};

3、修改启动命令

在 package.json 替换原有 scripts 字段:

"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test"
}

4、设置环境变量配置端口

在子应用根目录创建 .env:

app1-react/.env
PORT=7101
2.1.2 改造子应用入口文件

子应用除了在配置文件适配 qiankun 外,还需要改造入口文件,导出 Qiankun 生命周期钩子函数:

app1-react/src/index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";

let root = null;

// 独立运行逻辑(开发环境直接渲染)
// 通过 window.__POWERED_BY_QIANKUN__ 识别是否被 Qiankun 加载
if (process.env.NODE_ENV === "development" && !window.__POWERED_BY_QIANKUN__) {
const container = document.getElementById("root");
root = ReactDOM.createRoot(container);
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
}

// 导出 Qiankun 生命周期
export async function bootstrap() {
console.log("[React] app1 bootstraped");
}

// mount() 动态挂载
// 使用主应用传入的 container 挂载子应用,避免 DOM 冲突
export async function mount(props) {
console.log("[React] mount app1", props);
const { container } = props;

// 直接使用主应用传递的 container
const dom = container ? container : document.getElementById("root");

root = ReactDOM.createRoot(dom);
root.render(
<BrowserRouter
basename={window.__POWERED_BY_QIANKUN__ ? "/app1-react" : "/"}
>
<App />
</BrowserRouter>
);
}

// unmount() 清理资源
// 确保子应用卸载时释放内存,防止内存泄漏
export async function unmount() {
console.log("[React] Unmount");
root?.unmount();
root = null;
}

// 性能监控(可选)
reportWebVitals();

子应用如果用到 react-router,需要跟上面一样设置 basename 去匹配主应用路由,否则会导致路由混乱。

2.2 Vue 子应用接入

2.2.1 在配置文件适配 qiankun

以基于 Vite 的 Vue 子应用 app2-vue 为例:

1、安装依赖

cd app2-vue
yarn add vite-plugin-qiankun

2、修改配置文件

app2-vue/vite.config.ts
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'

import qiankun from 'vite-plugin-qiankun'

// https://vite.dev/config/
export default defineConfig({
base: '/app2-vue/', // 需与主应用注册的子应用路径匹配
plugins: [
vue(),
vueJsx(),
qiankun('app2-vue', {
// 子应用名称(需与主应用注册名称一致)
useDevMode: true, // 开发模式启用热更新
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 7102, // 指定端口(需与主应用注册的端口一致)
cors: true, // 必须开启跨域
headers: {
'Access-Control-Allow-Origin': '*', // 允许主应用跨域加载
},
},
build: {
// 打包成 UMD 格式以适配 Qiankun
lib: {
entry: 'src/main.ts',
name: 'app2-vue',
fileName: (format) => `app2-vue.${format}.js`,
},
},
})
2.2.2 改造子应用入口文件

子应用除了在配置文件适配 qiankun 外,还需要改造入口文件,导出 Qiankun 生命周期钩子函数:

app2-vue/src/main.ts
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import type { QiankunProps } from 'vite-plugin-qiankun/dist/helper'

import App from './App.vue'
import router from './router'

let app: ReturnType<typeof createApp>

declare global {
interface Window {
__POWERED_BY_QIANKUN__?: boolean
__INJECTED_PUBLIC_PATH_BY_QIANKUN__?: string
}
}

// 判断运行环境,如果是非 qiankun 环境,则直接挂载
if (!window.__POWERED_BY_QIANKUN__) {
// 动态同步 qiankun 注入的路径到 Vite
import.meta.env.BASE_URL = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ || '/'

app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
}

// 作为子应用时导出生命周期
export const bootstrap = async () => {
console.log('app2-vue bootstraped')
}

export const mount = async (props: QiankunProps) => {
console.log('app2-vue mount', props)
app = createApp(App)
app.use(createPinia())
app.use(router)
// 注意这里的容器 ID 需要与主应用注册时设置的 container 匹配
app.mount(props.container?.querySelector('#subapp-container') || '#subapp-container')
}

export const unmount = async () => {
app?.unmount()
}

3. 项目启用

1、启动子应用

cd main-app/packages/apps/app1-react
yarn start

预期输出:Project is running at http://localhost:7101

2、访问子应用独立页面

打开 http://localhost:7101,确认子应用正常显示。

3、启动主应用

cd main-app
yarn start

4、访问主应用并加载子应用

打开 http://localhost:3000/app1-react,子应用应正确挂载到主应用的 #subapp-container 容器中。

app2-vue 同理。

代码详情见 GitHub

4. 常见踩坑与解决方案

4.1 Target container is not a DOM element

运行 http://localhost:3000/app1-react,出现以下错误:

原因

容器选择逻辑错误,在子应用 app1-react/src/index.js 中的 mount 方法里,主应用传递的 container 本身就是目标 DOM 元素,不需要通过 container.querySelector("#subapp-container") 二次查询子容器。

解决

修改子应用的 mount 生命周期函数,直接使用主应用传递的 container,并确保容器存在:

app1-react/src/index.js
// ...

// mount() 动态挂载
// 使用主应用传入的 container 挂载子应用,避免 DOM 冲突
export async function mount(props) {
console.log("[React] mount app1", props);
const { container } = props;
const dom = container
? container.querySelector("#subapp-container")
: document.getElementById("subapp-container");

// 直接使用主应用传递的 container
const dom = container ? container : document.getElementById("root");

root = ReactDOM.createRoot(dom);
root.render(
<BrowserRouter
basename={window.__POWERED_BY_QIANKUN__ ? "/app1-react" : "/"}
>
<App />
</BrowserRouter>
);
}
// ...

4.2 Cannot use import statement outside a module

运行 http://localhost:3000/app2-vue,出现以下错误:

原因

日志的 virtual:vue-devtools-path:overlay.js 模块加载异常,该插件在开发模式下会注入虚拟模块,与 Qiankun 的脚本加载机制冲突。

解决

移除 vite-plugin-vue-devtools

app2-vue/vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import qiankun from 'vite-plugin-qiankun'

export default defineConfig({
base: '/app2-vue/',
plugins: [
vue(),
vueJsx(),
vueDevTools(),
qiankun('app2-vue', {
useDevMode: true,
}),
],
// ...其余配置保持不变
})

4.3 Failed to fetch

启动主应用和其中一个子应用 app1-react 并运行 http://localhost:3000/app1-react,出现以下错误:

原因

qiankun 的预加载机制尝试加载未启动的子应用导致该报错。

解决

可以在主应用 src/index.js 中 qiankun 的 start 配置中设置 prefetch: false,避免主应用初始化时预加载所有子应用资源:

main-app/src/index.js
// ...

// 启动配置
function initQiankun() {
// 注册微应用
registerMicroApps(microApps, {
beforeLoad: [(app) => console.log("Before load:", app.name)],
beforeMount: [(app) => console.log("Before mount:", app.name)],
afterMount: [(app) => console.log("After mount:", app.name)],
beforeUnmount: [(app) => console.log("Before unmount:", app.name)],
afterUnmount: [(app) => console.log("After unmount:", app.name)],
});

// 启动 qiankun
start({
prefetch: "all", // 预加载所有子应用
prefetch: false, // 关闭预加载所有子应用
sandbox: { experimentalStyleIsolation: true }, // 开启样式隔离
});
}

// 初始化流程
function init() {
renderMainApp(); // 先渲染主应用
initQiankun(); // 再初始化 Qiankun
}

// 启动应用
init();