微前端已經成為前端領域比較火爆的話題,在技術方面,微前端有一個始終繞不過去的話題就是前端沙箱
Sandboxie(又叫沙箱、沙盤)即是一個虛拟系統程式,允許你在沙盤環境中運作浏覽器或其他程式,是以運作所産生的變化可以随後删除。它創造了一個類似沙盒的獨立作業環境,在其内部運作的程式并不能對硬碟産生永久性的影響。 在網絡安全中,沙箱指在隔離環境中,用以測試不受信任的檔案或應用程式等行為的工具
簡單來說沙箱(sandbox)就是與外界隔絕的一個環境,内外環境互不影響,外界無法修改該環境内任何資訊,沙箱内的東西單獨屬于一個世界。
對于 JavaScript 來說,沙箱并非傳統意義上的沙箱,它隻是一種文法上的 Hack 寫法,沙箱是一種安全機制,把一些不信任的代碼運作在沙箱之内,使其不能通路沙箱之外的代碼。當需要解析或着執行不可信的 JavaScript 的時候,需要隔離被執行代碼的執行環境的時候,需要對執行代碼中可通路對象進行限制,通常開始可以把 JavaScript 中處理子產品依賴關系的閉包稱之為沙箱。
我們大緻可以把沙箱的實作總體分為兩個部分:
建構一個閉包環境
模拟原生浏覽器對象
我們知道 JavaScript 中,關于作用域(scope),隻有全局作用域(global scope)、函數作用域(function scope)以及從 ES6 開始才有的塊級作用域(block scope)。如果要将一段代碼中的變量、函數等的定義隔離出來,受限于 JavaScript 對作用域的控制,隻能将這段代碼封裝到一個 Function 中,通過使用 function scope 來達到作用域隔離的目的。也因為需要這種使用函數來達到作用域隔離的目的方式,于是就有 IIFE(立即調用函數表達式),這是一個被稱為 自執行匿名函數的設計模式
當函數變成立即執行的函數表達式時,表達式中的變量不能從外部通路,它擁有獨立的詞法作用域。不僅避免了外界通路 IIFE 中的變量,而且又不會污染全局作用域,彌補了 JavaScript 在 scope 方面的缺陷。一般常見于寫插件和類庫時,如 JQuery 當中的沙箱模式
當将 IIFE 配置設定給一個變量,不是存儲 IIFE 本身,而是存儲 IIFE 執行後傳回的結果。
模拟原生浏覽器對象的目的是為了,防止閉包環境,操作原生對象。篡改污染原生環境;完成模拟浏覽器對象之前我們需要先關注幾個不常用的 API。
eval 函數可将字元串轉換為代碼執行,并傳回一個或多個值
由于 eval 執行的代碼可以通路閉包和全局範圍,是以就導緻了代碼注入的安全問題,因為代碼内部可以沿着作用域鍊往上找,篡改全局變量,這是我們不希望的
Function 構造函數建立一個新的 Function 對象。直接調用這個構造函數可用動态建立函數
文法
<code>new Function ([arg1[, arg2[, ...argN]],] functionBody)</code>
arg1, arg2, ... argN 被函數使用的參數的名稱必須是合法命名的。參數名稱是一個有效的 JavaScript 辨別符的字元串,或者一個用逗号分隔的有效字元串的清單;例如“×”,“theValue”,或“a,b”。
functionBody
一個含有包括函數定義的 JavaScript 語句的字元串。
同樣也會遇到和 eval 類似的的安全問題和相對較小的性能問題。
與 eval 不同的是 Function 建立的函數隻能在全局作用域中運作。它無法通路局部閉包變量,它們總是被建立于全局環境,是以在運作時它們隻能通路全局變量和自己的局部變量,不能通路它們被 Function 構造器建立時所在的作用域的變量;但是,它仍然可以通路全局範圍。new Function()是 eval()更好替代方案。它具有卓越的性能和安全性,但仍沒有解決通路全局的問題。
with 是 JavaScript 中一個關鍵字,擴充一個語句的作用域鍊。它允許半沙盒執行。那什麼叫半沙盒?語句将某個對象添加到作用域鍊的頂部,如果在沙盒中有某個未使用命名空間的變量,跟作用域鍊中的某個屬性同名,則這個變量将指向這個屬性值。如果沒有同名的屬性,則将拋出 ReferenceError。
究其原理,<code>with</code>在内部使用<code>in</code>運算符。對于塊内的每個變量通路,它都在沙盒條件下計算變量。如果條件是 true,它将從沙盒中檢索變量。否則,就在全局範圍内查找變量。但是 with 語句使程式在查找變量值時,都是先在指定的對象中查找。是以對于那些本來不是這個對象的屬性的變量,查找起來會很慢,對于有性能要求的程式不适合(JavaScript 引擎會在編譯階段進行數項的性能優化。其中有些優化依賴于能夠根據代碼的詞法進行靜态分析,并預先确定所有變量和函數的定義位置,才能在執行過程中快速找到辨別符。)。with 也會導緻資料洩漏(在非嚴格模式下,會自動在全局作用域建立一個全局變量)
in 運算符能夠檢測左側操作數是否為右側操作數的成員。其中,左側操作數是一個字元串,或者可以轉換為字元串的表達式,右側操作數是一個對象或數組。
配合 with 用法可以稍微限制沙盒作用域,先從目前的 with 提供對象查找,但是如果查找不到依然還能從上擷取,污染或篡改全局環境。
由上部分内容思考,假如可以做到在使用<code>with</code>對于塊内的每個變量通路都限制在沙盒條件下計算變量,從沙盒中檢索變量。那麼是否可以完美的解決JavaScript沙箱機制。
使用 with 再加上 proxy 實作 JavaScript 沙箱
ES6 Proxy 用于修改某些操作的預設行為,等同于在語言層面做出修改,屬于一種“元程式設計”(meta programming)
我們前面提到<code>with</code>在内部使用<code>in</code>運算符來計算變量,如果條件是 true,它将從沙盒中檢索變量。理想狀态下沒有問題,但也總有些特例獨行的存在,比如 Symbol.unscopables。
Symbol.unscopables
Symbol.unscopables 對象的 Symbol.unscopables 屬性,指向一個對象。該對象指定了使用 with 關鍵字時,哪些屬性會被 with 環境排除。
上面代碼說明,數組有 6 個屬性,會被 with 指令排除。

由此我們的代碼還需要修改如下:
Symbol.unscopables 定義對象的不可作用屬性。Unscopeable 屬性永遠不會從 with 語句中的沙箱對象中檢索,而是直接從閉包或全局範圍中檢索。
以下是 qiankun 的 snapshotSandbox 的源碼,這裡為了幫助了解做部分精簡及注釋。
快照沙箱實作來說比較簡單,主要用于不支援 Proxy 的低版本浏覽器,原理是基于<code>diff</code>來實作的,在子應用激活或者解除安裝時分别去通過快照的形式記錄或還原狀态來實作沙箱,snapshotSandbox 會污染全局 window。
qiankun 架構 singular 模式下 proxy 沙箱實作,為了便于了解,這裡做了部分代碼的精簡和注釋。
legacySandBox 還是會操作 window 對象,但是他通過激活沙箱時還原子應用的狀态,解除安裝時還原主應用的狀态來實作沙箱隔離的,同樣會對 window 造成污染,但是性能比快照沙箱好,不用周遊 window 對象。
在 qiankun 的沙箱 proxySandbox 源碼裡面是對 fakeWindow 這個對象進行了代理,而這個對象是通過 createFakeWindow 方法得到的,這個方法是将 window 的 document、location、top、window 等等屬性拷貝一份,給到 fakeWindow。
源碼展示:
proxySandbox 由于是拷貝複制了一份 fakeWindow,不會污染全局 window,同時支援多個子應用同時加載。
詳細源碼請檢視:proxySandbox
常見的有:
CSS Module
namespace
Dynamic StyleSheet
css in js
Shadow DOM
常見的我們這邊不再贅述,這裡我們重點提一下Shadow DO。
Shadow DOM 允許将隐藏的 DOM 樹附加到正常的 DOM 樹中——它以 shadow root 節點為起始根節點,在這個根節點的下方,可以是任意元素,和普通的 DOM 元素一樣。