如何编写一个前端框架之三-代码运行沙箱(译)

本系列一共七章,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 地址请查阅这里,原文地址请查阅这里,接下来说解的是数据绑定简介。

相关文章
相关标签/搜索