微前端原理(篇一)

#微前端

目录

1. 总结

  • 微前端核心原理:
    • ==①== 应用之间隔离:确保应用间 JS CSS DOM 互不干扰
      • js 隔离
      • css 隔离
    • ==②== 生命周期管理
      • 主应用统一管理:加载 → 挂载 → 更新 → 卸载
    • ==③== 通信机制:主应用和子应用通讯
      • event bus:事件总线
      • props
      • 全局状态库
      • 自定义事件
    • ==④== 路由分发
      • 主应用统一管理,分发到不同子应用
  • 常用的 JavaScript 沙箱
    • ==①== 快照沙箱:
      • 启动时机
        • 启动时记录全局状态,在沙箱关闭时恢复全局状态
          • 启动:记录当前window对象快照
          • 关闭:还原window对象
        • ==进入一个新的容器,那么就使用启动,否则关闭==
      • 缺点:无法支持多个子应用同时运行
      • 性能:快照和恢复操作可能比较耗时
    • ==②== 代理沙箱
      • ==原理==:
        • 使用 Proxy 代理对象来实现沙箱,可以精确控制对全局对象的访问和修改
      • 优点:
        • 支持多个实例、多个应用
          • ==优先==从自己的环境中取值
          • 否则从全局取值
        • 性能好,隔离性强
      • 缺点
        • proxy 的兼容问题
    • ==③== Legacy 沙箱:
      • 基于 with + eval + new Function
    • ==④== iframe 沙箱:
      • 天然的沙箱
    • ==⑤== Node.js 的 vm2 模块 或者 go 的 js 沙箱模块
    • ==⑥== 组合沙箱 (Composite Sandbox)
  • 样式隔离方案
    • Shadow DOM 隔离
    • 应用切换时:动态样式表切换
      • 性能开销大,会出现闪烁
    • css modules
    • BFM 命名规范
    • 子应用添加唯一前缀等
  • 应用间通讯方式
    • 事件总线,比如 event-bus
    • 状态管理
      • 结合状态管理库比如 redux 或者 pinia 的==订阅==
      • 共享实例
    • 直接调用
    • props 传递等
  • qiankun 实现原理
    • 主应用:
      • 注册子应用,比如
        • entry
        • container
        • acitveRule
      • 监听全局路由,匹配需要加载、切换、卸载子应用等
        • fetch index.html → 抽取 js ,然后 eval
          • fetch 需要同域
        • 需要 umd 格式,故需要修改 webpack
        • 应用间相关跳转时,需要及时卸载
        • 样式隔离,两种方案
          1. 命名空间,类似于 vue scope style
          2. webcomponent
  • js 沙箱启动时机
    1. 子应用==加载时==启动沙箱
    2. 子应用==挂载时==启动沙箱
    3. 基于路由的沙箱管理
      • 找到要进入的路由对应的应用名
      • 如果果离开的是子应用,==关闭其沙箱==
      • 如果进入的是子应用,==启动其沙箱==
      • 可配置是否启动沙箱
    4. 当容器进入视口时启动沙箱
      • 使用IntersectionObserver 可优化性能
  • 问:如何实现window.addEventListener('micro-app-message', handler)
    • new CustomEvent('micro-app-message'
      • 然后 再 window.dispatchEvent
  • js 沙箱==性能优化==的要点
    • 按需启动沙箱
    • 使用 WeakMap 存储状态,避免内存泄漏
    • 使用 requestIdleCallback 进行==初始化==
    • 资源共享:比如 React、vue 、各类组件等
      • sandbox.setGlobalVariables(globalVars);
  • 基于DOM节点的隔离:子应用使用容器沙箱
    • container.attachShadow({ mode: 'closed' });

另外可参考

2. 微前端的核心原理

  • 应用隔离:确保各个子应用之间的 JavaScript、CSS、DOM 互不干扰
    • JavaScript 沙箱:
      • 确保各个子应用的 JS 运行环境相互隔离
    • CSS 隔离:
      • 防止样式冲突
    • 全局变量隔离:
      • 避免全局变量污染
  • 生命周期管理:主应用统一管理各个子应用的加载、挂载、卸载等生命周期
    • 加载(bootstrap)
    • 挂载(mount)
    • 卸载(unmount)
    • 更新(update)
  • 通信机制:在主应用和子应用之间建立通信渠道
    • 基于事件总线
    • 基于 Props 传递
    • 基于全局状态管理
    • 基于自定义事件
  • 路由分发:
    • 统一的路由管理,将不同路由分发到对应的子应用

3. 常见的 JavaScript 沙箱方案

主要解决子应用,污染了主应用的 window 对象

3.1. 快照沙箱 (Snapshot Sandbox)

快照沙箱的核心思想是在沙箱启动时记录全局状态,在沙箱关闭时恢复全局状态。

  • 比如,==进入一个新的容器,那么就使用启动,否则关闭==
  • 优点:
    • 实现简单
  • 缺点
    • 无法支持多个子应用同时运行
    • 性能:
      • 快照和恢复操作可能比较耗时
class SnapshotSandbox {
  constructor() {
    this.snapshot = {}; // 存储快照
    this.modifyPropsMap = {}; // 存储被修改的属性
  }

  // 激活沙箱
  active() {
    // 1. 记录当前window对象的快照
    for (const prop in window) {
      this.snapshot[prop] = window[prop];
    }

    // 2. 恢复之前被修改的属性
    Object.keys(this.modifyPropsMap).forEach(prop => {
      window[prop] = this.modifyPropsMap[prop];
    });
  }

  // 关闭沙箱
  inactive() {
    // 1. 记录被修改的属性
    for (const prop in window) {
      if (window[prop] !== this.snapshot[prop]) {
        this.modifyPropsMap[prop] = window[prop];
        // 2. 恢复原来的属性值
        window[prop] = this.snapshot[prop];
      }
    }
  }
}

// 使用示例
const sandbox = new SnapshotSandbox();
sandbox.active();   // 激活沙箱
// 在沙箱中运行代码
window.newVar = "test";
sandbox.inactive(); // 关闭沙箱
console.log(window.newVar); // undefined

3.2. 代理沙箱 (Proxy Sandbox)

使用 Proxy 代理对象来实现沙箱,可以精确控制对全局对象的访问和修改。

  • 优点:
    • 支持多个实例,多个应用
    • 性能较好
    • 隔离性强
  • 缺点:
    • 不支持低版本浏览器(需要 Proxy 支持)
    • 某些特殊场景可能存在兼容性问题
class ProxySandbox {
  constructor() {
    this.running = false;
    this.proxyWindow = {};
    
    const fakeWindow = Object.create(null);
    const proxy = new Proxy(fakeWindow, {
      set: (target, prop, value) => {
        if (this.running) {
          target[prop] = value;
          return true;
        }
        return false;
      },
      
      get: (target, prop) => {
        // 优先从代理对象中取值
        if (prop in target) {
          return target[prop];
        }
        // 否则从真实window对象中取值
        const value = window[prop];
        return typeof value === 'function' 
          ? value.bind(window) 
          : value;
      },

      has: (target, prop) => {
        return prop in target || prop in window;
      }
    });

    this.proxy = proxy;
  }

  active() {
    this.running = true;
  }

  inactive() {
    this.running = false;
  }
}

// 使用示例
const sandbox = new ProxySandbox();
sandbox.active();
sandbox.proxy.newVar = "test";
console.log(window.newVar); // undefined
console.log(sandbox.proxy.newVar); // "test"

3.3. Legacy 沙箱 (基于 with + eval)

使用 with 语句eval 来实现简单的沙箱隔离。

class LegacySandbox {
  constructor(context = {}) {
    this.context = context;
  }

  run(code) {
    const contextStr = Object.keys(this.context)
      .map(key => `let ${key} = this.context.${key}`)
      .join(';');
    
    return new Function(`
      with (this.context) {
        ${contextStr};
        return eval(\`${code}\`);
      }
    `).call(this);
  }
}

// 使用示例
const sandbox = new LegacySandbox({
  name: 'test',
  log: console.log
});

sandbox.run(`
  log(name);  // 输出: test
  log(window); // window 是未定义的
`);

3.4. iframe 沙箱

利用 iframe 的天然隔离特性实现沙箱。

class IframeSandbox {
  constructor() {
    this.iframe = document.createElement('iframe');
    this.iframe.style.display = 'none';
    document.body.appendChild(this.iframe);
    
    this.global = this.iframe.contentWindow;
  }

  run(code) {
    // 注入代码到 iframe 环境中执行
    const script = this.iframe.contentDocument.createElement('script');
    script.text = code;
    this.iframe.contentDocument.body.appendChild(script);
  }

  destroy() {
    document.body.removeChild(this.iframe);
  }
}

// 使用示例
const sandbox = new IframeSandbox();
sandbox.run(`
  window.testVar = "hello";
  console.log(window.testVar);
`);
console.log(window.testVar); // undefined

3.5. VM 沙箱 (基于 vm2)

使用 Node.js 的 vm2 模块实现更安全的沙箱(仅在 Node.js 环境中可用)。

const { VM } = require('vm2');

class VMSandbox {
  constructor(context = {}) {
    this.vm = new VM({
      timeout: 1000,
      sandbox: context
    });
  }

  run(code) {
    return this.vm.run(code);
  }
}

// 使用示例
const sandbox = new VMSandbox({
  name: 'test',
  console: console
});

sandbox.run(`
  console.log(name); // 输出: test
  // 以下代码将抛出错误,因为process是未定义的
  // console.log(process.env);
`);

无论是 Go 或者其他语言其实都有 JS 沙箱的实现,因为要基于它来做很多其他事情

一些低代码平台,为了增强编排的能力等,后端使用了 JS 沙箱,支持更强更灵活的配置编排能力

3.6. 组合沙箱 (Composite Sandbox)

在实际应用中,我们可能需要组合多种沙箱技术来实现更完善的隔离。

class CompositeSandbox {
  constructor() {
    this.proxySandbox = new ProxySandbox();
    this.snapshotSandbox = new SnapshotSandbox();
    this.running = false;
  }

  active() {
    if (!this.running) {
      this.snapshotSandbox.active();
      this.proxySandbox.active();
      this.running = true;
    }
  }

  inactive() {
    if (this.running) {
      this.snapshotSandbox.inactive();
      this.proxySandbox.inactive();
      this.running = false;
    }
  }

  // 获取代理对象
  get proxyWindow() {
    return this.proxySandbox.proxy;
  }
}

// 使用示例
const sandbox = new CompositeSandbox();
sandbox.active();

const proxyWindow = sandbox.proxyWindow;
proxyWindow.newVar = "test";

console.log(window.newVar); // undefined
console.log(proxyWindow.newVar); // "test"

sandbox.inactive();

4. 样式隔离方案与 JS 隔离

图片&文件

4.1.  Shadow DOM 隔离

  • 完全隔离,最彻底的方案
  • 浏览器原生支持
  • 弹窗类组件需要特殊处理 ❓
    • 其实不用,正好可以解决弹窗问题呀?

4.2. 动态样式表切换

应用切换时动态切换样式表

  • 常用于 qiankun 等方案中, 性能开销较大,可能出现样式闪烁

4.3. CSS Modules 方案

  • 优点
    • 编译时处理,运行时零开销
    • 局部作用域,避免冲突
  • 缺点
    • 需要修改构建配置
    • 所有样式需要模块化处理

4.4. BEM 命名约定、css-In—js、子应用添加唯一前缀 等

通过规范的命名约定来避免样式冲突

5. 常见的微前端方案及对比

特性无界(wujie)qiankunmicro-appsingle-spaiframeModule Federation
基础实现WebComponent + iframesingle-spa + import-html-entryWebComponent路由劫持原生 iframeWebpack 5 模块联邦
隔离方案CSS/JS 完全隔离快照沙箱/代理沙箱Shadow DOM天然隔离
性能优秀一般较好较好一般优秀
预加载支持支持支持不支持不支持支持
通信方式props + 发布订阅props + 发布订阅CustomEvent + 数据属性发布订阅postMessage模块导入导出
子应用改造极少中等较少较多中等
主应用改造较少中等较少较多中等
技术栈限制需使用 Webpack 5
子应用共享依赖支持支持不支持支持不支持原生支持
样式隔离完全隔离动态样式表切换Shadow DOM完全隔离
JS 沙箱iframe + ProxyProxy/Snapshotiframe Proxy天然隔离
CSP 策略友好不友好较友好友好友好友好
子应用调试便捷一般便捷一般便捷便捷

6. 实现一个主应用和子应用之间的通信系统

图片&文件

这个通信系统提供了三种主要的通信方式:

  1. 事件总线:用于发布-订阅模式的事件通信
  2. 状态管理:用于共享数据和状态
  3. 直接调用:用于直接调用其他应用提供的方法

主要特点:

  • 支持多种通信方式
  • 类型安全
  • 统一的API
  • 支持事件解绑和状态取消订阅
  • 错误处理
  • 支持所有框架(框架无关)

在实际使用中,可以根据需求选择合适的通信方式,并可以进一步扩展功能,如:

  • 添加通信日志
  • 实现通信加密
  • 添加权限控制
  • 实现通信超时处理
  • 添加消息队列
  • 实现通信重试机制

7. qiankun 的实现原理

更多见个人流程图整理: figjam

基座应用,需要做以下事情

  • 负责注册子应用,示例如下
    • entry 子应用 HTML 的入口去哪儿拿
    • container:==渲染到哪儿==
    • activeRule:路由匹配规则
    • 图片&文件
  • 基座里,需要监听全局路由,然后找到匹配子应用,然后加载子应用,再然后卸载或切换等
    • fetch 子应用的入口文件 index.html ,然后需要抽取 js , eval 执行它
    • 所以,需要==处理成兼容的 umd 格式==,故需要修改 webpack
    • fetch 所以要求同域
    • 执行完子应用的脚本后,需要挂载 #app 上,但可能会直接覆盖丢主应用;
      • 所以才会要求子应用有自己的 container 属性,这也是为什么建议子应用 name/id 唯一;
    • 图片路径可能 404,所以需要注入正确的子应用 public path
    • 两个子应用相互跳转时,如果不及时卸载,可能会出现两个子应用同时展示的情况
    • 关于样式隔离,两种方案
      • 命名空间,类似于 vue style scope
      • webcomponet 方案

8. 路由分发原理

图片&文件

9. 其他微前端框架和实现原理

9.1. iframe

图片&文件

9.2. systemjs : type=systemjs-importmap

图片&文件

9.3. micro-app

图片&文件

10. 微前端中JS沙箱具体使用场景

10.1. JS沙箱启动时机

10.1.1. 常见的启动时机

  1. 子应用==加载时==启动沙箱
  2. 子应用==挂载时==启动沙箱
// 1. 子应用加载时启动沙箱
class MicroApp {
  async loadApp(appConfig) {
    // 创建沙箱实例
    const sandbox = new Sandbox(appConfig.name);
    // 启动沙箱
    sandbox.start();
    
    try {
      // 加载子应用资源
      await loadScript(appConfig.entry);
    } catch (error) {
      console.error('Load app failed:', error);
    }
  }
}

// 2. 子应用挂载时启动沙箱
class Sandbox {
  async mount() {
    // 记录当前快照
    this.snapshotBefore = this.takeSnapshot();
    
    // 激活沙箱
    this.active = true;
    
    // 执行子应用的 mount 钩子
    await this.app.mount();
  }
  
  async unmount() {
    // 执行子应用的 unmount 钩子
    await this.app.unmount();
    
    // 关闭沙箱
    this.active = false;
    
    // 还原环境
    this.restoreSnapshot(this.snapshotBefore);
  }
}

10.2. 混合页面场景处理

10.2.1. 基于路由的隔离:匹配到路由了,启动

router.beforeEach 中检测是否需要启动

// 路由配置
const routes = [
  {
    path: '/main/*',  // 主应用路由
    component: MainApp,
    sandbox: false    // 不启用沙箱
  },
  {
    path: '/sub/*',   // 子应用路由
    component: SubApp,
    sandbox: true     // 启用沙箱
  }
];

// 路由守卫
router.beforeEach((to, from, next) => {
  const needSandbox = to.matched.some(record => record.sandbox);
  if (needSandbox) {
    // 启动沙箱
    sandbox.start();
  } else {
    // 关闭沙箱
    sandbox.stop();
  }
  next();
});

10.2.2. 基于DOM节点的隔离:子应用使用容器沙箱

class DomSandbox {
  constructor(container) {
    this.container = container;
    this.shadowRoot = container.attachShadow({ mode: 'closed' });
  }
  
  mount(component) {
    // 在 Shadow DOM 中渲染子应用
    this.shadowRoot.innerHTML = '';
    this.shadowRoot.appendChild(component);
  }
}

// 使用示例
const mainApp = document.querySelector('#main-app');
const subApp = document.querySelector('#sub-app');

// 子应用容器使用沙箱
const sandbox = new DomSandbox(subApp);
sandbox.mount(subAppComponent);

关于 { mode: 'closed' } ,更多参考 8. Shadow DOM 中的 closed mode 和 open mode

10.3. 不同类型的沙箱实现

10.3.1. 快照沙箱(适用于单个子应用)

class SnapshotSandbox {
  constructor() {
    this.snapshot = {};
    this.modifyPropsMap = {};
  }
  
  start() {
    // 记录当前window对象快照
    for (const prop in window) {
      this.snapshot[prop] = window[prop];
    }
  }
  
  stop() {
    // 还原window对象
    for (const prop in this.modifyPropsMap) {
      if (this.snapshot[prop] === undefined) {
        delete window[prop];
      } else {
        window[prop] = this.snapshot[prop];
      }
    }
  }
}

10.3.2. 代理沙箱(适用于多个子应用)

如何适用多个子应用的?

  • ==优先==从自己的环境中取值
  • 否则从全局取值
class ProxySandbox {
  constructor() {
    const fakeWindow = {};
    const proxy = new Proxy(fakeWindow, {
      get: (target, prop) => {
        // 优先从自己的环境中取值
        if (prop in target) {
          return target[prop];
        }
        // 否则从全局取值
        return window[prop];
      },
      set: (target, prop, value) => {
        target[prop] = value;
        return true;
      }
    });
    
    this.proxy = proxy;
  }
  
  start() {
    // 将代理对象作为子应用的全局对象
    window.__PROXY__ = this.proxy;
  }
  
  stop() {
    // 清理代理对象
    window.__PROXY__ = undefined;
  }
}

10.3.3. 组合沙箱(更完整的隔离)

class CompositeSandbox {
  constructor() {
    this.proxySandbox = new ProxySandbox();
    this.domSandbox = new DomSandbox();
    this.eventSandbox = new EventSandbox();
  }
  
  async start() {
    // 启动所有沙箱
    this.proxySandbox.start();
    this.domSandbox.start();
    this.eventSandbox.start();
  }
  
  async stop() {
    // 停止所有沙箱
    this.eventSandbox.stop();
    this.domSandbox.stop();
    this.proxySandbox.stop();
  }
}

10.4. 特殊场景处理

10.4.1. 共享依赖处理 → 比如共享 React、ReactDom 等全局类库

class SharedDependencySandbox {
  constructor(shared = {}) {
    this.shared = shared;
  }
  
  start() {
    // 注入共享依赖
    Object.keys(this.shared).forEach(key => {
      window[key] = this.shared[key];
    });
  }
}

// 使用示例
const sandbox = new SharedDependencySandbox({
  React: window.React,
  ReactDOM: window.ReactDOM
});

10.4.2. 通信机制

class MessageSandbox {
  constructor() {
    this.listeners = new Map();
  }
  
  // 发送消息
  postMessage(type, data) {
    const event = new CustomEvent('micro-app-message', {
      detail: { type, data }
    });
    window.dispatchEvent(event);
  }
  
  // 监听消息
  addEventListener(type, callback) {
    const listener = (event) => {
      if (event.detail.type === type) {
        callback(event.detail.data);
      }
    };
    
    this.listeners.set(callback, listener);
    window.addEventListener('micro-app-message', listener);
  }
  
  // 移除监听
  removeEventListener(callback) {
    const listener = this.listeners.get(callback);
    if (listener) {
      window.removeEventListener('micro-app-message', listener);
      this.listeners.delete(callback);
    }
  }
}

10.5. 最佳实践建议

10.5.1. 性能优化

  • 使用 WeakMap 存储状态,避免内存泄漏
  • 使用 requestIdleCallback 进行初始化
class OptimizedSandbox {
  constructor() {
    // 使用 WeakMap 存储状态,避免内存泄漏
    this.state = new WeakMap();
    
    // 使用 requestIdleCallback 进行初始化
    requestIdleCallback(() => {
      this.init();
    });
  }
  
  init() {
    // 初始化沙箱环境
  }
}

10.5.2. 错误处理

class ErrorBoundarySandbox {
  async executeInSandbox(code) {
    try {
      // 在沙箱中执行代码
      const result = await this.proxy.eval(code);
      return result;
    } catch (error) {
      // 错误处理
      console.error('Sandbox execution error:', error);
      // 通知主应用
      this.reportError(error);
      // 尝试恢复
      this.recover();
    }
  }
}

10.5.3. 生命周期管理

class LifecycleSandbox {
  constructor() {
    this.status = 'inactive';
    this.hooks = new Map();
  }
  
  registerHook(name, fn) {
    this.hooks.set(name, fn);
  }
  
  async start() {
    this.status = 'starting';
    await this.executeHook('beforeStart');
    // 启动沙箱逻辑
    this.status = 'active';
    await this.executeHook('afterStart');
  }
  
  async stop() {
    this.status = 'stopping';
    await this.executeHook('beforeStop');
    // 停止沙箱逻辑
    this.status = 'inactive';
    await this.executeHook('afterStop');
  }
}

使用建议:

  1. 根据应用场景选择合适的沙箱类型
  2. 注意性能影响,避免频繁创建销毁沙箱
  3. 合理处理共享资源和通信机制
  4. 实现完善的错误处理和恢复机制
  5. 做好沙箱的生命周期管理
  6. 考虑浏览器兼容性问题

11. 主应用和多个子应用并存时的沙箱处理方案:

11.1. 基于路由的沙箱管理(最常用)

关键点:

  • 找到要进入的路由对应的应用名
  • 如果果离开的是子应用,==关闭其沙箱==
  • 如果进入的是子应用,==启动其沙箱==
// 路由监听
router.beforeEach((to, from, next) => {
  // 找到要进入的路由对应的应用名
  const toAppName = to.matched[0]?.appName;
  // 找到要离开的路由对应的应用名
  const fromAppName = from.matched[0]?.appName;

  // 如果离开的是子应用,关闭其沙箱
  if (fromAppName) {
    sandboxManager.stopSandbox(fromAppName);
  }

  // 如果进入的是子应用,启动其沙箱
  if (toAppName) {
    sandboxManager.startSandbox(toAppName);
  }

  next();
});

11.1.1. 简单的沙箱管理类

// 简单的沙箱管理类
class SandboxManager {
  constructor() {
    // 存储所有子应用的沙箱实例
    this.sandboxes = new Map();
  }

  // 启动某个子应用的沙箱
  startSandbox(appName) {
    const sandbox = this.sandboxes.get(appName);
    if (sandbox) {
      sandbox.active = true;
    }
  }

  // 关闭某个子应用的沙箱
  stopSandbox(appName) {
    const sandbox = this.sandboxes.get(appName);
    if (sandbox) {
      sandbox.active = false;
    }
  }
}

// 路由配置示例
const routes = [
  {
    path: '/',              // 主应用路由
    component: MainApp,     // 不需要沙箱
  },
  {
    path: '/app1/*',        // 子应用1
    component: MicroApp1,
    appName: 'app1'         // 需要沙箱
  },
  {
    path: '/app2/*',        // 子应用2
    component: MicroApp2,
    appName: 'app2'         // 需要沙箱
  }
];


11.2. 基于 DOM 结构的沙箱管理(混合页面场景)

// HTML 结构
<div id="main-app">
  <!-- 主应用内容 -->
  <header>主应用的头部</header>
  
  <!-- 子应用容器 -->
  <div id="sub-app1"></div>
  <div id="sub-app2"></div>
</div>

// JavaScript 代码
class SimpleSandbox {
  constructor(appName, container) {
    this.appName = appName;
    this.container = container;
  }

  // 启动沙箱
  start() {
    console.log(`${this.appName} sandbox started`);
    // 这里添加沙箱隔离逻辑
  }

  // 关闭沙箱
  stop() {
    console.log(`${this.appName} sandbox stopped`);
    // 这里添加清理逻辑
  }
}


11.2.1. 当容器进入视口时启动沙箱:IntersectionObserver 为了性能优化

// 初始化子应用
function initSubApp(appName, containerId) {
  const container = document.getElementById(containerId);
  const sandbox = new SimpleSandbox(appName, container);
  
  // 当容器进入视口时启动沙箱
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        sandbox.start();
      } else {
        sandbox.stop();
      }
    });
  });
  
  observer.observe(container);
}

// 初始化所有子应用
initSubApp('app1', 'sub-app1');
initSubApp('app2', 'sub-app2');

11.3. 主子应用通信场景

// 简单的消息通道
class MessageChannel {
  constructor() {
    this.listeners = new Map();
  }

  // 发送消息
  send(from, to, message) {
    const event = new CustomEvent('micro-app-message', {
      detail: { from, to, message }
    });
    window.dispatchEvent(event);
  }

  // 接收消息
  listen(appName, callback) {
    const handler = (event) => {
      const { from, to, message } = event.detail;
      if (to === appName || to === '*') {
        callback(message, from);
      }
    };

    window.addEventListener('micro-app-message', handler);
    this.listeners.set(appName, handler);
  }
}

// 使用示例
const messageChannel = new MessageChannel();

// 主应用发送消息给子应用
messageChannel.send('main', 'app1', { type: 'update', data: {...} });

// 子应用监听消息
messageChannel.listen('app1', (message, from) => {
  console.log(`收到来自 ${from} 的消息:`, message);
});

11.4. 实际应用建议

11.4.1. 按需启动

// 只在必要时启动沙箱
if (isSubApp(appName)) {
  sandbox.start();
}

11.4.2. 资源共享

// 可以设置一些公共资源不进入沙箱
const globalVars = ['React', 'Vue', 'jQuery'];
sandbox.setGlobalVariables(globalVars);

11.4.3. 性能优化

// 使用延迟加载
const sandbox = new Proxy({}, {
  get(target, property) {
    // 只在实际使用时初始化
    if (!target.instance) {
      target.instance = new Sandbox();
    }
    return target.instance[property];
  }
});

这样的实现方式更加清晰和实用,主要关注点在于:

  • 何时启动沙箱(进入子应用时)
  • 何时关闭沙箱(离开子应用时)
  • 如何处理多个子应用(每个子应用独立的沙箱实例)
  • 主子应用如何通信(消息通道)

这种方式可以确保:

  1. 主应用正常运行不受影响
  2. 子应用间相互隔离
  3. 资源可以按需加载和释放
  4. 维护成本相对较低

12. 更多

  • 再把之前整理的草稿流程图看看,详见 figjam
  • https://www.garfishjs.org/blog/architecture.html
  • https://juejin.cn/post/7113503219904430111