中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

無界微前端是怎么渲染子應用的demo解析

發布時間:2023-04-18 16:01:27 來源:億速云 閱讀:162 作者:iii 欄目:開發技術

這篇文章主要介紹“無界微前端是怎么渲染子應用的demo解析”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“無界微前端是怎么渲染子應用的demo解析”文章能幫助大家解決問題。

    無界渲染子應用的步驟

    無界與其他微前端框架(例如qiankun)的主要區別在于其獨特的 JS 沙箱機制。無界使用 iframe 來實現 JS 沙箱,由于這個設計,無界在以下方面表現得更加出色:

    • 應用切換沒有清理成本

    • 允許一個頁面同時激活多個子應用

    • 性能相對更優

    無界渲染子應用,主要分為以下幾個步驟:

    • 創建子應用 iframe

    • 解析入口 HTML

    • 創建 webComponent,并掛載 HTML

    • 運行 JS 渲染 UI

    創建子應用 iframe

    要在 iframe 中運行 JS,首先得有一個 iframe。

    export function iframeGenerator(
      sandbox: WuJie,
      attrs: { [key: string]: any },
      mainHostPath: string,
      appHostPath: string,
      appRoutePath: string
    ): HTMLIFrameElement {
      // 創建 iframe 的 DOM
      const iframe = window.document.createElement("iframe");
      // 設置 iframe 的 attr
      setAttrsToElement(iframe, { 
          // iframe 的 url 設置為主應用的域名
          src: mainHostPath, 
          style: "display: none", 
          ...attrs, 
          name: sandbox.id, 
          [WUJIE_DATA_FLAG]: "" 
      });
      // 將 iframe 插入到 document 中
      window.document.body.appendChild(iframe);
      const iframeWindow = iframe.contentWindow;
      // 停止 iframe 的加載
      sandbox.iframeReady = stopIframeLoading(iframeWindow).then(() => {
          // 省略其他內容
      }
      // 注入無界的變量到 iframeWindow,例如 __WUJIE
      patchIframeVariable(iframeWindow, sandbox, appHostPath);                                                           
      // 省略其他內容
      return iframe;
    }

    創建 iframe 主要有以下流程:

    • 創建 iframe 的 DOM,并設置屬性

    • 將 iframe 插入到 document 中(此時 iframe 會立即訪問 src)

    • 停止 iframe 的加載(stopIframeLoading)

    為什么要停止 iframe 的加載?

    因為要創建一個純凈的 iframe,防止 iframe 被污染,假如該 url 的 JS 代碼,聲明了一些全局變量、函數,就可能影響到子應用的運行(假如子應用也有同名的變量、函數)

    為什么 iframe 的 src 要設置為主應用的域名

    為了實現應用間(iframe 間)通訊,無界子應用 iframe 的 url 會設置為主應用的域名(同域)

    • 主應用域名為 a.com

    • 子應用域名為 b.com,但它對應的 iframe 域名為 a.com,所以要設置 b.com 的資源能夠允許跨域訪問

    因此 iframe 的 location.href 并不是子應用的 url。

    解析入口 HTML

    iframe 中運行 js,首先要知道要運行哪些 js

    我們可以通過解析入口 HTML 來確定需要運行的 JS 內容

    假設有以下HTML

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <script defer="defer" src="./static/js/main.4000cadb.js"></script>
        <link href="./static/css/main.7d8ad73e.css" rel="external nofollow"  rel="stylesheet">
    </head>
    <body>
    	<div id="root"></div>
    </body>
    </html>

    經過 importHTML 處理后,結果如下:

    • template 模板部分,去掉了所有的 script 和 style

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <!-- defer script https://wujie-micro.github.io/demo-react16/static/js/main.4000cadb.js replaced by wujie -->
        <!--  link https://wujie-micro.github.io/demo-react16/static/css/main.7d8ad73e.css replaced by wujie -->
    </head>
    </head>
    <body>
    	<div id="root"></div>
    </body>
    </html>
    • getExternalScripts,獲取所有內聯和外部的 script

    [
        {
          async: false,
          defer: true,
          src: 'https://wujie-micro.github.io/demo-react16/static/js/main.4000cadb.js',
          module: false,
          crossorigin: false,
          crossoriginType: '',
          ignore: false,
          contentPromise: // 獲取 script 內容字符串的 Promise
    	}
    ]
    • getExternalStyleSheets,獲取所有內聯和外部的 style

    [
        {
            src: "https://wujie-micro.github.io/demo-react16/static/css/main.7d8ad73e.css",
            ignore: false,
            contentPromise: // 獲取 style 內容字符串的 Promise
        }
    ]

    為什么要將 script 和 style 從 HTML 中分離?

    • HTML 要作為 webComponent 的內容,掛載到微前端掛載點上

    • 因為無界有插件機制,需要單獨對 js/style 進行處理,再插入到 webComponent 中

    • script 除了需要經過插件處理外,還需要放到 iframe 沙箱中執行,因此也要單獨分離出來

    external 是外部的意思,為什么 getExternalScripts 拿到的卻是所有的 script,而不是外部的非內聯 script?

    external 是相對于解析后的 HTML 模板來說的,由于解析后的 HTML 不帶有任何的 js 和 css,所以這里的 external,就是指模板外的所有 JS

    無界與 qiankun 的在解析 HTML 上區別?

    無界和 qiankun 都是以 HTML 為入口的微前端框架。qiankun 基于 import-html-entry 解析 HTML,而無界則是借鑒 import-html-entry 代碼,實現了自己的 HTML 的解析,因此兩者在解析 HTML 上的不同,主要是在importHTML 的實現上。

    由于無界支持執行 esModule script,需要在分析的結果中,保留更多的信息

    [
        {
          async: false,
          defer: true,
          src: 'https://wujie-micro.github.io/demo-react16/static/js/main.4000cadb.js',
          module: false,
          crossorigin: false,
          crossoriginType: '',
          ignore: false,
          contentPromise: // 獲取 script 內容字符串的 Promise
    	}
    ]

    import-html-entry 的分析結果中,只有 script 的 js 內容字符串。

    無界是如何獲取 HTML 的外部的 script、style 內容的?

    分析 HTML,可以拿到外部 scriptstyle 的 url,用 fetch 發起 ajax 就可以獲取到 scriptstyle 的內容。

    但是 fetch 相對于原來 HTML script 標簽,有一個壞處,就是 ajax 不能跨域,因此在使用無界的時候必須要給請求的資源設置允許跨域

    處理 CSS 并重新嵌入 HTML

    單獨將 CSS 分離出來,是為了讓無界插件能夠對 對 CSS 代碼進行修改,下面是一個 CSS loader 插件:

    const plugins = [
      {
        // 對 css 腳本動態的進行替換
        // code 為樣式代碼、url為樣式的地址(內聯樣式為'')、base為子應用當前的地址
        cssLoader: (code, url, base) => {
          console.log("css-loader", url, code.slice(0, 50) + "...");
          // do something
          return code;
        },
      },
    ];

    無界會用以下代碼遍歷插件修改 CSS

    // 將所有 plugin 的 CSSLoader 函數,合成一個 css-loader 處理函數
    const composeCssLoader = compose(sandbox.plugins.map((plugin) => plugin.cssLoader));
    const processedCssList: StyleResultList = getExternalStyleSheets().map(({ 
        src, 
        contentPromise 
    }) => {
      return {
        src,
        // 傳入 CSS 文本處理處理函數
        contentPromise: contentPromise.then((content) => composeCssLoader(content, src, curUrl)),
      };
    });

    修改后的 CSS,會存儲在 processedCssList 數組中,需要遍歷該數組的內容,將 CSS 重新嵌入到 HTML 中。

    舉個例子,這是我們之前的 HTML

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <!-- defer script https://wujie-micro.github.io/demo-react16/static/js/main.4000cadb.js replaced by wujie -->
        <!--  link https://wujie-micro.github.io/demo-react16/static/css/main.7d8ad73e.css replaced by wujie -->
    </head>
    </head>
    <body>
    	<div id="root"></div>
    </body>
    </html>

    嵌入 CSS 之后的 HTML 是這樣子的

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <!-- defer script https://wujie-micro.github.io/demo-react16/static/js/main.4000cadb.js replaced by wujie -->
    -   <!--  link https://wujie-micro.github.io/demo-react16/static/css/main.7d8ad73e.css replaced by wujie -->
    +   <style>
    +     	/* https://wujie-micro.github.io/demo-react16/static/css/main.7d8ad73e.css */.
    + 		省略內容
    +   <style/>
    </head>
    </head>
    <body>
    	<div id="root"></div>
    </body>
    </html>

    將原來的 Link 標簽替換成 style 標簽,并寫入 CSS 。

    創建 webComponent 并掛載 HTML

    在執行 JS 前,需要先把 HTML 的內容渲染出來。

    無界子應用是掛載在 webComponent 中的,其定義如下:

    class WujieApp extends HTMLElement {
      //  首次被插入文檔 DOM 時調用
      connectedCallback(): void {
        if (this.shadowRoot) return;
        // 創建 shadowDOM
        const shadowRoot = this.attachShadow({ mode: "open" });
        // 通過 webComponent 的標簽 WUJIE_DATA_ID,拿到子應用 id,再通過 id 拿到無界實例對象
        const sandbox = getWujieById(this.getAttribute(WUJIE_DATA_ID));
        // 保存 shadowDOM
        sandbox.shadowRoot = shadowRoot;
      }
      // 從文檔 DOM 中刪除時,被調用
      disconnectedCallback(): void {
        const sandbox = getWujieById(this.getAttribute(WUJIE_DATA_ID));
        sandbox?.unmount();
      }
    }
    customElements?.define("wujie-app", WujieApp);

    于是就可以這樣創建 webComponent

    export function createWujieWebComponent(id: string): HTMLElement {
      const contentElement = window.document.createElement("wujie-app");
      // 設置 WUJIE_DATA_ID 標簽,為子應用的 id‘
      contentElement.setAttribute(WUJIE_DATA_ID, id);
      return contentElement;
    }

    然后為 HTML 創建 DOM,這個非常簡單

    let html = document.createElement("html");
    html.innerHTML = template;	// template 為解析處理后的 HTML

    直接用 innerHTML 設置 html 的內容即可

    然后再插入 CSS(上一小節的內容)

    // processCssLoaderForTemplate 返回注入 CSS 的 html DOM 對象
    const processedHtml = await processCssLoaderForTemplate(iframeWindow.__WUJIE, html)

    最后掛載到 shadowDOM

    shadowRoot.appendChild(processedHtml);

    這樣就完成了 HTML 和 CSS 的掛載了,CSS 由于在 shadowDOM 內,樣式也不會影響到外部,也不會受外部樣式影響。

    JS 的執行細節

    HTML 渲染到 webComponent 之后,我們就可以執行 JS 了

    簡單的實現

    export function insertScriptToIframe(
      scriptResult: ScriptObject | ScriptObjectLoader,
      iframeWindow: Window,
    ) {
      const { 
         content, 	// js 的代碼字符串
      } = scriptResult;
      const scriptElement = iframeWindow.document.createElement("script");
      scriptElement.textContent = content || "";
      // 獲取 head 標簽
      const container = rawDocumentQuerySelector.call(iframeWindow.document, "head");
      // 在 head 中插入 script 標簽,就會運行 js
      container.appendChild(scriptElement);
    }

    創建 script 標簽,并插入到 iframe 的 head 中,就在 iframe 中能運行對應的 JS 代碼。

    這樣雖然能運行 JS,但是產生的副作用(例如渲染的 UI),也會留在 iframe 中。

    如何理解這句話?

    當我們在 iframe 中,使用 document.querySelector查找 #app 的 DOM 時,它只能在 iframe 中查找(副作用留在 iframe 中),但 UI 是渲染到 webComponent 中的,webComponent 不在 iframe 中,且 iframe 不可見。

    因此在 iframe 中就會找不到 DOM

    那要怎么辦呢?

    將 UI 渲染到 shadowRoot

    我們先來看看現代的前端框架,是如何渲染 UI 的

    以 Vue 為例,需要給 Vue 指定一個 DOM 作為掛載點,Vue 會將組件,掛載到該 DOM 上

    import Comp from './comp.vue' 
    // 傳入根組件
    const app = createApp(Comp)
    // 指定掛載點
    app.mount('#app')

    掛載到 #app,實際上使用 document.querySelector 查找 DOM,然后掛載到 DOM 里面

    但是正如上一小節說的,在無界微前端會有問題:

    • 如果在 iframe 中運行 document.querySelector,就會在 iframe 中查找就會查找不到,因為子應用的 HTML 是渲染到外部的 shadowRoot

    因此這里必須要對 iframedocument.querySelector 進行改造,改為從 shadowRoot 里面查找,才能使 Vue 組件能夠正確找到掛載點,偽代碼如下:

    const proxyDocument = new Proxy(
        {},
        {
          get: function (_, propKey) {
            if (propKey === "querySelector" || propKey === "querySelectorAll") {
              // 代理 shadowRoot 的 querySelector/querySelectorAll 方法
              return new Proxy(shadowRoot[propKey], {
                apply(target, ctx, args) {
                  // 相當于調用 shadowRoot.querySelector
                  return target.apply(shadowRoot, args);
                },
              });
            }
          },
        }
    );

    這樣修改之后,調用 proxyDocument.querySelector 就會從 shadowRoot 中查找元素,就能掛載到 shadowRoot 中的 DOM 中了。

    Vue 的根組件,就能成功掛載上去,其他子組件,因為是掛載到根節點或它的子節點上,不需要修改掛載位置,就能夠正確掛載。

    到此為止,如果不考慮其他 js 非視圖相關的 js 代碼,整個DOM 樹就已經掛載成功,UI 就已經能夠渲染出來了。

    挾持 document 的屬性/方法

    上一小節,通過 proxyDocument.querySelector,就能從 shadowRoot 查找元素

    但這樣有一個壞處,就是要將 document 改成 proxyDocument,代碼才能正確運行。但這是有方法解決的。

    假如我們要運行的是以下代碼:

    const app = document.querySelector('#app')
    // do something

    我們可以包一層函數:

    (function (document){
    	const app = document.querySelector('#app')
    	// do something  
    })(proxyDocument)

    這樣就不需要修改子應用的源碼,直接使用 document.querySelector

    但是,這樣做又會有新的問題:

    • esModule 的 import 必須要在函數最外層

    • var 聲明的變量,原本是全局變量,包一層函數后,變量會被留在函數內

    于是就有了下面的方案:

    // 挾持 iframeWindow.Document.prototype 的 querySelector
    // 從 proxyDocument 中獲取
    Object.defineProperty(iframeWindow.Document.prototype, 'querySelector', {
        enumerable: true,
        configurable: true,
        get: () =&gt; sandbox.proxyDocument['querySelector'],
        set: undefined,
    });

    只要我們在 iframe 創建時(子應用 JS),先通過 Object.defineProperty 重寫 querySelector,挾持 document 的屬性/方法,然后從 proxyDocument 中取值,

    這樣,就能直接執行子應用的 JS 代碼,不需要另外包一層函數執行 JS

    在無界微前端中,有非常多像 querySelector 的屬性/方法,需要對每個屬性方法的副作用進行修正。因此除了 proxyDocument,還有 proxyWindowproxyLocation

    很可惜的是,location 對象不能使用 Object.defineProperty 進行挾持,因此實際上,運行非 esModule 代碼時,仍然需要用函數包一層運行,傳入 proxyLocation 代替 location 對象。

    但 esModule 由于不能在函數中運行,因此 esModule 代碼中獲取的 location 對象是錯誤的,這個無界的常見問題文檔也有提到。

    接下來稍微介紹一下無界對 DOM 和 iframe 副作用的一些處理

    副作用的處理

    無界通過創建代理對象、覆蓋屬性和函數等方式對原有的JavaScript對象進行挾持。需要注意的是,所有這些處理都必須在子應用 JS 運行之前,也就是在 iframe 創建時執行:

    const iframe = window.document.createElement("iframe");
    // 將 iframe 插入到 document 中
    window.document.body.appendChild(iframe);
    const iframeWindow = iframe.contentWindow;
    // 停止 iframe 的加載
    sandbox.iframeReady = stopIframeLoading(iframeWindow).then(() =&gt; {
      // 對副作用進行處理修正
    }

    stopIframeLoading 后,即停止 iframe 加載,獲得純凈的 iframe 后,再對副作用進行處理

    無界微前端 JS 有非常多的副作用需要修正處理,文章不會一一列舉,這里會說一下大概,讓大家對這個有點概念。

    DOM 相關的副作用處理

    下面是幾個例子

    修正相對 URl
    <img src = "./images/test.png" alt = "Test Image" />

    當我們在 DOM 中使用相對 url 時,會用 DOM 節點的 baseURI 作為基準,其默認值為 document.location.href

    但我們知道,子應用的 UI 是掛載在 shadowRoot,跟主應用是同一個 document 上下文,因此它的 baseURI 默認是主應用的 url,但實際上應該為子應用的 url 才對,因此需要修正。

    下面是部分修正的偽代碼:

    // 重寫 Node 原型的 appendChild,在新增 DOM 時修正
    iframeWindow.Node.prototype.appendChild = function(node) {
        const res = rawAppendChild.call(this, node);
        // 修正 DOM 的 baseURI
        patchElementEffect(node, iframeWindow);
        return res;
    };

    事實上,除了 appendChild,還有其他的函數需要修正,在每個能夠創建 DOM 的位置,都需要進行修正,例如 insertBefore

    修正 shadowRoot head、body

    shadowRoot 可以視為子應用的 document

    在前端項目中,經常會在 JS 中引入 CSS,實際上 CSS 文本會以 style 標簽的形式注入到 docuement.head 中,偽代碼如下:

    export default function styleInject(css) {
      const head = document.head
      const style = document.createElement('style')
      style.type = 'text/css'
      style.styleSheet.cssText = css
      head.appendChild(style)
    }

    在 iframe 中使用 document.head,需要用 Object.defineProperty 挾持 document 的 head 屬性,將其重定向到 shadowRoot 的 head 標簽

    Object.defineProperty(iframeWindow.document, 'head', {
      enumerable: true,
      configurable: true,
      // 改為從 proxyDocument 中取值
      get: () => sandbox.proxyDocument['head'],
      set: undefined,
    });

    proxyDocument 的 head 實際上為 shadowRoot 的 head

    shadowRoot.head = shadowRoot.querySelector("head");
    shadowRoot.body = shadowRoot.querySelector("body");

    同樣的,很多組件庫的彈窗,都會往 document.body 插入彈窗的 DOM,因此也要處理

    iframe 的副作用處理

    History API

    history API 在 SPA 應用中非常常見,例如 vue-router 就會使用到 history.pushStatehistory.replaceState 等 API。

    當前 url 改變時

    • 需要改變 document.baseURI,而它是個只讀的值,需要修改 document.head 中的 base 標簽

    • 需要將子應用的 url,同步到父應用的地址欄中

    history.pushState = function (data: any, title: string, url?: string): void {
      // 當前的 url
      const baseUrl = mainHostPath 
      					+ iframeWindow.location.pathname
      					+ iframeWindow.location.search
      					+ iframeWindow.location.hash;
      // 根據當前 url,計算出即將跳轉的 url 的絕對路徑
      const mainUrl = getAbsolutePath(url?.replace(appHostPath, ""), baseUrl);
      // 調用原生的 history.pushState
      rawHistoryPushState.call(history, data, title, ignoreFlag ? undefined : mainUrl);
      // 更新 head 中的 base 標簽
      updateBase(iframeWindow, appHostPath, mainHostPath);
      // 同步 url 到主應用地址欄
      syncUrlToWindow(iframeWindow);
    };
    window/document 屬性/事件

    有些屬性,應該是使用主應用 window 的屬性,例如:getComputedStyle

    有些事件,需要掛載到主應用,有些需要掛載到 iframe 中。這里直接舉個例子:

    • onunload 事件,需要掛載到 iframe 中

    • onkeyup 事件,需要掛載到主應用的 window 下(iframe 中沒有 UI,UI 掛載到主應用 document 的 shadowRoot 下)

    因此要挾持 onXXX 事件和 addEventListener,對每一個事件進行分發,將事件掛載到 window / iframeWindow

    將事件掛載到window 的代碼實現如下:

    // 挾持 onXXX 函數
    Object.defineProperty(iframeWindow, 'onXXX', {
        enumerable: true,
        configurable: true,
        // 從 window 取
        get: () => window['onXXX'],
        set: (handler) => {
            	// 設置到 window
                window['onXXX'] = typeof handler === "function" 
                    ? handler.bind(iframeWindow) 	// 將函數的 this 設置為 iframeWindow
                	: handler;
              }
    });

    通過 Object.defineProperty 挾持 onXXX,將事件設置到 window 上。

    location 對象

    當我們在子應用 iframe 中獲取 location.hreflocation.host 等屬性的時候,需要獲取的是子應用的 hrefhost(iframe 的 location href 并不是子應用的 url),因此這里也是需要進行改造。

    const proxyLocation = new Proxy(
      {},
      {
        get: function (_, propKey) {
          if (propKey === "href") {
            return // 獲取子應用真正的 url
          }
    	  // 省略其他屬性的挾持
        },
      }
    );

    為什么 iframe 的 location href 不是子應用的 url?

    為了實現應用間(iframe 間)通訊,無界子應用 iframe 的 url 會設置為主應用的域名(同域)

    關于“無界微前端是怎么渲染子應用的demo解析”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    大荔县| 板桥市| 淮安市| 当阳市| 克山县| 桃源县| 江孜县| 墨脱县| 久治县| 宁南县| 大渡口区| 雅安市| 遂宁市| 新化县| 宜春市| 漳浦县| 南投市| 长寿区| 南昌县| 阿拉尔市| 钦州市| 嘉义市| 镇江市| 四川省| 读书| 灵川县| 武定县| 灵丘县| 郴州市| 宁乡县| 贵德县| 临邑县| 横峰县| 绥芬河市| 来凤县| 尉氏县| 云安县| 长寿区| 射洪县| 舞阳县| 牡丹江市|