微前端架构详解
微前端架构详解
一、什么是微前端
微前端是微服务的概念在前端领域的扩展。将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
将公共库排除。
- Webpack 的
4、路由与导航
- 问题:主应用于子应用路由冲突,浏览器历史记录管理
- 方案:
- 主应用统一路由分发(URL前缀匹配)
- 使用history.pushState实现无刷新跳转
- 路由劫持
三、微前端的实现原理
1、通过路由映射分发
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
-
原理:提供加载器、路由托管,不处理资源加载/隔离。
-
工作流程:
- 子应用导出
bootstrap
,mount
,unmount
函数。 - 主应用注册子应用并配置路由匹配规则。
- 路由变化时,加载并执行对应子应用。
- 子应用导出
-
示例配置:
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 添加前缀隔离。
- 预加载:空闲时预加载子应用资源。
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.countconsole.log(res.count)})},bootstrap() { },unmount() {instance.unmount()instance._instance = nullinstance = 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 原生组件进行渲染的微前端框架,不同于目前流行的开源框架,它从组件化的思维实现微前端,旨在降低上手难度、提升工作效率。
核心技术实现
-
Web Components集成
- 自定义元素注册
class MicroApp extends HTMLElement {constructor() {super();this.attachShadow({ mode: 'open' });}connectedCallback() {this.loadApp();} }customElements.define('micro-app', MicroApp);
-
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; }
-
样式隔离策略
- Shadow DOM原生隔离
- 自动前缀转换
/* 输入 */ .container { background: #fff; }/* 输出 */ micro-app[name=app1] .container { background: #fff; }
-
数据通信机制
- 基于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 的全新微前端方案。
点击这里查看方案设计
核心技术:
-
无界沙箱
- 基于iframe的天然隔离
- 主应用创建代理iframe:
<iframe src="about:blank"></iframe>
- 子应用运行在隐藏iframe中
-
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到主应用容器
-
事件代理
- 劫持事件监听方法
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); };
-
路由同步
- 监听iframe的history变化
- 同步到主应用URL
iframe.contentWindow.history.pushState = new Proxy(history.pushState, {apply: function(target, thisArg, args) {const result = target.apply(thisArg, args);// 同步主应用URLwindow.history.pushState(...args);return result;} });
五、原理解析
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 DOM:
const host = document.getElementById('host'); const shadowRoot = host.attachShadow({ mode: 'open' });
-
样式隔离:
<style>:host { /* 宿主元素样式 */ }.btn { /* 仅内部有效 */ } </style>
-
插槽机制(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>
-
事件重定向:
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. 获取 HTMLconst html = await fetch(url).then(r => r.text());// 2. 解析 DOMconst 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;}
}
高级特性实现
-
资源预加载:
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);}); }
-
依赖去重:
const sharedLibs = ['react', 'react-dom'];function filterDuplicateScripts(scripts) {return scripts.filter(script => {return !sharedLibs.some(lib =>script.src.includes(`/${lib}/`) && window[lib]);}); }
-
样式作用域:
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支持 | ✗ | ✓ | ✗ |