沙箱模式

沙箱模式主要着眼于命名空间模式的短处,即:

  • 依赖一个全局变量成为应用的全局命名空间。在命名空间模式中,没有办法在同一个页面中运行同一个应用或者类库的不同版本,因为它们都会需要同一个全局变量名,比如MYAPP
  • 代码中以点分隔的名字比较长,无论写代码还是解析都需要处理这个很长的名字,比如MYAPP.utilities.array

顾名思义,沙箱模式为模块提供了一个环境,模块在这个环境中的任何行为都不会影响其它的模块和其它模块的沙箱。

这个模式在YUI3中用得很多,但是需要记住的是,下面的讨论只是一些示例实现,并不讨论YUI3中的沙箱是如何实现的。

全局构造函数

在命名空间模式中 ,有一个全局对象,而在沙箱模式中,唯一的全局变量是一个构造函数,我们把它命名为Sandbox()。我们使用这个构造函数来创建对象,同时也要传入一个回调函数,这个函数会成为代码运行的独立空间。

使用沙箱模式是像这样:

  1. new Sandbox(function (box) {
  2. // 你的代码……
  3. });

box对象和命名空间模式中的MYAPP类似,它包含了所有你的代码需要用到的功能。

我们要多做两件事情:

  • 通过一些手段(第3章中的强制使用new的模式),你可以在创建对象的时候不要求一定有new
  • Sandbox()构造函数可以接受一个(或多个)额外的配置参数,用于指定这个对象需要用到的模块名字。我们希望代码是模块化的,因此绝大部分Sandbox()提供的功能都会被包含在模块中。

有了这两个额外的特性之后,我们来看一下实例化对象的代码是什么样子。

你可以在创建对象时省略new并像这样使用已有的ajaxevent模块:

  1. Sandbox(['ajax', 'event'], function (box) {
  2. // console.log(box);
  3. });

下面的例子和前面的很像,但是模块名字是作为独立的参数传入的:

  1. Sandbox('ajax', 'dom', function (box) {
  2. // console.log(box);
  3. });

使用通配符“*”来表示“使用所有可用的模块”是个不错的想法,为了方便,我们也假设没有任何模块传入时,沙箱使用“*”。所以有两种使用所有可用模块的方法:

  1. Sandbox('*', function (box) {
  2. // console.log(box);
  3. });
  4. Sandbox(function (box) {
  5. // console.log(box);
  6. });

下面的例子展示了如何实例化多个沙箱对象,你甚至可以将它们嵌套起来而互不影响:

  1. Sandbox('dom', 'event', function (box) {
  2. // 使用dom和event模块
  3. Sandbox('ajax', function (box) {
  4. // 另一个沙箱中的box,这个box和外面的box不一样
  5. //...
  6. // 使用ajax模块的代码到此为止
  7. });
  8. // 这里的代码与ajax模块无关
  9. });

从这些例子中看到,使用沙箱模式可以通过将代码包裹在回调函数中的方式来保护全局命名空间。

如果需要的话,你也可以利用函数也是对象这一事实,将一些数据作为静态属性存放到Sandbox()构造函数。

最后,你可以根据需要的模块类型创建不同的实例,这些实例都是相互独立的。

现在我们来看一下如何实现Sandbox()构造函数和它的模块来支持上面讲到的所有功能。

添加模块

在动手实现构造函数之前,我们先来看一下如何添加模块。

Sandbox()构造函数也是一个对象,所以可以给它添加一个modules静态属性。这个属性也是一个包含名值(key-value)对的对象,其中key是模块的名字,value是模块的功能实现。

  1. Sandbox.modules = {};
  2. Sandbox.modules.dom = function (box) {
  3. box.getElement = function () {};
  4. box.getStyle = function () {};
  5. box.foo = "bar";
  6. };
  7. Sandbox.modules.event = function (box) {
  8. // 如果有需要的话可以访问Sandbox的原型
  9. // box.constructor.prototype.m = "mmm";
  10. box.attachEvent = function () {};
  11. box.dettachEvent = function () {};
  12. };
  13. Sandbox.modules.ajax = function (box) {
  14. box.makeRequest = function () {};
  15. box.getResponse = function () {};
  16. };

在这个例子中我们添加了domeventajax模块,这些模块在每个类库或者复杂的web应用中都很常见。

每个模块功能函数接受一个实例box作为参数,并给这个实例添加属性和方法。

实现构造函数

最后,我们来实现Sandbox()构造函数(你可能会很自然地想将这类构造函数命名为对你的类库或者应用有意义的名字):

  1. function Sandbox() {
  2. // 将参数转换为数组
  3. var args = Array.prototype.slice.call(arguments),
  4. // 最后一个参数是回调函数
  5. callback = args.pop(),
  6. // 参数可以作为数组或者单独的参数传递
  7. modules = (args[0] && typeof args[0] === "string") ? args : args[0], i;
  8. // 保证函数是作为构造函数被调用
  9. if (!(this instanceof Sandbox)) {
  10. return new Sandbox(modules, callback);
  11. }
  12. // 根据需要给this添加属性
  13. this.a = 1;
  14. this.b = 2;
  15. // 给this对象添加模块
  16. // 未指明模块或者*都表示“使用所有模块”
  17. if (!modules || modules[0] === '*') {
  18. modules = [];
  19. for (i in Sandbox.modules) {
  20. if (Sandbox.modules.hasOwnProperty(i)) {
  21. modules.push(i);
  22. }
  23. }
  24. }
  25. // 初始化指定的模块
  26. for (i = 0; i < modules.length; i += 1) {
  27. Sandbox.modules[modules[i]](this);
  28. }
  29. // 调用回调函数
  30. callback(this);
  31. }
  32. // 需要添加在原型上的属性
  33. Sandbox.prototype = {
  34. name: "My Application",
  35. version: "1.0",
  36. getName: function () {
  37. return this.name;
  38. }
  39. };

这个实现中的一些关键点:

  • 有一个检查this是否是Sandbox()实例的过程,如果不是(也就是调用Sandbox()时没有加new),我们将这个函数作为构造函数再调用一次。
  • 你可以在构造函数中给this添加属性,也可以给构造函数的原型添加属性。
  • 被依赖的模块可以以数组的形式传递,也可以作为单独的参数传递,甚至以*通配符(或者省略)来表示加载所有可用的模块。值得注意的是,我们在这个示例实现中并没有考虑从外部文件中加载模块,但明显这是一个值得考虑的事情。比如YUI3就支持这种情况,你可以只加载最基本的模块(作为“种子”),其余需要的任何模块都通过将模块名和文件名对应的方式从外部文件中加载。
  • 当我们知道依赖的模块之后就初始化它们,也就是调用实现每个模块的函数。
  • 构造函数的最后一个参数是回调函数。这个回调函数会在最后使用新创建的实例来调用。事实上这个回调函数就是用户的沙箱,它被传入一个box对象,这个对象包含了所有依赖的功能。