漫谈B端的沙箱技术

从语言学的角度上来说,允许代码无节制地使用全局变量,是最错误的选择之一。而更可怕的,就是一个变量\"可能\"成为全局的(在未知的时间与地点)。但是这两项,却伴随JavaScript这门语言成功地走到了现在。

\

也许是限于浏览器应用的规模,所以这一切还迟迟没有酿成灾难。在此之前,出现了两种解决方案。一种是ECMA在新的规范(Edition 5)中对此做出了限制,其中最重要的一条便是eval()的使用变得不再随意和无度。而另一种方案,则是相对没有那么官僚与学术的,尽管也拥有一个同样学术的名字:沙箱。

\

沙箱(Sandbox)并不是一个新东西,即使对于JavaScript来说,也已经存在了相当长的时间。在SpiderMonkey JS的源代码中,就明确地将一个闭包描述为一个沙箱。这包含着许多潜在的信息:它有一个初始环境,可以被重置,可以被复制,以及最重要的,在它内部的所有操作,不会影响到外部

\

当然事实上远非如此。JavaScript里的闭包只是一个\"貌似沙箱\"的东西--仍然是出于JavaScript早期的语言规范的问题,闭包不得不允许那些\"合法泄漏\"给外部的东西。而对于这一切无法忍受的前端工程师们,开始寻求另外的解决之道,这其中相对较早的尝试,是基于IFRAME的实践。例如dean.edwards在2006年提出过的方案(注1):

\
\a_frames.document.write(\  \"\u0026lt;script\u0026gt;\"+\  \"var MSIE/*@cc_on =1@*/;\"+ // sniff\  \"parent.sandbox=MSIE?this:{eval:function(s){return eval(s)}}\"+\  \"\u0026lt;\\/script\u0026gt;\"\);\
\

显然,由于在不同的IFRAME中运行着各自的JavaScript引擎实例,所以上述的方案也意味着沙箱是\"引擎\"这个级别的:在任何一个沙箱中的崩溃,将导致该引擎以及对应IFRAME崩溃。但--理论上说--不会影响整个浏览器。

\

问题是,这并不那么理想。往往的,引擎会导致整个浏览器锁在那里,例如用alert()弹出一个对话框而又因为某种意外失去了焦点。又或者单个的IFRAME会导致全局的CPU被耗光,例如一个死循环。于是更加复杂的方案--在JavaScritp中包含一个完整的执行器--出现了。最有名的则是Narrative JavaScript,它内建了一个执行器,用于逐行地解释执行JavaScript代码,这使得它可以控制所有的代码执行序列,或者随时重置整个执行引擎--如同一个沙箱所要做的那样。

\

这一切或者太过依赖于环境,又或者太过复杂,但都不乏追随者。例如jsFiddle这个项目(注2)在\"嵌入或装载\"这样的路子上就已经有了不俗的成绩。但是,YUI在新版本中却给出了它自己的选择:以更加明确的编程约定,来实现应用级别的沙箱。这包括一个非常简单的、新的YUI语法:

\
\YUI().use('dom-base', function(Y) {\  // Y是一个新的沙箱\});\
\

在'dom-base'位置上,可以是1到N个字符串,表明一个需要在沙箱中装载的模块列表。这可以是沙箱的初始列表,或者后续的callback函数(亦即是用户代码)所需依赖的模块列表。在这种实现方案中,YUI为每个沙箱维护各自的装载模块列表和上下文环境中的变量、成员。但是出于JavaScript语言自己的局限,这个沙箱依然是相当脆弱的。例如下一示例中沙箱内的代码就会污染到全局:

\
\YUI().use('', function(Y) {\  abc = 1234;  //\u0026lt;--这里可能导致一个全局变量'abc'被隐式地声明\});\
\

同样,在上述的沙箱里也可以使用类似window、document等全局变量、修改它们的成员或无限制地调用方法(例如使用setTimeout()来创建时钟)。所以YUI的沙箱事实上是靠\"规约\"来约束的,而不是真正意义上的沙箱。当然,这也意味着,如果用户能按照规约来处理沙箱内的代码,那么也就能自由地享用它带来的便利:安全、移植和有效的隔离副作用。

\

而我们再穷究其根底,YUI沙箱的实质不过是一行:

\
\\// code from yui.js\//  - mod.fn(this, name)\mod.entryFunc(sandbox, modName);\
\

其实际含义是:

\
  • mod :沙箱当前装载的模块;\
  • entryFunc : 上述模块的入口函数;\
  • sandbox :当前的沙箱的实例,即YUI()返回值;\
  • modName:模块名\

除了依赖关系(以及可能需要的异步加载)之外,YUI沙箱环境仅是用下面的代码来简单地调用callback函数:

\
\callback(Y, response);\
\

然而这些需求的实现并不那么复杂。首先,我们设定数据结构mod为一个对象:

\
\{ name:modName, fn: entryFunc, req: [], use: [] }
\

则一个环境对象env,将包括多个mod(将它们处理成对象而非数组,主要是便于使用名字来索引模块)和以及对它们进行管理操作的方法:

\
\{ mods:{}, used:{}, add:..., use:...}\
\

最后,所谓一个沙箱sandbox,就是上述环境对象的一个实例,并在初始时sandbox.mods与sandbox.used为空。由此简单的实现为:

\
\/**\ * tiny sandbox framework\ * mirror from YUI3 by aimingoo.\**/\function Sandbox() {\  if (!(this instanceof arguments.callee)) return new arguments.callee();\  this.mods = this.mods || {};\  this.used = {};\}\\Sandbox.prototype = {\  add: function(modName, entryFunc, reqArr, useArr) {\    this.mods[modName] = { fn: entryFunc, req: reqArr, use: useArr }\  },\\  use: function() { \    var mods = [].slice.call(arguments, 0); \t// 0..length-2 is modNames\    var callback = mods.pop();  \t// length-1 is callback\    var recursive_load = function(name, mod) {\      if (!this.used[name] \u0026amp;\u0026amp; (mod=this.mods[name])) {\        mod.req.forEach(recursive_load, this);\        mod.fn(this, name);\        mod.use.forEach(recursive_load, this);\        this.used[name] = true;\      }\    }\    mods.forEach(recursive_load, this);\    callback(this);\  }\}\
\

现在我们来尝试一个与YUI类似的语法风格:

\
\Sandbox().use('', function(){\   alert('user code.');\});\
\

或者,先向整个Sandbox环境注册一些模块(在真实的框架实现中,这一步可能是通过框架的装载器来初始化):

\
\// for test, entry of mods\f1 = function() { alert('f1') };\f2 = function() { alert('f2') };\f3 = function() { alert('f3') };\\// mods for global/common env.\Sandbox.prototype.mods = {\  'core': { fn: f1, req: [], use: [] },\  'oo':   { fn: f2, req: ['core'], use: ['xml'] },\  'xml':  { fn: f3, req: [], use: [] }\}\
\

然后再尝试在一个沙箱实例中运行代码:

\
\// f1 -\u0026gt; f2 -\u0026gt; f3 -\u0026gt; user code\Sandbox().use('oo', function(){\   alert('user code.');\});\
\

其实即便是上述代码中用于处理模块依赖的逻辑,也并不是什么\"神奇的\"代码或者技巧。除开这些,这样的沙箱隔离泄露的能力还抵不过一个嵌入式DSL语言。而后者所应用的技巧很简单,看不出什么花招(注3):

\
\with (YUI()) this.eval(\"... mod_context ... \");\
\

这样一来,在mod_context里的代码就只会在YUI()的一个实例中造成污染了。当然,仍然是源于JavaScript的限制,我们还是无法避免一个变量泄露到全局--除非,我们回到js in js这个项目(注4),真的在环境中重新初始化一个js引擎。

\

从这一意义上来说,引擎级别的沙箱与操作系统的进程一样,带来的是终级的解决方案,所以Chrome、IE等等主流浏览器纷纷有了\"独立进程\"模式。而在这样的背景之下,试图用\"框架内置沙箱\"来改善ECMAScript ed3中一些设计疏失的种种努力,不过是一张张空头的支票罢了。

\

甚至,用这本支票签完单也未必有人会收的。

\

备注

\

注1:http://dean.edwards.name/weblog/2006/11/sandbox/
\注2:http://jsfiddle.net/
\注3:http://blog.csdn.net/aimingoo/archive/2009/09/08/4532496.aspx
\注4:http://mxr.mozilla.org/mozilla/source/js/narcissus/

\

作者简介:周爱民,国内软件开发界资深软件工程师,架构师。有十余年的软件开发、项目管理、团队建设的经验,历任部门经理、区域总经理、高级软件工程师、平台架构师等职。现任支付宝(中国)公司业务架构师。

\

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者朋友交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值