2016-11-18

[译] 手把手教你写一个 Javascript 框架:沙箱求值

原文作者: Bertalan Miklos
原文地址: https://blog.risingstack.com/writing-a-javascript-framework-sandboxed-code-evaluation/
译文地址: http://www.wemlion.com/post/sandbox-code-evaluation
本文由 文蔺 翻译,转载请保留此声明。
著作权属于原作者,本译文仅用于学习、研究和交流目的,请勿用于商业目的。

本文是“编写 JavaScript 框架”系列的第三章。在本章中,我将介绍浏览器中对代码求值的几种不同方式及其存在的问题,也会介绍一种依赖 JavaScript 新特性的方法。

本系列主要是如何开发一个开源的客户端框架,框架名为 NX。我将在本系列中分享框架编写过程中如何克服遇到的主要困难。对 NX 感兴趣的朋友可以点击 NX 项目主页查看。

本系列章节如下:

邪恶 eval

函数用来对字符串形式的 JavaScript 代码进行求值。

常见的代码求值方法是使用 函数。通过 执行的代码可以访问闭包和全局作用域,所以可能导致代码注入(code injection),正因此 成为 JavaScript 中最臭名昭著的特性之一。

抛开上述缺点不说, 在某些情况下还是很有用的。多数现代前端框架都需要 的这种功能,但是往往又因前述问题畏手畏脚。因此出现许多字符串求值方案,在沙箱而非全局作用域中进行操作。沙箱可以阻止代码访问与安全相关的数据,它通常是一个简单对象,用于替换代码中的全局对象。

常见做法

替代 最常见的方式是彻底重新实现。重新实现的过程由解析(parsing)、解释(interpreting)两步组成。首先由解析器创建抽象语法树,然后由解释器遍历语法树,将其译为运行在沙箱中的代码。

这种方案使用广泛,但可谓是杀鸡拿了把牛刀。放弃修补 ,选择从零开始重写,带来的后果就是,许多 bug 蠢蠢欲动,准备伺机而出。而随着语言的升级更新,也不得不频繁修改源码。

另一种思路

NX 尽可能避免了重新实现代码,采用一个很小的库处理求值,该库使用了一些较可能少为人知的新特性。

这一节逐步介绍这些特性,并使用它们解释用于代码求值的 nx-compile 库。这个库有一个名为 的函数,工作方式如下:

待到本文结束,我们会用不到 20 行的代码实现 函数。

Function 构造函数用于创建新的 Function 对象。在 JavaScript 中,所有函数都是 Function 对象。

Function 构造函数可以达到 同样的目的。 对传入的 字符进行求值,并返回执行这段代码的函数。 的不同主要体现在以下两方面:

  • 方法只会对传入的代码求值一次。调用返回函数时,只会运行代码,而不会重新求值。

  • 方法无法访问闭包中的本地变量;不过还是可以访问全局作用域。

对我们来说, 要优于 。它性能更好,也更安全。不过要使其完全可用,还需要阻止其访问全局作用域。

关键词

能够扩展声明的作用域链。

JavaScript 中, 关键词较少露面。 可以帮我们半沙箱化地执行代码。 语句块首先会试着从传递的沙箱对象检索变量,如果没有找到,则会到闭包和全局作用域中寻找。前面说过, 能够阻止访问闭包中的变量,故现在只需考虑全局作用域的问题。

在内部实现中, 使用了 操作。对于语句块中的所有变量访问,都会使用 条件进行判断。若条件为真,则从沙箱对象中读取变量;否则会去全局变量中寻找变量。在 操作过程中,我们可以让 永远返回 true,这样就能阻止访问全局变量。

Sandboxed code evaluation: Simple 'with' statement

ES6 Proxy

Proxy 对象用于自定义 Object 的一些基本操作,如属性读取、赋值等行为。

ES6 封装对象,并定义一些 trap 函数,这些函数可以拦截该对象的基本操作行为。操作对象时,就会调用相应的 trap 函数。使用 封装沙箱对象,定义一个 操作 trap,即可覆盖 操作符的默认行为。

上面的代码耍了 代码块一把。 将永远为真,因为 trap 函数总是返回 true。 代码块中的代码永远无法访问全局对象。

Sandboxed code evaluation: 'with' statement and proxies

Symbol 是一种唯一的、不可变的数据类型,可用作对象属性标识符。

是一个驰名 symbol(Well-known symbol)。所谓“驰名 symbol”,实际上是一些内置 JavaScript ,代表某些内部语言行为。驰名 symbol 可以用于添加或重写一些行为,如数据的迭代、基本类型转换。

Symbol.unscopables 用于指定对象的一些固有和继承属性,这些属性被排除在 所绑定的环境之外无法读取。

用于定义对象的 unscopable 属性(译者:不译,请自行领会)。 声明中的沙箱对象的 unscopable 属性无法读取,这些属性会从闭包、全局作用域中读取。通常极少需要用到 。在这里可以看到引入 的原因。

Sandboxed code evaluation: 'with' statement and proxies. A security issue.

我们为沙箱对象 proxy 添加一个 trap 函数,拦截检索 属性的行为,总是返回 undefined。这样会骗到 代码块,使其认为沙箱对象没有任何 unscopable 属性。

Sandboxed code evaluation: 'with' statement and proxies. Has and get traps.

使用 WeakMap 进行缓存

代码现在是安全的,但性能还有可提升之处:可以看到,每次调用返回的函数时都会新建一个 。通过缓存可以避免该问题,每次调用时,若沙箱对象相同,则可以使用同一个 对象。

Proxy 对象与沙箱对象一一对应,故可以单纯地将其作为沙箱对象的一个属性。不过,这可能会对外暴露代码实现细节。另外,若使用的是 冻结之后的不可变沙箱对象也不行。所以采用 才是更好的选择。

WeakMap 对象是一个键值对集合。键为弱引用,必须是对象;值可以为任意类型。

可在不直接扩展对象属性的情况下为该对象附加数据。通过 间接为沙箱对象添加缓存的

这样一来,只会为每个沙箱对象新建一次 对象。

最后一点

上面的 例子仅 19 行代码,已经是一个可以工作的沙箱代码求值工具。如果有兴趣看看 nx-compile 的完整代码,可以访问 Github 仓库

除解释代码求值外,本章的主要目的是展示一些 ES6 新特性,用它们替代原有方式。贯穿整个例子,我试图展示了 的强大力量。

写在最后

如果对 NX 框架感兴趣,请访问 主页。胆大的读者还可以在 Github 上查看 NX 源码nx-observe 源码

希望你喜欢这篇文章。下一章我们将讨论 数据绑定