微前端架构设计与实践

Created on

前言

随着前端应用规模的不断扩大,单体应用的弊端逐渐显现:代码耦合严重、构建时间过长、团队协作困难。微前端作为一种架构模式,将前端应用拆分为多个独立的子应用,每个子应用可以独立开发、部署和运维。

什么是微前端?

核心理念

微前端是一种将多个可独立交付的小型前端应用聚合为一个整体的架构风格。 —— Micro Frontends

特点

微前端方案对比

1. iframe

<iframe src="https://sub-app.example.com" />

优点

缺点

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();

优点

缺点

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",
      },
    }),
  ],
};

优点

缺点

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. 子应用改造原则

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. 全局状态共享

4. 错误边界

// 主应用添加错误处理
registerMicroApps(apps, {
  beforeLoad: async (app) => {
    try {
      // 预处理
    } catch (error) {
      console.error("子应用加载失败", error);
      // 降级方案
      showErrorPage();
    }
  },
});

常见问题

1. 子应用白屏

原因:跨域问题 解决:配置 CORS

// webpack devServer
devServer: {
  headers: {
    'Access-Control-Allow-Origin': '*',
  },
}

2. 样式冲突

解决方案

3. 路由跳转失效

原因:子应用路由 base 配置错误 解决

const router = new VueRouter({
  base: window.__POWERED_BY_QIANKUN__ ? "/vue-app/" : "/",
  routes,
});

总结

微前端不是银弹,适合以下场景:

  1. 大型应用拆分:应用规模庞大,需要拆分团队
  2. 遗留系统迁移:老项目需要逐步重构
  3. 多团队协作:不同团队使用不同技术栈

选择微前端前需要考虑:

记住:架构的选择永远是权衡的艺术。

参考资源