大型JavaScript应用程序架构模式

11月中旬在伦敦举行的jQuery Summit顶级大会上有个session讲的是大型JavaScript应用程序架构,看完PPT之后以为甚是不错,因而整理一下发给你们共勉。javascript

PDF版的PPT下载地址:http://www.slideshare.net/jibyjohnc/jqquerysummit-largescale-javascript-application-architecturehtml

注:在整理的过程当中,发现做者有些思想是返来复去地说,因此删减了一部分,若是你的英文良好,请直接阅读英文的PPT。java

 

如下是本文的主要章节:web

1. 什么叫“JavaScript大型程序”?数据库

2. 顾当前的程序架构设计模式

3. 长远考虑数组

4. 头脑风暴浏览器

5. 建议的架构安全

   5.1 设计模式性能优化

        5.1.1 模块论

            5.1.1.1 综述

            5.1.1.2 Module模式

            5.1.1.3 对象自面量

            5.1.1.4 CommonJS模块

        5.1.2 Facade模式

        5.1.3 Mediator模式

    5.2 应用到你的架构

        5.2.1 Facade - 核心抽象

        5.2.2 Mediator - 程序核心

        5.2.3 紧密联合运做起来

6. 发布Pub/订阅Sub的延伸:自动注册事件

7. Q & A

8. 致谢

什么叫“JavaScript大型程序”?

在咱们开始以前,咱们来定义一下什么叫大型JavaScript站点,不少有经验的JS开发高手也都被challenge住了,有人说超过10万行JavaScript代码才算大型,也有人说JavaScript代码要超过1MB大小才算,其实2者都不能算对,由于不能安装代码量的多少来衡量,不少琐碎的JS代码很容易超过10万行的。

我对“大”的定义以下,虽然可能不太对,可是应该是比较接近了:

我我的认为,大型JavaScript程序应该是很是重要而且融入了不少卓越开发人员努力,对重量级数据进行处理而且展现给浏览器的程序。

 

回顾当前的程序架构

我不能强调说这个问题有多重要,不少有经验的开发人员常常说:“现有的创意和设计模式在我上一个中型项目上运行得很是好,因此在稍微大型点的程序里再次使用,应该没问题,对吧?”,在必定程序上是没错的,但别忘记了,既然是大型程序,一般就应该有大的Concerns须要分解关注,我简短解释一下要花时间来review当前运行了好久的程序架构。大多数状况下,当前的JavaScript程序架构应该是以下这个样子的(注意,是JS架构,不是你们常说的ASP.NET MVC):
    custom widgets
    models
    views
    controllers
    templates
    libraries/toolkits
    an application core.

你可能还会将程序单独封装成多个modules,或者使用其余的设计模式,这很好,可是若是这些结构彻底表明你的架构的话,就可能会有一些潜在的问题,咱们来看看几个重要的点:

1.你架构里的东西,有多少能够当即拿出来重用?
有没有一些单独的module不依赖别的代码?是自包含么?若是我到大家正在使用的代码库上去随即挑选一些模块module代码,而后放在一个新页面,是否能当即就能使用?你可能会说原理通就能够了,我建议你长久打算一下,若是你的公司以前开发不少重要的程序,忽然有一天有人说,这个项目里的聊天模块不错,咱们拿出来放在另一个项目里吧,你能直接拿过来不修改代码就能使用么?

2.系统里有多少模块module须要依赖其余模块?
系统的各个模块是否是都很紧耦合?在我将这个问题做为concern以前,我先解释一下,不是说全部的模块都绝对不能有任何依赖,好比一个细粒度的功能多是从base功能扩展来的,个人问题和这种状况不同,我说的是不一样功能模块以前的依赖,理论上,全部的不一样功能模块都不该该有太多的依赖。

3.若是你程序的某一部分出错了,其余部分是否可以依然工做?
若是你构建一个和Gmail差很少的程序,你能够发现Gmail里不少模块都是动态加载的,好比聊天chat模块,在初始化页面的时候是不加载的,并且就算加载之后出错了,页面的其余部分也能正常使用。

4.你的各个模块Module能很简单的进行测试么?
你的每个模块都有可能用在数百万用户的大型站点上,甚至多个站点都使用它,因此你的模块须要能经得住测试,也就是说,无论是在架构内部仍是架构外部,都应该能很简单的去测试,包括大部分的断言在不一样的环境下都可以经过。

长远考虑

架构大型程序的时候,最重要的是要有前瞻性,不能只考虑一个月或者一年之后的状况,要考虑更长久的状况下,有什么改变的可能性?开发人员常常将DOM操做的代码和程序绑定得太紧,尽管有时候已经封装单独的逻辑到不一样的模块里了,想一想一下,长久之后,为何不是很好。

个人一个同事曾经说过,一个精确的架构可能不适合将来的情景,有时候是正确的,可是当你须要该作的话,你所付出的money那但是至关地多哦。好比,你可能由于某些性能,安全,设计的缘由须要在Dojo, jQuery, Zepto, YUI之间须要选择替换,这时候就有问题了,大部分模块都有依赖,须要钱呀,须要时间啊,须要人呀,对不?

对于一些小型站点没事,可是大型站点确实须要提供一个更加灵活的机制,而不去担忧各个模块之间的各类问题,这既然节约钱,又能节约时间。

总结一下,如今你能肯定你能不重写整个程序就能替换一些类库么?若是不能,那估计咱们下面要讲的内容,就比较适合你了。

不少有经验的JavaScript开发者给出了一些关键的notes:


JavaScriptMVC的做者Justin Meyer说

构建大型程序最大的秘密就是历来不构建大型程序,而是将程序分解成各个小的模块去作,让每一个小模块均可测试,可size化,而后集成到程序里。

High-performance JavaScript websites做者Nicholas,Zakas
"The key is to acknowledge from the start that you have no idea how this will grow. When you accept that you don't know everything, you begin to design the system defensively. You identify the key areas that may change, which often is very easy when you put a little bit of time into it. For instance, you should expect that any part of the app that communicates with another system will likely change, so you need to abstract that away." -

一大堆文字问题,太麻烦了,总结一句就是,一切皆可变,因此要抽象。

jQuery Fundamentals做者Rebecca Murphey:
各个模块之间联系的越密切,重用性越小,改变起来困难越大。

以上这些重要观点,是构建架构的核心要素,咱们须要时刻铭记。

头脑风暴

咱们来头脑风暴一下,咱们须要一个松耦合的架构,各模块之间没有依赖,各个模块和程序进行通讯,而后中间层接管和处理反馈相应的消息。

例如,咱们若是有一个JavaScript构建在线面包店程序,一个模块发出了一个信息多是“有42个圆面包须要派件”。咱们使用不一样的layer层来处理模块发来的消息,作到以下:

  1. 模块不直接访问程序核心
  2. 模块不直接调用或影响其它的模块

这将防止咱们由于某个模块出错,而致使全部的模块出错。

另一个问题是安全,真实的状况是,大多数人都不认为内部安全是个问题,咱们本身内心说,程序是我本身构建的,我知道哪些是公开的那些私有的,安全没问题,但你有没有办法去定义哪一个模块才能权限访问程序核心?例如,有一个chat聊天模块,我不想让他调用admin模块,或者不想让它调用有DB写权限的模块,由于这之间存在很脆弱,很容易致使XSS攻击。每一个模块不该该能作全部的事情,可是当前大多数架构里的JavaScript代码都有这种的问题。提供一个中间层来控制,哪一个模块能够访问那个受权的部分,也就是说,该模块最多只能作到咱们所受权的那部分。

建议的架构

咱们本文的重点来了,此次咱们提议的架构使用了咱们都很熟知的设计模式:module, facade和mediator。

和传统的模型不同的是,为了解耦各个模块,咱们只让模块发布一些event事件,mediator模式能够负责从这些模块上订阅消息message,而后控制通知的response,facade模式用户限制各模块的权限。

如下是咱们要注意讲解的部分:
    1 设计模式
        1.1 模块论
            1.1.1 综述
            1.1.2 Module模式
            1.1.3 对象自面量
            1.1.4 CommonJS模块
        1.2 Facade模式
        1.3 Mediator模式
    2 应用到你的架构
        2.1 Facade - 核心抽象
        2.2 Mediator - 程序核心
        2.3 紧密联合运做起来

模块论

你们可能都或多或少地使用了模块化的代码,模块是一个完整的强健程序架构的一部分,每一个模块都是为了单独的目的为建立的,回到Gmail,咱们来个例子,chat聊天模块看起来是个单独的一部分,其实它是有不少单独的子模块来构成,例如里面的表情模块其实就是单独的子模块,也被用到了发送邮件的窗口上。

另一个是模块能够动态加载,删除和替换。

在JavaScript里,咱们又几种方式来实现模块,你们熟知的是module模式和对象字面量,若是你已经熟悉这些,请忽略此小节,直接跳到CommonJS部分。

Module模式
module模式是一个比较流行的设计模式,它能够经过大括号封装私有的变量,方法,状态的,经过包装这些内容,通常全局的对象不能直接访问,在这个设计模式里,只返回一个API,其它的内容所有被封装成私有的了。

另外,这个模式和自执行的函数表达式比较类似,惟一的不一样是module返回的是对象,而自执行函数表达式返回的是function。

众所周知, JavaScript不想其它语言同样有访问修饰符,不能为每一个字段或者方法声明private,public修饰符,那这个模式咱们是如何实现的呢?那就是return一个对象,里面包括一些公开的方法,这些方法有能力去调用内部的对象。

看一下,下面的代码,这段代码是一个自执行代码,声明里包括了一个全局的对象basketModule, basket数组是一个私有的,因此你的整个程序是不能访问这个私有数组的,同时咱们return了一个对象,其内包含了3个方法(例如addItem,getItemCount,getTotal),这3个方法能够访问私有的basket数组。

var basketModule = (function() {
var basket = []; //private
return { //exposed to public
  addItem: function(values) {
    basket.push(values);
  },
  getItemCount: function() {
    return basket.length;
  },
  getTotal: function(){
    var q = this.getItemCount(),p=0;
    while(q--){
    p+= basket[q].price;
    }
    return p;
  }
 }
}());


同时注意,咱们return的对象直接赋值给了basketModule,因此咱们能够像下面同样使用:

//basketModule is an object with properties which can also be methods
basketModule.addItem({item:'bread',price:0.5});
basketModule.addItem({item:'butter',price:0.3});

console.log(basketModule.getItemCount());
console.log(basketModule.getTotal());

//however, the following will not work:
console.log(basketModule.basket);// (undefined as not inside the returned object)
console.log(basket); //(only exists within the scope of the closure)


那在各个流行的类库(如Dojo, jQuery)里是如何来作呢?

Dojo

Dojo试图使用dojo.declare来提供class风格的声明方式,咱们能够利用它来实现Module模式,例如若是你想再store命名空间下声明basket对象,那么能够这么作:

    //traditional way
var store = window.store || {};
store.basket = store.basket || {};

//using dojo.setObject
dojo.setObject("store.basket.object", (function() {
  var basket = [];
  function privateMethod() {
    console.log(basket);
  }
  return {
    publicMethod: function(){
      privateMethod();
    }
   };
}()));

结合dojo.provide一块儿来使用,很是强大。

YUI

下面的代码是YUI原始的实现方式:

YAHOO.store.basket = function () {

//"private" variables:
var myPrivateVar = "I can be accessed only within YAHOO.store.basket .";

//"private" method:
var myPrivateMethod = function () {
YAHOO.log("I can be accessed only from within YAHOO.store.basket");
}

return {
myPublicProperty: "I'm a public property.",
myPublicMethod: function () {
YAHOO.log("I'm a public method.");

//Within basket, I can access "private" vars and methods:
YAHOO.log(myPrivateVar);
YAHOO.log(myPrivateMethod());

//The native scope of myPublicMethod is store so we can
//access public members using "this":
YAHOO.log(this.myPublicProperty);
}
};

} ();

 

jQuery

jQuery里有不少Module模式的实现,咱们来看一个不一样的例子,一个library函数声明了一个新的library,而后建立该library的时候,在document.ready里自动执行init方法。

function library(module) {
  $(function() {
    if (module.init) {
      module.init();
    }
  });
  return module;
}

var myLibrary = library(function() {
  return {
    init: function() {
      /*implementation*/
      }
  };
}());


对象自面量
对象自面量使用大括号声明,而且使用的时候不须要使用new关键字,若是对一个模块里的属性字段的publice/private不是很在乎的话,可使用这种方式,不过请注意这种方式和JSON的不一样。对象自面量:var item={name: "tom", value:123} JSON:var item={"name":"tom", "value":123}。

var myModule = {
myProperty: 'someValue',
//object literals can contain properties and methods.
//here, another object is defined for configuration
//purposes:
myConfig: {
useCaching: true,
language: 'en'
},
//a very basic method
myMethod: function () {
console.log('I can haz functionality?');
},
//output a value based on current configuration
myMethod2: function () {
console.log('Caching is:' + (this.myConfig.useCaching) ? 'enabled' : 'disabled');
},
//override the current configuration
myMethod3: function (newConfig) {
if (typeof newConfig == 'object') {
this.myConfig = newConfig;
console.log(this.myConfig.language);
}
}
};

myModule.myMethod(); //I can haz functionality
myModule.myMethod2(); //outputs enabled
myModule.myMethod3({ language: 'fr', useCaching: false }); //fr


CommonJS
关于 CommonJS的介绍,这里就很少说了,博客园有不少帖子都有介绍,咱们这里要提一下的是CommonJS标准里里有2个重要的参数exports和require,exports是表明要加载的模块,require是表明这些加载的模块须要依赖其它的模块,也须要将它加载进来。

    /*
Example of achieving compatibility with AMD and standard CommonJS by putting boilerplate around the standard CommonJS module format:
*/

(function(define){
  define(function(require,exports){
    // module contents
    var dep1 = require("dep1");
    exports.someExportedFunction = function(){...};
    //...
  });
})(typeof define=="function"?define:function(factory){factory(require,exports)});

有不少CommonJS标准的模块加载实现,我比较喜欢的是RequireJS,它可否很是好的加载模块以及相关的依赖模块,来一个简单的例子,例如须要将图片转化成ASCII码,咱们先加载encoder模块,而后获取他的encodeToASCII方法,理论上代码应该是以下:

    var encodeToASCII = require("encoder").encodeToASCII;
exports.encodeSomeSource = function(){
  //其它操做之后,而后调用encodeToASCII
}

可是上述代码并没用工做,由于encodeToASCII函数并没用附加到window对象上,因此不能使用,改进之后的代码须要这样才行:

    define(function(require, exports, module) {
  var encodeToASCII = require("encoder").encodeToASCII;
    exports.encodeSomeSource = function(){
    //process then call encodeToASCII
  }
});

CommonJS 潜力很大,可是因为大叔不太熟,因此就不过多地介绍了。

Facade模式

Facade模式在本文架构里占有重要角色,关于这个模式不少JavaScript类库或者框架里都有体现,其中最大的做用,就是包括High level的API,以此来隐藏具体的实现,这就是说,咱们只暴露接口,内部的实现咱们能够本身作主,也意味着内部实现的代码能够很容易的修改和更新,好比今天你是用jQuery来实现的,明天又想换YUI了,这就很是方便了。

下面这个例子了,能够看到咱们提供了不少私有的方法,而后经过暴露一个简单的 API来让外界执行调用内部的方法:

var module = (function () {
var _private = {
i: 5,
get: function () {
console.log('current value:' + this.i);
},
set: function (val) {
this.i = val;
},
run: function () {
console.log('running');
},
jump: function () {
console.log('jumping');
}
};
return {
facade: function (args) {
_private.set(args.val);
_private.get();
if (args.run) {
_private.run();
}
}
}
} ());

module.facade({run:true, val:10});
//outputs current value: 10, running


Facade和下面咱们所说的mediator的区别是,facade只提供现有存在的功能,而mediator能够增长新功能。

 

Mediator模式

讲modiator以前,咱们先来举个例子,机场飞行控制系统,也就是传说中的塔台,具备绝对的权利,他能够控制任何一架飞机的起飞和降落时间以及地方,而飞机和飞机以前不容许通讯,也就是说塔台是机场的核心,mediator就至关于这个塔台。

mediator就是用在程序里有多个模块,而你又不想让各个模块有依赖的话,那经过mediator模式能够达到集中控制的目的。实际场景中也是,mediator封装了不少不想干的模块,让他们经过mediator联系在一块儿,同时也松耦合他们,使得他们之间必须经过mediator才能通讯。

那mediator模式的优势是什么?那就是解耦,若是你以前对观察者模式比较了解的话,那理解下面的mediator图就相对简单多了,下图是一个high level的mediator模式图:

想一想一下,各模块是发布者,mediator既是发布者又是订阅者。

  1. Module 1向Mediator广播一个实际,说须要作某事
  2. Mediator捕获消息之后,当即启动处理该消息须要使用的Module 2,Module 2处理结束之后返回信息给Mediator
  3. 与此同时,Mediator也启动了Module 3,当接受Module 2 返回消息的时候自动记录日志到Module 3里

能够看到,各模块之间并无通讯,另外Mediator也能够实现监控各模块状态的功能,例如若是Module 3出错了,Mediator能够暂时只想其它模块,而后重启Module 3,而后继续执行。

回顾一下,能够看到,Mediator的优势是:松耦合的模块由同一的Mediator来控制,模块只须要广播和监听事件就能够了,而模块之间不须要直接联系,另外,一次信息的处理可使用多个模块,也方便咱们之后统一的添加新的模块到现有的控制逻辑里。

肯定是:因为全部的模块直接都不能直接通讯,全部相对来讲,性能方面可能会有少量降低,可是我认为这是值得的。

 

咱们根据上面的讲解来一个简单的Demo:

    var mediator = (function(){
var subscribe = function(channel, fn){
if (!mediator.channels[channel]) mediator.channels[channel] = [];
mediator.channels[channel].push({ context: this, callback: fn });
return this;
},

publish = function(channel){
if (!mediator.channels[channel]) return false;
var args = Array.prototype.slice.call(arguments, 1);
for (var i = 0, l = mediator.channels[channel].length; i < l; i++) {
var subscription = mediator.channels[channel][i];
subscription.callback.apply(subscription.context, args);
}
return this;
};

return {
channels: {},
publish: publish,
subscribe: subscribe,
installTo: function(obj){
obj.subscribe = subscribe;
obj.publish = publish;
}
};

}());

而后有2个模块分别调用:

    //Pub/sub on a centralized mediator

mediator.name = "tim";
mediator.subscribe('nameChange', function(arg){
console.log(this.name);
this.name = arg;
console.log(this.name);
});

mediator.publish('nameChange', 'david'); //tim, david


//Pub/sub via third party mediator

var obj = { name: 'sam' };
mediator.installTo(obj);
obj.subscribe('nameChange', function(arg){
console.log(this.name);
this.name = arg;
console.log(this.name);
});

obj.publish('nameChange', 'john'); //sam, john


应用Facade: 应用程序核心的抽象

一个facade是做为应用程序核心的一个抽象来工做的,在mediator和模块之间负责通讯,各个模块只能经过这个facade来和程序核心进行通讯。做为抽象的职责是确保任什么时候候都能为这些模块提供一个始终如一的接口(consistent interface),和sendbox controller的角色比较相似。全部的模块组件经过它和mediator通讯,因此facade须要是可靠的,可信赖的,同时做为为模块提供接口的功能,facade还须要扮演另一个角色,那就是安全控制,也就是决定程序的哪一个部分能够被一个模块访问,模块组件只能调用他们本身的方法,而且不能访问任何未受权的内容。例如,一个模块可能广播dataValidationCompletedWriteToDB,这里的安全检查须要确保该模块拥有数据库的写权限。


总之,mediator只有在facade受权检测之后才能进行信息处理。

应用Mediator:应用程序的核心

Mediator是做为应用程序核心的角色来工做的,咱们简单地来讲一下他的职责。最核心的工做就是管理模块的生命周期(lifecycle),当这个核心扑捉到任何信息进来的时候,他须要判断程序如何来处理——也就是说决定启动或中止哪个或者一些模块。当一个模块开始启动的时候,它应该可否自动执行,而不须要应用程序核心来决定是否该执行(好比,是否要在DOM ready的时候才能执行),因此说须要模块自身须要去断定。

你可能还有问题,就是一个模块在什么状况下才会中止。当程序探测到一个模块失败了,或者是出错了,程序须要作决定来防止继续执行该模块里的方法,以便这个组件能够从新启动,目的主要是提升用户体验。


另外,该核心应该能够动态添加或者删除模块,而不影响其余任何功能。常见的例子是,一个模块在页面加载初期是不可用,可是用户操做之后,须要动态加载这个模块而后执行,就像Gmail里的chat聊天功能同样,从性能优化的目的来看,应该是很好理解的吧。


异常错误处理,也是由应用程序核心来处理的,另外各模块在广播信息的时候,也广播任何错误到该核内心,以便程序核心能够根据状况去中止/重启这些模块。这也是松耦合架构一个很重要的部分,咱们不须要手工改变任何模块,经过mediator使用发布/订阅就能够来作到这个。


组装起来

各模块包含了程序里各类各样的功能,他们有信息须要处理的时候,发布信息通知程序(这是他们的主要职责),下面的QA小节里提到了,模块能够依赖一些DOM工具操做方法,可是不该该和系统的其它模块有依赖,一个模块不该该关注以下内容:

  1. 哪一个对象或者模块订阅了这个模块发布的信息
  2. 这些对象是客户端对象仍是服务器端对象
  3. 多少对象订阅了你的信息

 


Facade抽象应用程序的核心,避免各个模块之间直接通讯,它从各模块上订阅信息,也负责受权检测,确保每一个模块有用本身单独的受权。


Mediator(应用程序核心)使用mediator模式扮演发布/订阅管理器的角色,负责模块管理以及启动/中止模块执行,能够动态加载以及重启有错误的模块。


这个架构的结果是:各模块之间没有依赖,由于松耦合的应用,它们能够很容易地被测试和维护,各模块能够很容易地在其它项目里被重用,也能够在不影响程序的状况下动态添加和删除。

发布Pub/订阅Sub的延伸:自动注册事件(Automatic Event Registration)

关于自动注册事件,须要遵照必定的命名规范,好比若是一个模块发布了一个名字为messageUpdate的事件,那么全部带有messageUpdate方法的模块都会被自动执行。有好处也有利弊,具体实现方式,能够看我另一篇帖子:jQuery自定义绑定的魔法升级版

QA

1.有可能不使用facade或者相似的sandbox模式么?

尽管架构的大纲里提出了facade能够实现受权检查的功能,其实彻底可能由mediator去作,轻型架构要作的事情实际上是几乎同样的,那就是解耦,确保各模块直接和应用程序核心通讯是没问题的就行。

2.你提升了模块直接不能有依赖,是否意味着不能依赖任何第三方类库(例如jQuery)。

这其实就是一个两面性的问题,咱们上面说到了,一个模块也许有一些子模块,或者基础模块,好比基本的DOM操做工具类等,在这个层面上讲,咱们是能够用第三方类库的,可是请确保,咱们能够很容易地可否替换掉他们。

3.我喜欢这个架构,而且想开始使用这个架构,有任何代码样本能够参考么?

我打算去搞一份代码样本供你们参考,不过在这以前,你能够参考Andrew Burgees的帖子Writing Modular JavaScript

4.若是模块须要和应用程序核心直接通讯,是否可行?

技术上来将,没有理由如今模块不能和应用程序核心直接通讯,可是对于大多数应用体验来讲,仍是不要。既然你选择了这个架构,那就要遵照该架构所定义的规则。

致谢

感谢Nicholas Zakas的原始贴,将思想总结在一块儿,感谢Andree Hansson的technical review,感谢Rebecca Murphey, Justin Meyer, John Hann, Peter Michaux, Paul Irish和Alex Sexton,他们全部的人都提供了和本Session相关的不少资料。

也很是感谢博客园的汤姆大叔(TomXu),将本Session的内容整理成中文版本,如对你有用,请推荐一把。

相关文章
相关标签/搜索