微前端-无界源码实现原理解析

939 阅读4分钟

无界与其他微前端框架(例如qiankun)的主要区别在于其独特的 JS 沙箱机制。qiankun采用了自定义沙箱,而无界使用 iframe 来实现 JS 沙箱。iframe沙箱具有其独特的优势,具体如下:

1.应用切换没有清理成本

2.允许一个页面同时激活多个子应用 

3.iframe性能相对更优

下面将从以下几步,分析无界渲染子应用步骤以及常见问题。

· 1.创建子应用 iframe

· 2.解析 HTML分离js和css

· 3.创建 webComponent,并挂载 HTML

1.创建和主应用同源的子应用iframe。

export function iframeGenerator(
  sandbox: WuJie,
  attrs: { [key: string]: any },
  mainHostPath: string,
  appHostPath: string,
  appRoutePath: string
): HTMLIFrameElement {
  const iframe = window.document.createElement("iframe");
  const attrsMerge = { src: mainHostPath, style: "display: none",
   ...attrs, name: sandbox.id, [WUJIE_DATA_FLAG]: "" };
  setAttrsToElement(iframe, attrsMerge);
  window.document.body.appendChild(iframe);

  const iframeWindow = iframe.contentWindow;
  // 变量需要提前注入,在入口函数通过变量防止死循环
  patchIframeVariable(iframeWindow, sandbox, appHostPath);
  sandbox.iframeReady = stopIframeLoading(iframeWindow).then(() => {
    if (!iframeWindow.__WUJIE) {
      patchIframeVariable(iframeWindow, sandbox, appHostPath);
    }
    initIframeDom(iframeWindow, sandbox, mainHostPath, appHostPath);
    /**
     * 如果有同步优先同步,非同步从url读取
     */
    if (!isMatchSyncQueryById(iframeWindow.__WUJIE.id)) {
      iframeWindow.history.replaceState(null, "", mainHostPath + appRoutePath);
    }
  });
  return iframe;
}

上述代码逻辑为

1.创建一个新的iframe,避免各个应用之间相互污染。

2.给iframe设置子应用相关属性,如主应用域名等。

3.将iframe插入到baody中。

4.调用stopIframeLoading函数停止iframe加载

为什么子应用的iframe的地址和主应用的一样?

无界为了解决iframe之间的通信,所以将iframe的src设置为主应用域名(即同源状态下的状态共享,如共用localstroage等)。比如主应用域名为a.com,子应用iframe的src指向同样也为a.com。但子应用的资源会在别的域名下如b.com。所以会造成跨域,这也就是无界的子应用为什么需要设置允许跨域。

如何设置跨域?

在开发环境下,通过在vue.config.js文件中devServer下添加请求头headers:{“Access-Control-Allow-Origin”:”*”},来允许开发环境跨域。 在生产环境下可通过在nginx上添加代理,add_header Access-Control-Allow-Origin “主应用域名”,来解决跨域。这两种方式对于前端来说也算轻车熟路。

为什么要调用stopIframeLoading函数来停止iframe加载?

因为会造成变量污染,例如主应用和子应用如果存在相同名称的变量,而其实我们只是需要一个空的iframe,并不需要执行主应用的代码,无界通过判断iframeWindow.document是否存在,来调用iframeWindow.stop停止iframe加载,来解决了iframe执行主应用代码问题。

2.解析 HTML 分离js和css。

因为子应用的dom运行在WebComponents中,而js运行在iframe中,所以无界会将html文件进行解析通过(类似import-html-entry插件功能,无界实现了自己的插件)最终分离出css和js相关信息。然后通过fetch发送请求将css和js资源请求回来,最终的css代码将会形成内部样式。

export function getExternalStyleSheets(
  styles: StyleObject[],
  fetch: (input: RequestInfo, init?: RequestInit) => Promise<Response> = defaultFetch,
  loadError: loadErrorHandler
): StyleResultList {
  return styles.map(({ src, content, ignore }) => {
    // 内联
    if (content) {
      return { src: "", contentPromise: Promise.resolve(content) };
    } else if (isInlineCode(src)) {
      // if it is inline style
      return { src: "", contentPromise: Promise.resolve(getInlineCode(src)) };
    } else {
      // external styles
      return {
        src,
        ignore,
        contentPromise: ignore ? Promise.resolve("") : 
        fetchAssets(src, styleCache, fetch, true, loadError),
      };
    }
  });
}

子应用为什么采用了内部样式而link标签引入?

因为需要对样式经常处理,所以需要将样式请求回来进行处理再放回去,还有一个就是子应用切换后样式需要恢复必须把样式收集起来,内联样式更好收集处理。

子应用里面js文件中使用var声明的变量为什么不能共享?

在无界微前端中子应用代码运行在闭包内,为什么要在闭包内执行代码?为了劫持修改 window.location,因为 location 的 configuable 为 false, 所以采用闭包解决

code = `(function(window, self, global, location) {      

${code}}).bind(window.__WUJIE.proxy)

(  window.__WUJIE.proxy,

 window.__WUJIE.proxy,

 window.__WUJIE.proxy,

 window.__WUJIE.proxyLocation,);`;

 

这样就会导致子应用的window是无界的代理对象而非真实的window对象,而闭包也会导致失去变量声明提升。可通过在plugins添加jsIgnores来决定让子应用自己执行某些js,来解决这个问题。

3.创建 webComponent,并挂载 HTML

首先创建一个webComponent,并设置id与css。

export function createWujieWebComponent(id: string): HTMLElement {
  const contentElement = window.document.createElement("wujie-app");
  contentElement.setAttribute(WUJIE_APP_ID, id);
  contentElement.classList.add(WUJIE_IFRAME_CLASS);
  return contentElement;
}

调用processCssLoaderForTemplate函数,处理css-before-loader以及css-after-loader,,将通过fetch请求下来的css内容与plugins中的loader合并生成内部样式。

调用renderTemplateToShadowRoot函数,将生成的css等内容挂载到shadowRoot中,最后再挂载在主应用的dom中。

 const processedHtml = await processCssLoaderForTemplate(iframeWindow.__WUJIE, html);
  // change ownerDocument
  shadowRoot.appendChild(processedHtml);
  const shade = document.createElement("div");
  shade.setAttribute("style", WUJIE_SHADE_STYLE);
  processedHtml.insertBefore(shade, processedHtml.firstChild);
  shadowRoot.head = shadowRoot.querySelector("head");
  shadowRoot.body = shadowRoot.querySelector("body");

以上就是对无界源码的初步解析,通过了解框架的设计思路可以帮我们更好解决在使用中的问题。感兴趣的朋友可以在官网与github进行深入学习与了解。

欢迎大家一起交流~~