ZDecode
Frontend

微前端

微前端(Micro-Frontend)是一种前端架构模式,它将前端应用分解成一些更小、更易于管理的部分,这些部分可以由不同的团队独立开发、测试和部署,最终组合成一个完整的应用。

核心理念

  • 技术栈无关性:每个微前端可以使用不同的JavaScript框架(如React、Vue、Angular等)独立开发
  • 团队自治:不同团队可以负责不同的微前端,独立开发和部署
  • 松耦合:各微前端之间保持低耦合,通过定义好的接口进行通信
  • 渐进式升级:可以逐步将旧系统迁移到新架构,无需一次性重写整个应用
  • 独立部署:各微前端可以独立部署,不影响其他部分

微前端特别适合大型应用和需要多团队协作的场景。

常见实现方式

1. 模块联邦

模块联邦允许一个JavaScript应用在运行时动态加载另一个应用的代码,实现应用间代码共享。

  • 优点

    • 真正的代码共享
    • 构建时集成,性能好
    • 支持热更新
    • 减少重复依赖
  • 缺点

    • 依赖Web
  • 简单示例

主应用(host)配置:

// webpack.config.js (主应用)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  // ...
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        app2: 'app2@http://localhost:3002/remoteEntry.js',
      },
    }),
  ],
};

主应用使用子应用组件:

// 主应用中使用
import React, { Suspense } from 'react';

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

const App = () => (
  <div>
    <h1>主应用</h1>
    <Suspense fallback="加载中...">
      <RemoteButton />
    </Suspense>
  </div>
);

子应用配置:

// webpack.config.js (子应用)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  // ...
  plugins: [
    new ModuleFederationPlugin({
      name: 'app2',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/Button',
      },
    }),
  ],
};

2. 基于路由的分发集成

使用前端路由将不同URL映射到不同的微前端应用。

  • 优点

    • 实现简单,易于理解
    • 子应用完全解耦
    • 天然支持SEO
    • 部署独立
  • 缺点

    • 页面刷新体验差
    • 应用间状态共享困难
    • 不利于细粒度集成
  • 简单示例

// 主应用 app.js
import { BrowserRouter, Route, Switch } from 'react-router-dom';

const App = () => (
  <BrowserRouter>
    <Switch>
      <Route path="/app1" component={() => {
        // 动态加载 app1
        const script = document.createElement('script');
        script.src = 'http://localhost:3001/app1.js';
        document.body.appendChild(script);
        return <div id="app1-container"></div>;
      }} />
      <Route path="/app2" component={() => {
        // 动态加载 app2
        const script = document.createElement('script');
        script.src = 'http://localhost:3002/app2.js';
        document.body.appendChild(script);
        return <div id="app2-container"></div>;
      }} />
    </Switch>
  </BrowserRouter>
);

3. iframe集成

使用iframe将各个微前端嵌入主应用。

  • 优点

    • 提供天然隔离
    • 实现简单
    • 技术栈完全无关
    • 安全性高
  • 缺点

    • 性能较差,白屏时间长
    • 共享状态困难
    • 用户体验割裂(样式、导航、历史记录)
    • 不利于SEO
  • 简单示例

<!-- 主应用 index.html -->
<div id="app">
  <header>主应用导航</header>
  <main>
    <iframe id="micro-frontend-container" src=""></iframe>
  </main>
</div>

<script>
  // 根据路由切换iframe内容
  const iframe = document.getElementById('micro-frontend-container');
  
  function navigateTo(path) {
    switch(path) {
      case '/app1':
        iframe.src = 'http://localhost:3001/';
        break;
      case '/app2':
        iframe.src = 'http://localhost:3002/';
        break;
    }
  }
  
  // 监听路由变化
  window.addEventListener('popstate', () => {
    navigateTo(window.location.pathname);
  });
  
  // 初始化
  navigateTo(window.location.pathname);
</script>

4. Web Components

使用Custom Elements将微前端封装为标准Web组件。

  • 优点

    • 浏览器原生支持
    • 良好的封装性
    • 可与任何框架集成
    • DOM级别隔离
  • 缺点

    • 兼容性有限
    • 状态管理相对复杂
    • 生态相对不成熟
    • 学习成本较高
  • 简单示例

// 子应用 app1
class MicroApp1 extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<div>
      <h2>微前端应用1</h2>
      <button>App1 按钮</button>
    </div>`;
    
    this.querySelector('button').addEventListener('click', () => {
      alert('来自App1的点击');
    });
  }
}

customElements.define('micro-app-1', MicroApp1);

主应用使用:

<!-- 主应用 index.html -->
<div id="app">
  <header>主应用导航</header>
  <main>
    <micro-app-1></micro-app-1>
  </main>
</div>

<script src="http://localhost:3001/micro-app-1.js"></script>

5. JavaScript运行时集成

使用专门的微前端框架如single-spaqiankun

  • 优点

    • 应用级隔离
    • 框架成熟,生态丰富
    • 支持多框架共存
    • 配置灵活,集成度高
  • 缺点

    • 实现复杂度高
    • 运行时依赖强
    • 打包体积较大
    • 初始加载性能可能受影响
  • 简单示例

// 主应用 index.js
import { registerApplication, start } from 'single-spa';

// 注册微前端应用
registerApplication(
  'app1',
  () => import('http://localhost:3001/js/app.js'),
  location => location.pathname.startsWith('/app1')
);

registerApplication(
  'app2',
  () => import('http://localhost:3002/js/app.js'),
  location => location.pathname.startsWith('/app2')
);

// 启动
start();

子应用需要导出生命周期函数:

// 子应用 app1
export function bootstrap() {
  // 应用初始化
  return Promise.resolve();
}

export function mount(props) {
  // 挂载应用到DOM
  document.getElementById('app1-container').innerHTML = '<h1>App1已加载</h1>';
  return Promise.resolve();
}

export function unmount() {
  // 卸载应用
  document.getElementById('app1-container').innerHTML = '';
  return Promise.resolve();
}