微前端架构设计与实践
Created on
前言
随着前端应用规模的不断扩大,单体应用的弊端逐渐显现:代码耦合严重、构建时间过长、团队协作困难。微前端作为一种架构模式,将前端应用拆分为多个独立的子应用,每个子应用可以独立开发、部署和运维。
什么是微前端?
核心理念
微前端是一种将多个可独立交付的小型前端应用聚合为一个整体的架构风格。 —— Micro Frontends
特点
- 技术栈无关:子应用可使用不同的框架(React、Vue、Angular)
- 独立开发部署:每个子应用独立的 Git 仓库和 CI/CD
- 增量升级:可以逐步迁移旧应用
- 团队自治:不同团队负责不同的子应用
微前端方案对比
1. iframe
<iframe src="https://sub-app.example.com" />
优点:
- 天然隔离(JS、CSS、全局变量)
- 实现简单
- 支持任意技术栈
缺点:
- 性能较差(每个 iframe 都是独立的浏览上下文)
- 用户体验不佳(URL 不同步、刷新问题、弹窗居中)
- SEO 不友好
2. qiankun
基于 single-spa 封装的微前端框架,阿里开源。
// 主应用
import { registerMicroApps, start } from "qiankun";
registerMicroApps([
{
name: "react-app",
entry: "//localhost:7100",
container: "#subapp-viewport",
activeRule: "/react",
},
{
name: "vue-app",
entry: "//localhost:7101",
container: "#subapp-viewport",
activeRule: "/vue",
},
]);
start();
优点:
- JS 沙箱隔离
- CSS 样式隔离
- 资源预加载
- 通信机制完善
缺点:
- 对 Vite 支持不友好
- 需要改造子应用
- 体积较大
3. Module Federation (Webpack 5)
// 主应用 webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "host",
remotes: {
app1: "app1@http://localhost:3001/remoteEntry.js",
app2: "app2@http://localhost:3002/remoteEntry.js",
},
}),
],
};
// 子应用 webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "app1",
filename: "remoteEntry.js",
exposes: {
"./Button": "./src/components/Button",
},
}),
],
};
优点:
- 运行时加载
- 共享依赖(避免重复加载)
- 双向依赖
- 更细粒度的代码共享
缺点:
- 需要 Webpack 5
- 配置复杂
- 版本管理困难
qiankun 实战
主应用配置
// main-app/src/main.js
import { registerMicroApps, start, setDefaultMountApp } from "qiankun";
// 注册子应用
registerMicroApps(
[
{
name: "vue-app",
entry:
process.env.NODE_ENV === "development"
? "//localhost:8081"
: "//production-domain.com/vue-app",
container: "#subapp-container",
activeRule: "/vue-app",
props: {
// 传递给子应用的数据
data: { user: "admin" },
},
},
{
name: "react-app",
entry: "//localhost:8082",
container: "#subapp-container",
activeRule: "/react-app",
},
],
{
// 生命周期钩子
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);
},
}
);
// 设置默认子应用
setDefaultMountApp("/vue-app");
// 启动 qiankun
start({
prefetch: true, // 预加载
sandbox: {
strictStyleIsolation: true, // 严格样式隔离
},
});
Vue 子应用配置
// vue-app/src/main.js
import Vue from "vue";
import VueRouter from "vue-router";
import App from "./App.vue";
import routes from "./router";
Vue.use(VueRouter);
let router = null;
let instance = null;
function render(props = {}) {
const { container } = props;
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? "/vue-app" : "/",
mode: "history",
routes,
});
instance = new Vue({
router,
render: (h) => h(App),
}).$mount(container ? container.querySelector("#app") : "#app");
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// 导出生命周期函数
export async function bootstrap() {
console.log("[vue] app bootstraped");
}
export async function mount(props) {
console.log("[vue] props from main", props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = "";
instance = null;
router = null;
}
// vue-app/vue.config.js
const { name } = require("./package.json");
module.exports = {
devServer: {
port: 8081,
headers: {
"Access-Control-Allow-Origin": "*", // 允许跨域
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: "umd",
chunkLoadingGlobal: `webpackJsonp_${name}`,
},
},
};
React 子应用配置
// react-app/src/index.js
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
let root = null;
function render(props) {
const { container } = props;
root = ReactDOM.createRoot(
container
? container.querySelector("#root")
: document.querySelector("#root")
);
root.render(
<BrowserRouter
basename={window.__POWERED_BY_QIANKUN__ ? "/react-app" : "/"}
>
<App />
</BrowserRouter>
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log("[react] app bootstraped");
}
export async function mount(props) {
console.log("[react] props from main", props);
render(props);
}
export async function unmount(props) {
root.unmount();
}
样式隔离方案
1. Shadow DOM
start({
sandbox: {
strictStyleIsolation: true,
},
});
优点:最严格的隔离 缺点:弹窗等挂载到 body 的元素样式丢失
2. Scoped CSS
start({
sandbox: {
experimentalStyleIsolation: true,
},
});
给每个子应用的样式添加特殊前缀:
/* 原样式 */
.container {
color: red;
}
/* 转换后 */
div[data-qiankun-react-app] .container {
color: red;
}
3. CSS Modules
import styles from "./App.module.css";
function App() {
return <div className={styles.container}>Content</div>;
}
4. CSS-in-JS
import styled from "styled-components";
const Container = styled.div`
color: red;
font-size: 16px;
`;
通信机制
1. Props 传递
// 主应用
registerMicroApps([
{
name: "vue-app",
entry: "//localhost:8081",
props: {
data: { token: "xxx" },
methods: {
getUserInfo: () => fetch("/api/user"),
},
},
},
]);
// 子应用
export async function mount(props) {
console.log(props.data.token);
const user = await props.methods.getUserInfo();
}
2. 全局状态管理
// 主应用
import { initGlobalState } from "qiankun";
const actions = initGlobalState({
user: null,
token: null,
});
actions.onGlobalStateChange((state, prev) => {
console.log("state changed", state, prev);
});
actions.setGlobalState({ user: { name: "admin" } });
// 子应用
export async function mount(props) {
props.onGlobalStateChange((state, prev) => {
console.log("子应用接收到状态变化", state);
});
props.setGlobalState({ token: "new-token" });
}
3. Event Bus
// shared/eventBus.js
class EventBus {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach((callback) => callback(data));
}
}
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter((cb) => cb !== callback);
}
}
}
export default new EventBus();
公共依赖处理
externals + CDN
// webpack.config.js
module.exports = {
externals: {
react: "React",
"react-dom": "ReactDOM",
vue: "Vue",
},
};
<!-- index.html -->
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>
共享模块(Module Federation)
new ModuleFederationPlugin({
shared: {
react: { singleton: true },
"react-dom": { singleton: true },
},
});
部署策略
独立部署
https://main-app.com/ # 主应用
https://vue-app.com/ # Vue 子应用
https://react-app.com/ # React 子应用
统一部署
https://app.com/ # 主应用
https://app.com/vue-app/ # Vue 子应用
https://app.com/react-app/ # React 子应用
Nginx 配置
server {
listen 80;
server_name app.com;
# 主应用
location / {
root /var/www/main-app;
try_files $uri $uri/ /index.html;
}
# Vue 子应用
location /vue-app {
alias /var/www/vue-app;
try_files $uri $uri/ /vue-app/index.html;
}
# React 子应用
location /react-app {
alias /var/www/react-app;
try_files $uri $uri/ /react-app/index.html;
}
}
性能优化
1. 预加载
import { prefetchApps } from "qiankun";
// 预加载子应用
prefetchApps([{ name: "vue-app", entry: "//localhost:8081" }]);
2. 懒加载
// 只在需要时加载子应用
registerMicroApps([
{
name: "admin-app",
entry: "//localhost:8083",
activeRule: "/admin",
loader: (loading) => {
console.log("loading", loading);
},
},
]);
3. 资源缓存
// webpack.config.js
output: {
filename: '[name].[contenthash:8].js',
}
最佳实践
1. 子应用改造原则
- ✅ 最小化改造(只改 entry 文件)
- ✅ 保持独立运行能力
- ✅ 避免全局变量污染
- ✅ 导出标准生命周期函数
2. 路由管理
// 主应用
const routes = [
{ path: "/vue-app", component: null }, // 由子应用接管
{ path: "/react-app", component: null },
{ path: "/home", component: Home },
];
// 子应用使用 hash 路由或配置 base
const router = new VueRouter({
mode: "history",
base: window.__POWERED_BY_QIANKUN__ ? "/vue-app/" : "/",
routes,
});
3. 全局状态共享
- ✅ 使用 qiankun 的 globalState
- ✅ 使用独立的状态管理库(如 Redux Store)
- ❌ 避免依赖 window 对象传递数据
4. 错误边界
// 主应用添加错误处理
registerMicroApps(apps, {
beforeLoad: async (app) => {
try {
// 预处理
} catch (error) {
console.error("子应用加载失败", error);
// 降级方案
showErrorPage();
}
},
});
常见问题
1. 子应用白屏
原因:跨域问题 解决:配置 CORS
// webpack devServer
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
}
2. 样式冲突
解决方案:
- 使用 CSS Modules
- CSS-in-JS
- BEM 命名规范
- qiankun 样式隔离
3. 路由跳转失效
原因:子应用路由 base 配置错误 解决:
const router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? "/vue-app/" : "/",
routes,
});
总结
微前端不是银弹,适合以下场景:
- 大型应用拆分:应用规模庞大,需要拆分团队
- 遗留系统迁移:老项目需要逐步重构
- 多团队协作:不同团队使用不同技术栈
选择微前端前需要考虑:
- 团队规模和组织结构
- 技术债务和维护成本
- 性能要求
- 开发和运维复杂度
记住:架构的选择永远是权衡的艺术。