微前端无界机制浅析

简介

随着项目的发展,前端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结构和数据

作者:京东物流 张燕燕、刘海鼎

来源:京东云开发者社区 自猿其说Tech 转载请注明来源

相关推荐
C语言魔术师12 分钟前
【小游戏篇】三子棋游戏
前端·算法·游戏
匹马夕阳1 小时前
Vue 3中导航守卫(Navigation Guard)结合Axios实现token认证机制
前端·javascript·vue.js
你熬夜了吗?1 小时前
日历热力图,月度数据可视化图表(日活跃图、格子图)vue组件
前端·vue.js·信息可视化
桂月二二8 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062069 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb9 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角9 小时前
CSS 颜色
前端·css
九酒9 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔10 小时前
HTML5 新表单属性详解
前端·html·html5
lee57610 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm