每天一个高级前端知识 - Day 10
今日主题:微前端完全指南 - 从single-spa到Module Federation 2.0
核心概念:将前端应用拆分为独立自治的微应用
微前端不是框架,而是一套架构模式,让多个团队独立开发、部署前端模块。
🔬 微前端三大核心模式
┌─────────────────────────────────────────────┐
│ 基座应用 (Container) │
│ - 路由分发 │
│ - 应用生命周期管理 │
│ - 全局状态共享 │
└─────────────────────────────────────────────┘
↓ ↓ ↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 子应用 A │ │ 子应用 B │ │ 子应用 C │
│ (React 18) │ │ (Vue 3) │ │ (Angular) │
│ 独立仓库 │ │ 独立部署 │ │ 独立团队 │
└──────────────┘ └──────────────┘ └──────────────┘
🚀 方案一:Single-SPA(成熟方案)
// 1. 基座应用配置
import { registerApplication, start } from 'single-spa';
// 注册React子应用
registerApplication({
name: '@team/react-app',
app: () => System.import('@team/react-app'), // 使用SystemJS动态加载
activeWhen: ['/react'],
customProps: {
getGlobalState: () => globalStore.getState(),
setGlobalState: (state) => globalStore.setState(state)
}
});
// 注册Vue子应用
registerApplication({
name: '@team/vue-app',
app: () => System.import('@team/vue-app'),
activeWhen: ['/vue'],
customProps: { theme: 'dark' }
});
// 注册Angular子应用(懒加载)
registerApplication({
name: '@team/angular-app',
app: () => System.import('@team/angular-app'),
activeWhen: (location) => location.pathname.startsWith('/angular'),
customProps: {}
});
// 启动微前端
start();
// 2. React子应用入口(single-spa-react)
import React from 'react';
import ReactDOM from 'react-dom/client';
import singleSpaReact from 'single-spa-react';
import Root from './Root.component';
const reactLifecycles = singleSpaReact({
React,
ReactDOMClient: ReactDOM,
rootComponent: Root,
errorBoundary(err, info, props) {
return <div>子应用加载失败</div>;
}
});
// 导出生命周期
export const { bootstrap, mount, unmount } = reactLifecycles;
// 独立运行时直接启动(开发模式)
if (!window.singleSpaNavigate) {
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Root />);
}
// 3. 全局状态管理(跨应用通信)
class GlobalStore {
constructor() {
this.state = {
user: null,
theme: 'light',
permissions: []
};
this.listeners = new Set();
}
getState() {
return this.state;
}
setState(partialState) {
this.state = { ...this.state, ...partialState };
this.notify();
}
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
notify() {
this.listeners.forEach(listener => listener(this.state));
}
}
const globalStore = new GlobalStore();
// 子应用订阅状态
const unsubscribe = globalStore.subscribe((state) => {
console.log('状态更新:', state);
// 更新子应用内部状态
});
// 子应用修改状态
globalStore.setState({ theme: 'dark' });
🚀 方案二:Module Federation 2.0(Webpack 5+)
// webpack.config.js - 基座应用配置
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'container',
remotes: {
reactApp: 'react_app@http://localhost:3001/remoteEntry.js',
vueApp: 'vue_app@http://localhost:3002/remoteEntry.js',
angularApp: 'angular_app@http://localhost:3003/remoteEntry.js'
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true },
vue: { singleton: true, requiredVersion: '^3.0.0' },
lodash: { eager: true } // 立即加载共享依赖
}
})
]
};
// 基座应用动态加载子应用
class MicroAppLoader {
constructor() {
this.apps = new Map();
this.loadedApps = new Set();
}
async loadApp(name, remoteUrl) {
if (this.loadedApps.has(name)) {
return this.apps.get(name);
}
// 动态添加remote(MF 2.0新特性)
await __webpack_require__.federation.addRemote({
name,
url: remoteUrl
});
// 加载子应用模块
const module = await import(`${name}/App`);
const appModule = module.default || module;
// 创建应用实例
const app = {
name,
instance: new appModule(),
mounted: false,
element: null
};
this.apps.set(name, app);
this.loadedApps.add(name);
return app;
}
mount(appName, containerId) {
const app = this.apps.get(appName);
if (app && !app.mounted) {
app.element = document.getElementById(containerId);
app.instance.mount(app.element);
app.mounted = true;
}
}
unmount(appName) {
const app = this.apps.get(appName);
if (app && app.mounted) {
app.instance.unmount();
app.mounted = false;
}
}
// MF 2.0新特性:预加载
preload(appName) {
return new Promise((resolve) => {
requestIdleCallback(() => {
this.loadApp(appName).then(resolve);
});
});
}
}
// 使用
const loader = new MicroAppLoader();
await loader.preload('reactApp'); // 空闲时预加载
await loader.loadApp('reactApp', 'http://localhost:3001/remoteEntry.js');
loader.mount('reactApp', 'react-container');
// 子应用暴露组件(Module Federation导出)
// react子应用 - webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'react_app',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App',
'./Button': './src/components/Button',
'./store': './src/store' // 暴露共享store
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
// React子应用入口
import React from 'react';
import ReactDOM from 'react-dom/client';
// 必须导出mount/unmount用于动态加载
export function mount(container) {
const root = ReactDOM.createRoot(container);
root.render(<App />);
return root;
}
export function unmount(root) {
root.unmount();
}
🚀 方案三:无界微前端(腾讯开源)
<!-- 无界:基于WebComponent + iframe沙箱 -->
<wujie-app
name="vue-app"
url="http://localhost:3002"
props="{{ userInfo }}"
sync="true"
@load="handleLoad"
@beforeLoad="handleBeforeLoad">
</wujie-app>
<script>
import Wujie from 'wujie';
// 预加载所有子应用
Wujie.preloadApp({
name: 'vue-app',
url: 'http://localhost:3002'
});
// 手动启动
Wujie.startApp({
name: 'vue-app',
el: '#container',
url: 'http://localhost:3002',
alive: true, // 保活模式
fetch: (url, options) => {
// 自定义资源加载
return window.fetch(url, { ...options, credentials: 'include' });
},
props: {
token: localStorage.getItem('token'),
onLogout: () => console.log('退出')
}
});
// 通信
Wujie.bus.$on('appEvent', (data) => {
console.log('收到子应用事件:', data);
});
Wujie.bus.$emit('globalEvent', { type: 'themeChange', value: 'dark' });
</script>
🎯 高级模式:样式隔离与JS沙箱
// 1. CSS样式隔离方案
class StyleIsolator {
constructor(appName) {
this.appName = appName;
this.originalStyleSheets = [];
this.styleElement = null;
}
// 方案A: CSS Module (Scoped CSS)
scopedCSS(css, scopeId) {
// 将CSS转换为带作用域的版本
// .button -> .scope-xxx .button
const scopedCSS = css.replace(/([^\s{]+)\s*{/g, (match, selector) => {
if (selector.startsWith('@')) return match;
return `.${scopeId} ${selector} {`;
});
return scopedCSS;
}
// 方案B: Shadow DOM(最强隔离)
shadowDOM(element) {
const shadow = element.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>${this.appStyles}</style>
<div class="app-content">
<slot></slot>
</div>
`;
return shadow;
}
// 方案C: CSS-in-JS(运行时注入)
cssInJS(styles) {
const styleId = `style-${this.appName}`;
let styleEl = document.getElementById(styleId);
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = styleId;
document.head.appendChild(styleEl);
}
styleEl.textContent = Object.keys(styles).map(selector => {
const rules = styles[selector];
return `${selector} { ${Object.entries(rules).map(([k, v]) => `${k}: ${v};`).join(' ')} }`;
}).join('\n');
}
}
// 2. JS沙箱(基于Proxy)
class JSSandbox {
constructor(appName) {
this.appName = appName;
this.proxyWindow = null;
this.modifiedProps = new Map();
this.originalValues = new Map();
}
create() {
const fakeWindow = Object.create(null);
this.proxyWindow = new Proxy(fakeWindow, {
get: (target, prop) => {
// 优先返回沙箱内的修改
if (this.modifiedProps.has(prop)) {
return this.modifiedProps.get(prop);
}
// 从真实window读取
const value = window[prop];
// 防止逃逸
if (prop === 'document' || prop === 'window' || prop === 'parent') {
return this.createSafeProxy(value);
}
return value;
},
set: (target, prop, value) => {
if (!this.originalValues.has(prop)) {
// 记录原始值
this.originalValues.set(prop, window[prop]);
}
// 记录修改
this.modifiedProps.set(prop, value);
// 可选:同步到真实window
if (prop === 'location' || prop === 'history') {
window[prop] = value;
}
return true;
},
has: (target, prop) => {
return this.modifiedProps.has(prop) || prop in window;
}
});
return this.proxyWindow;
}
destroy() {
// 恢复被修改的全局变量
for (const [prop, value] of this.originalValues) {
window[prop] = value;
}
this.modifiedProps.clear();
this.originalValues.clear();
}
createSafeProxy(obj) {
return new Proxy(obj, {
get: (target, prop) => {
if (prop === 'parent' || prop === 'top' || prop === 'opener') {
return null;
}
return target[prop];
}
});
}
}
// 使用沙箱
const sandbox = new JSSandbox('app1');
const sandboxWindow = sandbox.create();
// 在沙箱中执行代码
function execInSandbox(code, sandboxWindow) {
const script = `
with (sandbox) {
(function() {
${code}
}).call(sandbox);
}
`;
const func = new Function('sandbox', script);
func(sandboxWindow);
}
execInSandbox(`console.log('在沙箱中执行'); window.a = 1;`, sandboxWindow);
console.log(window.a); // undefined - 未污染全局
🎯 今日挑战
实现一个完整的微前端框架(简化版):
要求:
- 支持注册/注销子应用
- 实现路由驱动(hash或history)
- CSS隔离(动态添加/移除样式表)
- JS沙箱(基于Proxy)
- 全局通信机制
// 使用示例
const microApp = new MicroAppFramework();
microApp.registerApps([
{
name: 'react-app',
entry: 'http://localhost:3001',
container: '#react-container',
activeRule: '/react',
props: { theme: 'light' }
},
{
name: 'vue-app',
entry: 'http://localhost:3002',
container: '#vue-container',
activeRule: '/vue',
sandbox: {
strict: true, // 严格沙箱模式
css: 'shadow-dom' // 样式隔离策略
}
}
]);
microApp.start();
// 通信
microApp.on('global-message', (data) => {
console.log('收到消息:', data);
});
microApp.send('vue-app', { type: 'navigate', path: '/about' });
核心实现框架
class MicroAppFramework {
constructor() {
this.apps = new Map();
this.currentApp = null;
this.sandboxes = new Map();
this.eventBus = new EventBus();
// 监听路由变化
window.addEventListener('popstate', () => this.matchRoute());
}
registerApps(apps) {
apps.forEach(app => this.registerApp(app));
}
registerApp(config) {
this.apps.set(config.name, {
...config,
status: 'NOT_LOADED',
instance: null
});
}
async start() {
await this.loadApps();
this.matchRoute();
}
async loadApps() {
const loadPromises = [];
for (const [name, app] of this.apps) {
loadPromises.push(this.loadApp(name));
}
await Promise.all(loadPromises);
}
async loadApp(name) {
const app = this.apps.get(name);
if (app.status !== 'NOT_LOADED') return;
app.status = 'LOADING';
// 加载资源
const { template, scripts, styles } = await this.fetchResources(app.entry);
// 创建沙箱
const sandbox = new JSSandbox(name);
const proxyWindow = sandbox.create();
// 注入样式
const styleId = this.injectStyles(name, styles);
// 执行脚本
const execContext = this.execScripts(scripts, proxyWindow);
app.status = 'LOADED';
app.instance = {
sandbox,
styleId,
execContext,
mount: execContext.mount,
unmount: execContext.unmount
};
return app;
}
async fetchResources(entry) {
// 获取HTML入口
const html = await fetch(entry).then(r => r.text());
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// 提取资源
const scripts = Array.from(doc.querySelectorAll('script[src]')).map(s => s.src);
const styles = Array.from(doc.querySelectorAll('link[rel="stylesheet"]')).map(l => l.href);
const template = doc.querySelector('div#app')?.outerHTML || '';
return { template, scripts, styles };
}
matchRoute() {
const path = window.location.pathname;
for (const [name, app] of this.apps) {
if (this.matchActiveRule(path, app.activeRule)) {
if (this.currentApp !== name) {
this.switchApp(name);
}
break;
}
}
}
async switchApp(name) {
// 卸载当前应用
if (this.currentApp) {
const current = this.apps.get(this.currentApp);
if (current.instance?.unmount) {
await current.instance.unmount();
}
// 移除样式
if (current.instance?.styleId) {
const styleEl = document.getElementById(current.instance.styleId);
styleEl?.remove();
}
// 销毁沙箱
current.instance?.sandbox?.destroy();
}
// 加载并挂载新应用
const app = this.apps.get(name);
if (app.status !== 'LOADED') {
await this.loadApp(name);
}
const container = document.querySelector(app.container);
if (app.instance?.mount) {
await app.instance.mount(container, app.props);
}
this.currentApp = name;
}
matchActiveRule(path, pattern) {
if (typeof pattern === 'string') {
return path.startsWith(pattern);
}
if (pattern instanceof RegExp) {
return pattern.test(path);
}
if (typeof pattern === 'function') {
return pattern(path);
}
return false;
}
}
📊 微前端方案对比
| 方案 | 样式隔离 | JS隔离 | 性能 | 学习成本 | 适用场景 |
|---|---|---|---|---|---|
| Single-SPA | 需自行实现 | 需自行实现 | ⭐⭐⭐⭐ | ⭐⭐ | 复杂迁移 |
| Module Federation | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 新项目 |
| 无界 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 极致隔离 |
| qiankun | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | 中型项目 |
| EMP | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | Webpack生态 |
明日预告:浏览器API深度挖掘 - 20个你不知道的现代浏览器超能力
💡 架构智慧:微前端的本质不是技术,而是组织架构在技术上的映射——康威定律的完美体现!