• home > webfront > engineer > Architecture >

    微前端学习笔记(2): 无界方案分析

    Author:zhoulujun Date:

    无界:https: wujie-micro github io doc 无界的核心思想:利用Iframe特性实现沙箱,让子应用的脚本在iframe内运行(如果子应用与主应用

    无界:https://wujie-micro.github.io/doc/

    无界的核心思想:利用Iframe特性实现沙箱,让子应用的脚本在iframe内运行(如果子应用与主应用存在跨域,需要做cors设置处理),利用shadow dom实现样式隔离,子应用的dom在主应用容器下的webcomponent内。通过代理 iframe的document到webcomponent,可以实现两者的互联。

    无界的实现方案

    wujie的核心代码也十分简单,总共14个文件,入口在index.ts,可以顺着入口一点一点深入源码进行了解。

    无界源码文件结构

    wujie-core核心代码思维导图

    33.png


    思维导图:https://www.kdocs.cn/view/l/cdZmhNpp4rIA



    应用加载机制和 js 沙箱机制

    将子应用的js注入主应用同域的iframe中运行,iframe是一个原生的window沙箱,内部有完整的history和location接口,子应用实例instance运行在iframe中,路由也彻底和主应用解耦,可以直接在业务组件里面启动应用。

    创建iframe的逻辑 

    3.png

    iframe将web应用完美隔离,无论是dom、css还是js都完全的隔离了起来,但dom隔离太严重,子应用的dom无法突破iframe的限制,比如一个fixed定位的元素也只能在iframe区域展示。估计无界也是因此只采用iframe来实现JS沙箱,而是使用 Web Components 来隔离html、css。


    iframe 连接机制和 css 沙箱机制

    无界采用webcomponent来实现页面的样式隔离,无界会创建一个wujie自定义元素,然后将子应用的完整结构渲染在内部

    子应用的实例instance在iframe内运行,dom在主应用容器下的webcomponent内,通过代理 iframe的document到webcomponent,可以实现两者的互联

    将document的查询类接口:getElementsByTagName、getElementsByClassName、getElementsByName、getElementById、querySelector、querySelectorAll、head、body全部代理到webcomponent,这样instance和webcomponent就精准的链接起来。

    当子应用发生切换,iframe保留下来,子应用的容器可能销毁,但webcomponent依然可以选择保留,这样等应用切换回来将webcomponent再挂载回容器上,子应用可以获得类似vue的keep-alive的能力.

    注册自定义标签在无界加载的时候就注册了而且只执行一次,connect里的逻辑则是在自定义元素链接到dom文档中时执行

    25e68e82cd21424d90810015a10ae97d~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.png

    WuJie类是无界沙箱的核心,沙箱实例就是WuJie类的实例,里面存储着shadowroot、模板以及各种子应用的配置属性

    2.png

    子应用执行window的函数时,期望函数的this指向子应用的window,但是由于代理window是在基座生成的,所以this指向的基座的window,所以需要修正this指向

    6.png

    分析出document的所有属性以及方法,有些属性需要代理到全局的document,有些需要代理到沙箱的shadow root节点上【这里是子应用js和Webcomponents链接的关键步骤

    7.png

    这一步和qiankun都一样,解析子应用模板的脚本、样式、模板

    8.png

    将解析的脚本插入到子应用iframe内,这里如果开启了fiber并且浏览器支持,可以在预加载时优化一些性能,在空闲时间去执行该操作

    9.png

    插入脚本时,并不是直接插入,需要对其进行改造。需要将代理的window、location作用于子应用脚步的执行环境,你可能会说这里怎么没有将代理对象proxyDocument传入? 这里我也无法理解

    10.png

    最后将HTML以及CSS添加到shadowroot中

    11.png

    wujie引起无法预知的bug主要还是围绕着 iframe 和 shadow dom之间的通信发生的。


    路由同步机制

    在iframe内部进行history.pushState,浏览器会自动的在joint session history中添加iframe的session-history,浏览器的前进、后退在不做任何处理的情况就可以直接作用于子应用

    劫持iframe的history.pushState和history.replaceState,就可以将子应用的url同步到主应用的query参数上,当刷新浏览器初始化iframe时,读回子应用的url并使用iframe的history.replaceState进行同步

    重写子应用的pushState实现子应用路由和基座路由的联动

    4.png

    子应用执行window的函数时,期望函数的this指向子应用的window,但是由于代理window是在基座生成的,所以this指向的基座的window,所以需要修正this指向


    通信机制

    承载子应用的iframe和主应用是同域的,所以主、子应用天然就可以很好的进行通信,在无界我们提供三种通信方式

    props 注入机制

    子应用通过$wujie.props可以轻松拿到主应用注入的数据

    window.parent 通信机制

    子应用iframe沙箱和主应用同源,子应用可以直接通过window.parent和主应用通信

    去中心化的通信机制

    无界提供了EventBus实例,注入到主应用和子应用,所有的应用可以去中心化的进行通信

    21.png


    子应用如何加载,生命周期管理

    子应用的加载与乾坤的方式相同,都通过一个importHTML函数进行加载;它的具体过程就是:

    fetch我们传入的这个url,得到一个html字符串,然后通过正则表达式匹配到内部样式表、外部样式表、脚本;源码通过/<(link)\s+.*?>/gis匹配外部样式,通过/(<script[\s\S]*?>)[\s\S]*?<\/script>/gi匹配脚本;通过/<style[^>]*>[\s\S]*?<\/style>/gi匹配内部样式;我们尝试一下自己写一个importHTML,解析一下我们当前这一篇文章:

    const STYLE_REG = /<style>(.*)<\/style>/gi;
    const SCRIPT_REG = /<script>(.*)<\/script>/gi;
    const LINK_REG = /<(link)\s+.*?>/gi
    
    async function imoprtHTML() {
        let html = await fetch("https://juejin.cn/post/7209162467928096825");
        html = await html.text();
        const ans = html.replace(STYLE_REG, match=>{
            // ... 很多逻辑
            return match;
        }
        ).replace(SCRIPT_REG, match=>{
            // ... 很多逻辑
            return match;
        }
        ).replace(LINK_REG, match=>{
            // ... 很多逻辑
            debugger
            return match;
        })
    }

    第二步对于外部样式表、外部脚本我们也需要通过fetch获取到内容然后将代码存储起来

    将合并的样式表添加到页面上

    /**
     * convert external css link to inline style for performance optimization
     * @return embedHTML
     */
    async function getEmbedHTML(template, styleResultList: StyleResultList): Promise<string> {
      let embedHTML = template;
    
      return Promise.all(
        styleResultList.map((styleResult, index) =>
          styleResult.contentPromise.then((content) => {
            if (styleResult.src) {
              embedHTML = embedHTML.replace(
                genLinkReplaceSymbol(styleResult.src),
                styleResult.ignore
                  ? `<link href="${styleResult.src}" rel="stylesheet" type="text/css">`
                  : `<style>/* ${styleResult.src} */${content}</style>`
              );
            } else if (content) {
              embedHTML = embedHTML.replace(
                getInlineStyleReplaceSymbol(index),
                `<style>/* inline-style-${index} */${content}</style>`
              );
            }
          })
        )
      ).then(() => embedHTML);
    }


    执行js,这个详细过程我们后文分析

    /**
     * iframe插入脚本
     * @param scriptResult script请求结果
     * @param iframeWindow
     * @param rawElement 原始的脚本
     */
    export function insertScriptToIframe(
      scriptResult: ScriptObject | ScriptObjectLoader,
      iframeWindow: Window,
      rawElement?: HTMLScriptElement
    ) {
      const { src, module, content, crossorigin, crossoriginType, async, attrs, callback, onload } =
        scriptResult as ScriptObjectLoader;
      const scriptElement = iframeWindow.document.createElement("script");
      const nextScriptElement = iframeWindow.document.createElement("script");
      const { replace, plugins, proxyLocation } = iframeWindow.__WUJIE;
      const jsLoader = getJsLoader({ plugins, replace });
      let code = jsLoader(content, src, getCurUrl(proxyLocation));
      // 添加属性
      attrs &&
        Object.keys(attrs)
          .filter((key) => !Object.keys(scriptResult).includes(key))
          .forEach((key) => scriptElement.setAttribute(key, String(attrs[key])));
    
      // 内联脚本
      if (content) {
        // patch location
        if (!iframeWindow.__WUJIE.degrade && !module) {
          code = `(function(window, self, global, location) {
          ${code}
    }).bind(window.__WUJIE.proxy)(
      window.__WUJIE.proxy,
      window.__WUJIE.proxy,
      window.__WUJIE.proxy,
      window.__WUJIE.proxyLocation,
    );`;
        }
        const descriptor = Object.getOwnPropertyDescriptor(scriptElement, "src");
        // 部分浏览器 src 不可配置 取不到descriptor表示无该属性,可写
        if (descriptor?.configurable || !descriptor) {
          // 解决 webpack publicPath 为 auto 无法加载资源的问题
          try {
            Object.defineProperty(scriptElement, "src", { get: () => src || "" });
          } catch (error) {
            console.warn(error);
          }
        }
      } else {
        src && scriptElement.setAttribute("src", src);
        crossorigin && scriptElement.setAttribute("crossorigin", crossoriginType);
      }
      module && scriptElement.setAttribute("type", "module");
      scriptElement.textContent = code || "";
      nextScriptElement.textContent =
        "if(window.__WUJIE.execQueue && window.__WUJIE.execQueue.length){ window.__WUJIE.execQueue.shift()()}";
    
      const container = rawDocumentQuerySelector.call(iframeWindow.document, "head");
      const execNextScript = () => !async && container.appendChild(nextScriptElement);
      const afterExecScript = () => {
        onload?.();
        execNextScript();
      };
    }

    子应用加载完毕

    这个过程中涉及到哪些生命周期呢?

    beforeLoad:子应用开始加载静态资源前触发,也就是importHTML之前触发

    if (alive) {
        // 保活
        await sandbox.active({ url, sync, prefix, el, props, alive, fetch, replace });
        // 预加载但是没有执行的情况
        if (!sandbox.execFlag) {
          sandbox.lifecycles?.beforeLoad?.(sandbox.iframe.contentWindow);
          const { getExternalScripts } = await importHTML({
            url,
            html,
            opts: {
              fetch: fetch || window.fetch,
              plugins: sandbox.plugins,
              loadError: sandbox.lifecycles.loadError,
              fiber,
            },
          });
          await sandbox.start(getExternalScripts);
        }


    beforeMount:子应用渲染前触发 (生命周期改造专用)




    参考文章:

    极致的微前端方案_无界的源码剖析 https://juejin.cn/post/7158777745806196743

    假如你是『无界』微前端框架的开发者 https://juejin.cn/post/7212597327578808380?from=search-suggest

    微前端-无界源码实现原理解析 https://juejin.cn/post/7331180722214109196




    转载本站文章《微前端学习笔记(2): 无界方案分析》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/engineer/Architecture/9052.html