微前端无界(wujie)源码浅析——子应用加载与js、css沙箱实现

wujie源码目录是怎样的?

wujie是如何实现js、css沙箱的?

wujie子应用是如何被加载到主应用中的?

wujie是如何实现路由同步的?

wujie可以实现应用保活吗?

让我们带着疑问,一探究竟

1、无界源码目录

wujie-core是无界的核心源码部分 wujie-vue、wujie-react都是在wujie-core的基础上做了一层封装,代码很简单,主要看core里面内容

core包中

index.js---导出 startApp setupApp preloadApp destroyApp

iframe.js---iframeGenerator,js沙箱负责创建和主应用同源的iframe

sanbox.js---Wujie类,基于 Proxy和iframe 实现的沙箱

shadow.js---处理wujie webComponent相关内容

proxy.js---代理window、document、location

event.js---eventBus 事件中心

sync.js---路由同步相关

2、无界微前端解决方案

微前端关注点主要是看

1、js、css沙箱

2、各个应用通信问题

3、路由切换各个应用如何切换

4、子应用激活卸载能力

js沙箱无界js沙箱采用创建iframe的方式,iframe天然具有js沙箱的优势,而且带有location以及history,路由也彻底和主应用解耦

css沙箱web component天然具有样式隔离的特性,对web component不熟悉的话,建议先看完mdn上的介绍,理解了之后再看wujie源码!

应用通信

props通信: 加载无界的子应用相当于加载一个组件,所以可以传props给子应用

window通信:由于子应用运行的iframesrc和主应用是同域的,所以相互可以直接通信

eventBus通信: 无界提供的通信方式

3、无界 startApp流程

在index.js中会定义webComponent容器,就是自定义一个元素标签wujie-app,然后创建shadow dom,并且把shadow dom赋值给sandbox.shadowRoot,源码如下所示:

js 复制代码
// index.js中
// 定义webComponent容器
defineWujieWebComponent();
--------------------------------------------------------------------------------------
// shadow.js中
/**
 * 定义 wujie webComponent,将shadow包裹并获得dom装载和卸载的生命周期
 */
export function defineWujieWebComponent() {
  const customElements = window.customElements;
  if (customElements && !customElements?.get("wujie-app")) {
    class WujieApp extends HTMLElement {
      connectedCallback(): void {
        if (this.shadowRoot) return;
        const shadowRoot = this.attachShadow({ mode: "open" });
        const sandbox = getWujieById(this.getAttribute(WUJIE_APP_ID));
        patchElementEffect(shadowRoot, sandbox.iframe.contentWindow);
        sandbox.shadowRoot = shadowRoot;
      }

      disconnectedCallback(): void {
        const sandbox = getWujieById(this.getAttribute(WUJIE_APP_ID));
        sandbox?.unmount();
      }
    }
    customElements?.define("wujie-app", WujieApp);
  }
}

无界有三种模式,流程如上图所示,这个流程在startApp中可以看出来。

  • 如果存在sandbox证明已经运行过应用,如果保活模式开启,进行应用激活等操作,然后结束。
  • 如果改造过生命周期执行sandbox.unmount(),然后进行应用激活,执行生命周期函数,然后return结束。
  • 如果没有保活,没有改造生命周期,则进行sandbox.destroy();销毁实例,然后重新生成newSandbox。

startApp流程图如下:

startApp源码如下:

js 复制代码
/**
 * 运行无界app
 */
export async function startApp(startOptions: startOptions): Promise<Function | void> {
   。。。。省略一些合并配置代码。。。。。。。。
  // 已经初始化过的应用,快速渲染
  if (sandbox) {
    sandbox.plugins = getPlugins(plugins);
    sandbox.lifecycles = lifecycles;
    const iframeWindow = sandbox.iframe.contentWindow;
    if (sandbox.preload) {
      await sandbox.preload;
    }
    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);
      }
      sandbox.lifecycles?.activated?.(sandbox.iframe.contentWindow);
      return sandbox.destroy;
    } else if (isFunction(iframeWindow.__WUJIE_MOUNT)) {
      /**
       * 子应用切换会触发webcomponent的disconnectedCallback调用sandbox.unmount进行实例销毁
       * 此处是防止没有销毁webcomponent时调用startApp的情况,需要手动调用unmount
       */
      sandbox.unmount();
      await sandbox.active({ url, sync, prefix, el, props, alive, fetch, replace });
      // 正常加载的情况,先注入css,最后才mount。重新激活也保持同样的时序
      sandbox.rebuildStyleSheets();
      // 有渲染函数
      sandbox.lifecycles?.beforeMount?.(sandbox.iframe.contentWindow);
      iframeWindow.__WUJIE_MOUNT();
      sandbox.lifecycles?.afterMount?.(sandbox.iframe.contentWindow);
      sandbox.mountFlag = true;
      return sandbox.destroy;
    } else {
      // 没有渲染函数
      sandbox.destroy();
    }
  }

  // 设置loading
  addLoading(el, loading);
  const newSandbox = new WuJie({ name, url, attrs, degradeAttrs, fiber, degrade, plugins, lifecycles });
  newSandbox.lifecycles?.beforeLoad?.(newSandbox.iframe.contentWindow);
  const { template, getExternalScripts, getExternalStyleSheets } = await importHTML({
    url,
    html,
    opts: {
      fetch: fetch || window.fetch,
      plugins: newSandbox.plugins,
      loadError: newSandbox.lifecycles.loadError,
      fiber,
    },
  });

  const processedHtml = await processCssLoader(newSandbox, template, getExternalStyleSheets);
  await newSandbox.active({ url, sync, prefix, template: processedHtml, el, props, alive, fetch, replace });
  await newSandbox.start(getExternalScripts);
  return newSandbox.destroy;
}

接下来主要围绕startApp流程所执行的每一步展开,顺着阅读就能get到wujie的js沙箱以及css沙箱是如何生成的。

4、new WuJie(基于iframe的js沙箱初始化)

从startApp中可以看到 new WuJie创建子应用实例

new Wujie()负责创建基于iframe的js沙箱,然后产生proxyWindow、proxyDocument、proxyLocation

degrade---兼容低版本浏览器(不支持Proxy) 如果浏览器不支持Proxy就采用Object.defineProperties,支持就采用Proxy

js 复制代码
  constructor(options) {
     。。。。省略一些赋值无关紧要。。。。。。。
    // 创建iframe
    this.iframe = iframeGenerator(this, attrs, mainHostPath, appHostPath, appRoutePath);

    if (this.degrade) {
      const { proxyDocument, proxyLocation } = localGenerator(this.iframe, urlElement, mainHostPath, appHostPath);
      this.proxyDocument = proxyDocument;
      this.proxyLocation = proxyLocation;
    } else {
      const { proxyWindow, proxyDocument, proxyLocation } = proxyGenerator(
        this.iframe,
        urlElement,
        mainHostPath,
        appHostPath
      );
      this.proxy = proxyWindow;
      this.proxyDocument = proxyDocument;
      this.proxyLocation = proxyLocation;
    }
    this.provide.location = this.proxyLocation;
    addSandboxCacheWithWujie(this.id, this);
  }

4-1、iframeGenerator

1、patchIframeVariable注入wujie相关的全局变量到iframeWindow

2、initIframeDom 初始化iframe的dom结构

js 复制代码
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;
}

4-2、initIframeDom

主要负责了路由同步操作

子应用js执行在iframeWindow中执行,拦截路由pushState和replaceState,进行子应用--->主应用的路由同步操作
TODO:这里一些拦截操作有待细看。。。

js 复制代码
function initIframeDom(iframeWindow, wujie, mainHostPath, appHostPath) {
  const iframeDocument = iframeWindow.document;
  const newDoc = window.document.implementation.createHTMLDocument("");
  const newDocumentElement = iframeDocument.importNode(newDoc.documentElement, true);
  iframeDocument.documentElement
    ? iframeDocument.replaceChild(newDocumentElement, iframeDocument.documentElement)
    : iframeDocument.appendChild(newDocumentElement);
  iframeWindow.__WUJIE_RAW_DOCUMENT_HEAD__ = iframeDocument.head;
  iframeWindow.__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR__ = iframeWindow.Document.prototype.querySelector;
  iframeWindow.__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR_ALL__ = iframeWindow.Document.prototype.querySelectorAll;
  iframeWindow.__WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__ = iframeWindow.Document.prototype.createElement;
  iframeWindow.__WUJIE_RAW_DOCUMENT_CREATE_TEXT_NODE__ = iframeWindow.Document.prototype.createTextNode;
  // -------------初始化base标签-----------------
  initBase(iframeWindow, wujie.url);
  
  // 对iframe的history的pushState和replaceState进行拦截、同步路由到主应用
  // 同步子应用路由到主应用
  // 子应用调用history.pushState()后,会同步作用到主应用url上,并且更新base标签的herf
  patchIframeHistory(iframeWindow, appHostPath, mainHostPath);
  
  // 对iframe的事件进行拦截
  // 运行插件钩子函数
  // 子应用window监听需要挂载到iframe沙箱上的事件,子应用以下事件会被代理到iframeWindow上
// export const appWindowAddEventListenerEvents = [
//   "hashchange",
//   "popstate",
//   "DOMContentLoaded",
//   "load",
//   "beforeunload",
//   "unload",
//   "message",
//   "error",
//   "unhandledrejection",
// ];
  patchIframeEvents(iframeWindow);
  
  // 降级情况下 Node记录节点的监听事件
  if (wujie.degrade) recordEventListeners(iframeWindow);
  
  // 监听子应用事件hashchange、popstate,子应用前进后退,同步路由到主应用
  syncIframeUrlToWindow(iframeWindow);

  // 下面几个是代理Window、Node、Document的
  patchWindowEffect(iframeWindow);
  patchDocumentEffect(iframeWindow);
  patchNodeEffect(iframeWindow);
  patchRelativeUrlEffect(iframeWindow);
}

5、importHTML && processCssLoader

5-1、importHTML

这个importHTML非常类似qiankun中的import-html-entry

这个推荐看大佬写的文章乾坤的资源加载机制(import-html-entry的内部实现)wujie的importHTML与qiankun的类似。

流程大致是通过fetch请求,加载html,然后解析html中的srcipt标签与style、link标签,拿到如下返回值
TODO 这里有待细看。。。

js 复制代码
      return {
        template: template, // html
        assetPublicPath, // 资源地址
        getExternalScripts: () =>  // 拿到scripts的函数
          getExternalScripts(
            scripts
              .filter((script) => !script.src || !isMatchUrl(script.src, jsExcludes))
              .map((script) => ({ ...script, ignore: script.src && isMatchUrl(script.src, jsIgnores) })),
            fetch,
            loadError,
            fiber
          ),
        getExternalStyleSheets: () => // 拿到styles的函数
          getExternalStyleSheets(
            styles
              .filter((style) => !style.src || !isMatchUrl(style.src, cssExcludes))
              .map((style) => ({ ...style, ignore: style.src && isMatchUrl(style.src, cssIgnores) })),
            fetch,
            loadError
          ),
      };

5-2、processCssLoader

通过processCssLoader,处理cssloader相关内容,然后把加载到的css资源内联进入html中

js 复制代码
export async function processCssLoader(
  sandbox: Wujie,
  template: string,
  getExternalStyleSheets: () => StyleResultList
): Promise<string> {
  const curUrl = getCurUrl(sandbox.proxyLocation);
  /** css-loader */
  const composeCssLoader = compose(sandbox.plugins.map((plugin) => plugin.cssLoader));
  const processedCssList: StyleResultList = getExternalStyleSheets().map(({ src, ignore, contentPromise }) => ({
    src,
    ignore,
    contentPromise: contentPromise.then((content) => composeCssLoader(content, src, curUrl)),
  }));
  const embedHTML = await getEmbedHTML(template, processedCssList);
  return sandbox.replace ? sandbox.replace(embedHTML) : 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);
}

回顾一下!!!!!!!!

回顾一下,到现在为止,下图中步骤

1、new WuJie()

2、importHTML()

3、processCssLoader()
都进行完毕了!!!!!!!!!!!!!!!

剩余 newSanbox.active()

newSanbox.start()

6、Sanbox.active(子应用激活---也是创建dom的过程)

acticve中主要做了以下几件事情

1、处理子应用路由同步

2、降级处理

3、createWujieWebComponent---之前定义过的wujie-app元素,在这里被创建

4、renderElementToContainer---创建好的wujie-app标签,在被插入指定位置,这里会首次执行没有容器可以挂载,会暂时插入到iframeBody中,触发Web Component的connect,在connectedCallback中会给wujie-app创建shadow dom,并且将shadowRoot赋值给实例的shadowRoot

connectedCallback源码如下:

js 复制代码
      connectedCallback(): void {
        if (this.shadowRoot) return;
        const shadowRoot = this.attachShadow({ mode: "open" });
        const sandbox = getWujieById(this.getAttribute(WUJIE_APP_ID));
        patchElementEffect(shadowRoot, sandbox.iframe.contentWindow);
        sandbox.shadowRoot = shadowRoot;
      }

5、renderTemplateToShadowRoot---之前通过processCssLoader获取到的html将在renderTemplateToShadowRoot中渲染到shadowRoot 这里就是html的插入shadow dom的过程,这里也是wujie css沙箱的原理

可以在wujie官网的demo中看到确实是这样的shadow dom 点击链接查看官网demo

子应用激活active的大致的流程图如下:

子应用激活active源码如下:

js 复制代码
  public async active(options) {
    。。。。。。省略部分代码。。。。。。。
    // 处理子应用路由同步
    if (this.execFlag && this.alive) {
      // 当保活模式下子应用重新激活时,只需要将子应用路径同步回主应用
      syncUrlToWindow(iframeWindow);
    } else {
      // 先将url同步回iframe,然后再同步回浏览器url
      syncUrlToIframe(iframeWindow);
      syncUrlToWindow(iframeWindow);
    }

    // inject template
    this.template = template ?? this.template;

    /* 降级处理 */
    if (this.degrade) {
      const iframeBody = rawDocumentQuerySelector.call(iframeWindow.document, "body") as HTMLElement;
      const { iframe, container } = initRenderIframeAndContainer(this.id, el ?? iframeBody, this.degradeAttrs);
      this.el = container;
      // 销毁js运行iframe容器内部dom
      if (el) clearChild(iframeBody);
      // 修复vue的event.timeStamp问题
      patchEventTimeStamp(iframe.contentWindow, iframeWindow);
      // 当销毁iframe时主动unmount子应用
      iframe.contentWindow.onunload = () => {
        this.unmount();
      };
      if (this.document) {
        if (this.alive) {
          iframe.contentDocument.replaceChild(this.document.documentElement, iframe.contentDocument.documentElement);
          // 保活场景需要事件全部恢复
          recoverEventListeners(iframe.contentDocument.documentElement, iframeWindow);
        } else {
          await renderTemplateToIframe(iframe.contentDocument, this.iframe.contentWindow, this.template);
          // 非保活场景需要恢复根节点的事件,防止react16监听事件丢失
          recoverDocumentListeners(this.document.documentElement, iframe.contentDocument.documentElement, iframeWindow);
        }
      } else {
        await renderTemplateToIframe(iframe.contentDocument, this.iframe.contentWindow, this.template);
      }
      this.document = iframe.contentDocument;
      return;
    }

    if (this.shadowRoot) {
      /*
       document.addEventListener was transfer to shadowRoot.addEventListener
       react 16 SyntheticEvent will remember document event for avoid repeat listen
       shadowRoot have to dispatchEvent for react 16 so can't be destroyed
       this may lead memory leak risk
       */
      this.el = renderElementToContainer(this.shadowRoot.host, el);
      if (this.alive) return;
    } else {
      // 预执行无容器,暂时插入iframe内部触发Web Component的connect
      const iframeBody = rawDocumentQuerySelector.call(iframeWindow.document, "body") as HTMLElement;
      this.el = renderElementToContainer(createWujieWebComponent(this.id), el ?? iframeBody);
    }

    await renderTemplateToShadowRoot(this.shadowRoot, iframeWindow, this.template);
    this.patchCssRules();

    // inject shadowRoot to app
    this.provide.shadowRoot = this.shadowRoot;
  }

7、Sanbox.start(子应用运行---js执行)

万事俱备,开始执行start,也就是运行子应用js代码

1、const scriptResultList = await getExternalScripts() --- 拿到script列表

2、按照脚本类型分类(defer、async、同步script)push入不同数组

js 复制代码
    // 用户自定义代码前
    const beforeScriptResultList: ScriptObjectLoader[] = getPresetLoaders("jsBeforeLoaders", this.plugins);
    // 用户自定义代码后
    const afterScriptResultList: ScriptObjectLoader[] = getPresetLoaders("jsAfterLoaders", this.plugins);
    // 同步代码
    const syncScriptResultList: ScriptResultList = [];
    // async代码无需保证顺序,所以不用放入执行队列
    const asyncScriptResultList: ScriptResultList = [];
    // defer代码需要保证顺序并且DOMContentLoaded前完成,这里统一放置同步脚本后执行
    const deferScriptResultList: ScriptResultList = [];
    --------------------------------------------------------------
    scriptResultList.forEach((scriptResult) => {
      if (scriptResult.defer) deferScriptResultList.push(scriptResult);
      else if (scriptResult.async) asyncScriptResultList.push(scriptResult);
      else syncScriptResultList.push(scriptResult);
    });

3、按照以下顺序构建子应用js执行队列(execQueue)
sanbox.execQueue = beforeScriptResultList-->syncScriptResultList-->deferScriptResultList-->mount-->domContentLoadedTrigger-->afterScriptResultList-->domLoadedTrigger

4、execQueue中每一项都是一个函数如下:

js 复制代码
insertScriptToIframe({ ...scriptResult, content }, iframeWindow)

insertScriptToIframe---执行后会在iframe插入脚本,创建当前的执行脚本script以及下一个执行脚本script

js 复制代码
  scriptElement.textContent = code || "";
  nextScriptElement.textContent =
    "if(window.__WUJIE.execQueue && window.__WUJIE.execQueue.length){ window.__WUJIE.execQueue.shift()()}";

nextScriptElement的作用就是取出队列的下一个元素执行,这样直到队列所有元素出队后执行完毕 start流程图如下:

start源码如下:

js 复制代码
  public async start(getExternalScripts: () => ScriptResultList): Promise<void> {
    this.execFlag = true;
    // 执行脚本
    const scriptResultList = await getExternalScripts();
    // 假如已经被销毁了
    if (!this.iframe) return;
    const iframeWindow = this.iframe.contentWindow;
    // 标志位,执行代码前设置
    iframeWindow.__POWERED_BY_WUJIE__ = true;
    // 用户自定义代码前
    const beforeScriptResultList: ScriptObjectLoader[] = getPresetLoaders("jsBeforeLoaders", this.plugins);
    // 用户自定义代码后
    const afterScriptResultList: ScriptObjectLoader[] = getPresetLoaders("jsAfterLoaders", this.plugins);
    // 同步代码
    const syncScriptResultList: ScriptResultList = [];
    // async代码无需保证顺序,所以不用放入执行队列
    const asyncScriptResultList: ScriptResultList = [];
    // defer代码需要保证顺序并且DOMContentLoaded前完成,这里统一放置同步脚本后执行
    const deferScriptResultList: ScriptResultList = [];
    scriptResultList.forEach((scriptResult) => {
      if (scriptResult.defer) deferScriptResultList.push(scriptResult);
      else if (scriptResult.async) asyncScriptResultList.push(scriptResult);
      else syncScriptResultList.push(scriptResult);
    });

    // 插入代码前
    beforeScriptResultList.forEach((beforeScriptResult) => {
      this.execQueue.push(() =>
        this.fiber
          ? this.requestIdleCallback(() => insertScriptToIframe(beforeScriptResult, iframeWindow))
          : insertScriptToIframe(beforeScriptResult, iframeWindow)
      );
    });

    // 同步代码
    syncScriptResultList.concat(deferScriptResultList).forEach((scriptResult) => {
      this.execQueue.push(() =>
        scriptResult.contentPromise.then((content) =>
          this.fiber
            ? this.requestIdleCallback(() => insertScriptToIframe({ ...scriptResult, content }, iframeWindow))
            : insertScriptToIframe({ ...scriptResult, content }, iframeWindow)
        )
      );
    });

    // 异步代码
    asyncScriptResultList.forEach((scriptResult) => {
      scriptResult.contentPromise.then((content) => {
        this.fiber
          ? this.requestIdleCallback(() => insertScriptToIframe({ ...scriptResult, content }, iframeWindow))
          : insertScriptToIframe({ ...scriptResult, content }, iframeWindow);
      });
    });

    //框架主动调用mount方法
    this.execQueue.push(this.fiber ? () => this.requestIdleCallback(() => this.mount()) : () => this.mount());

    //触发 DOMContentLoaded 事件
    const domContentLoadedTrigger = () => {
      eventTrigger(iframeWindow.document, "DOMContentLoaded");
      eventTrigger(iframeWindow, "DOMContentLoaded");
      this.execQueue.shift()?.();
    };
    this.execQueue.push(this.fiber ? () => this.requestIdleCallback(domContentLoadedTrigger) : domContentLoadedTrigger);

    // 插入代码后
    afterScriptResultList.forEach((afterScriptResult) => {
      this.execQueue.push(() =>
        this.fiber
          ? this.requestIdleCallback(() => insertScriptToIframe(afterScriptResult, iframeWindow))
          : insertScriptToIframe(afterScriptResult, iframeWindow)
      );
    });

    //触发 loaded 事件
    const domLoadedTrigger = () => {
      eventTrigger(iframeWindow.document, "readystatechange");
      eventTrigger(iframeWindow, "load");
      this.execQueue.shift()?.();
    };
    this.execQueue.push(this.fiber ? () => this.requestIdleCallback(domLoadedTrigger) : domLoadedTrigger);
    // 由于没有办法准确定位是哪个代码做了mount,保活、重建模式提前关闭loading
    if (this.alive || !isFunction(this.iframe.contentWindow.__WUJIE_UNMOUNT)) removeLoading(this.el);
    this.execQueue.shift()();

    // 所有的execQueue队列执行完毕,start才算结束,保证串行的执行子应用
    return new Promise((resolve) => {
      this.execQueue.push(() => {
        resolve();
        this.execQueue.shift()?.();
      });
    });
  }

大致我们了解了wujie的子应用加载与js、css沙箱

回头一望直呼nb看完之后才发现,其实整个框架真的不是很难,但是自己肯定写不出来,为什么? 基本功不够扎实!!!中间许多dom、bom、js的操作很骚气,但是不能一眼看出来用意,所以阅读起来造成一些困扰,但是遇到不懂的就去弄明白这操作,逐渐发现,阅读源码的过程其实也是基础夯实的过程!

wujie还有许多细节值得深入研究,比如应用之间如何通信、如何执行预加载、importHTML详细步骤、无界的插件机制、降级处理是如何操作的等等
如有不对,欢迎斧正

相关推荐
qiyi.sky几秒前
JavaWeb——Vue组件库Element(3/6):常见组件:Dialog对话框、Form表单(介绍、使用、实际效果)
前端·javascript·vue.js
煸橙干儿~~4 分钟前
分析JS Crash(进程崩溃)
java·前端·javascript
安冬的码畜日常13 分钟前
【D3.js in Action 3 精译_027】3.4 让 D3 数据适应屏幕(下)—— D3 分段比例尺的用法
前端·javascript·信息可视化·数据可视化·d3.js·d3比例尺·分段比例尺
l1x1n041 分钟前
No.3 笔记 | Web安全基础:Web1.0 - 3.0 发展史
前端·http·html
昨天;明天。今天。1 小时前
案例-任务清单
前端·javascript·css
zqx_72 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己2 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称3 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色3 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2343 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js