这个难度要比CSS隔离难一些了。要考虑的东西也非常多。

方案:

  1. 使用 WebAssembly 进行隔离,WebAssembly 会被限制运行在一个安全的沙箱执行环境中,但运行时不能直接调用 Web API

  2. 使用 Web Worker 进行隔离,每个 Worker 有自己独立的 Isolate 实例。只能使用部分 Web API

  3. iframe 隔离: 空白页(src="about:blank") iframe 隔离和服务端同源的 iframe 隔离方案设计。不仅可以利用不同的浏览上下文实现彻底的微应用隔离,与普通 iframe 方案而言,还可以解决白屏体验问题,是微前端框架实现隔离的重要手段;但是无法调用 history API,URL 状态无法同步。

  4. iframe + Proxy 隔离: 解决空白页 iframe 隔离无法调用 history API 的问题,并可用于解决 iframe 方案中无法处理的 URL 状态同步问题;

  5. 快照隔离: 浏览器无法兼容 Proxy 时(ES6以下),可以通过简单的快照实现 window 变量的隔离,但是这种隔离方案限制较多,例如无法实现主子应用的隔离,无法实现多个微应用并存的隔离。当然大多数场景是一个时刻运行一个微应用,但是是一种兼容性良好的隔离方案(2024年了让我看看谁还在用IE6、7、8)。

讲方案之前,先给大家简单聊一下V8隔离方案

V8隔离

如果我们在最外层同时声明两个名字一样的值,就会导致变量名冲突(2024年非必要不要用var了,即便用了var后者也会覆盖前者,一样有bug)。

隔离的最初需求就是为了解决全局变量产生冲突,而单页面应用全局变量冲突的可能性更高。如果子应用处于 MPA 模式,那么 JS 可以做到天然隔离,这都得益于V8引擎对 JS 的执行上下文做了隔离处理。

V8核心概念:

  1. IsolateIsolate 是隔离的 V8 运行时实例,在 V8 中使用 Isolate 来实现 Web 页面、Web Workder 以及 Chrome 插件中的 JavaScript 运行时环境隔离。(物理隔离,同一个标签页中如果存在多个相同站点的页面,那么页面会共享 Isolate)

  2. HandleHandle (指向 JavaScript 对象在堆中存储的地址,如果 JavaScript 对象需要被释放,则首先会从 HandleScope 对应的栈中推出相应的 Handle,然后会被垃圾回收器标注,方便后续可以快速通过释放的 Handle 寻找需要被释放的 JavaScript 对象所在的内存地址。);HandleScope(主要用于管理 JavaScript 对象的生命周期的范围,在 C++ 中会开辟栈空间来存储 Handle,当栈中的 Handle 释放后,会从栈中推出该 Handle。如果释放 HandleScope,则栈中所有的 Handle 都会被释放,因此 Handle Scope 便于管理内部所有 Handle 的自动释放。)

  3. Context:JavaScript 中的 window 对象隔离则是通过 Context 来实现。所以一个页面有一个Isolate,一个Isolate有多个Context;全局执行上下文栈在 Isolate 中执行 JavaScript,可以通过切换 Context 来实现不同 JavaScript 代码的运行,可以简单理解为用于切换 JavaScript 中的 window 变量;执行上下文栈:在当前的 Context 中运行时,会有执行上下文栈的概念,即为:全局上下文、函数上下文以及 eval上下文。JavaScript 的执行通过上下文栈进行控制,当函数被执行时,当前函数对应的上下文会被推入一个上下文栈,当函数执行完毕后,上下文栈会弹出该函数的上下文,并将控制权返回给之前的上下文

V8效果:

  1. 如果两个 JS 文件在相同的全局执行上下文,声明的全局属性会产生覆盖

  2. 如果两个 JS 文件在不同的全局执行上下文,声明的全局属性互不干扰

以后还能单开一篇深层V8原理给大家(疯狂画饼骗关注,嘿嘿嘿)

iframe隔离与iframe+ Proxy 隔离

iframe隔离在 V8 的隔离中我们了解到可以通过创建不同的 Isolate 或者 Context 对 JS 代码进行上下文隔离处理,但是这种能力没有直接开放给浏览器,因此我们无法直接利用 Web API 实现微应用的 JS 隔离。但是在浏览器中创建 iframe 会创建相应的全局执行上下文栈,用于切换主应用和iframe 应用的全局执行上下文环境,因此可以通过在应用框架中创建空白的 iframe 来隔离微应用的 JS 运行环境。

思路:

  1. 通过请求获取后端的微应用列表数据(并进行预渲染),动态创建主导航

  2. 根据导航切换微应用,切换时会跨域请求微应用 JS 的文本内容并进行缓存处理

  3. 切换微应用的同时创建一个同域的 iframe 应用,请求主应用下空白的 HTML 进行渲染

  4. DOM 渲染完成后,微应用的 JS 会在 iframe 环境中通过 Script 标签进行隔离执行

但是存在以下问题未解决

  1. src = about:blank iframe 的 history 无法正常工作,框架的路由功能丢失

  2. 虽然 DOM 环境天然隔离,却无法使得 iframe 中的 Modal 相对于主应用居中

  3. 主应用和微应用的 URL 状态没有同步

iframe+ Proxy 隔离:

我们将 src = about:blank iframe 中的 history 使用主应用的 history 代替运行

  1. 不同微应用可以拥有各自 iframe 对应的全局上下文执行环境,可以实现 JS 的彻底隔离

  2. 使用主应用的 history,iframe 可以设置成 src = about:blank,不会产生运行时错误

  3. 使用主应用的 history,未来可以处理主应用和微应用的历史会话同步问题

我们就可以修改上面的思路4为:

  1. DOM 渲染完成后,微应用的 JS 会在 iframe 环境中通过iframe + Proxy + With代理隔离执行JS。

!!!只是提供简单的解决思路,真正要实现会话同步还需要考虑主子应用之间的路由冲突问题等等

Proxy 可以对需要访问的对象进行拦截,并可以通过拦截函数对返回值进行修改,这种特性可以使我们在微应用中访问 window 对象的属性时,返回定制化的属性值

代码demo:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>JS隔离</title>
</head>

<body>
  <!-- 主应用导航 -->
  <div id="nav"></div>
  <!-- 主应用内容区 -->
  <div id="container"></div>

  <script type="text/javascript">
    // 隔离类
    class IframeSandbox {
      // 沙箱配置信息
      options = null;
      // iframe 实例
      iframe = null;
      // 被代理的 iframe 的 Window 实例
      iframeWindow = null;
      // 是否执行过 JS
      execScriptFlag = false;

      constructor(options) {
        this.options = options;
        // 创建 iframe 时浏览器会创建新的全局执行上下文,用于隔离主应用的全局执行上下文
        this.iframe = this.createIframe();
        // 获取 iframe 的 Window 实例
        this.iframeWindow = this.iframe.contentWindow;
        // 代理 iframe 的 Window 实例
        this.proxyIframeWindow();
      }

      // 创建 iframe
      createIframe() {
        // 获取 iframe 基础配置
        const { rootElm, id, url } = this.options;
        const iframe = window.document.createElement("iframe");
        const attrs = {
          src: "about:blank", // 默认 src:about:blank不可变,仅作为隔离使用
          "app-id": id, // id
          "app-src": url, // url
          style: "border:none;width:100%;height:100%;", // 默认CSS
        };
        Object.keys(attrs).forEach((name) => {
          iframe.setAttribute(name, attrs[name]);
        });
        rootElm?.appendChild(iframe);
        return iframe;
      }

      // 被绑定的函数本身没有 prototype
      // 识别出被绑定的函数,构造函数
      isBoundedFunction(fn) {
        return (
          fn.name.indexOf("bound ") === 0 && !fn.hasOwnProperty("prototype")
        );
      }

      // 可以识别 Object、Array 等原生构造函数,也可以识别用户自己创建的构造函数
      isConstructable(fn) {
        return (
          fn.prototype &&
          // 通常情况下构造函数和类的 prototype.constructor 指向本身
          fn.prototype.constructor === fn &&
          // 通常情况下构造函数和类都会存在 prototype.constructor,因此长度至少大于 1
          // 需要注意普通函数中也会存在 prototype.constructor,
          // 因此如果 prototype 有自定义属性或者方法,那么可以判定为类或者构造函数,因此这里的判断是大于 1
          // 注意不要使用 Object.keys 进行判断,Object.keys 无法获取 Object.defineProperty 定义的属性
          Object.getOwnPropertyNames(fn.prototype).length > 1
        );
      }

      // 因此这里需要重新将这些原生 native api 的 this 修正为 iframe 的 window
      getTargetValue(target, prop) {
        const value = target[prop];
        // 过滤出 window.alert、window.addEventListener 等 API
        // 修正this 指向
        if (
          typeof value === "function" &&
          !this.isBoundedFunction(value) &&
          !this.isConstructable(value)
        ) {
          // 修正 value 的 this 指向为 target
          const boundValue = Function.prototype.bind.call(value, target);
          // 重新恢复 value 在 bound 之前的属性和原型(bind 之后会丢失)
          for (const key in value) {
            boundValue[key] = value[key];
          }
          // 如果原来的函数存在 prototype 属性,而 bound 之后丢失了,那么重新设置回来
          if (
            value.hasOwnProperty("prototype") &&
            !boundValue.hasOwnProperty("prototype")
          ) {
            boundValue.prototype = value.prototye;
          }
          return boundValue;
        }
        return value;
      }

      // 代理 iframe 的 Window 实例
      proxyIframeWindow() {
        this.iframeWindow.proxy = new Proxy(this.iframeWindow, {
          get: (target, prop) => {
            // 只解决 src:about:blank 下的 history 同域问题并没有真正设计主子应用的路由冲突问题
            if (prop === "history" || prop === "location") {
              // 获取主应用的 history 实例或者location 实例
              return window[prop];
            }

            if (prop === "window" || prop === "self") {
              // 获取iframe应用的 window 实例
              return this.iframeWindow.proxy;
            }

            // 获取主应用的 window 实例
            return this.getTargetValue(target, prop);
          },

          set: (target, prop, value) => {
            target[prop] = value;
            return true;
          },

          has: (target, prop) => true,
        });
      }

      // 执行 JS
      execScript() {
        const scriptElement =
          this.iframeWindow.document.createElement("script");
        scriptElement.textContent = `
              (function(window) {
                with(window) {
                  ${this.options.scriptText}
                }
              }).bind(window.proxy)(window.proxy);
              `;
        this.iframeWindow.document.head.appendChild(scriptElement);
      }

      // 激活
      async active() {
        this.iframe.style.display = "block";
        // 如果已经通过 Script 加载并执行过 JS,则无需重新加载处理
        if (this.execScriptFlag) return; // 跳过
        this.execScript();
        this.execScriptFlag = true;
      }

      // 预渲染
      prerender() {
        this.iframe.style.display = "none";
        // 如果已经通过 Script 加载并执行过 JS,则无需重新加载处理
        if (this.execScriptFlag) return; // 跳过
        this.execScript();
        this.execScriptFlag = true;
      }

      // 失活
      // INFO: JS 加载以后无法通过移除 Script 标签去除执行状态
      // INFO: 因此这里不是指代失活 JS,如果是真正想要失活 JS,需要销毁 iframe 后重新加载 Script
      inactive() {
        this.iframe.style.display = "none";
      }

      // 销毁沙箱
      destroy() {
        this.options = null;
        this.execScriptFlag = false;
        if (this.iframe) {
          this.iframe.parentNode?.removeChild(this.iframe);
        }
        this.iframe = null;
      }
    }

    // 微应用管理
    class MicroAppManager {
      scriptText = ""; // 缓存微应用的脚本文本,目前仅支持一个微应用
      // 隔离实例
      sandbox = null;
      // 微应用挂载的根节点
      rootElm = null;

      constructor(rootElm, app) {
        this.rootElm = rootElm;
        this.app = app;
      }

      // 获取 JS 文本(微应用服务需要支持跨域请求)
      async fetchScript() {
        try {
          const res = await window.fetch(this.app.script);
          return await res.text();
        } catch (err) {
          console.error(err);
        }
      }

      // 预渲染
      rerender() {
        // 当前主线程中存在多个并行执行的 requestIdleCallback 时,浏览器会根据空闲时间来决定要在当前 Frame 还是下一个 Frame 执行
        requestIdleCallback(async () => {
          // 预请求资源
          this.scriptText = await this.fetchScript();
          // 预渲染处理
          this.idlePrerender();
        });
      }

      // 预渲染
      idlePrerender() {
        // 预渲染
        requestIdleCallback((dealline) => {
          // 打印 空闲时
          console.log("deadline: ", dealline.timeRemaining());
          // 这里只有在浏览器非常空闲时才可以进行操作
          if (dealline.timeRemaining() > 40) {
            // TODO: active 中还可以根据 Performance 性能面板进行再分析,如果内部的某些操作比较耗时,可能会影响下一帧的渲染,则可以放入新的 requestIdleCallback 中进行处理
            // 除此之外,例如在子应用中可以先生成虚拟 DOM 树,预渲染不做 DOM 更改处理,真正切换应用的时候进行 DOM 挂载
            // 也可以在挂载应用的时候放入 raF 中进行处理
            this.active(true);
          } else {
            this.idlePrerender();
          }
        });
      }

      // 激活
      async active(isPrerender) {
        // 缓存资源处理
        if (!this.scriptText) {
          this.scriptText = await this.fetchScript();
        }

        // 如果没有创建沙箱,则实时创建
        // 需要注意只给激活的微应用创建 iframe 沙箱,因为创建 iframe 会产生内存损耗
        if (!this.sandbox) {
          this.sandbox = new IframeSandbox({
            rootElm: this.rootElm,
            scriptText: this.scriptText,
            url: this.app.script,
            id: this.app.id,
          });
        }

        isPrerender ? this.sandbox.prerender() : this.sandbox.active();
      }

      // 失活
      inactive() {
        this.sandbox?.inactive();
      }
    }

    // 微前端管理
    class MicroManager {
      // 微应用实例映射表
      appsMap = new Map(); // 微应用实例映射表
      rootElm = null; // 微应用挂载的根节点信息

      constructor(rootElm, apps) {
        this.rootElm = rootElm;
        // this.setAppMaps(apps);
        this.initApps(apps);
      }

      // 初始化微应用
      initApps(apps) {
        apps.forEach((app) => {
          const appManager = new MicroAppManager(this.rootElm, app);
          this.appsMap.set(app.id, appManager);
          if (app.prerender) {
            appManager.rerender();
          }
        });
      }

      // 激活微应用
      activeApp(id) {
        const current = this.appsMap.get(id);
        current && current.active();
      }

      // 失活微应用
      inactiveApp(id) {
        const current = this.appsMap.get(id);
        current && current.inactive();
      }
    }

    // 主应用管理
    class MainApp {
      microApps = []; // 微应用列表
      microManager = null; // 微前端管理实例

      constructor() {
        this.init();
      }

      async init() {
        this.microApps = await this.fetchMicroApps(); // 获取微应用列表
        this.createNav(); // 创建导航
        this.navClickListener(); // 监听导航点击事件
        this.hashChangeListener(); // 监听 hash 变化
        // 创建微前端管理实例
        this.microManager = new MicroManager(
          document.getElementById("container"),
          this.microApps
        );
      }

      // 从主应用服务器获请求微应用列表信息
      async fetchMicroApps() {
        /**
         * name: 微应用名称
         * id: 微应用 ID
         * script: 微应用 JS 文件地址
         * style: 微应用 CSS 文件地址
         * mount: 挂载到 window 上的启动函数 window.micro1_mount
         * unmount: 挂载到 window 上的启动函数 window.micro1_unmount
         * prerender: 预渲染函数(用于预加载,进行性能优化)
        */
        try {
          const res = await window.fetch("/microapps", {
            method: "post",
          });
          return await res.json();
        } catch (err) {
          console.error(err);
        }
      }

      // 根据微应用列表创建主导航
      createNav(microApps) {
        const fragment = new DocumentFragment(); // 创建文档片段
        this.microApps?.forEach((microApp) => {
          // TODO: APP 数据规范检测 (例如是否有 script)
          const button = document.createElement("button");
          button.textContent = microApp.name; // 导航按钮显示微应用名称
          button.id = microApp.id; // 导航按钮 id
          fragment.appendChild(button); // 添加到文档片段
        });
        nav.appendChild(fragment); // 添加到导航
      }

      // 导航点击的监听事件
      navClickListener() {
        const nav = document.getElementById("nav");
        nav.addEventListener("click", (e) => {
          // 并不是只有 button 可以触发导航变更,例如 a 标签也可以,因此这里不直接处理微应用切换,只是改变 Hash 地址
          // 不会触发刷新,类似于框架的 Hash 路由
          window.location.hash = event?.target?.id;
        });
      }

      // hash 路由变化的监听事件
      hashChangeListener() {
        // 监听 Hash 路由的变化,切换微应用(这里设定一个时刻只能切换一个微应用)
        window.addEventListener("hashchange", () => {
          this.microApps?.forEach(async ({ id }) => {
            id === window.location.hash.replace("#", "")
              ? this.microManager.activeApp(id)
              : this.microManager.inactiveApp(id);
          });
        });
      }
    }

    new MainApp();
  </script>
</body>

</html>

快照隔离

Window 快照会完全复用主应用的 Context,本质上没有形成隔离,仅仅是在主应用 Context 的基础上记录运行时需要的差异属性,每一个微应用内部都需要维护一个和主应用 window 对象存在差异的对象。不管是调用 Web API 还是设置 window 属性值,本质上仍然是在主应用的 window 对象上进行操作,只是会在微应用切换的瞬间恢复主应用的 window 对象,此方案无法做到真正的 Context 隔离,并且在一个时刻只能运行一个微应用,无法实现多个微应用同时运行。

如果微应用在运行时仅仅需要隔离 window 对象的属性冲突,那么快照隔离是一个非常不错的隔离方案

实现思路:

  1. 通过请求获取后端的微应用列表数据,动态创建主导航

  2. 根据导航切换微应用,切换时会跨域请求微应用 JS 的文本内容并进行缓存处理

  3. 切换微应用时需要先失活已经激活的微应用,确保一个时刻只有一个微应用运行

  4. 运行微应用前需要将微应用之前运行记录的 DIFF 对象和主应用的 window 快照进行合并,从而恢复微应用之前运行的 window 对象

  5. 失活微应用前需要先通过当前运行时的 window 对象和主应用 window 快照进行对比,计算出本次运行时的 DIFF 差异对象,为下一次恢复微应用的 window 对象做准备,同时通过快照恢复主应用的 window 对象

我们只需要修改MicroAppSandbox、MicroApp、MainApp即可实现

 class MicroAppSandbox {
    // 配置信息
    options = null;
    // 是否执行过 JS
    exec = false;
    // 微应用 JS 运行之前的主应用 window 快照
    mainWindow = {};
    // 微应用 JS 运行之后的 window 对象(用于理解)
    microWindow = {};
    // 微应用失活后和主应用的 window 快照存在差异的属性集合
    diffPropsMap = {};

    constructor(options) {
      this.options = options;
      // 重新包装需要执行的微应用 JS 脚本
      this.wrapScript = this.createWrapScript();
    }

    createWrapScript() {
      // 微应用的代码运行在立即执行的匿名函数中,隔离作用域
      return `;(function(window){
        ${this.options.scriptText}
      })(window)`;
    }

    execWrapScript() {
      // 在全局作用域内执行微应用代码
      (0, eval)(this.wrapScript);
    }

    // 微应用 JS 运行之前需要记录主应用的 window 快照(用于微应用失活后的属性差异对比)
    recordMainWindow() {
      for (const prop in window) {
        if (window.hasOwnProperty(prop)) {
          this.mainWindow[prop] = window[prop];
        }
      }
    }

    // 微应用 JS 运行之前需要恢复上一次微应用执行后的 window 对象
    recoverMicroWindow() {
      // 如果微应用和主应用的 window 对象存在属性差异
      // 上一次微应用 window = 主应用 window + 差异属性(在微应用失活前会记录运行过程中涉及到更改的 window 属性值,再次运行之前需要恢复修改的属性值)
      Object.keys(this.diffPropsMap).forEach((p) => {
        // 更改 JS 运行之前的微应用 window 对象,注意微应用本质上共享了主应用的 window 对象,因此一个时刻只能运行一个微应用
        window[p] = this.diffPropsMap[p];
      });
      // 用于课程理解
      this.microWindow = window;
    }

    recordDiffPropsMap() {
      // 这里的 microWindow 是微应用失活之前的 window(在微应用执行期间修改过 window 属性的 window)
      for (const prop in this.microWindow) {
        // 如果微应用运行期间存在和主应用快照不一样的属性值
        if (
          window.hasOwnProperty(prop) &&
          this.microWindow[prop] !== this.mainWindow[prop]
        ) {
          // 记录微应用运行期间修改或者新增的差异属性(下一次运行微应用之前可用于恢复微应用这一次运行的 window 属性)
          this.diffPropsMap[prop] = this.microWindow[prop];
          // 恢复主应用的 window 属性值
          window[prop] = this.mainWindow[prop];
        }
      }
    }

    active() {
      // 记录微应用 JS 运行之前的主应用 window 快照
      this.recordMainWindow();
      // 恢复微应用需要的 window 对象
      this.recoverMicroWindow();
      if (this.exec) {
        return;
      }
      this.exec = true;
      // 执行微应用(注意微应用的 JS 代码只需要被执行一次)
      this.execWrapScript();
    }

    inactive() {
      // 清空上一次记录的属性差异
      this.diffPropsMap = {};
      // 记录微应用运行后和主应用 Window 快照存在的差异属性
      this.recordDiffPropsMap();
      console.log(
        `${this.options.appId} diffPropsMap: `,
        this.diffPropsMap
      );
    }
  }
  class MicroApp {
   
    scriptText = "";

    sandbox = null;

    rootElm = null;

    constructor(rootElm, app) {
      this.rootElm = rootElm;
      this.app = app;
    }


    async fetchScript(src) {
      try {
        const res = await window.fetch(src);
        return await res.text();
      } catch (err) {
        console.error(err);
      }
    }


    async active() {

      if (!this.scriptText) {
        this.scriptText = await this.fetchScript(this.app.script);
      }

      if (!this.sandbox) {
        this.sandbox = new MicroAppSandbox({
          scriptText: this.scriptText,
          appId: this.app.id,
        });
      }

      this.sandbox.active();

      // 获取元素并进行展示,这里先临时约定微应用往 body 下新增 id 为 `${this.app.id}-dom` 的元素
      const microElm = document.getElementById(`${this.app.id}-dom`);
      if (microElm) {
        microElm.style = "display: block";
      }
    }

    inactive() {
    
     // 获取元素并进行隐藏,这里先临时约定微应用往 body 下新增 id 为 `${this.app.id}-dom` 的元素
      const microElm = document.getElementById(`${this.app.id}-dom`);
      if (microElm) {
        microElm.style = "display: none";
      }
      this.sandbox?.inactive();
    }
  }


  class MicroApps {

    appsMap = new Map();

    rootElm = null;

    constructor(rootElm, apps) {
      this.rootElm = rootElm;
      this.setAppMaps(apps);
    }

    setAppMaps(apps) {
      apps.forEach((app) => {
        this.appsMap.set(app.id, new MicroApp(this.rootElm, app));
      });
    }


    prefetchApps() {}


    activeApp(id) {
      const app = this.appsMap.get(id);
      app?.active();
    }


    inactiveApp(id) {
      const app = this.appsMap.get(id);
      app?.inactive();
    }
  }

  class MainApp {
    microApps = [];
    microAppsManager = null;

    constructor() {
      this.init();
    }

    async init() {
      this.microApps = await this.fetchMicroApps();
      this.createNav();
      this.navClickListener();
      this.hashChangeListener();
      this.microAppsManager = new MicroApps(
        document.getElementById("container"),
        this.microApps
      );
    }

    async fetchMicroApps() {
      try {
        const res = await window.fetch("/microapps", {
          method: "post",
        });
        return await res.json();
      } catch (err) {
        console.error(err);
      }
    }
    
    createNav(microApps) {
      const fragment = new DocumentFragment();
      this.microApps?.forEach((microApp) => {
        const button = document.createElement("button");
        button.textContent = microApp.name;
        button.id = microApp.id;
        fragment.appendChild(button);
      });
      nav.appendChild(fragment);
    }

    navClickListener() {
      const nav = document.getElementById("nav");
      nav.addEventListener("click", (e) => {
        // 此时有一个微应用已经被激活运行
        console.log("主应用 window.a: ", window.a);
        window.location.hash = event?.target?.id;
      });
    }

    hashChangeListener() {
      window.addEventListener("hashchange", () => {
      
        // 需要失活应用,为了确保一个时刻只能激活一个应用(这里可以设计微应用的运行状态,根据状态进行处理)
        this.microApps?.forEach(async ({ id }) => {
          if (id !== window.location.hash.replace("#", "")) {
            this.microAppsManager.inactiveApp(id);
          }
        });

        // 没有微应用被激活时,主应用的 window 对象会被恢复
        console.log("恢复主应用的 window.a: ", window.a);

        // 激活应用
        this.microApps?.forEach(async ({ id }) => {
          if (id === window.location.hash.replace("#", "")) {
            this.microAppsManager.activeApp(id);
          }
        });

      });
    }
  }

  new MainApp();
  • 可以解决 let 或者 const 声明变量的隔离问题

  • 可以解决微应用之间的全局属性隔离问题,包括使用未限定标识符的变量、this

  • 无法实现主应用和微应用同时运行时的全局属性隔离问题

未曾清贫难成人,不经打击老天真。 自古英雄出炼狱,从来富贵入凡尘。 醉生梦死谁成气,拓马长枪定乾坤。 挥军千里山河在,立名扬威传后人。