翼度科技»论坛 编程开发 JavaScript 查看内容

微前端无界机制浅析

11

主题

11

帖子

33

积分

新手上路

Rank: 1

积分
33
简介

随着项目的发展,前端SPA应用的规模不断加大、业务代码耦合、编译慢,导致日常的维护难度日益增加。同时前端技术的发展迅猛,导致功能扩展吃力,重构成本高,稳定性低。
为了能够将前端模块解耦,通过相关技术调研,最终选择了无界微前端框架作为物流客服系统解耦支持。为了更好的使用无界微前端框架,我们对其运行机制进行了相关了解,以下是对无界运行机制的一些认识。
基本用法

主应用配置
  1. import WujieVue from 'wujie-vue2';
  2. const { setupApp, preloadApp, bus } = WujieVue;
  3. /*设置缓存*/
  4. setupApp({
  5. });
  6. /*预加载*/
  7. preloadApp({
  8.   name: 'vue2'
  9. })
  10. <WujieVue width="100%" height="100%" name="vue2" :url="vue2Url" :sync="true" :alive="true"></WujieVue
复制代码
4-3,降级模式localGenerator
  1. import Vue from "vue";
  2. import { bus, preloadApp, startApp as rawStartApp, destroyApp, setupApp } from "wujie";
  3. const wujieVueOptions = {
  4.   name: "WujieVue",
  5.   props: {
  6.        /*传入配置参数*/
  7.   },
  8.   data() {
  9.     return {
  10.       startAppQueue: Promise.resolve(),
  11.     };
  12.   },
  13.   mounted() {
  14.     bus.$onAll(this.handleEmit);
  15.     this.execStartApp();
  16.   },
  17.   methods: {
  18.     handleEmit(event, ...args) {
  19.       this.$emit(event, ...args);
  20.     },
  21.     async startApp() {
  22.       try {
  23.         // $props 是vue 2.2版本才有的属性,所以这里直接全部写一遍
  24.         await rawStartApp({
  25.           name: this.name,
  26.           url: this.url,
  27.           el: this.$refs.wujie,
  28.           loading: this.loading,
  29.           alive: this.alive,
  30.           fetch: this.fetch,
  31.           props: this.props,
  32.           attrs: this.attrs,
  33.           replace: this.replace,
  34.           sync: this.sync,
  35.           prefix: this.prefix,
  36.           fiber: this.fiber,
  37.           degrade: this.degrade,
  38.           plugins: this.plugins,
  39.           beforeLoad: this.beforeLoad,
  40.           beforeMount: this.beforeMount,
  41.           afterMount: this.afterMount,
  42.           beforeUnmount: this.beforeUnmount,
  43.           afterUnmount: this.afterUnmount,
  44.           activated: this.activated,
  45.           deactivated: this.deactivated,
  46.           loadError: this.loadError,
  47.         });
  48.       } catch (error) {
  49.         console.log(error);
  50.       }
  51.     },
  52.     execStartApp() {
  53.       this.startAppQueue = this.startAppQueue.then(this.startApp);
  54.     },
  55.     destroy() {
  56.       destroyApp(this.name);
  57.     },
  58.   },
  59.   beforeDestroy() {
  60.     bus.$offAll(this.handleEmit);
  61.   },
  62.   render(c) {
  63.     return c("div", {
  64.       style: {
  65.         width: this.width,
  66.         height: this.height,
  67.       },
  68.       ref: "wujie",
  69.     });
  70.   },
  71. };
  72. const WujieVue = Vue.extend(wujieVueOptions);
  73. WujieVue.setupApp = setupApp;
  74. WujieVue.preloadApp = preloadApp;
  75. WujieVue.bus = bus;
  76. WujieVue.destroyApp = destroyApp;
  77. WujieVue.install = function (Vue) {
  78.   Vue.component("WujieVue", WujieVue);
  79. };
  80. export default WujieVue;
复制代码
实例化化主要是建立起js运行时的沙箱iframe, 通过非降级模式下proxy和降级模式下对document,location,window等全局操作属性的拦截修改将其和对应的js沙箱操作关联起来

5 importHTML入口文件解析

importHtml方法(entry.ts)
  1. import { defineWujieWebComponent } from "./shadow";
  2. // 定义webComponent容器
  3. defineWujieWebComponent();
  4. // 定义webComponent  存在shadow.ts 文件中
  5. export function defineWujieWebComponent() {
  6.   class WujieApp extends HTMLElement {
  7.     connectedCallback(){
  8.       if (this.shadowRoot) return;
  9.       const shadowRoot = this.attachShadow({ mode: "open" });
  10.       const sandbox = getWujieById(this.getAttribute(WUJIE_DATA_ID));
  11.       patchElementEffect(shadowRoot, sandbox.iframe.contentWindow);
  12.       sandbox.shadowRoot = shadowRoot;
  13.     }
  14.     disconnectedCallback() {
  15.       const sandbox = getWujieById(this.getAttribute(WUJIE_DATA_ID));
  16.       sandbox?.unmount();
  17.     }
  18.   }
  19.   customElements?.define("wujie-app", WujieApp);
  20. }
复制代码
importHTML结构如图:

注意点: 通过Fetch url加载子应用资源,这里也是需要子应用支持跨域设置的原因
6 CssLoader和样式加载优化
  1. startApp(options) {
  2.   const newSandbox = new WuJie({ name, url, attrs, degradeAttrs, fiber, degrade, plugins, lifecycles });
  3.   const { template, getExternalScripts, getExternalStyleSheets } = await importHTML({
  4.     url,
  5.     html,
  6.     opts: {
  7.       fetch: fetch || window.fetch,
  8.       plugins: newSandbox.plugins,
  9.       loadError: newSandbox.lifecycles.loadError,
  10.       fiber,
  11.     },
  12.   });
  13.   const processedHtml = await processCssLoader(newSandbox, template, getExternalStyleSheets);
  14.   await newSandbox.active({ url, sync, prefix, template: processedHtml, el, props, alive, fetch, replace });
  15.   await newSandbox.start(getExternalScripts);
  16.   return newSandbox.destroy;
复制代码

7 子应用active

active方法主要用于做 子应用激活, 同步路由,动态修改iframe的fetch, 准备shadow, 准备子应用注入
7-1, active方法(sandbox.ts)
  1. // wujie
  2. class wujie {
  3.   constructor(options) {
  4.     /** iframeGenerator在 iframe.ts中**/
  5.     this.iframe = iframeGenerator(this, attrs, mainHostPath, appHostPath, appRoutePath);
  6.     if (this.degrade) { // 降级模式
  7.       const { proxyDocument, proxyLocation } = localGenerator(this.iframe, urlElement, mainHostPath, appHostPath);
  8.       this.proxyDocument = proxyDocument;
  9.       this.proxyLocation = proxyLocation;
  10.     } else {           // 非降级模式
  11.       const { proxyWindow, proxyDocument, proxyLocation } = proxyGenerator();
  12.       this.proxy = proxyWindow;
  13.       this.proxyDocument = proxyDocument;
  14.       this.proxyLocation = proxyLocation;
  15.     }
  16.     this.provide.location = this.proxyLocation;
  17.     addSandboxCacheWithWujie(this.id, this);
  18.   }
  19. }
复制代码
7-2,createWujieWebComponent, renderElementToContainer, renderTemplateToShadowRoot
  1. export function proxyGenerator(
  2.   iframe: HTMLIFrameElement,
  3.   urlElement: HTMLAnchorElement,
  4.   mainHostPath: string,
  5.   appHostPath: string
  6. ): {
  7.   proxyWindow: Window;
  8.   proxyDocument: Object;
  9.   proxyLocation: Object;
  10. } {
  11.   const proxyWindow = new Proxy(iframe.contentWindow, {
  12.     get: (target: Window, p: PropertyKey): any => {
  13.       // location进行劫持
  14.       /*xxx*/
  15.       // 修正this指针指向
  16.       return getTargetValue(target, p);
  17.     },
  18.     set: (target: Window, p: PropertyKey, value: any) => {
  19.       checkProxyFunction(value);
  20.       target[p] = value;
  21.       return true;
  22.     },
  23.     /**其他方法属性**/
  24.   });
  25.   // proxy document
  26.   const proxyDocument = new Proxy(
  27.     {},
  28.     {
  29.       get: function (_fakeDocument, propKey) {
  30.         const document = window.document;
  31.         const { shadowRoot, proxyLocation } = iframe.contentWindow.__WUJIE;
  32.         const rawCreateElement = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__;
  33.         const rawCreateTextNode = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_TEXT_NODE__;
  34.         // need fix
  35.         /* 包括元素创建,元素选择操作等
  36.          createElement,createTextNode, documentURI,URL,querySelector,querySelectorAll
  37.          documentElement,scrollingElement ,forms,images,links等等
  38.         */
  39.         // from shadowRoot
  40.         if (propKey === "getElementById") {
  41.           return new Proxy(shadowRoot.querySelector, {
  42.             // case document.querySelector.call
  43.             apply(target, ctx, args) {
  44.               if (ctx !== iframe.contentDocument) {
  45.                 return ctx[propKey]?.apply(ctx, args);
  46.               }
  47.               return target.call(shadowRoot, `[id="${args[0]}"]`);
  48.             },
  49.           });
  50.         }
  51.       },
  52.     }
  53.   );
  54.   // proxy location
  55.   const proxyLocation = new Proxy(
  56.     {},
  57.     {
  58.       get: function (_fakeLocation, propKey) {
  59.         const location = iframe.contentWindow.location;
  60.         if (
  61.           propKey === "host" || propKey === "hostname" || propKey === "protocol" || propKey === "port" ||
  62.           propKey === "origin"
  63.         ) {
  64.           return urlElement[propKey];
  65.         }
  66.         /** 拦截相关propKey, 返回对应lication内容
  67.         propKey =="href","reload","replace"
  68.         **/
  69.         return getTargetValue(location, propKey);
  70.       },
  71.       set: function (_fakeLocation, propKey, value) {
  72.         // 如果是跳转链接的话重开一个iframe
  73.         if (propKey === "href") {
  74.           return locationHrefSet(iframe, value, appHostPath);
  75.         }
  76.         iframe.contentWindow.location[propKey] = value;
  77.         return true;
  78.       }
  79.     }
  80.   );
  81.   return { proxyWindow, proxyDocument, proxyLocation };
  82. }
复制代码

8 子应用启动执行start

start 开始执行子应用,运行js,执行无界js插件列表
  1. export function localGenerator(
  2. ){
  3.   // 代理 document
  4.   Object.defineProperties(proxyDocument, {
  5.     createElement: {
  6.       get: () => {
  7.         return function (...args) {
  8.           const element = rawCreateElement.apply(iframe.contentDocument, args);
  9.           patchElementEffect(element, iframe.contentWindow);
  10.           return element;
  11.         };
  12.       },
  13.     },
  14.   });
  15.   // 普通处理
  16.   const {
  17.     modifyLocalProperties,
  18.     modifyProperties,
  19.     ownerProperties,
  20.     shadowProperties,
  21.     shadowMethods,
  22.     documentProperties,
  23.     documentMethods,
  24.   } = documentProxyProperties;
  25.   modifyProperties
  26.     .filter((key) => !modifyLocalProperties.includes(key))
  27.     .concat(ownerProperties, shadowProperties, shadowMethods, documentProperties, documentMethods)
  28.     .forEach((key) => {
  29.       Object.defineProperty(proxyDocument, key, {
  30.         get: () => {
  31.           const value = sandbox.document?.[key];
  32.           return isCallable(value) ? value.bind(sandbox.document) : value;
  33.         },
  34.       });
  35.     });
  36.   // 代理 location
  37.   const proxyLocation = {};
  38.   const location = iframe.contentWindow.location;
  39.   const locationKeys = Object.keys(location);
  40.   const constantKey = ["host", "hostname", "port", "protocol", "port"];
  41.   constantKey.forEach((key) => {
  42.     proxyLocation[key] = urlElement[key];
  43.   });
  44.   Object.defineProperties(proxyLocation, {
  45.     href: {
  46.       get: () => location.href.replace(mainHostPath, appHostPath),
  47.       set: (value) => {
  48.         locationHrefSet(iframe, value, appHostPath);
  49.       },
  50.     },
  51.     reload: {
  52.       get() {
  53.         warn(WUJIE_TIPS_RELOAD_DISABLED);
  54.         return () => null;
  55.       },
  56.     },
  57.   });
  58.   return { proxyDocument, proxyLocation };
  59. }
复制代码
[code]// getExternalScriptsexport function getExternalScripts(  scripts: ScriptObject[],  fetch: (input: RequestInfo, init?: RequestInit) => Promise = 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,  cssFlag?: boolean,  loadError?: loadErrorHandler) =>  cache[src] ||  (cache[src] = fetch(src)    .then((response) => {     /**status > 400按error处理**/      return response.text();    }) }));// insertScriptToIframeexport 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 (/^

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具