<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          社區(qū)精選|淺析微前端沙箱

          共 14813字,需瀏覽 30分鐘

           ·

          2023-09-26 22:41

          今天小編為大家?guī)淼氖巧鐓^(qū)作者 Grewer 的文章,讓我們一起來學(xué)習(xí)淺析微前端沙箱。




          前言

          在大型項目中,微前端是一種常見的優(yōu)化手段,本文就微前端中沙箱的機(jī)制及原理,作一下講解。


          首先什么是微前端

          Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends

          前端是一種多個團(tuán)隊通過獨立發(fā)布功能的方式來共同構(gòu)建現(xiàn)代化 web 應(yīng)用的技術(shù)手段及方法策略。


          常見的微前端實現(xiàn)機(jī)制


          iframe

          如果你還是不了解什么是微前端, 那么就將它當(dāng)做一種 iframe 即可, 但我們又為什么不直接用它呢?


          iframe 最大的特性就是提供了瀏覽器原生的硬隔離方案,不論是樣式隔離、js 隔離這類問題統(tǒng)統(tǒng)都能被完美解決。但他的最大問題也在于他的隔離性無法被突破,導(dǎo)致應(yīng)用間上下文無法被共享,隨之帶來的開發(fā)體驗、產(chǎn)品體驗的問題。


          1. url 不同步。瀏覽器刷新 iframe url 狀態(tài)丟失、后退前進(jìn)按鈕無法使用。

          2. UI 不同步,DOM 結(jié)構(gòu)不共享。想象一下屏幕右下角 1/4 的 iframe 里來一個帶遮罩層的彈框,同時我們要求這個彈框要瀏覽器居中顯示,還要瀏覽器 resize 時自動居中..

          3. 全局上下文完全隔離,內(nèi)存變量不共享。iframe 內(nèi)外系統(tǒng)的通信、數(shù)據(jù)同步等需求,主應(yīng)用的 cookie 要透傳到根域名都不同的子應(yīng)用中實現(xiàn)免登效果。

          4. 慢。每次子應(yīng)用進(jìn)入都是一次瀏覽器上下文重建、資源重新加載的過程。

          其中有的問題比較好解決(問題 1),有的問題我們可以睜一只眼閉一只眼(問題 4),但有的問題我們則很難解決(問題 3)甚至無法解決(問題 2),而這些無法解決的問題恰恰又會給產(chǎn)品帶來非常嚴(yán)重的體驗問題, 最終導(dǎo)致我們舍棄了 iframe 方案。


          取自文章:Why Not Iframe



          微前端沙箱

          在微前端的場景,由于多個獨立的應(yīng)用被組織到了一起,在沒有類似 iframe 的原生隔離下,勢必會出現(xiàn)沖突,如全局變量沖突、樣式?jīng)_突,這些沖突可能會導(dǎo)致應(yīng)用樣式異常,甚至功能不可用。這時候我們就需要一個獨立的運(yùn)行環(huán)境,而這個環(huán)境就叫做沙箱,即 sandbox


          實現(xiàn)沙盒的第一步就是創(chuàng)建一個作用域。這個作用域不會包含全局的屬性對象。首先需要隔離掉瀏覽器的原生對象,但是如何隔離,建立一個沙箱環(huán)境呢?



          基于代理(Proxy)的沙箱

          假設(shè)當(dāng)前一個頁面中只有一個微應(yīng)用在運(yùn)行,那他可以獨占整個 window 環(huán)境, 在切換微應(yīng)用時,只有將 window 環(huán)境恢復(fù)即可,保證下一個的使用。

          這便是單實例場景


          單實例

          一個最簡單的實現(xiàn) demo

          const varBox = {};const fakeWindow = new Proxy(window, {  get(target, key) {    return varBox[key] || window[key];  },  set(target, key, value) {    varBox[key] = value;    return true;  },});
          window.test = 1;

          通過一個簡單的 proxy 即可實現(xiàn)一個 window 的代理,將數(shù)據(jù)存儲到 varBox 中,而不影響原有的 window 的值


          而在某些文章里,他把沙箱實現(xiàn)的更加具體,還擁有啟用停用功能:

          // 修改全局對象 window 方法const setWindowProp = (prop, value, isDel) => {    if (value === undefined || isDel) {        delete window[prop];    } else {        window[prop] = value;    }}
          class Sandbox { name; proxy = null;
          // 沙箱期間新增的全局變量 addedPropsMap = new Map();
          // 沙箱期間更新的全局變量 modifiedPropsOriginalValueMap = new Map();
          // 持續(xù)記錄更新的(新增和修改的)全局變量的 map,用于在任意時刻做沙箱激活 currentUpdatedPropsValueMap = new Map();
          // 應(yīng)用沙箱被激活 active() { // 根據(jù)之前修改的記錄重新修改 window 的屬性,即還原沙箱之前的狀態(tài) this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v)); }
          // 應(yīng)用沙箱被卸載 inactive() { // 1 將沙箱期間修改的屬性還原為原先的屬性 this.modifiedPropsOriginalValueMap.forEach((v, p) => setWindowProp(p, v)); // 2 將沙箱期間新增的全局變量消除 this.addedPropsMap.forEach((_, p) => setWindowProp(p, undefined, true)); }
          constructor(name) { this.name = name; const fakeWindow = Object.create(null); // 創(chuàng)建一個原型為 null 的空對象 const { addedPropsMap, modifiedPropsOriginalValueMap, currentUpdatedPropsValueMap } = this; const proxy = new Proxy(fakeWindow, { set(_, prop, value) { if(!window.hasOwnProperty(prop)) { // 如果 window 上沒有的屬性,記錄到新增屬性里 addedPropsMap.set(prop, value); } else if (!modifiedPropsOriginalValueMap.has(prop)) { // 如果當(dāng)前 window 對象有該屬性,且未更新過,則記錄該屬性在 window 上的初始值 const originalValue = window[prop]; modifiedPropsOriginalValueMap.set(prop, originalValue); }
          // 記錄修改屬性以及修改后的值 currentUpdatedPropsValueMap.set(prop, value);
          // 設(shè)置值到全局 window 上 setWindowProp(prop,value); console.log('window.prop', window[prop]);
          return true; }, get(target, prop) { return window[prop]; }, }); this.proxy = proxy; }}
          // 初始化一個沙箱const newSandBox = new Sandbox('app1');const proxyWindow = newSandBox.proxy;proxyWindow.test = 1;console.log(window.test, proxyWindow.test) // 1 1;
          // 關(guān)閉沙箱newSandBox.inactive();console.log(window.test, proxyWindow.test); // undefined undefined;
          // 重啟沙箱newSandBox.active();console.log(window.test, proxyWindow.test) // 1 1 ;

          添加了沙箱的 activeinactive 方案來激活或者卸載沙箱,核心的功能 proxy 的創(chuàng)建則在構(gòu)造函數(shù)中 原理和上述的簡單 demo 中的實現(xiàn)類似,但是沒有直接攔截 window, 而是創(chuàng)建一個 fakeWindow,這就引出了我們要講的實例沙箱


          多實例

          我們把 fakeWindow 使用起來,將微應(yīng)用使用到的變量放到 fakeWindow 中,而共享的變量都從 window 中讀取。

          class Sandbox {    name;    constructor(name, context = {}) {        this.name = name;        const fakeWindow = Object.create({});
          return new Proxy(fakeWindow, { set(target, name, value) { if (Object.keys(context).includes(name)) { context[name] = value; } target[name] = value; }, get(target, name) { // 優(yōu)先使用共享對象 if (Object.keys(context).includes(name)) { return context[name]; } if (typeof target[name] === 'function' && /^[a-z]/.test(name)) { return target[name].bind && target[name].bind(target); } else { return target[name]; } } }); } // ...}
          /** * 注意這里的 context 十分關(guān)鍵,因為我們的 fakeWindow 是一個空對象,window 上的屬性都沒有, * 實際項目中這里的 context 應(yīng)該包含大量的 window 屬性, */
          // 初始化2個沙箱,共享 doucment 與一個全局變量const context = { document: window.document, globalData: 'abc' };
          const newSandBox1 = new Sandbox('app1', context);const newSandBox2 = new Sandbox('app2', context);
          newSandBox1.test = 1;newSandBox2.test = 2;window.test = 3;
          /** * 每個環(huán)境的私有屬性是隔離的 */console.log(newSandBox1.test, newSandBox2.test, window.test); // 1 2 3;
          /** * 共享屬性是沙盒共享的,這里 newSandBox2 環(huán)境中的 globalData 也被改變了 */newSandBox1.globalData = '123';console.log(newSandBox1.globalData, newSandBox2.globalData); // 123 123;

          基于 diff 的沙箱

          他也叫做快照沙箱,顧名思義,即在某個階段給當(dāng)前的運(yùn)行環(huán)境打一個快照,再在需要的時候把快照恢復(fù),從而實現(xiàn)隔離。

          類似玩游戲的 SL 大法,在某個時刻保存起來,操作完畢再重新 Load,回到之前的狀態(tài)。

          他的實現(xiàn)可以說是單實例的簡化版,分為激活與卸載兩個部分的操作。

          active() {  // 緩存active狀態(tài)的沙箱  this.windowSnapshot = {};  for (const item in window) {    this.windowSnapshot[item] = window[item];  }
          Object.keys(this.modifyMap).forEach(p => { window[p] = this.modifyMap[p]; })}
          inactive() {  for (const item in window) {    if (this.windowSnapshot[item] !== window[item]) {      // 記錄變更      this.modifyMap[item] = window[item];      // 還原window      window[item] = this.windowSnapshot[item];    }  }}

          activate 的時候遍歷 window 上的變量,存為 windowSnapshot

          deactivate 的時候再次遍歷 window 上的變量,分別和 windowSnapshot 對比,將不同的存到 modifyMap 里,window 恢復(fù)

          當(dāng)應(yīng)用再次切換的時候,就可以把 modifyMap 的變量恢復(fù)回 window 上,實現(xiàn)一次沙箱的切換。

          class Sandbox {    private windowSnapshot    private modifyMap    activate: () => void;    deactivate: () => void;}
          const sandbox = new Sandbox();sandbox.activate();// 執(zhí)行任意代碼sandbox.deactivate();

          此方案在實際項目中實現(xiàn)起來要復(fù)雜的多,其對比算法需要考慮非常多的情況,比如對于 window.a.b.c = 123 這種修改或者對于原型鏈的修改,這里都不能做到回滾到應(yīng)用加載前的全局狀態(tài)。所以這個方案一般不作為首選方案,是對老舊瀏覽器的一種降級處理。


          qiankun 中也有該降級方案,被稱為 SnapshotSandbox


          基于 iframe 的沙箱

          在上文講述了 iframe 作為微前端的一種實現(xiàn)方式,在沙箱中 iframe 也有他的獨特作用。

          const iframe = document.createElement('iframe', { url: 'about:blank' });
          const sandboxGlobal = iframe.contentWindow;sandbox(sandboxGlobal);

          注意:只有同域的 iframe 才能取出對應(yīng)的的 contentWindow。所以需要提供一個宿主應(yīng)用空的同域 URL 來作為這個 iframe 初始加載的 URL。根據(jù) HTML 的規(guī)范 這個 URL 用了 about:blank 一定保證保證同域,也不會發(fā)生資源加載。

          class SandboxWindow {    constructor(options, context, frameWindow) {        return new Proxy(frameWindow, {            set(target, name, value) {                if(Object.keys(context).includes(name)) {                    context[name] = value;                }                target[name] = value;            },            get(target, name) {                // 優(yōu)先使用共享對象                if(Object.keys(context).includes(name)) {                    return context[name];                }
          if(typeof target[name] === 'function' && /^[a-z]/.test(name)) { return target[name].bind && target[name].bind(target); } else { return target[name]; } } }); } // ...}
          const iframe = document.createElement('iframe', { url: 'about:blank' });document.body.appendChild(iframe);const sandboxGlobal = iframe.contentWindow;// 需要全局共享的變量const context = { document: window.document, history: window.histroy };const newSandBoxWindow = new SandboxWindow({}, context, sandboxGlobal);// newSandBoxWindow.history 全局對象// newSandBoxWindow.abc 為 'abc' 沙箱環(huán)境全局變量// window.abc 為 undefined

          總結(jié)一些,利用 iframe 沙箱可以實現(xiàn)以下特性:


          • 全局變量隔離,如 setTimeoutlocationreact 不同版本隔離

          • 路由隔離,應(yīng)用可以實現(xiàn)獨立路由,也可以共享全局路由

          • 多實例,可以同時存在多個獨立的微應(yīng)用同時運(yùn)行

          • 安全策略,可以配置微應(yīng)用對 CookielocalStorage 資源加載的限制


          在沙箱方案上 iframe 是比較好的,但是仍然存在以下問題:


          1. 兼容性問題, 不同的瀏覽器之間的實現(xiàn)方案可能存在差異,會導(dǎo)致兼容性問題。

          2. 額外的性能開銷

          3. 相對于其他的方案,應(yīng)用間的通信手段更麻煩



          基于 ShadowRealm 的沙箱

          ShadowRealm 提議提供了一種新的機(jī)制,可在新的全局對象和 JavaScript 內(nèi)置程序集的上下文中執(zhí)行 JavaScript 代碼。

          const sr = new ShadowRealm();
          // Sets a new global within the ShadowRealm onlysr.evaluate('globalThis.x = "my shadowRealm"');
          globalThis.x = "root"; //
          const srx = sr.evaluate('globalThis.x');
          srx; // "my shadowRealm"x; // "root"

          除了直接指向字符串代碼, 還可以引用文件執(zhí)行:

          const sr = new ShadowRealm();
          const redAdd = await sr.importValue('./inside-code.js', 'add');
          let result = redAdd(2, 3);
          console.assert(result === 5);

          點此查看詳細(xì)介紹


          回到正題,ShadowRealm 在安全性上的限制很多,并且缺少一些信息交互手段,最后他的兼容性也是一大痛點:


          截止目前 Chrome 版本 117.0.5938.48, 并未支持此 API,我們?nèi)匀恍枰?polyfill 才能使用。



          基于 VM 沙箱

          VM 沙箱使用類似于 nodevm 模塊,通過創(chuàng)建一個沙箱,然后傳入需要執(zhí)行的代碼。

          const vm = require('node:vm');
          const x = 1;
          const context = { x: 2 };vm.createContext(context); // Contextify the object.
          const code = 'x += 40; var y = 17;';// `x` and `y` are global variables in the context.// Initially, x has the value 2 because that is the value of context.x.vm.runInContext(code, context);
          console.log(context.x); // 42console.log(context.y); // 17
          console.log(x); // 1; y is not defined.

          vm 雖然在 node 中已實現(xiàn)了 sandbox, 但是在前端項目的微前端實現(xiàn)上并沒有起到太大的作用。



          總結(jié)

          本文列舉了多種沙箱的實現(xiàn)方案,在目前的前端領(lǐng)域中,有著各類沙箱的實現(xiàn),現(xiàn)在并沒有一個完美的解決方案,更多的是在適合的場景采用適合的解決方案。



          引用

          • https://www.garfishjs.org/blog
          • https://qiankun.umijs.org/zh/guide
          • https://zqianduan.com/pages/micro-app-sandbox.html


          點擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動和交流,公眾號后臺回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -



          往期推薦


          社區(qū)精選|現(xiàn)代 CSS 解決方案:原生嵌套(Nesting)


          社區(qū)精選|都用 HTTPS 了,還能被查出瀏覽記錄?


          社區(qū)精選|談?wù)?H5 移動端適配原理



          瀏覽 300
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产福利一区二区在线观看 | 亚洲色图处女 | 免费69成人无码无遮又大 | 在线高清视频无码不卡 | 在线国产黄色视频 |