老席老席杂货铺
Picture of github

微前端概览

背景

考虑下面问题:

  • 在开发一个大系统的时候,每个模块可能有不同的团队在开发,大家用的技术栈也可能不相同,如何将所有子模块整合到一个系统里,需要考虑。
  • 大系统中,多个模块更新时间不一致,如何解决?
  • 当一个系统有小变大时,想使用新的技术,同时又不想重写老的系统,或者没有资源重新老的系统,要怎么办?

什么是微前端

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

微前端特点

  • 技术栈无关。主系统框架不限制接入应用的技术栈,微应用具备完全自主权
  • 开发独立,部署独立。每个微应用都可以作为独立的应用提供服务
  • 增量升级。当在老的系统中想使用新技术时,可以通过微应用的方式做增量升级,而不用动老的系统
  • 应用隔离。每个微应用状态相互隔离,运行时状态互不影响

微前端实现方式

微前端iframe

微前端的实现方式最容易想到的就是iframe。

优点

  • iframe 提供了完美的沙箱机制,不论是样式隔离、js 隔离这类问题统统都能被完美解决。

缺点

  • url 不同步。浏览器刷新iframe url状态丢失,后退前进按钮无法使用。
  • UI 不同步,DOM 结构不共享。比如在iframe 中弹出一个对话框,它的位置是相对于iframe的,无法在当前应用中居中。
  • 父子应用上下文隔离,内存变量不共享。父子通讯、数据同步、免登陆都需要解决。
  • 加载速度慢。

微前端自定义加载方式

通过动态加载子应用的内容,动态渲染在主应用的元素中。

优点

  • UI 同步,DOM结构共享,可以解决子应用弹出窗口的问题
  • url 同步,浏览器刷新后子应用状态不丢失,前进、后退功能完整
  • 父子应用上下文共享,通讯方便,数据同步、免登录都容易实现

缺点

  • 上下文可能被修改,需要提供沙箱解决方案
  • CSS 覆盖,需要提供样式隔离方案
  • 父子应用使用同技术栈的情况下,资源重复加载

相关技术

使用自定义微前端加载方式,有许多技术细节需要自己实现,比如 CSS隔离,window 沙箱等。

CSS 隔离

CSS 隔离目前比较常用的有两种方式,一种是 Scoped CSS,另外一种是 Shadow DOM 的方式。

Scoped CSS

Scoped CSS 实现方式是为微应用的style 和 class 上添加当前微应用的标识符,限制 CSS 的作用域

const host = document.querySelector("#host");
const dataAttr = 'data-app';
const miroAPPId = 'unique-miro-app-id';
host.setAttribute(dataAttr, miroAPPId);

const styleNodes = host.querySelectorAll('style') || [];
if (styleNodes.length > 0) {
  const prefix = `[${dataAttr}=${miroAPPId}]`;
  const tempStyleNode = document.createElement('style');
  document.body.appendChild(tempStyleNode);
  styleNodes.forEach(node => {
    if (node.textContent) {
      const textNode = document.createTextNode(node.textContent || '');
      tempStyleNode.appendChild(textNode);
      const rules = Array.from(tempStyleNode.sheet.cssRules);
      let scopedCss = '';
      rules.forEach(rule => {
        const cssText = rule.cssText;
        scopedCss += cssText.replace(/^[\s\S]+{/, (selectors) => {
          return selectors.split(',').map(item => `${prefix} ${item}`).join(',')
        });
      });
      node.textContent = scopedCss;
      tempStyleNode.removeChild(textNode);
    }
  });
  document.body.removeChild(tempStyleNode);
}

Shadow DOM

Shadow DOM 是浏览器默认支持的

  const sheet = new CSSStyleSheet();
  sheet.replaceSync("span { color: red; border: 2px dotted black;}");

  const host = document.querySelector("#host");

  const shadow = host.attachShadow({ mode: "open" });
  shadow.adoptedStyleSheets = [sheet];

Window 沙箱

Window 沙箱目前常见的有 Proxy沙箱, 快照沙箱, iframe沙箱

Proxy 沙箱

Proxy沙箱是通过构造一个FakeWindow 作为代理,微应用的操作都在FakeWindow上操作,不污染主应用的window。

快照沙箱

快照沙箱是在微应用创建时,将当前的快照存储下来,微应用销毁时,再恢复到之前的状态。

iframe 沙箱

iframe 沙箱是通过创建一个 iframe,将 iframe.contentWindow 作为微应用的window 对象。隔离性很好,天然支持。

如何将沙箱注入微应用?

微应用打包成umd格式的library,包裹后的代码如下,那如何将我们的沙箱注入呢?

  (function(root, factory) {
    if (typeof module === 'object' && typeof module.exports === 'object') {
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        define([], factory);
    } else if (typeof exports === 'object') {
        exports = factory();
    } else {
        root.umdModule = factory();
    }
  }(window, function() {
  }))

使用快照沙箱时,用的父应用的window,可以直接注入,不用做任何修改。

使用Proxy 沙箱 或者 iframe 沙箱时,window作为全局变量不能修改,可以通过外面包一层的方式实现


  (wrapper(window, self, globalThis) {
    eval('/** miro application code **/');
  }).bind(sandbox)(sandbox, sandbox, sandbox)