如何編寫一個前端框架之三-代碼運行沙箱(譯)

本系列一共七章,Github 地址請查閱這裏,原文地址請查閱這裏javascript

沙箱中代碼求值

這是編寫一個前端框架系列的第三章,本章我將會闡述瀏覽器端不一樣的代碼求值的方法及其所產生的問題。我也將會介紹一個方法,它依賴於一些新穎或者少見的 JavaScript 功能。前端

邪惡的 eval

eval() 函數用於對字符串形式的 JavaScript 代碼進行求值。java

代碼求值的最多見的解決方案即便用 eval() 函數。由 eval() 執行的代碼可以訪問閉包和全局做用域,這會致使被稱爲代碼注入 code injection 的安全隱患,正所以讓 eval() 成爲 JavaScript 最臭名昭著的功能之一。git

雖然讓人不爽,可是在某些狀況下 eval() 是很是有用的。大多數的現代框架須要它的功能,可是由於上面提到的問題而不敢使用。結果,出現了許多在沙箱而非全局做用域中的字符串求值的替代方案。沙箱防止代碼訪問安全數據。通常狀況下,它是一個簡單的對象,這個對象會爲求值代碼替換掉全局的對象。github

常規方案

替代 eval() 最多見的方式即爲徹底重寫 - 分兩步走,包括解析和解釋字符串。首先解析器建立一個抽象語法樹(AST),而後解釋器遍歷語法樹並在沙箱中解釋爲代碼。瀏覽器

這是被最爲普遍使用的方案,可是對於如此簡單的事情被認爲是牛刀小用。從零開始重寫全部的東西而不是爲 eval() 打補丁會致使易出不少的 bug, 而且它還要求頻繁地修改以匹配語言的升級更新。緩存

替代方案

NX 試圖避免從新實現原生代碼。代碼求值是由一個使用了一些新或者冷門的 JavaScript 功能的小型庫來處理的。安全

本節將會按部就班地介紹這些功能,而後由它們來介紹 nx-compile 是如何運行代碼的。此庫含有一個被稱爲 compileCode() 的庫,運行方式相似如下代碼:bash

const code = compileCode('return num1 + num2')
// this logs 17 to the console
console.log(code({num1: 10, num2: 7}))

const globalNum = 12
const otherCode = compileCode('return globalNum')

// global scope access is prevented
// this logs undefined to the console
console.log(otherCode({num1: 2, num2: 3}))
複製代碼

在本章末尾,咱們將會以少於 20 行的代碼來實現 compileCode 函數。前端框架

new Function()

函數構建器建立了一個新的函數對象。在 JavaScript 中,每一個函數都其實是一個函數對象。

Function 構造器是 eval() 的一個替代方案。new Function(...args, 'funcBody') 對傳入的 'funcBody' 字符串進行求值,並返回執行這段代碼的函數。它和 eval() 主要有兩點區別:

  • 它只會對傳入的代碼求值一次。調用返回的函數會直接運行代碼,而不會從新求值。
  • 它不能訪問本地閉包變量,可是仍然能夠訪問全局做用域。
function compileCode(src) {
	return new Function(src)
}
複製代碼

new Function() 在咱們的需求中是一個更好的替代 eval() 的方案。它有很好的性能和安全性,可是爲使其可行須要屏蔽其對全局做用域的訪問。

With 關鍵字

with 聲明爲一個聲明語句拓展了做用域鏈

with 是 JavaScript 一個冷門的關鍵字。它容許一個半沙箱的運行環境。with 代碼塊中的代碼會首先試圖從傳入的沙箱對象得到變量,可是若是沒找到,則會在閉包和全局做用域中尋找。閉包做用域的訪問能夠用 new Function() 來避免,因此咱們只須要處理全局做用域。

function compileCode(src) {
  src = 'with (sandbox) {' + src + '}'
  return new Function('sandbox', src)
}
複製代碼

with 內部使用 in 運算符。在塊中訪問每一個變量,都會使用variable in sandbox 條件進行判斷。若條件爲真,則從沙箱對象中讀取變量。不然,它會在全局做用域中尋找變量。經過欺騙 with 可讓variable in sandbox 一直返回真,咱們能夠防止它訪問全局做用域。

ES6 代理

代理對象用於定義基本操做的自定義行爲,如屬性查找或賦值。

一個 ES6 proxy 封裝一個對象並定義陷阱函數,這些函數能夠攔截對該對象的基本操做。當操做發生的時候,陷阱函數會被調用。經過在Proxy 中包裝沙箱對象並定義一個 has 陷阱,咱們能夠重寫 in 運算符的默認行爲。

function compileCode(src) {
  src ='with (sandbox) {' + src + '} const code = new Function('sandbox', src) return function(sandbox) { const sandboxProxy = new Proxy(sandbox, {has}) return code(sandboxProxy) } } // this trap intercepts 'in' operations on sandboxProxy function has(target, key) { return true } 複製代碼

以上代碼欺騙了 with 代碼塊。variable in sandbox 求值將會一直是 true 值,由於 has 陷阱函數會一直返回 true。with 代碼塊將永遠都不會嘗試訪問全局對象。

Symbol.unscopables

標記是一個惟一和不可變的數據類型,能夠被用做對象屬性的一個標識符。

Symbol.unscopables 是一個著名的標記。一個著名的標記便是一個內置的 JavaScript Symbol,它能夠用來表明內部語言行爲。例如,著名的標記能夠被用做添加或者覆寫遍歷或者基本類型轉換。

Symbol.unscopables 著名標記用來指定一個對象自身和繼承的屬性的值,這些屬性被排除在 with 所綁定的環境以外。

Symbol.unscopables 定義了一個對象的 unscopable(不可限定)屬性。在with語句中,不能從Sandbox對象中檢索Unscopable屬性,而是直接從閉包或全局做用域檢索屬性。Symbol.unscopables 是一個不經常使用的功能。你能夠在本頁上閱讀它被引入的緣由。

咱們能夠經過在沙箱的 Proxy 屬性中定義一個 get 陷阱來解決以上的問題,這能夠攔截 Symbol.unscopables 檢索,而且一直返回未定義。這將會欺騙 with 塊的代碼認爲咱們的沙箱對象沒有 unscopable 屬性。

function compileCode(src) {
  src = 'with(sandbox) {' + src + '}'
  const code = new Function('sandbox', src)
  
  return function(sandbox) {
    const sandboxProxy = new Proxy(sandbox, {has, get})
    return code(sandboxProxy)
  }
}

function has(target, key) {
  return true
}
  
function get(target, key) {
  if (key === Symbol.unscopables) return undefined
  return target[key]
}
複製代碼

使用 WeakMaps 來作緩存

如今代碼是安全的,可是它的性能仍然能夠升級,由於它每次調用返回函數時都會建立一個新的代理。可使用緩存來避免,每次調用時,若沙箱對象相同,則可使用同一個 Proxy 對象。

一個代理屬於一個沙箱對象,因此咱們能夠簡單地把代理添加到沙箱對象中做爲一個屬性。然而,這將會對外暴露咱們的實現細節,而且若是不可變的沙箱對象被 Object.freeze() 函數凍結了,這就行不通了。在這種狀況下,使用 WeakMap 是一個更好的替代方案。

WeakMap 對象是一個鍵/值對的集合,其中鍵是弱引用。鍵必須是對象,而值能夠是任意值。

一個 WeakMap 能夠用來爲對象添加數據,而不用直接用屬性來擴展數據。咱們可使用 WeakMaps 來間接地爲沙箱對象添加緩存代理。

const sandboxProxies = new WeakMap()

function compileCode (src) {
	src = 'with (sandbox) {' + src + '}'
	const code = new Function('sandbox', src)
	
	return function(sandbox) {
		if (!sandboxProxies.has(sandbox)) {
      const sandboxProxy = new Proxy(sandbox, {has, get})
      sandboxProxies.set(sandbox, sandboxProxy)
		}
		return code(sandboxProxies.get(sandbox))
	}
}

function has(target, key) {
  return true
}

function get(target, key) {
  if (key === Symbol.unscopables) return undefined
  return target[key]
}
複製代碼

這樣,每一個沙箱對象只能建立一個Proxy

最後說明

以上的 compileCode 例子是一個只有 19 行代碼的可用的沙箱代碼評估器。若是你想要看 nx-compile 庫的完整源碼,能夠參見這裏

除了解釋代碼求值,本章的目標是爲了展現如何利用新的 ES6 功能來改變現有的功能,而不是從新發明它們。我試圖經過這些例子來展現 ProxiesSymbols 的全部功能。

Github 地址請查閱這裏,原文地址請查閱這裏,接下來說解的是數據綁定簡介。

相關文章
相關標籤/搜索