您好,登錄后才能下訂單哦!
這篇文章主要講解了“如何實現web微前端沙箱”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“如何實現web微前端沙箱”吧!
背景
應用沙箱可能是微前端技術體系里面最有意思的部分。一般來說沙箱是微前端技術體系中不是必須要做的事情,因為如果規范做的足夠好,是能夠避免掉一些變量沖突讀寫,CSS 樣式沖突的情況。但是如果你在一個足夠大的體系中,總不能僅僅通過規范來保證應用的可靠性,還是需要技術手段去治理運行時的一些沖突問題,這個也是沙箱方案成為微前端技術體系的一部分原因。
首先縱觀各類技術方案,有一個大前提決定了這個沙箱如何做:最終微應用是單實例 or 多實例存在宿主應用中。這個直接決定了這個沙箱的復雜度和技術方案。
單實例:同一個時刻只有一個微應用實例存在,此刻瀏覽器所有瀏覽器資源都是這個應用獨占的,方案要解決的很大程度是應用切換的時候的清理和現場恢復。比較輕量,實現起來也簡單。
多實例:資源不是應用獨占,就要解決資源共享的情況,比如路由,樣式,全局變量讀寫,DOM。可能需要考慮的情況比較多,實現較為復雜。
最開始我們的想法是:
從業務場景:我們可能存在的情況是當用戶操作一個產品 A 的同時和另一個產品 B 發生了關聯操作,需要喚醒應用 B 做操作。雖然從產品維度可以規避掉,比如先切到 B,然后切回 A,但是從某種程度上因為技術的原因,我們限制了產品交互的發揮。
從技術角度:解決了多實例當然單實例的場景也不在話下,并且單實例的方案某種程度上給編碼上帶來了一定復雜度,比如業務代碼需要自己做業務上下文的切換。
最近 qiankun 2 也轉變了思路,從單實例的支持到開始支持多實例,多多少少也側面說明了,多實例是一個值得投入和技術攻克的場景。
基于上面的考量,我們就開始了我們 Browser VM 沙箱的實現探索。總結起來可以用下圖表示:
JavaScript 沙箱實現
沙箱環境構造
要實現沙箱,我們需要隔離掉瀏覽器的原生對象,但是如何隔離,建立一個沙箱環境呢?Node 中 有 vm 模塊,來實現類似的能力,但是瀏覽器就不行了,但是我們可以利用閉包的能力、利用變量作用域去模擬一個沙箱環境,比如下面的代碼:
function foo(window) { console.log(window.document); } foo({ document: {}; });
比如這段代碼的輸出一定是 {},而不是原生瀏覽器的 document。
所以 ConsoleOS(面向阿里云管體系的微前端方案) 實現了一個 Wepback 的插件,在應用代碼構建的時候給子應用代碼加上一層 wrap 代碼,創建一個閉包,把需要隔離的瀏覽器原生對象變成從下面函數閉包中獲取,從而我們可以在應用加載的時候,傳入模擬的 window、document 之類的對象。
// 打包代碼 __CONSOLE_OS_GLOBAL_HOOK__(id, function (require, module, exports, {window, document, location, history}) { /* 打包代碼 */ }) function __CONSOLE_OS_GLOBAL_HOOK__(id, entry) { entry(require, module, exports, {window, document, location, history}) }
當然也可以不靠工程化的手段來實現,也可以通過請求腳本,然后在運行時拼接這段代碼,然后eval 或者 new Function 來達到相同的目的。
原生對象模擬
沙箱隔離能力有了,剩下的問題就是如何實現這一堆瀏覽器的原生對象了。最開始的想法是我們根據 ECMA 的規范實現(現在仍然有類似的想法),但是發現成本太高。不過在我們各種實驗之后,發現了一個很“取巧”的做法,我們可以 new iframe 對象,把里面的原生瀏覽器對象通過contentWindow 取出來,因為這些對象天然隔離,就省去了自己實現的成本。
const iframe = document.createElement( 'iframe' );
當然里面有很多的細節需要考量,比如:只有同域的 iframe 才能取出對應的的contentWindow。所以需要提供一個宿主應用空的同域 URL 來作為這個 iframe 初始加載的 URL。當然根據 HTML 的規范,這個 URL 用了 about:blank 一定保證同域,也不會發生資源加載,但是會發生和這個 iframe 中關聯的 history 不能被操作,這個時候路由的變換只能變成 hash 模式。
如下圖所示,我們取出對應的 iframe 中原生的對象之后,就會對特定需要隔離的對象生成對應的 Proxy, 然后對一些屬性獲取和屬性設置,做一些特定的設置,比如 window.document 需要返回特定的沙箱 document 而不是當前瀏覽器的 document。
class Window { constructor(options, context, frame) { return new Proxy(frame.contentWindow, { set(target, name, value) { target[name] = value; return true; }, get(target, name) { switch( name ) { case 'document': return context.document; default: } if( typeof target[ name ] === 'function' && /^[a-z]/.test( name ) ){ return target[ name ].bind && target[ name ].bind( target ); }else{ return target[ name ]; } } }); } }
對于每一個對象的實現這里不講細節了,有興趣可以看看我們的開源之后的代碼 :https://github.com/aliyun/alibabacloud-console-os/tree/master/packages/browser-vm
但是為了文檔能夠被加載在同一個 DOM 樹上,對于 document,大部分的 DOM 操作的屬性和方法還是直接用的宿主瀏覽器中的 document 的屬性和方法。
由于子應用有自己的沙箱環境,之前所有獨占式的資源現在都變成了應用獨享(尤其是 location、history),所以子應用也能同時被加載。并且對于一些變量,我們還能在 proxy 中設置一些訪問權限的事情,從而限制子應用的能力,比如 Cookie, LocalStoage 讀寫。
當這個 iframe 被移除時,寫在 window 的變量和設置的一些 timeout 時間也會一并被移除(當然 DOM 事件需要沙箱記錄,然后在宿主中移除)。
總結一下,我們的沙箱可以做到如下的特性:
CSS 隔離
CSS 隔離方案相對來說比較常規,常見的有:
CSS Module
添加 CSS 的 namespace
Dynamic StyleSheet
Shadow DOM
CSS Module or CSS Namespace
通過修改基礎組件樣式前綴來實現框架和微應用依賴基礎組件樣式的隔離性(依賴于工程上 CSS 的預處理器編譯和運行時基礎組件庫配置),同時避免全局樣式的書寫(依賴于約定或工程 lint 手段)。
Dynamic StyleSheet
隔離方式是通過 JS 運行時動態加載卸載微應用樣式表來避免樣式的沖突,局限性一是對于站點框架本身或其部件(header/menu/footer)與當前運行的微應用間仍存在樣式沖突的可能性,二是沒有辦法支持多個微應用同時運行顯示的情況。
Shadow DOM
優點是瀏覽器級別提供的樣式隔離能力,可以做到完全隔離。缺點在于,目前兼容性還是不太好,并且改造會涉及到舊應用的業務代碼的改造,對子應用侵入性比較高。
最終經過實踐,我們選擇的方式是 CSS Module + 添加 CSS 的 namespace。CSS module 保證的是應用業務樣式不沖突,Namespace 保證公共庫不沖突。我們實現了一個 postcss 插件,會在應用構建的時候給所有的樣式都加上應用前綴包括應用公共庫的 CSS(這樣方便做到同一個 組件庫新舊版本樣式的兼容)。如下圖所示:
// 宿主 host app .next-btn { color: #eee; } // 子應用 sub app aliyun-slb .next-btn { color: #eee; } //宿主中生成的節點 <aliyun-slb> <!-- 子應用的節點 --> </aliyun-slb>
這樣實現的好處在于:
每個應用都有 namespace,可以多實例共存。
不依賴特定的 CSS 預處理器。
對于同一個庫不同版本的 CSS(如 fusion1 和 fusion2),可以做到徹底隔離。
鑒于上面 JS 沙箱的存在,對于一些彈窗類的組件,這個微應用獲取的 body 實際上是宿主生成的節點,所以彈窗會被添加到微應用的節點(也就是上面的 aliyun-slb)這個節點,樣式不會失效。
不過也會有一些問題,比如:
嵌套應用組件樣式優先級的問題。由于 CSS module 的存在,一般只會發生在公共 CSS 樣式中,這個就是只能盡量避免嵌套。
fusion 不同版本庫公用字體的問題。目前的解決辦法:比較 hack,使用工程化的手段替換掉 next 字體的名字。
如何和其他體系結合
如果看完上面的文章,覺得這個沙箱方案不錯,但是又已經有自己的微前端體系了,想套用咋辦?
目前 ConsoleOS 的代碼已經在 Github 上開源:http://github.com/aliyun/alibabacloud-console-os,這里不妨可以嘗試試用一下。
JS 沙箱部分
如果看懂了上面關于原理的介紹可以看到其實沙箱實現包括兩個層面:
原生瀏覽器對象的模擬(Browser-VM)
如何構建一個閉包環境
Browser-VM 可以直接用起來,這部分完全是通用普適的。但是涉及到閉包構建的這部分,每個微前端體系不太一致,可能需要改造,比如:
import { createContext, removeContext } from '@alicloud/console-os-browser-vm'; const context = await createContext(); const run = window.eval(` (() => function({window, history, locaiton, document}) { window.test = 1; })() `) run(context); console.log(context.window.test); console.log(window.test); // 操作虛擬化瀏覽器對象 context.history.pushState(null, null, '/test'); context.locaiton.hash = 'foo' // 銷毀一個 context await removeContext( context );
當然可以直接選擇沙箱提供好的 evalScripts 方法:
import { evalScripts } from '@alicloud/console-os-browser-vm'; const context = evalScripts('window.test = 1;') console.log(window.test === undefined) // true
CSS 沙箱
如果用 Webpack 構建,可以直接配置如下:
const postcssWrap = require('@alicloud/console-toolkit-plugin-os/lib/postcssWrap') // 下面是 webpack config { test: /\.css$/, use: [ 'style-loader', { loader: 'postcss-loader', options: { plugins: [ // 加入插件 postcssWrap({ stackableRoot: '.prefix', repeat: 1 }) ], }, }, 'css-loader', ], exclude: /^node_modules$/, }
感謝各位的閱讀,以上就是“如何實現web微前端沙箱”的內容了,經過本文的學習后,相信大家對如何實現web微前端沙箱這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。