微前端架构落地实战:用qiankun轻松拆分巨石应用

引言

随着前端项目规模不断膨胀,代码库动辄数十万行,团队协作变得困难,构建部署耗时也越来越长。传统的单体应用(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 运行验证

  1. 启动子应用:
bash 复制代码
cd sub-app && npm start

访问 http://localhost:3001,确认子应用可独立运行。

  1. 启动主应用:
bash 复制代码
cd main-app && npm start

默认在 http://localhost:3000 打开。

  1. 点击导航栏的"用户管理模块",主应用路由变为 /sub,随即加载并渲染子应用。子应用的样式被隔离(因为开启了 experimentalStyleIsolation),且能够在 /sub/list 等子路由下正常切换。

至此,一个生产可用的微前端最小原型便完成了。


三、常见问题与注意事项

3.1 样式隔离

qiankun 提供两种样式隔离:

  • Shadow DOM 隔离strictStyleIsolation: true(实验性),样式完全封闭,但会有细小的 UI 差异(如弹窗挂载问题)。
  • Scoped CSSexperimentalStyleIsolation: true,自动为所有样式规则添加一个特殊属性选择器,避免污染。

建议优先使用 Scoped CSS,若仍存在冲突,可在子应用中使用 CSS Modules 或 BEM 命名规范。

3.2 JS 沙箱与全局变量污染

qiankun 默认使用 ProxySandbox,每个微应用运行在独立的 window 代理对象中,避免全局变量冲突。但若子应用使用某些浏览器原生 API(如 postMessagelocalStorage)需注意:它们可能在多个子应用间共享,需要设计命名空间或用通信方案隔离。

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 微应用的全过程。你可以以此为起点,尝试接入不同技术栈的子应用,并进一步探索自动化部署、自定义沙箱、性能监控等高级话题。

希望这篇文章能帮你理清微前端的思路,并让你有信心在实际项目中迈出第一步。有任何疑问,欢迎在评论区交流。