开发者社区 > 博文 > 微前端无界机制浅析
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

微前端无界机制浅析

  • jd****
  • 2023-11-01
  • IP归属:北京
  • 8560浏览

    作者:张燕燕、刘海鼎  

    简介

    随着项目的发展,前端SPA应用的规模不断加大、业务代码耦合、编译慢,导致日常的维护难度日益增加。同时前端技术的发展迅猛,导致功能扩展吃力,重构成本高,稳定性低。

    为了能够将前端模块解耦,通过相关技术调研,最终选择了无界微前端框架作为物流客服系统解耦支持。为了更好的使用无界微前端框架,我们对其运行机制进行了相关了解,以下是对无界运行机制的一些认识。

    基本用法

    主应用配置

    import WujieVue from 'wujie-vue2';
    
    const { setupApp, preloadApp, bus } = WujieVue;
    /*设置缓存*/
    setupApp({
    });
    /*预加载*/
    preloadApp({
      name: 'vue2'
    })
    <WujieVue width="100%" height="100%" name="vue2" :url="vue2Url" :sync="true" :alive="true"></WujieVue
    

    具体实践详细介绍参考:

    http://3.cn/1GyPHN-i/shendeng

    https://wujie-micro.github.io/doc/guide/start.html

    无界源码解析

    1 源码包目录结构

    packages 包里包含无界框架核心代码wujie-core和对应不同技术栈应用包

    examples 使用案例,main-xxx对应该技术栈主应用的使用案例,其他代表子应用的使用案例

    2 wujie-vue2组件

    该组件默认配置了相关参数,简化了无界使用时的一些配置项,作为一个全局组件被主引用使用

    这里使用wujie-vue2示例,其他wujie-react,wujie-vue3大家可自行阅读,基本作用和wujie-vue2相同都是用来简化无界配置,方便快速使用

    import Vue from "vue";
    import { bus, preloadApp, startApp as rawStartApp, destroyApp, setupApp } from "wujie";
    
    const wujieVueOptions = {
      name: "WujieVue",
      props: {
           /*传入配置参数*/
      },
      data() {
        return {
          startAppQueue: Promise.resolve(),
        };
      },
      mounted() {
        bus.$onAll(this.handleEmit);
        this.execStartApp();
      },
      methods: {
        handleEmit(event, ...args) {
          this.$emit(event, ...args);
        },
        async startApp() {
          try {
            // $props 是vue 2.2版本才有的属性,所以这里直接全部写一遍
            await rawStartApp({
              name: this.name,
              url: this.url,
              el: this.$refs.wujie,
              loading: this.loading,
              alive: this.alive,
              fetch: this.fetch,
              props: this.props,
              attrs: this.attrs,
              replace: this.replace,
              sync: this.sync,
              prefix: this.prefix,
              fiber: this.fiber,
              degrade: this.degrade,
              plugins: this.plugins,
              beforeLoad: this.beforeLoad,
              beforeMount: this.beforeMount,
              afterMount: this.afterMount,
              beforeUnmount: this.beforeUnmount,
              afterUnmount: this.afterUnmount,
              activated: this.activated,
              deactivated: this.deactivated,
              loadError: this.loadError,
            });
          } catch (error) {
            console.log(error);
          }
        },
        execStartApp() {
          this.startAppQueue = this.startAppQueue.then(this.startApp);
        },
        destroy() {
          destroyApp(this.name);
        },
      },
      beforeDestroy() {
        bus.$offAll(this.handleEmit);
      },
      render(c) {
        return c("div", {
          style: {
            width: this.width,
            height: this.height,
          },
          ref: "wujie",
        });
      },
    };
    
    const WujieVue = Vue.extend(wujieVueOptions);
    
    WujieVue.setupApp = setupApp;
    WujieVue.preloadApp = preloadApp;
    WujieVue.bus = bus;
    WujieVue.destroyApp = destroyApp;
    WujieVue.install = function (Vue) {
      Vue.component("WujieVue", WujieVue);
    };
    export default WujieVue;
    

    3 入口defineWujieWebComponent和StartApp

    首先从入口文件index看起,defineWujieWebComponent

    import { defineWujieWebComponent } from "./shadow";
    // 定义webComponent容器
    defineWujieWebComponent();
    
    // 定义webComponent  存在shadow.ts 文件中
    export function defineWujieWebComponent() {
      class WujieApp extends HTMLElement {
        connectedCallback(){
          if (this.shadowRoot) return;
          const shadowRoot = this.attachShadow({ mode: "open" });
          const sandbox = getWujieById(this.getAttribute(WUJIE_DATA_ID));
          patchElementEffect(shadowRoot, sandbox.iframe.contentWindow);
          sandbox.shadowRoot = shadowRoot;
        }
        disconnectedCallback() {
          const sandbox = getWujieById(this.getAttribute(WUJIE_DATA_ID));
          sandbox?.unmount();
        }
      }
      customElements?.define("wujie-app", WujieApp);
    }
    

    startApp方法

    startApp(options) {
      const newSandbox = new WuJie({ name, url, attrs, degradeAttrs, fiber, degrade, plugins, lifecycles });
      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;
    
    
    

    4 实例化

    4-1, wujie (sandbox.ts)

    // wujie
    class wujie {
      constructor(options) {
        /** iframeGenerator在 iframe.ts中**/
        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.proxy = proxyWindow;
          this.proxyDocument = proxyDocument;
          this.proxyLocation = proxyLocation;
        }
        this.provide.location = this.proxyLocation;
        addSandboxCacheWithWujie(this.id, this);
      }
    }
    

    4-2.非降级Proxygenerator

    非降级模式window、document、location代理

    window代理拦截,修改this指向

    export function proxyGenerator(
      iframe: HTMLIFrameElement,
      urlElement: HTMLAnchorElement,
      mainHostPath: string,
      appHostPath: string
    ): {
      proxyWindow: Window;
      proxyDocument: Object;
      proxyLocation: Object;
    } {
      const proxyWindow = new Proxy(iframe.contentWindow, {
        get: (target: Window, p: PropertyKey): any => {
          // location进行劫持
          /*xxx*/
          // 修正this指针指向
          return getTargetValue(target, p);
        },
        set: (target: Window, p: PropertyKey, value: any) => {
          checkProxyFunction(value);
          target[p] = value;
          return true;
        },
        /**其他方法属性**/
      });
    
      // proxy document
      const proxyDocument = new Proxy(
        {},
        {
          get: function (_fakeDocument, propKey) {
            const document = window.document;
            const { shadowRoot, proxyLocation } = iframe.contentWindow.__WUJIE;
            const rawCreateElement = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__;
            const rawCreateTextNode = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_TEXT_NODE__;
            // need fix
            /* 包括元素创建,元素选择操作等
             createElement,createTextNode, documentURI,URL,querySelector,querySelectorAll
             documentElement,scrollingElement ,forms,images,links等等
            */
            // from shadowRoot
            if (propKey === "getElementById") {
              return new Proxy(shadowRoot.querySelector, {
                // case document.querySelector.call
                apply(target, ctx, args) {
                  if (ctx !== iframe.contentDocument) {
                    return ctx[propKey]?.apply(ctx, args);
                  }
                  return target.call(shadowRoot, `[id="${args[0]}"]`);
                },
              });
            }
          },
        }
      );
    
      // proxy location
      const proxyLocation = new Proxy(
        {},
        {
          get: function (_fakeLocation, propKey) {
            const location = iframe.contentWindow.location;
            if (
              propKey === "host" || propKey === "hostname" || propKey === "protocol" || propKey === "port" ||
              propKey === "origin"
            ) {
              return urlElement[propKey];
            }
            /** 拦截相关propKey, 返回对应lication内容
            propKey =="href","reload","replace"
            **/
            return getTargetValue(location, propKey);
          },
          set: function (_fakeLocation, propKey, value) {
            // 如果是跳转链接的话重开一个iframe
            if (propKey === "href") {
              return locationHrefSet(iframe, value, appHostPath);
            }
            iframe.contentWindow.location[propKey] = value;
            return true;
          }
        }
      );
      return { proxyWindow, proxyDocument, proxyLocation };
    }
    
    

    4-3,降级模式localGenerator

    export function localGenerator(
    ){
      // 代理 document
      Object.defineProperties(proxyDocument, {
        createElement: {
          get: () => {
            return function (...args) {
              const element = rawCreateElement.apply(iframe.contentDocument, args);
              patchElementEffect(element, iframe.contentWindow);
              return element;
            };
          },
        },
      });
      // 普通处理
      const {
        modifyLocalProperties,
        modifyProperties,
        ownerProperties,
        shadowProperties,
        shadowMethods,
        documentProperties,
        documentMethods,
      } = documentProxyProperties;
      modifyProperties
        .filter((key) => !modifyLocalProperties.includes(key))
        .concat(ownerProperties, shadowProperties, shadowMethods, documentProperties, documentMethods)
        .forEach((key) => {
          Object.defineProperty(proxyDocument, key, {
            get: () => {
              const value = sandbox.document?.[key];
              return isCallable(value) ? value.bind(sandbox.document) : value;
            },
          });
        });
    
      // 代理 location
      const proxyLocation = {};
      const location = iframe.contentWindow.location;
      const locationKeys = Object.keys(location);
      const constantKey = ["host", "hostname", "port", "protocol", "port"];
      constantKey.forEach((key) => {
        proxyLocation[key] = urlElement[key];
      });
      Object.defineProperties(proxyLocation, {
        href: {
          get: () => location.href.replace(mainHostPath, appHostPath),
          set: (value) => {
            locationHrefSet(iframe, value, appHostPath);
          },
        },
        reload: {
          get() {
            warn(WUJIE_TIPS_RELOAD_DISABLED);
            return () => null;
          },
        },
      });
      return { proxyDocument, proxyLocation };
    }
    

    实例化化主要是建立起js运行时的沙箱iframe, 通过非降级模式下proxy和降级模式下对document,location,window等全局操作属性的拦截修改将其和对应的js沙箱操作关联起来

    5 importHTML入口文件解析

    importHtml方法(entry.ts)

    export default function importHTML(params: {
      url: string;
      html?: string;
      opts: ImportEntryOpts;
    }): Promise<htmlParseResult> {
      /*xxxx*/
      const getHtmlParseResult = (url, html, htmlLoader) =>
        (html
          ? Promise.resolve(html)
          : fetch(url).then( /** 使用fetch Api 加载子应用入口**/
              (response) => response.text(),
              (e) => {
                embedHTMLCache[url] = null;
                loadError?.(url, e);
                return Promise.reject(e);
              }
            )
        ).then((html) => {
          const assetPublicPath = getPublicPath(url);
          const { template, scripts, styles } = processTpl(htmlLoader(html), assetPublicPath);
          return {
            template: template,
            assetPublicPath,
            getExternalScripts: () =>
              getExternalScripts(
                scripts
                  .filter((script) => !script.src || !isMatchUrl(script.src, jsExcludes))
                  .map((script) => ({ ...script, ignore: script.src && isMatchUrl(script.src, jsIgnores) })),
                fetch,
                loadError,
                fiber
              ),
            getExternalStyleSheets: () =>
              getExternalStyleSheets(
                styles
                  .filter((style) => !style.src || !isMatchUrl(style.src, cssExcludes))
                  .map((style) => ({ ...style, ignore: style.src && isMatchUrl(style.src, cssIgnores) })),
                fetch,
                loadError
              ),
          };
        });
    
      if (opts?.plugins.some((plugin) => plugin.htmlLoader)) {
        return getHtmlParseResult(url, html, htmlLoader);
        // 没有html-loader可以做缓存
      } else {
        return embedHTMLCache[url] || (embedHTMLCache[url] = getHtmlParseResult(url, html, htmlLoader));
      }
    }
    

    importHTML结构如图:

    注意点: 通过Fetch url加载子应用资源,这里也是需要子应用支持跨域设置的原因

    6 CssLoader和样式加载优化

    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;
    }
    

    7 子应用active

    active方法主要用于做 子应用激活, 同步路由,动态修改iframe的fetch, 准备shadow, 准备子应用注入

    7-1, active方法(sandbox.ts)

    public async active(options){
        /**  options的检查  **/
        // 处理子应用自定义fetch
        // TODO fetch检验合法性
        const iframeWindow = this.iframe.contentWindow;
        iframeWindow.fetch = iframeFetch;
        this.fetch = iframeFetch;
       
    
        // 处理子应用路由同步
        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) {
          return;
        }
    
        if (this.shadowRoot) {
          this.el = renderElementToContainer(this.shadowRoot.host, el);
          if (this.alive) return;
        } else {
          // 预执行无容器,暂时插入iframe内部触发Web Component的connect
          // rawDocumentQuerySelector.call(iframeWindow.document, "body") 相当于Document.prototype.querySelector('body')
          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-2,createWujieWebComponent, renderElementToContainer, renderTemplateToShadowRoot

    // createWujieWebComponent
    export function createWujieWebComponent(id: string): HTMLElement {
      const contentElement = window.document.createElement("wujie-app");
      contentElement.setAttribute(WUJIE_DATA_ID, id);
      contentElement.classList.add(WUJIE_IFRAME_CLASS);
      return contentElement;
    }
    
    /**
     * 将准备好的内容插入容器
     */
    export function renderElementToContainer(
      element: Element | ChildNode,
      selectorOrElement: string | HTMLElement
    ): HTMLElement {
      const container = getContainer(selectorOrElement);
      if (container && !container.contains(element)) {
        // 有 loading 无需清理,已经清理过了
        if (!container.querySelector(`div[${LOADING_DATA_FLAG}]`)) {
          // 清除内容
          clearChild(container);
        }
        // 插入元素
        if (element) {
          //  rawElementAppendChild = HTMLElement.prototype.appendChild;
          rawElementAppendChild.call(container, element);
        }
      }
      return container;
    }
    /**
     * 将template渲染到shadowRoot
     */
    export async function renderTemplateToShadowRoot(
      shadowRoot: ShadowRoot,
      iframeWindow: Window,
      template: string
    ): Promise<void> {
      const html = renderTemplateToHtml(iframeWindow, template);
      // 处理 css-before-loader 和 css-after-loader
      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");
    
      // 修复 html parentNode
      Object.defineProperty(shadowRoot.firstChild, "parentNode", {
        enumerable: true,
        configurable: true,
        get: () => iframeWindow.document,
      });
    
      patchRenderEffect(shadowRoot, iframeWindow.__WUJIE.id, false);
    }
    /**
     * 将template渲染成html元素
     */
    function renderTemplateToHtml(iframeWindow: Window, template: string): HTMLHtmlElement {
      const sandbox = iframeWindow.__WUJIE;
      const { head, body, alive, execFlag } = sandbox;
      const document = iframeWindow.document;
      let html = document.createElement("html");
      html.innerHTML = template;
      // 组件多次渲染,head和body必须一直使用同一个来应对被缓存的场景
      if (!alive && execFlag) {
        html = replaceHeadAndBody(html, head, body);
      } else {
        sandbox.head = html.querySelector("head");
        sandbox.body = html.querySelector("body");
      }
      const ElementIterator = document.createTreeWalker(html, NodeFilter.SHOW_ELEMENT, null, false);
      let nextElement = ElementIterator.currentNode as HTMLElement;
      while (nextElement) {
        patchElementEffect(nextElement, iframeWindow);
        const relativeAttr = relativeElementTagAttrMap[nextElement.tagName];
        const url = nextElement[relativeAttr];
        if (relativeAttr) nextElement.setAttribute(relativeAttr, getAbsolutePath(url, nextElement.baseURI || ""));
        nextElement = ElementIterator.nextNode() as HTMLElement;
      }
      if (!html.querySelector("head")) {
        const head = document.createElement("head");
        html.appendChild(head);
      }
      if (!html.querySelector("body")) {
        const body = document.createElement("body");
        html.appendChild(body);
      }
      return html;
    }
    /*
    // 保存原型方法
    // 子应用的Document.prototype已经被改写了
    export const rawElementAppendChild = HTMLElement.prototype.appendChild;
    export const rawElementRemoveChild = HTMLElement.prototype.removeChild;
    export const rawHeadInsertBefore = HTMLHeadElement.prototype.insertBefore;
    export const rawBodyInsertBefore = HTMLBodyElement.prototype.insertBefore;
    export const rawAddEventListener = Node.prototype.addEventListener;
    export const rawRemoveEventListener = Node.prototype.removeEventListener;
    export const rawWindowAddEventListener = window.addEventListener;
    export const rawWindowRemoveEventListener = window.removeEventListener;
    export const rawAppendChild = Node.prototype.appendChild;
    export const rawDocumentQuerySelector = window.__POWERED_BY_WUJIE__
      ? window.__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR__
      : Document.prototype.querySelector;
    */
    
    

    8 子应用启动执行start

    start 开始执行子应用,运行js,执行无界js插件列表

    public async start(getExternalScripts: () => ScriptResultList): Promise<void> {
        this.execFlag = true;
        // 执行脚本
        const scriptResultList = await getExternalScripts();
        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
              ? requestIdleCallback(() => insertScriptToIframe(beforeScriptResult, iframeWindow))
              : insertScriptToIframe(beforeScriptResult, iframeWindow)
          );
        });
        // 同步代码
        syncScriptResultList.concat(deferScriptResultList).forEach((scriptResult) => {
          /**xxxxx**/
        });
    
        // 异步代码
        asyncScriptResultList.forEach((scriptResult) => {
          scriptResult.contentPromise.then((content) => {
            this.fiber
              ? requestIdleCallback(() => insertScriptToIframe({ ...scriptResult, content }, iframeWindow))
              : insertScriptToIframe({ ...scriptResult, content }, iframeWindow);
          });
        });
    
        //框架主动调用mount方法
        this.execQueue.push(this.fiber ? () => requestIdleCallback(() => this.mount()) : () => this.mount());
    
        //触发 DOMContentLoaded 事件
        const domContentLoadedTrigger = () => {
          eventTrigger(iframeWindow.document, "DOMContentLoaded");
          eventTrigger(iframeWindow, "DOMContentLoaded");
          this.execQueue.shift()?.();
        };
        this.execQueue.push(this.fiber ? () => requestIdleCallback(domContentLoadedTrigger) : domContentLoadedTrigger);
    
        // 插入代码后
        afterScriptResultList.forEach((afterScriptResult) => {
          /**xxxxx**/
        });
    
        //触发 loaded 事件
        const domLoadedTrigger = () => {
          eventTrigger(iframeWindow.document, "readystatechange");
          eventTrigger(iframeWindow, "load");
          this.execQueue.shift()?.();
        };
        this.execQueue.push(this.fiber ? () => 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()?.();
          });
        });
      }
    
    // getExternalScripts
    export function getExternalScripts(
      scripts: ScriptObject[],
      fetch: (input: RequestInfo, init?: RequestInit) => Promise<Response> = defaultFetch,
      loadError: loadErrorHandler,
      fiber: boolean
    ): ScriptResultList {
      // module should be requested in iframe
      return scripts.map((script) => {
        const { src, async, defer, module, ignore } = script;
        let contentPromise = null;
        // async
        if ((async || defer) && src && !module) {
          contentPromise = new Promise((resolve, reject) =>
            fiber
              ? requestIdleCallback(() => fetchAssets(src, scriptCache, fetch, false, loadError).then(resolve, reject))
              : fetchAssets(src, scriptCache, fetch, false, loadError).then(resolve, reject)
          );
          // module || ignore
        } else if ((module && src) || ignore) {
          contentPromise = Promise.resolve("");
          // inline
        } else if (!src) {
          contentPromise = Promise.resolve(script.content);
          // outline
        } else {
          contentPromise = fetchAssets(src, scriptCache, fetch, false, loadError);
        }
        return { ...script, contentPromise };
      });
    }
    
    // 加载assets资源 
    // 如果存在缓存则从缓存中获取
    const fetchAssets = (
      src: string,
      cache: Object,
      fetch: (input: RequestInfo, init?: RequestInit) => Promise<Response>,
      cssFlag?: boolean,
      loadError?: loadErrorHandler
    ) =>
      cache[src] ||
      (cache[src] = fetch(src)
        .then((response) => {
         /**status > 400按error处理**/
          return response.text();
        })
     }));
    
    // insertScriptToIframe
    export function insertScriptToIframe(
      scriptResult: ScriptObject | ScriptObjectLoader,
      iframeWindow: Window,
      rawElement?: HTMLScriptElement
    ) {
      const { src, module, content, crossorigin, crossoriginType, async, 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));
    
      // 内联脚本处理
      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,
    );`;
        }
      } else {
        // 外联自动触发onload
        onload && (scriptElement.onload = onload as (this: GlobalEventHandlers, ev: Event) => any);
        src && scriptElement.setAttribute("src", src);
        crossorigin && scriptElement.setAttribute("crossorigin", crossoriginType);
      }
      // esm 模块加载
      module && scriptElement.setAttribute("type", "module");
      scriptElement.textContent = code || "";
      // 执行script队列检测
      nextScriptElement.textContent =
        "if(window.__WUJIE.execQueue && window.__WUJIE.execQueue.length){ window.__WUJIE.execQueue.shift()()}";
    
      const container = rawDocumentQuerySelector.call(iframeWindow.document, "head");
      if (/^<!DOCTYPE html/i.test(code)) {
        error(WUJIE_TIPS_SCRIPT_ERROR_REQUESTED, scriptResult);
        return !async && container.appendChild(nextScriptElement);
      }
      container.appendChild(scriptElement);
    
      // 调用回调
      callback?.(iframeWindow);
      // 执行 hooks
      execHooks(plugins, "appendOrInsertElementHook", scriptElement, iframeWindow, rawElement);
      // 外联转内联调用手动触发onload
      content && onload?.();
      // async脚本不在执行队列,无需next操作
      !async && container.appendChild(nextScriptElement);
    }
    
    

    9 子应用销毁

    /** 销毁子应用 */
      public destroy() {
        this.bus.$clear();
        // thi.xxx = null;
        // 清除 dom
        if (this.el) {
          clearChild(this.el);
          this.el = null;
        }
        // 清除 iframe 沙箱
        if (this.iframe) {
          this.iframe.parentNode?.removeChild(this.iframe);
        }
        // 删除缓存
        deleteWujieById(this.id);
      }
    

    主应用,无界,子应用之间的关系

    主应用创建自定义元素和创建iframe元素

    无界将子应用解析后的html,css加入到自定义元素,进行元素和样式隔离

    同时建立iframe代理,将iframe和自定义元素shadowDom进行关联,

    将子应用中的js放入iframe执行,iframe中js执行的结果被代理到修改shadowDom结构和数据