每天一个高级前端知识 - Day 10

2 阅读4分钟

每天一个高级前端知识 - 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 - 未污染全局

🎯 今日挑战

实现一个完整的微前端框架(简化版):

要求:

  1. 支持注册/注销子应用
  2. 实现路由驱动(hash或history)
  3. CSS隔离(动态添加/移除样式表)
  4. JS沙箱(基于Proxy)
  5. 全局通信机制
// 使用示例
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个你不知道的现代浏览器超能力

💡 架构智慧:微前端的本质不是技术,而是组织架构在技术上的映射——康威定律的完美体现!