前端工程Technical Deep Dive

微前端落地实战 —— qiankun、single-spa 与模块联邦

发布时间2026/03/29
分类前端工程
预计阅读2 分钟
作者吴长龙
*

为什么需要微前端

01.内容

为什么需要微前端

随着应用复杂度增长,单体前端架构面临挑战:

code snippetcode
单体应用问题:
├── 代码仓库庞大,团队协作困难
├── 构建时间长,每次修改需要全量构建
├── 技术栈升级困难,存量代码难以迁移
├── 多人维护同一项目,冲突频繁
└── 无法按需加载,性能受限

微前端:将大型应用拆分为独立的小应用。

微前端方案对比

方案原理优点缺点
iframe嵌入页面隔离强通信难、体验差
Web Components浏览器标准原生支持生态弱
single-spa路由劫持兼容性好功能少
qiankunsingle-spa + sandbox功能完善蚂蚁技术栈
模块联邦webpack 5原生支持限制较多
Garfish插件化中立文档少

single-spa:基础框架

javascript snippetjavascript
import { registerApplication, start } from 'single-spa';

// 注册子应用
registerApplication(
  'app1',
  () => import('./app1/App.js'),
  (activityFn) => activityFn.hashChanges('#/app1')
);

registerApplication(
  'app2', 
  () => import('./app2/App.js'),
  (activityFn) => activityFn.hashChanges('#/app2')
);

start();
javascript snippetjavascript
// 子应用导出生命周期
const App = () => <div>App 1</div>;

export const bootstrap = async () => console.log('bootstrap');
export const mount = async (props) => {
  ReactDOM.render(<App />, document.getElementById('app1-root'));
};
export const unmount = async (props) => {
  ReactDOM.unmountComponentAtNode(document.getElementById('app1-root'));
};

qiankun:生产级方案

qiankun 是蚂蚁金服的微前端方案,基于 single-spa,提供了更完善的功能:

#### 主应用

javascript snippetjavascript
// main/src/index.js
import { start, registerMicroApps, setDefaultMountApp } from 'qiankun';

registerMicroApps([
  {
    name: 'react-app',
    entry: '//localhost:3001',
    container: '#container',
    activeRule: '/react',
  },
  {
    name: 'vue-app',
    entry: '//localhost:3002',
    container: '#container',
    activeRule: '/vue',
  },
  {
    name: 'static-app',
    entry: '//localhost:3003',
    container: '#container',
    activeRule: '/static',
  },
]);

setDefaultMountApp('/react');

start({
  sandbox: {
    strictStyleIsolation: true,  // 样式隔离
    experimentalStyleIsolation: true,  // CSS Modules 方案
  },
});
javascript snippetjavascript
// 主应用布局
function Layout() {
  const [app, setApp] = useState('react');
  
  return (
    <div>
      <nav>
        <button onClick={() => setApp('react')}>React</button>
        <button onClick={() => setApp('vue')}>Vue</button>
      </nav>
      <main id="container">
        {/* 子应用容器 */}
      </main>
    </div>
  );
}

#### React 子应用

javascript snippetjavascript
// 子应用入口 react-app/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

let root;

function render(props) {
  const { container } = props;
  
  root = ReactDOM.createRoot(container ? container.querySelector('#root') : document.getElementById('root'));
  
  root.render(<App />);
}

function unmount() {
  if (root) {
    root.unmount();
  }
}

// 导出生命周期
export async function bootstrap() {
  console.log('[react] bootstrap');
}

export async function mount(props) {
  console.log('[react] mount', props);
  render(props);
}

export async function unmount() {
  console.log('[react] unmount');
  unmount();
}

// 开发模式独立运行
if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}
javascript snippetjavascript
// webpack 配置 react-app/webpack.config.js
const { name } = require('./package.json');

module.exports = {
  output: {
    library: `${name}-[name]`,
    libraryTarget: 'umd',
    globalObject: 'window',
  },
  devServer: {
    port: 3001,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
};

#### Vue 子应用

javascript snippetjavascript
// Vue 子应用 src/main.js
import Vue from 'vue';
import App from './App.vue';

let app;

export function bootstrap() {
  console.log('[vue] bootstrap');
}

export async function mount(props) {
  console.log('[vue] mount', props);
  
  app = new Vue({
    render: h => h(App),
  }).$mount('#app');
}

export async function unmount() {
  console.log('[vue] unmount');
  if (app) {
    app.$destroy();
    app.$el.innerHTML = '';
  }
}

// 开发模式独立运行
if (!window.__POWERED_BY_QIANKUN__) {
  mount();
}

模块联邦 (Module Federation)

Webpack 5 的原生微前端方案:

javascript snippetjavascript
// host/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
      },
      shared: { 
        react: { singleton: true }, 
        'react-dom': { singleton: true } 
      },
    }),
  ],
};
javascript snippetjavascript
// host/src/App.js
import React, { Suspense } from 'react';

const RemoteButton = React.lazy(() => import('remoteApp/Button'));

function App() {
  return (
    <Suspense fallback="Loading...">
      <RemoteButton />
    </Suspense>
  );
}
javascript snippetjavascript
// remote/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'remoteApp',
      exposes: {
        './Button': './src/Button',
      },
      shared: { 
        react: { singleton: true, eager: true }, 
        'react-dom': { singleton: true } 
      },
    }),
  ],
};

通信机制

#### 1. 基于 Props

javascript snippetjavascript
// 主应用
<MicroApp 
  onDataChange={(data) => console.log(data)}
/>

// 子应用
export async function mount(props) {
  props.onDataChange({ from: 'child', message: 'hello' });
}

#### 2. 基于 Event

javascript snippetjavascript
// 工具函数
import { emit } from 'qiankun';

function dispatch(props) {
  // 发送消息
  emit('app1-event', { data: 'hello' });
}

// 监听
import { on } from 'qiankun';

export async function mount(props) {
  on('app1-event', (data) => {
    console.log('received:', data);
  });
}

#### 3. 共享状态

javascript snippetjavascript
// host/src/store.js
import { createStore } from 'qiankun/state';

export const { getGlobalState, setGlobalState, subscribe } = createStore({
  user: null,
  token: null,
});

// 子应用使用
import { useStore } from 'qiankun/state';

function App() {
  const user = useStore('user');
  return <div>{user?.name}</div>;
}

样式隔离

#### 1. CSS Modules

javascript snippetjavascript
// webpack 配置
{
  loader: 'css-loader',
  options: {
    modules: {
      localIdentName: '[name]__[local]--[hash:base64:5]',
    },
  },
}

#### 2. Shadow DOM

javascript snippetjavascript
// qiankun 配置
start({
  sandbox: {
    styleSheetStrategy: 'shadowDOM',
  },
});

#### 3. 动态添加前缀

javascript snippetjavascript
// 主应用
import Garfish from '@garfish/runtime';

Garfish.run({
  apps: [{
    name: 'child',
    entry: '//localhost:3001',
    props: { prefix: 'child' },
  }],
});

最佳实践

#### 1. 应用加载策略

javascript snippetjavascript
registerMicroApps([
  {
    name: 'app1',
    entry: '//localhost:3001',
    loader: (loading) => <Spin loading={loading} />,
  },
]);

#### 2. 预加载

javascript snippetjavascript
// qiankun 自动预加载
start({
  prefetch: true,  // 默认 true
});

#### 3. 错误处理

javascript snippetjavascript
// 全局错误处理
import { addErrorHandler } from 'qiankun';

addErrorHandler((error) => {
  console.error('Global error:', error);
  // 上报错误
});

总结

方案适用场景推荐度
qiankunReact/Vue 为主⭐⭐⭐⭐⭐
模块联邦Webpack 5 项目⭐⭐⭐⭐
single-spa简单场景⭐⭐⭐
iframe强隔离需求⭐⭐

微前端不是银弹,适合才最好。