引言
随着前端项目规模不断膨胀,代码库动辄数十万行,团队协作变得困难,构建部署耗时也越来越长。传统的单体应用(Monolith)已经难以应对多团队并行开发、技术栈交叉并存的复杂场景。微前端(Micro Frontends)借鉴了后端微服务的思想,将前端拆分为更小、更自治的"微应用",由不同团队独立开发、测试和部署,最后在浏览器端通过一个容器应用将它们无缝集成。
本文将先梳理微前端的核心概念,随后以一个基于 qiankun 的完整示例,展示如何从零搭建可运行的微前端架构,并分享生产环境中必须解决的问题。所有代码均可直接复制运行,帮助你快速上手。
一、核心概念
1. 什么是微前端
微前端是一种架构风格,将前端应用分解为多个可独立交付的垂直切片。每个微应用可以使用不同的技术栈(React、Vue、Angular 甚至原生 JS),拥有独立的仓库、构建流程和部署周期。主应用(基座)负责生命周期调度、路由分发和全局状态管理。
2. 实现方式对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| iframe | 完美隔离,简单易用 | 通信繁琐,性能差,URL 不同步,样式难以统一 |
| Web Components | 标准原生,跨技术栈 | 浏览器兼容性、状态管理困难,生态不完善 |
| Module Federation (Webpack 5) | 运行时动态加载,去中心化 | 配置复杂,对非 Webpack 技术栈不友好 |
| single-spa / qiankun | 成熟稳定,开箱即用,社区活跃 | 微应用需暴露生命周期钩子,改造量适中 |
qiankun 基于 single-spa,封装了更为友好的 API,内置样式隔离(Shadow DOM / Scoped CSS)和 JS 沙箱(快照 / Proxy),是目前业界采用最广的微前端框架,下文将围绕它展开实战。
二、实战:用 qiankun 搭建 React + React 微前端
我们实现一个主应用(基座)和一个子应用(用户 management 模块)。两者均使用 Create React App 创建,以便演示原生环境下的集成。
完整项目结构:
micro-frontend-demo/ ├── main-app/ # 主应用(基座) └── sub-app/ # 子应用
2.1 创建主应用 main-app
bash
npx create-react-app main-app
cd main-app
npm install qiankun
修改 src/index.js,注册微应用并启动 qiankun:
jsx
// main-app/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { registerMicroApps, start } from 'qiankun';
// 主应用自身渲染
ReactDOM.render(<App />, document.getElementById('root'));
// 1. 注册子应用列表
registerMicroApps([
{
name: 'sub-app', // 子应用唯一名称
entry: '//localhost:3001', // 子应用运行地址(开发环境)
container: '#sub-app-container', // 子应用挂载的 DOM 节点
activeRule: '/sub', // 路由匹配规则,激活子应用
},
]);
// 2. 启动 qiankun
start({
sandbox: {
experimentalStyleIsolation: true, // 启用 Scoped CSS 样式隔离
},
});
接着在 src/App.js 中放置子应用的挂载容器和导航:
jsx
// main-app/src/App.js
import React from 'react';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<h1>微前端基座</h1>
<nav>
<a href="/">Home</a> | <a href="/sub">用户管理模块</a>
</nav>
</header>
<main>
{/* 子应用挂载点:id 必须与 registerMicroApps 中的 container 一致 */}
<div id="sub-app-container"></div>
</main>
</div>
);
}
export default App;
为支持 qiankun 的路由匹配,需要将 CRA 默认的页面刷新行为改为 history 模式,但为了避免端口冲突,我们仅通过 activeRule 做前缀匹配,无需额外配置路由库。这里我们使用的是传统 <a> 标签切换路径,qiankun 会监听 URL 变化并自动挂载/卸载子应用。
2.2 创建子应用 sub-app
同样用 CRA 创建,但需注意端口设为 3001(与 entry 一致):
bash
npx create-react-app sub-app
cd sub-app
首先安装 react-router-dom(用于子应用内部路由):
bash
npm install react-router-dom
修改 src/public-path.js,让子应用资源加载路径正确(用于生产环境):
js
// sub-app/src/public-path.js
if (window.__POWERED_BY_QIANKUN__) {
// 动态设置 webpack publicPath,防止资源加载 404
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
在 src/index.js 中暴露生命周期钩子,并配置路由:
jsx
// sub-app/src/index.js
import './public-path'; // 必须放在最顶部
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
let root = null;
// 渲染函数,打包为微应用和独立运行时复用
function render(props = {}) {
const { container } = props;
const dom = container
? container.querySelector('#root') // 挂载到 qiankun 容器中
: document.getElementById('root'); // 独立运行时挂载到 body
root = ReactDOM.createRoot(dom);
root.render(
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/sub' : '/'}>
<App />
</BrowserRouter>
);
}
// 如果是独立运行(非 qiankun 环境),直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
/**
* qiankun 要求暴露三个生命周期:
* bootstrap:初始化(仅一次)
* mount:挂载时调用
* unmount:卸载时调用
*/
export async function bootstrap() {
console.log('sub-app bootstraped');
}
export async function mount(props) {
console.log('sub-app mounted', props);
render(props);
}
export async function unmount(props) {
console.log('sub-app unmounted');
if (root) {
root.unmount(); // React 18 卸载方式
root = null;
}
}
为了适应子应用端口 3001,在 package.json 中添加 dev 脚本的端口指定,并允许跨域:
json
// sub-app/package.json 的 scripts 部分
"scripts": {
"start": "PORT=3001 WDS_SOCKET_PORT=3001 react-scripts start",
...
}
同时,子应用的 webpack 需要配置 Access-Control-Allow-Origin,以便主应用加载资源时不受跨域限制。由于 CRA 默认不暴露 webpack 配置,我们借助 craco 或直接在 src/setupProxy.js 中设置(开发环境使用 http-proxy-middleware 是代理请求,但这里是资源加载,需要响应头)。简单方案:在子应用的 src/setupProxy.js 中无法直接修改 webpack-dev-server 的 headers。更直接的:在 package.json 中加入 CORS 头:
bash
npm install -D @craco/craco
创建 craco.config.js:
js
// sub-app/craco.config.js
module.exports = {
devServer: (devServerConfig) => {
devServerConfig.headers = {
'Access-Control-Allow-Origin': '*',
};
return devServerConfig;
},
};
然后将 package.json 的 scripts 改为使用 craco:
json
"scripts": {
"start": "PORT=3001 craco start",
"build": "craco build",
"test": "craco test"
}
最后,修改子应用的 App.js,加入几个简单页面和路由:
jsx
// sub-app/src/App.js
import React from 'react';
import { Routes, Route, Link } from 'react-router-dom';
function Home() {
return <h2>用户管理首页</h2>;
}
function List() {
return <h2>用户列表</h2>;
}
function Detail() {
return <h2>用户详情</h2>;
}
function App() {
return (
<div style={{ padding: 20, background: '#f0f2f5' }}>
<h3>【子应用】用户管理</h3>
<nav>
<Link to="/">首页</Link> | <Link to="/list">列表</Link> | <Link to="/detail">详情</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/list" element={<List />} />
<Route path="/detail" element={<Detail />} />
</Routes>
</div>
);
}
export default App;
2.3 运行验证
- 启动子应用:
bash
cd sub-app && npm start
访问 http://localhost:3001,确认子应用可独立运行。
- 启动主应用:
bash
cd main-app && npm start
默认在 http://localhost:3000 打开。
- 点击导航栏的"用户管理模块",主应用路由变为
/sub,随即加载并渲染子应用。子应用的样式被隔离(因为开启了experimentalStyleIsolation),且能够在/sub/list等子路由下正常切换。
至此,一个生产可用的微前端最小原型便完成了。
三、常见问题与注意事项
3.1 样式隔离
qiankun 提供两种样式隔离:
- Shadow DOM 隔离 :
strictStyleIsolation: true(实验性),样式完全封闭,但会有细小的 UI 差异(如弹窗挂载问题)。 - Scoped CSS :
experimentalStyleIsolation: true,自动为所有样式规则添加一个特殊属性选择器,避免污染。
建议优先使用 Scoped CSS,若仍存在冲突,可在子应用中使用 CSS Modules 或 BEM 命名规范。
3.2 JS 沙箱与全局变量污染
qiankun 默认使用 ProxySandbox,每个微应用运行在独立的 window 代理对象中,避免全局变量冲突。但若子应用使用某些浏览器原生 API(如 postMessage、localStorage)需注意:它们可能在多个子应用间共享,需要设计命名空间或用通信方案隔离。
3.3 微应用间通信
qiankun 通过 initGlobalState 提供简易的全局状态管理:
js
// 主应用
import { initGlobalState } from 'qiankun';
const actions = initGlobalState({ user: 'admin' });
actions.onGlobalStateChange((state, prev) => {
console.log('状态变化', state, prev);
});
// 子应用可通过 props.onGlobalStateChange 和 props.setGlobalState 收发
对于复杂通信,建议使用自定义事件(CustomEvent)或公共依赖(如 Redux Store 注入)。
3.4 公共依赖抽取
多个子应用共享 React、ReactDOM 等大体积库时,可在主应用通过 externals 排除,并通过 CDN 或全局变量方式引入,减少重复加载。qiankun 也支持通过 getTemplate 钩子自动提取公共资源。
3.5 部署与 Nginx 配置
生产环境中,主应用和子应用需部署在同一个域名下(防止跨域),并配置 Nginx 的 try_files 使其正确回退到 index.html:
nginx
location / {
try_files $uri $uri/ /index.html;
}
location /sub {
try_files $uri $uri/ /sub/index.html;
}
子应用的 publicPath 需要设置为对应的路径前缀。
3.6 技术栈混合
若接入 Vue 子应用,需在 mount 钩子中实例化 Vue,在 unmount 中销毁,并注意路由的 basename。qiankun 官方文档有详细的 Vue 适配指南。
四、总结
微前端不是银弹,它引入了一定的复杂度:基座与微应用的协调、样式隔离的副作用、跨应用调试困难等。但对于大型组织、多团队协作、需要逐步迁移旧系统的场景,它带来的价值远远超过成本。qiankun 通过简洁的生命周期模型、健壮的沙箱机制和丰富的插件能力,极大降低了微前端的落地门槛。
本文从概念到完整代码,展示了 React 主应用接入 React 微应用的全过程。你可以以此为起点,尝试接入不同技术栈的子应用,并进一步探索自动化部署、自定义沙箱、性能监控等高级话题。
希望这篇文章能帮你理清微前端的思路,并让你有信心在实际项目中迈出第一步。有任何疑问,欢迎在评论区交流。