微前端架构详解

发布于:2025-07-16 ⋅ 阅读:(12) ⋅ 点赞:(0)

微前端架构详解

一、什么是微前端

微前端是微服务的概念在前端领域的扩展。将Web应用拆分成多个子应用,每个应用由独立的团队开发、测试、部署。

请添加图片描述

微前端的原则:

  • 技术栈无关:可根据业务领域灵活混用Vue\React\Angular等技术栈
  • 代码隔离,无法隔离的代码,应用之间采用前缀来区分
  • 独立交付:独立开发、独立测试、独立部署
  • 团队自治:团队负责全生命周期管理,减少协作成本

典型应用场景

  • 遗留系统现代化改造
  • 多团队协作的大型 SaaS 平台
  • 需要动态加载不同功能模块的 Portal 类应用

二、微前端要解决的问题

1、通信机制

  • 子应用之间通信:共享状态库(Redux、Vuex、Pina等)、URL参数等
  • 父子应用通信: 使用CustomEvent发布订阅模式

2、应用隔离

隔离类型 问题描述 解决方案
JS沙箱 全局变量/事件监听污染 Proxy代理、Shadow Realm、快照沙箱
CSS隔离 样式冲突、选择器冲突 CSS Scope、命名空间、Shadow Dom
存储隔离 Cookie/LocalStorage冲突 前缀命名、封装API

3、资源加载与共享

  • 问题:公共依赖如何加载,避免公共库被多次加载
  • 方案:
    • Webpack 的 externals 将公共库排除。

4、路由与导航

  • 问题:主应用于子应用路由冲突,浏览器历史记录管理
  • 方案:
    • 主应用统一路由分发(URL前缀匹配)
    • 使用history.pushState实现无刷新跳转
    • 路由劫持

三、微前端的实现原理

1、通过路由映射分发

路由匹配 /app1/*
路由匹配 /app2/*
主应用
子应用1
子应用2

2、iframe嵌套

  • 原理:每个子应用独立运行在 iframe 中。
  • 优点:天然隔离(JS/CSS/DOM)。
  • 缺点:URL 不同步、性能差、通信复杂(postMessage)。

3、WebComponent

webComponent有三个步骤:

  • 使用customElements定义组件
  • 使用template包含组件的HTML内容,支持使用slot插槽
  • 使用Shadow DOM将元素的样式和行为封装在一个隔离的DOM中
<!-- 主应用调用 -->
<micro-app name="cart" src="https://child.com/cart.js"></micro-app>

<!-- 子应用封装 -->
<template id="tpl">
  <style>/* Scoped CSS */</style>
  <div>子应用内容</div>
</template>
<script>
  class MicroApp extends HTMLElement {
    connectedCallback() {
      this.attachShadow({ mode: 'open' }).append(tpl.content.cloneNode(true));
    }
  }
  customElements.define('micro-app', MicroApp);
</script>

3、Module Federation

Module Federation是Webpack5提出来的概念,用来解决多个应用之间代码共享的问题。

核心概念

  • Host(主应用):消费远程模块的容器
  • Remote(子应用):暴露模块的远程服务
  • Shared:共享依赖(如 React、Vue)
  • **Container:**被MF构建的模块
/// 配置webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
new ModuleFederationPlugin({
  name: "appA",
 //出口文件
  filename: "remoteEntry.js",
 //暴露可访问的组件
  exposes: {
    "./input": "./src/input",
  },
 //或者其他模块的组件
 //如果把这一模块当作基座模块的话,
 //这里应该配置其他子应用模块的入口文件
  remotes: {
    appB: "appB@http://localhost:3002/remoteEntry.js",
  },
 //共享依赖,其他模块不需要再次下载,便可使用
  shared: ['react', 'react-dom'],
})

四、微前端的框架

4.1 single-spa

  • 原理:提供加载器、路由托管,不处理资源加载/隔离。

  • 工作流程

    1. 子应用导出 bootstrap, mount, unmount 函数。
    2. 主应用注册子应用并配置路由匹配规则。
    3. 路由变化时,加载并执行对应子应用。
  • 示例配置

    singleSpa.registerApplication(
      'app1',
      () => System.import('app1'), // 动态加载
      location => location.pathname.startsWith('/app1')
    );
    

4.2 qiankun(基于 single-spa)

  • 核心增强
    • HTML Entry:解析子应用 HTML 获取资源列表,替代 JS Entry。
    • JS 沙箱:支持 Proxy 和快照沙箱(兼容 IE)。
    • 样式隔离:自动为子应用 CSS 添加前缀隔离。
    • 预加载:空闲时预加载子应用资源。
主应用
路由分发
匹配子应用
qiankun-loader
HTML Entry解析
JS沙箱环境
执行子应用
挂载到容器
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'reactApp',
    entry: '//localhost:3000',
    container: '#container',
    activeRule: '/app-react',
  },
  {
    name: 'vueApp',
    entry: '//localhost:8080',
    container: '#container',
    activeRule: '/app-vue',
  },
  {
    name: 'angularApp',
    entry: '//localhost:4200',
    container: '#container',
    activeRule: '/app-angular',
  },
]);
// 启动 qiankun
start();

需要注意的是,如果采用vite构建的应用,需要使用插件vite-plugin-qiankun

// 子应用vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun'

export default defineConfig({
  plugins: [
    vue(),
    qiankun('vue-app', { // 子应用名字,与主应用注册的子应用名字保持一致
      useDevMode: true
    })
  ],
  server: {
    origin: 'http://localhost:5174', // 解决静态资源加载404问题
    host: 'localhost',
    port: 5174
  }
})

//子应用main.js
import { createApp } from 'vue'
import App from './App.vue'
import { store } from './store'

import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'

let instance = null;
const initQianKun = () => {
    renderWithQiankun({
        mount(props) {
            render(props.container)
            //  可以通过props读取主应用的参数:msg
            // 监听主应用传值
            props.onGlobalStateChange((res) => {
                store.count = res.count
                console.log(res.count)
            })
        },
        bootstrap() { },
        unmount() {
            instance.unmount()
            instance._instance = null
            instance = null
        },
        update() { }
    })
}

const render = (container) => {
    if (instance) return;
    // 如果是在主应用的环境下就挂载主应用的节点,否则挂载到本地
    // 注意:这边需要避免 id(app) 重复导致子应用挂载失败
    const appDom = container ? container.querySelector("#app") : "#app"
    instance = createApp(App)
    instance.mount(appDom)
}

// 判断当前应用是否在主应用中
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render()

// route文件
import { createWebHashHistory } from 'vue-router'
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
const router = createRouter({
    // 值和主应用中的activeRule保持一致    
    history: createWebHashHistory(qiankunWindow.__POWERED_BY_QIANKUN__ ? '/vueApp' : '/'),     
    routes: routes 
});

详细实践: 点击这里查看实践指南

4.3 Mirco-Ap

Mirco-App是京东出的一款基于 Web Component 原生组件进行渲染的微前端框架,不同于目前流行的开源框架,它从组件化的思维实现微前端,旨在降低上手难度、提升工作效率。

主应用
micro-app
自定义元素
创建Shadow DOM
资源加载器
JS作用域隔离
样式隔离
子应用渲染

核心技术实现

  1. Web Components集成

    • 自定义元素注册
    class MicroApp extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: 'open' });
      }
    
      connectedCallback() {
        this.loadApp();
      }
    }
    
    customElements.define('micro-app', MicroApp);
    
  2. JS作用域隔离

    • 基于Proxy的沙箱
    function createSandbox(appName) {
      const proxy = new Proxy(window, {
        get(target, key) {
          if (key in target) {
            return target[key];
          }
          // 返回子应用自有属性
        },
        set(target, key, value) {
          if (!target.hasOwnProperty(key)) {
            // 存储到子应用独立空间
            target[`${appName}_${key}`] = value;
          } else {
            target[key] = value;
          }
          return true;
        }
      });
      return proxy;
    }
    
  3. 样式隔离策略

    • Shadow DOM原生隔离
    • 自动前缀转换
    /* 输入 */
    .container { background: #fff; }
    
    /* 输出 */
    micro-app[name=app1] .container { background: #fff; }
    
  4. 数据通信机制

    • 基于CustomEvent
    // 主应用发送数据
    const event = new CustomEvent('micro-app-event', {
      detail: { type: 'data-update', payload }
    });
    document.querySelector('micro-app').dispatchEvent(event);
    
    // 子应用接收
    window.addEventListener('micro-app-event', (e) => {
      console.log(e.detail);
    });
    

4.4 无界

无界是腾讯推出的一款微前端解决方式。它是一种基于 Web Components + iframe 的全新微前端方案。

点击这里查看方案设计

核心技术:

  1. 无界沙箱

    • 基于iframe的天然隔离
    • 主应用创建代理iframe:<iframe src="about:blank"></iframe>
    • 子应用运行在隐藏iframe中
  2. DOM代理机制

    • 劫持iframe内的document
    const proxyDocument = new Proxy(document, {
      get: function(target, key) {
        if (key === 'createElement') {
          return function(tagName) {
            const element = target.createElement(tagName);
            // 代理元素到主应用
            return elementProxy(element);
          };
        }
        return target[key];
      }
    });
    
    • 同步DOM到主应用容器
  3. 事件代理

    • 劫持事件监听方法
    Element.prototype.addEventListener = function(type, listener, options) {
      // 存储事件监听器
      this._listeners = this._listeners || {};
      this._listeners[type] = this._listeners[type] || [];
      this._listeners[type].push(listener);
    
      // 在主应用容器上绑定代理事件
      hostElement.addEventListener(type, proxyListener);
    };
    
  4. 路由同步

    • 监听iframe的history变化
    • 同步到主应用URL
    iframe.contentWindow.history.pushState = new Proxy(history.pushState, {
      apply: function(target, thisArg, args) {
        const result = target.apply(thisArg, args);
        // 同步主应用URL
        window.history.pushState(...args);
        return result;
      }
    });
    
主应用
wujie实例
创建iframe
iframe沙箱
DOM代理
事件代理
子应用运行

五、原理解析

1、JS沙箱机制实现

  • 快照沙箱: 通过将window的属性复制到windowSnapshot
class SnapshotSandbox {
  constructor() {
    this.proxy = window;
    this.modifyPropsMap = {};
    this.active();
  }
  
  active() {
    this.windowSnapshot = {};
    for (const prop in window) {
      this.windowSnapshot[prop] = window[prop];
    }
    Object.keys(this.modifyPropsMap).forEach(p => {
      window[p] = this.modifyPropsMap[p];
    });
  }
  
  inactive() {
    for (const prop in window) {
      if (window[prop] !== this.windowSnapshot[prop]) {
        this.modifyPropsMap[prop] = window[prop];
        window[prop] = this.windowSnapshot[prop];
      }
    }
  }
}
  • 代理沙箱: 使用proxy实现
class ProxySandbox {
  constructor() {
    const rawWindow = window;
    const fakeWindow = {};
    this.proxy = new Proxy(fakeWindow, {
      set(target, key, value) {
        target[key] = value;
        return true;
      },
      get(target, key) {
        return target[key] || rawWindow[key];
      }
    });
  }
}

2、Shadow Realm

Shadow Realm 是 ECMAScript 2023 正式标准,提供真正的 JavaScript 运行时隔离环境。与传统沙箱不同,它创建完全独立的全局对象和内置函数副本,实现真正的环境隔离。

class ShadowRealm {
  constructor() {
    // 创建全新的全局对象
    this.global = new Proxy({}, {
      get: (_, key) => this._scopedGlobals[key]
    });
    
    // 复制内置函数
    this._scopedGlobals = {
      Array: function() { /* 独立实现 */ },
      Date: function() { /* 独立实现 */ },
      // ...其他内置对象
    };
  }
  
  evaluate(sourceText) {
    // 在隔离环境中执行代码
    const wrapped = `(function(__global__) {
      with(__global__) { 
        ${sourceText} 
      }
    })`;
    return (0, eval)(wrapped)(this.global);
  }
}

// 创建子应用沙箱
const appRealm = new ShadowRealm();

// 执行子应用代码
appRealm.evaluate(`
  class MyApp {
    mount(container) {
      this.el = document.createElement('div');
      this.el.textContent = '隔离的子应用';
      container.appendChild(this.el);
    }
  }
  new MyApp();
`);

// 获取子应用导出
const { MyApp } = await appRealm.importValue('./app.js', 'MyApp');
const app = new MyApp();
app.mount(document.getElementById('container'));

3、CSS添加命名空间

function scopeCSS(css, prefix) {
  return css.replace(/([^{}]+)(?=\s*{)/g, selector => {
    return selector.split(',').map(part => {
      return `${prefix} ${part.trim()}`
    }).join(',');
  });
}

// 输入: .btn, .header { color: red; }
// 输出: [data-app="app1"] .btn, [data-app="app1"] .header { color: red; }

4、 Scoped CSS

通过属性选择器实现作用域隔离

<style>
  /* 自动转换 */
  .container[data-v-7ba5bd90] { color: red; }
</style>

<div class="container" data-v-7ba5bd90>内容</div>

5、Shadow DOM

架构原理

宿主元素
Shadow Root
Shadow Tree
样式隔离
DOM 封装
元素隐藏

核心特性实现

  1. 创建 Shadow DOM

    const host = document.getElementById('host');
    const shadowRoot = host.attachShadow({ mode: 'open' });
    
  2. 样式隔离

    <style>
      :host { /* 宿主元素样式 */ }
      .btn { /* 仅内部有效 */ }
    </style>
    
  3. 插槽机制(Slot)

    <!-- 定义 -->
    <div class="card">
      <slot name="title">默认标题</slot>
      <slot name="content"></slot>
    </div>
    
    <!-- 使用 -->
    <my-card>
      <h1 slot="title">自定义标题</h1>
      <p slot="content">自定义内容</p>
    </my-card>
    
  4. 事件重定向

    shadowRoot.addEventListener('click', e => {
      // 事件目标重定向
      e.target = host;
      host.dispatchEvent(new CustomEvent('shadow-click', e));
    });
    

微前端应用示例

class MicroApp extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: block; border: 1px solid #ddd; }
        h2 { color: var(--primary-color, blue); }
      </style>
      <div class="app-container">
        <h2>子应用标题</h2>
        <slot name="content"></slot>
      </div>
    `;
  }
}

customElements.define('micro-app', MicroApp);

6、 HTML Enity

与传统 JS Entry 对比

特性 JS Entry HTML Entry
入口文件 app.js index.html
资源声明 需手动配置 externals 自动解析 link/script 标签
样式处理 独立加载 CSS 文件 自动提取并注入样式
子应用结构 需暴露生命周期函数 无需改造,直接接入

实现原理

核心代码实现

class HTMLEntryLoader {
  async loadApp(url, container) {
    // 1. 获取 HTML
    const html = await fetch(url).then(r => r.text());

    // 2. 解析 DOM
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');

    // 3. 提取资源
    const styles = [...doc.querySelectorAll('link[rel="stylesheet"]')];
    const scripts = [...doc.querySelectorAll('script')];

    // 4. 创建沙箱容器
    const appContainer = document.createElement('div');
    appContainer.id = `app-${Date.now()}`;
    container.appendChild(appContainer);

    // 5. 加载样式
    styles.forEach(style => {
      const link = document.createElement('link');
      link.rel = 'stylesheet';
      link.href = new URL(style.href, url).href;
      document.head.appendChild(link);
    });

    // 6. 执行脚本
    const execContext = new ShadowRealm();
    for (const script of scripts) {
      if (script.src) {
        const scriptUrl = new URL(script.src, url).href;
        const code = await fetch(scriptUrl).then(r => r.text());
        execContext.evaluate(code);
      } else {
        execContext.evaluate(script.textContent);
      }
    }

    // 7. 渲染内容
    appContainer.innerHTML = doc.body.innerHTML;
  }
}

高级特性实现

  1. 资源预加载

    function prefetchResources(html) {
      const parser = new DOMParser();
      const doc = parser.parseFromString(html, 'text/html');
    
      // 预加载所有资源
      [...doc.querySelectorAll('[src],[href]')].forEach(res => {
        const url = res.src || res.href;
        const link = document.createElement('link');
        link.rel = 'prefetch';
        link.href = url;
        document.head.appendChild(link);
      });
    }
    
  2. 依赖去重

    const sharedLibs = ['react', 'react-dom'];
    
    function filterDuplicateScripts(scripts) {
      return scripts.filter(script => {
        return !sharedLibs.some(lib =>
          script.src.includes(`/${lib}/`) && window[lib]
        );
      });
    }
    
  3. 样式作用域

    function scopeStyles(styleContent, prefix) {
      return styleContent.replace(
        /([^{}]+)(?=\s*{)/g,
        selector => `${prefix} ${selector}`);
    }
    

六、三大框架对比分析

特性 qiankun wujie micro-app
隔离机制 JS沙箱+样式重写 iframe沙箱+DOM代理 Shadow DOM+Proxy沙箱
通信方式 全局状态管理 props/$wujie对象 CustomEvent+数据监听
路由同步 手动配置activeRule 自动同步 自动同步
性能开销 中等(需要解析HTML) 较低(iframe原生隔离) 低(原生Web Components)
接入成本 需子应用改造导出生命周期 无侵入 无侵入
多实例支持
IE兼容性 ✓(降级方案)
SSR支持

网站公告

今日签到

点亮在社区的每一天
去签到