参考
最后更新于:2022-04-01 11:47:03
# 参考文献
1. Design Principles and Design Patterns - Robert C Martin[http://www.objectmentor.com/resources/articles/Principles_and_Patterns.pdf](http://www.objectmentor.com/resources/articles/Principles_and_Patterns.pdf)
2. Ralph Johnson - Special Issue of ACM On Patterns and Pattern Languages -[http://www.cs.wustl.edu/~schmidt/CACM-editorial.html](http://www.cs.wustl.edu/~schmidt/CACM-editorial.html)
3. Hillside Engineering Design Patterns Library - [http://hillside.net/patterns/](http://hillside.net/patterns/)
4. Pro JavaScript Design Patterns - Ross Harmes and Dustin Diaz [http://jsdesignpatterns.com/](http://jsdesignpatterns.com/)
5. Design Pattern Definitions - [http://en.wikipedia.org/wiki/Design_Patterns](http://en.wikipedia.org/wiki/Design_Patterns)
6. Patterns and Software Terminology [http://www.cmcrossroads.com/bradapp/docs/patterns-intro.html](http://www.cmcrossroads.com/bradapp/docs/patterns-intro.html)
7. Reap the benefits of Design Patterns - Jeff Juday [http://articles.techrepublic.com.com/5100-10878_11-5173591.html](http://articles.techrepublic.com.com/5100-10878_11-5173591.html)
8. JavaScript Design Patterns - Subramanyan Guhan [http://www.slideshare.net/rmsguhan/javascript-design-patterns](http://www.slideshare.net/rmsguhan/javascript-design-patterns)
9. What Are Design Patterns and Do I Need Them? - James Moaoriello[http://www.developer.com/design/article.php/1474561](http://www.developer.com/design/article.php/1474561)
10. Software Design Patterns - Alex Barnett [http://alexbarnett.net/blog/archive/2007/07/20/software-design-patterns.aspx](http://alexbarnett.net/blog/archive/2007/07/20/software-design-patterns.aspx)
11. Evaluating Software Design Patterns - Gunni Rode [http://www.rode.dk/thesis/](http://www.rode.dk/thesis/)
12. SourceMaking Design Patterns [http://sourcemaking.com/design_patterns](http://sourcemaking.com/design_patterns)
13. The Singleton - Prototyp.ical [http://prototyp.ical.ly/index.php/2007/03/01/javascript-design-patterns-1-the-singleton/](http://prototyp.ical.ly/index.php/2007/03/01/javascript-design-patterns-1-the-singleton/)
14. JavaScript Patterns - Stoyan Stevanov - [http://www.slideshare.net/stoyan/javascript-patterns](http://www.slideshare.net/stoyan/javascript-patterns)
15. Stack Overflow - Design Pattern Implementations in JavaScript (discussion)[http://stackoverflow.com/questions/24642/what-are-some-examples-of-design-pattern-implementations-using-javascript](http://stackoverflow.com/questions/24642/what-are-some-examples-of-design-pattern-implementations-using-javascript)
16. The Elements of a Design Pattern - Jared Spool[http://www.uie.com/articles/elements_of_a_design_pattern/](http://www.uie.com/articles/elements_of_a_design_pattern/)
17. Stack Overflow - Examples of Practical JS Design Patterns (discussion)[http://stackoverflow.com/questions/3722820/examples-of-practical-javascript-object-oriented-design-patterns](http://stackoverflow.com/questions/3722820/examples-of-practical-javascript-object-oriented-design-patterns)
18. Design Patterns in JavaScript Part 1 - Nicholas Zakkas[http://www.webreference.com/programming/javascript/ncz/column5/](http://www.webreference.com/programming/javascript/ncz/column5/)
19. Stack Overflow - Design Patterns in jQuery [http://stackoverflow.com/questions/3631039/design-patterns-used-in-the-jquery-library](http://stackoverflow.com/questions/3631039/design-patterns-used-in-the-jquery-library)
20. Classifying Design Patterns By AntiClue - Elyse Neilson [http://www.anticlue.net/archives/000198.htm](http://www.anticlue.net/archives/000198.htm)
21. Design Patterns, Pattern Languages and Frameworks - Douglas Schmidt[http://www.cs.wustl.edu/~schmidt/patterns.html](http://www.cs.wustl.edu/~schmidt/patterns.html)
22. Show Love To The Module Pattern - Christian Heilmann [http://www.wait-till-i.com/2007/07/24/show-love-to-the-module-pattern/](http://www.wait-till-i.com/2007/07/24/show-love-to-the-module-pattern/)
23. JavaScript Design Patterns - Mike G. [http://www.lovemikeg.com/2010/09/29/javascript-design-patterns/](http://www.lovemikeg.com/2010/09/29/javascript-design-patterns/)
24. Software Designs Made Simple - Anoop Mashudanan [http://www.scribd.com/doc/16352479/Software-Design-Patterns-Made-Simple](http://www.scribd.com/doc/16352479/Software-Design-Patterns-Made-Simple)
25. JavaScript Design Patterns - Klaus Komenda [http://www.klauskomenda.com/code/javascript-programming-patterns/](http://www.klauskomenda.com/code/javascript-programming-patterns/)
26. Introduction to the JavaScript Module Pattern [https://www.unleashed-technologies.com/blog/2010/12/09/introduction-javascript-module-design-pattern](https://www.unleashed-technologies.com/blog/2010/12/09/introduction-javascript-module-design-pattern)
27. Design Patterns Explained - [http://c2.com/cgi/wiki?DesignPatterns](http://c2.com/cgi/wiki?DesignPatterns)
28. Mixins explained [http://en.wikipedia.org/wiki/Mixin](http://en.wikipedia.org/wiki/Mixin)
29. Working with GoF's Design Patterns In JavaScript[http://aspalliance.com/1782_Working_with_GoFs_Design_Patterns_in_JavaScript_Programming.all](http://aspalliance.com/1782_Working_with_GoFs_Design_Patterns_in_JavaScript_Programming.all)
30. Using Object.create[http://stackoverflow.com/questions/2709612/using-object-create-instead-of-new](http://stackoverflow.com/questions/2709612/using-object-create-instead-of-new)
31. t3knomanster's JavaScript Design Patterns - [http://t3knomanser.livejournal.com/922171.html](http://t3knomanser.livejournal.com/922171.html)
32. Working with GoF Design Patterns In JavaScript Programming -[http://aspalliance.com/1782_Working_with_GoFs_Design_Patterns_in_JavaScript_Programming.7](http://aspalliance.com/1782_Working_with_GoFs_Design_Patterns_in_JavaScript_Programming.7)
33. JavaScript Advantages - Object Literals [http://stackoverflow.com/questions/1600130/javascript-advantages-of-object-literal](http://stackoverflow.com/questions/1600130/javascript-advantages-of-object-literal)
34. JavaScript Class Patterns - Liam McLennan[http://geekswithblogs.net/liammclennan/archive/2011/02/06/143842.aspx](http://geekswithblogs.net/liammclennan/archive/2011/02/06/143842.aspx)
35. Understanding proxies in jQuery - [http://stackoverflow.com/questions/4986329/understanding-proxy-in-jquery](http://stackoverflow.com/questions/4986329/understanding-proxy-in-jquery)
36. Observer Pattern Using JavaScript - [http://www.codeproject.com/Articles/13914/Observer-Design-Pattern-Using-JavaScript](http://www.codeproject.com/Articles/13914/Observer-Design-Pattern-Using-JavaScript)
37. Speaking on the Observer pattern - [http://www.javaworld.com/javaworld/javaqa/2001-05/04-qa-0525-observer.html](http://www.javaworld.com/javaworld/javaqa/2001-05/04-qa-0525-observer.html)
38. Singleton examples in JavaScript - Hardcode.nl - [http://www.hardcode.nl/subcategory_1/article_526-singleton-examples-in-javascript.htm](http://www.hardcode.nl/subcategory_1/article_526-singleton-examples-in-javascript.htm)
39. Design Patterns by Gamma, Helm supplement - [http://exciton.cs.rice.edu/javaresources/DesignPatterns/](http://exciton.cs.rice.edu/javaresources/DesignPatterns/)
总结
最后更新于:2022-04-01 11:47:01
# 结尾
JavaScript和jQuery设计模式的入门之旅到此就结束了,我希望你已经从中受益了。
设计模式能够让我站在巨人的肩膀上,享受其他开发者们长期以来在一些有挑战性问题上的解决方案以及优秀的架构。我希望本书的内容对于你在自己的脚本、插件和web应用程序里面开始使用设计模式有足够的帮助。
对我们来讲,知道有这些设计模式是很重要的,但更重要的是应该知道怎样以及什么时候去使用它们。在想使用每个模式前先去了解下它的优缺点。要真正的理解模式能给你带来什么好处需要花时间去尝试,以实际情况中模式给你的程序带来的好处作为标准来选择
如果我已经成功挑逗你对这个领域的兴趣,你想学习更多的设计模式的东西,其实有很多关于通用软件开发(当然也包括JavaScript)设计模式方面的文章和书籍。
我很乐意推荐两本书:
1. "[Patterns Of Enterprise Application Architecture](http://www.amazon.com/Patterns-Enterprise-Application-Architecture-Martin/dp/0321127420)" 作者Martin Fowler
2. "[JavaScript Patterns](http://www.amazon.com/JavaScript-Patterns-Stoyan-Stefanov/dp/0596806752/ref=sr_1_1?ie=UTF8&s=books&qid=1289759956&sr=1-1)" 作者Stoyan Stefanov
非常感谢您阅读这篇《*Learning JavaScript Design Patterns》*. 更多的JavaScript学习资料,请参考我的博客[http://addyosmani.com](http://addyosmani.com/) 或者在Twitter@我: [@addyosmani](http://twitter.com/addyosmani).
JavaScript的探索之旅,祝你好运,下次再见!
JavaScript 命名空间模式
最后更新于:2022-04-01 11:46:58
# 命名空间模式
在这一节中,我们将探索JavaScript中关于命名空间的模式。命名空间可被看作位于一个唯一标识符下的代码单元的逻辑组合。标识符可以被很多命名空间引用,每一个命名空间本身可以包含一个分支的嵌套命名空间(或子命名空间)。
在应用开发过程中,出于很多原因,我们都要使用命名空间。在JavaScript中,它们帮助我们避免在全局空间中于其他对象或者变量出现冲突。它们对于在代码库中组织功能块也非常有用,这样使用代码就更容易被使用。
将任何重要的脚本或者应用纳入命名空间是非常重要的,因为这是我们代码的一层重要保护,使其免于与页面中使用相同变量或方法名的其它脚本发生冲突。现在由于许多第三方标记规律的插入页面,这可能是我们在职业生涯的某个时刻都需要处理的一个普遍的问题。作为一个行为端正的全局命名空间的“公民”,同样重要的是,因为同样的问题,我们最好不要阻碍其他开发人员的脚本运行。
虽然JavaScript并没有像其它语言一样真正内置的支持名称空间,它具有对象和闭包,也可以用来达到相似的效果。
## 命名空间原理
几乎所有重要的 Javascript 程序中都会用到命名空间。除非我们只是编写简单的代码,否则尽力确保正确地实现命名空间是很有必要的。这也能避免自己的代码收到第三方代码的污染。本小节将阐述以下设计模式:
1. 单一全局变量
2. 对象序列化的表示
3. 内嵌的命名空间
4. 即时调用的函数表达式
5. 命名空间注入
### 单一全局变量
在 JavaScript 中实现命名空间的一个流行模式是,选择一个全局变量作为引用的主对象。下面显示的是此方法的框架实现,示例代码中返回一个包含函数和属性的对象:
~~~
var myApplication = (function () {
function(){
//...
},
return{
//...
}
})();
~~~
虽然这段代码能在特定的环境下运行,单一全局变量模式的最大挑战是如何确保同一页面中的其他代码不会使用相同的全局变量名称。
### 前缀命名空间
一个解决上面所述问题的方法,正如Peter Michaux提到的, 是使用前缀命名空间. 它本质上是一个简单的概念,但原理是,我们选择一个我们想用的(这个例子中我们用的是myApplication_)唯一的前缀命名空间,然后在这个前缀的后面定义任意的方法,变量或者其他对象,就像下面一样:
~~~
var myApplication_propertyA = {};
var myApplication_propertyB = {};
function myApplication_myMethod(){
//...
}
~~~
从减少全局变量的角度来讲这是非常有效的,但请记住,使用一个具有唯一命名的对象也能达到同样的效果。
另一方面,这种模式的最大问题在于,一旦我们的应用开始增长,它会产生大量的全局对象。全局区域中对于我们没有被其他开发人员使用的前缀也存在严重的依赖,所以当你选择使用的时候,一定要小心。
### 对象文字表示
对象文字表示(我们在本书的模块模式一节中也提到过)可被认为是一个对象包含了一个集合,这个集合中存储的是键值对,它们使用分号将每个键值对的键和值分隔开,这样这些键也可以表示新的命名空间。
~~~
var myApplication = {
// As we've seen, we can easily define functionality for
// this object literal..
getInfo:function(){
//...
},
// but we can also populate it to support
// further object namespaces containing anything
// anything we wish:
models : {},
views : {
pages : {}
},
collections : {}
};
~~~
你也可以直接给命名空间添加属性:
~~~
myApplication.foo = function(){
return "bar";
}
myApplication.utils = {
toString:function(){
//...
},
export: function(){
//...
}
}
~~~
对象文字具有在不污染全局命名空间的情况下帮助组织代码和参数的优点。如果我们希望创建易读的可以支持深度嵌套的结构,这将非常有用。与简单的全局变量不同,对象文字也经常考虑测试相同名字的变量的存在,这样就极大的降低了冲突的可能性。
下面例子中,我们展示了几种方法,它们检查是否变量(对象或者插件命名空间)存在,如果不存在就定义该变量。
~~~
// This doesn't check for existence of "myApplication" in
// the global namespace. Bad practice as we can easily
// clobber an existing variable/namespace with the same name
var myApplication = {};
// The following options *do* check for variable/namespace existence.
// If already defined, we use that instance, otherwise we assign a new
// object literal to myApplication.
//
// Option 1: var myApplication = myApplication || {};
// Option 2 if( !MyApplication ){ MyApplication = {} };
// Option 3: window.myApplication || ( window.myApplication = {} );
// Option 4: var myApplication = $.fn.myApplication = function() {};
// Option 5: var myApplication = myApplication === undefined ? {} : myApplication;
~~~
我们经常看到开发人员使用Option1或者Option2,它们都很容易理解,而且他们的结果也是一样的。 Option 3 假定我们在全局命名空间中,但也可以写成下面的方式:
~~~
myApplication || (myApplication = {});
~~~
这种改变假定myApplication已经被初始化,所以它只对参数有效,如下:
~~~
function foo() {
myApplication || ( myApplication = {} );
}
// myApplication hasn't been initialized,
// so foo() throws a ReferenceError
foo();
// However accepting myApplication as an
// argument
function foo( myApplication ) {
myApplication || ( myApplication = {} );
}
foo();
// Even if myApplication === undefined, there is no error
// and myApplication gets set to {} correctly
~~~
Options 4 对于写jQuery插件很有效:
~~~
// If we were to define a new plugin..
var myPlugin = $.fn.myPlugin = function() { ... };
// Then later rather than having to type:
$.fn.myPlugin.defaults = {};
// We can do:
myPlugin.defaults = {};
~~~
这样的结果是代码压缩(最小化)效果好,而且可以节省查找范围。
Option 5 跟Option 4有些类似,但它是一个较长的形式,它用内联的方式验证myApplication是否未定义,如果未定义就将它定义为一个对象,否则就把已经定义的值赋给myApplication。
Option 5的展示是为了完整透彻起见,但在大多数情况下Option 1-4就足够满足大多数需求了。
当然,在使用对象文字实习组织代码结构方面有很多变体. 对于希望为一个内部封闭的模块暴漏一个嵌套的API的小应用来说,我们会发现自己使用“展示模块模式”, 这个模式之前在本书中讲过:
~~~
var namespace = (function () {
// defined within the local scope
var privateMethod1 = function () { /* ... */ },
privateMethod2 = function () { /* ... */ }
privateProperty1 = "foobar";
return {
// the object literal returned here can have as many
// nested depths as we wish, however as mentioned,
// this way of doing things works best for smaller,
// limited-scope applications in my personal opinion
publicMethod1: privateMethod1,
// nested namespace with public properties
properties:{
publicProperty1: privateProperty1
},
// another tested namespace
utils:{
publicMethod2: privateMethod2
}
...
}
})();
~~~
对象文字的好处就是他们为我们提供了一种非常优雅的Key/Value语法,使用它,我们可以很容易的封装我们应用中任意独特的逻辑,而且能够清楚的将它与其他代码区分开,同时它为代码扩展提供了坚实的基础。
一个可能的弊端就是,对象文字可能会导致很长的语法结构,你可以选择利用嵌套命名空间模式(它也使用了同样的模式作为基础)
这种模式也有很多有用的应用。除了命名空间,它也被用来把应用的默认配置缩减到一个独立的区域中,这样一来就修改配置就不需要查遍整个代码库了,对象文字在这方面表现非常好。下面的例子是一个假想的配置:
~~~
var myConfig = {
language: "english",
defaults: {
enableGeolocation: true,
enableSharing: false,
maxPhotos: 20
},
theme: {
skin: "a",
toolbars: {
index: "ui-navigation-toolbar",
pages: "ui-custom-toolbar"
}
}
}
~~~
> 注意:JSON是对象文字表示的一个子集,它与上面的例子(比如:JSON的键必须是字符串)只有细微的语法差异。如果出于某种原因,有人想使用JSON来存储配置信息(比如:当发送到前端的时候),也是可以的。想了解更多关于对象文字表示模式,我建议阅读Rebecca Murphey 的优秀文章 ,她讲到了很多我们上面没有提到的问题。
### 嵌套命名空间
文字对象表示的一个扩展就是嵌套命名空间.它也是一个常用的模式,它降低了代码冲突的可能,即使某个命名空间已经存在,它嵌套的命名空间冲突的可能性却很小。
下面的代码看起来熟悉吗?
~~~
YAHOO.util.Dom.getElementsByClassName("test");
~~~
Yahoo!'s YUI 库经常使用嵌套命名空间模式, 当我在AOL当工程师的时候,我们在很多大型应用中也使用过这种模式。下面是嵌套命名空间的一个简单的实现:
~~~
var myApp = myApp || {};
// perform a similar existence check when defining nested
// children
myApp.routers = myApp.routers || {};
myApp.model = myApp.model || {};
myApp.model.special = myApp.model.special || {};
// nested namespaces can be as complex as required:
// myApp.utilities.charting.html5.plotGraph(/*..*/);
// myApp.modules.financePlanner.getSummary();
// myApp.services.social.facebook.realtimeStream.getLatest();
~~~
> 注意: 上面的代码与YUI3实现命名空间是不同的。上面的模块使用沙盒API来保存对象,而且使用了更少、更短的命名空间。
我们也可以像下面一样,选择使用索引属性来定义新的嵌套命名空间/属性:
~~~
myApp["routers"] = myApp["routers"] || {};
myApp["models"] = myApp["models"] || {};
myApp["controllers"] = myApp["controllers"] || {};
~~~
两种选择可读性都很强,而且很有条理,它们都提供了与我们可能在其他语言中使用的类似的一种相对安全的方式来给我们的应用添加命名空间.唯一需要注意的是,这需要我们浏览器中的JavaScript引擎首先定位到myApp对象,然后深入挖掘,直到找到我们想使用的方法为止。
这就以为着在查找方面会增加很多工作,然后开发人员比如Juriy Zaytsev 以前就做过测试,而且发现单个对象命名空间与嵌套命名空间在性能方面的差异是可以忽略不计的。
### 即时调用的函数表达式(IIFE)s
早在本书中,我们就简单的介绍过IIFE (即时调用的函数表达式) ,它是一个未命名的函数,在它被定义之后就会立即执行。如果听起来觉得耳熟,是因为你以前遇到过并将它称之为自动生效的(或者自动调用的)匿名函数,然而我个人更认为 Ben Alman的 IIFE 命名更准确。在JavaScript中,因为在一个作用域中显示定义的变量和函数只能在作用域中可见,函数调用为实现隐私提供了简单的方式。
IIFEs 将应用逻辑封装从而将它在全局命名空间中保护起来,但可以在命名空间范围内使用。
下面是IIFEs的例子:
~~~
// an (anonymous) immediately-invoked function expression
(function () { /*...*/})();
// a named immediately-invoked function expression
(function foobar () { /*..*/}());
// this is technically a self-executing function which is quite different
function foobar () { foobar(); }
~~~
对于第一个例子稍微进行一下扩展:
~~~
var namespace = namespace || {};
// here a namespace object is passed as a function
// parameter, where we assign public methods and
// properties to it
(function( o ){
o.foo = "foo";
o.bar = function(){
return "bar";
};
})( namespace );
console.log( namespace );
~~~
虽然可读,这个例子可以被更大范围的扩展到说明通用的开发问题,例如定义隐私的级别(public/private函数和变量),以及方便的命名空间扩展。我们来浏览更多的代码:
~~~
// namespace (our namespace name) and undefined are passed here
// to ensure 1\. namespace can be modified locally and isn't
// overwritten outside of our function context
// 2\. the value of undefined is guaranteed as being truly
// undefined. This is to avoid issues with undefined being
// mutable pre-ES5.
;(function ( namespace, undefined ) {
// private properties
var foo = "foo",
bar = "bar";
// public methods and properties
namespace.foobar = "foobar";
namespace.sayHello = function () {
speak( "hello world" );
};
// private method
function speak(msg) {
console.log( "You said: " + msg );
};
// check to evaluate whether "namespace" exists in the
// global namespace - if not, assign window.namespace an
// object literal
}( window.namespace = window.namespace || {} ));
// we can then test our properties and methods as follows
// public
// Outputs: foobar
console.log( namespace.foobar );
// Outputs: hello world
namescpace.sayHello();
// assigning new properties
namespace.foobar2 = "foobar";
// Outputs: foobar
console.log( namespace.foobar2 );
~~~
对任何可扩展的命名空间模式,可扩展性当然是关键,可以通过使用IIFEs很容易的达到这个目标。在下面的例子中,我们的"namespace"再次被当作参数传递给匿名函数,之后扩展(或装饰)了更多的功能:
~~~
// let's extend the namespace with new functionality
(function( namespace, undefined ){
// public method
namespace.sayGoodbye = function () {
console.log( namespace.foo );
console.log( namespace.bar );
speak( "goodbye" );
}
}( window.namespace = window.namespace || {});
// Outputs: goodbye
namespace.sayGoodbye();
~~~
### 命名空间注入
命名空间注入是关于IIFE的另外一种变种,为了一个来自函数封装中使用this作为命名空间代理的特定的命名空间,我们将方法和属性“注入”, 这一模式提供的好处就是对于多个对象或者命名空间的应用程序的功能性行为的便利性,并且在应用一堆晚些时候将被构建的基础方法(如getter和setter),这将会变得很有用处。
这一模式的缺点就是,如我在本节前面所述,也许还会有达成此目的更加简单并且更加优化的方法存在(如,深度对象扩展/混合)。
下面我们马上可以看到这一模式的一个示例,我们使用它来填充两个命名空间的行为:一个最开始就定义(utils),而另外一个我们则将其作为utils的功能性赋值的一部分来动态创建(一个称作tools的新的命名空间)。
~~~
var myApp = myApp || {};
myApp.utils = {};
(function () {
var val = 5;
this.getValue = function () {
return val;
};
this.setValue = function( newVal ) {
val = newVal;
}
// also introduce a new sub-namespace
this.tools = {};
}).apply( myApp.utils );
// inject new behaviour into the tools namespace
// which we defined via the utilities module
(function () {
this.diagnose = function(){
return "diagnosis";
}
}).apply( myApp.utils.tools );
// note, this same approach to extension could be applied
// to a regular IIFE, by just passing in the context as
// an argument and modifying the context rather than just
// "this"
// Usage:
// Outputs our populated namespace
console.log( myApp );
// Outputs: 5
console.log( myApp.utils.getValue() );
// Sets the value of `val` and returns it
myApp.utils.setValue( 25 );
console.log( myApp.utils.getValue() );
// Testing another level down
console.log( myApp.utils.tools.diagnose() );
~~~
Angus Croll先前也出过使用调用API来提供上下文环境和参数之间自然分离的主意。这一模式感觉上像是一个模块创建器,但是由于模块仍然提供了一个封装的解决方案, 为全面起见,我们还是将简要的介绍一下它:
~~~
// define a namespace we can use later
var ns = ns || {},
ns2 = ns2 || {};
// the module/namespace creator
var creator = function( val ){
var val = val || 0;
this.next = function () {
return val++
};
this.reset = function () {
val = 0;
}
}
creator.call( ns );
// ns.next, ns.reset now exist
creator.call( ns2 , 5000 );
// ns2 contains the same methods
// but has an overridden value for val
// of 5000
~~~
如前所述,这种类型的模式对于将一个类似的功能的基础集合分派给多个模块或者命名空间是非常有用的。然而我会只建议将它使用在要在一个对象/闭包中明确声明功能,而直接访问并没有任何意义的地方。
## 高级命名空间模式
接下来说说我在开发大型应用过程中发现的几种有用的模式和工具,其中一些需要我们重新审视传统应用的命名空间的使用方式.需要注意的是,我并非有意夸大以下几种是正确的命名空间之路,只是我在工作中发现他们确实好用。
### 自动嵌套命名空间
我们提到过,嵌套命名空间可以为代码提供一个组织良好的层级结构.下边是一个例子:application.utilities.drawing.canvas.2d . 可以用文字对象模式展开如下:
~~~
var application = {
utilities:{
drawing:{
canvas:{
2d:{
//...
}
}
}
}
};
~~~
使用这种模式会遇到一些问题,一个显而易见的就是每天加一个层级,就需要我们在顶级命名空间下的某个父级元素里定义一个额外的对象.当应用越来越复杂的时候,我们需要的层级增多,解决这个问题也就更加困难。
怎样更好的解决这个问题呢? 在JavaScript设计模式中, Stoyan Stefanov 提出了一个非常精巧的方法以便在已存在的全局变量下定义嵌套的命名空间。 他建议的简便方法是为每一层嵌套提供一个单字符声明,解析这个声明就可以自动算出包含必要对象的命名空间。
我(笔者)将他建议使用的方法改进为一个通用方法,以便对多重命名空间更容易地做出复用,方法如下:
~~~
// 顶级命名空间赋值为对象字面量
var myApp = myApp || {};
// 解析字符命名空间并自动生成嵌套命名空间的快捷方法 function extend( ns, ns_string ) {
var parts = ns_string.split("."),
parent = ns,
pl;
pl = parts.length;
for ( var i = 0; i < pl; i++ ) {
// create a property if it doesn't exist
if ( typeof parent[parts[i]] === "undefined" ) {
parent[parts[i]] = {};
}
parent = parent[parts[i]];
}
return parent;
}
// 用法:
// extend为myApp加入深度嵌套的命名空间
var mod = extend(myApp, "modules.module2");
// 输出深度嵌套的正确对象
console.log(mod);
// 用于检查mod的实例作为包含扩展的一个实体也能够被myApp命名空间以外被使用的少量测试
// 输出: true
console.log(mod == myApp.modules.module2);
// 进一步演示用extend赋予嵌套命名空间更简单
extend(myApp, "moduleA.moduleB.moduleC.moduleD");
extend(myApp, "longer.version.looks.like.this");
console.log(myApp);
~~~
Web审查工具输出:
![](http://wiki.jikexueyuan.com/project/javascript-design-patterns/images/Fmos.png)
一行简洁的代码就可以很轻松地,为他们的命名空间像以前的对象那样明确声明各种各样的嵌套。
## 依赖声明模式
现在我们将探索一种对嵌套命名空间模式的一种轻微的增强,它将被我们引申为依赖声明模式。我们都知道对于对象的本地引用能够降低全局查找的时间,但让我们来将它应用在命名空间中,看看实践中它表现怎么样:
~~~
// common approach to accessing nested namespaces
myApp.utilities.math.fibonacci( 25 );
myApp.utilities.math.sin( 56 );
myApp.utilities.drawing.plot( 98,50,60 );
// with local/cached references
var utils = myApp.utilities,
maths = utils.math,
drawing = utils.drawing;
// easier to access the namespace
maths.fibonacci( 25 );
maths.sin( 56 );
drawing.plot( 98, 50,60 );
// note that the above is particularly performant when
// compared to hundreds or thousands of calls to nested
// namespaces vs. a local reference to the namespace
~~~
这里使用一个本地变量相比顶层上一个全局的(如,myApp)几乎总是会更快。相比访问其后每行嵌套的属性/命名空间,这也更加的方便,性能表现更好,并且能够在更加复杂的应用程序场景下面提升可读性。
Stoyan建议在我们的函数范围(使用单变量模式)的顶部声明函数或者模块需要的局部命名空间,并把这称为依赖声明模式。其中的一个好处是减少了定位和重定向依赖关系的时间,从而使我们有一个可扩展的架构,当需要时可以在命名空间里动态地加载模块。
在我看来,这种方式应用于模块化级别时,将被其他方法使用的命名空间局部化是最有效。我建议尽量避免把命名空间局部化在单个函数级别,尤其是对于命名空间的依赖关系上有明显的重叠的情况。对应的方法是,在上部定义并使它们可以进入同一个引用。
### 深度对象扩展
另一种实现自动命名空间的方式就是深度对象扩展. 使用对象文字表示的命名空间可以很容易地与其他对象(或命名空间)扩展(或者合并) 这样两个命名空间下的属性和方法就可以在同一个合并后的命名空间下被访问。
一些现代的JavaScript框架已经把这个变得非常容易(例如,jQuery的$.extend),然而,如果你想寻找一种使用普通的JS来扩展对象(命名空间)的方式,下面的内容将很有帮助。
~~~
// extend.js
// Written by Andrew Dupont, optimized by Addy Osmani
function extend( destination, source ) {
var toString = Object.prototype.toString,
objTest = toString.call({});
for ( var property in source ) {
if ( source[property] && objTest === toString.call(source[property]) ) {
destination[property] = destination[property] || {};
extend(destination[property], source[property]);
} else {
destination[property] = source[property];
}
}
return destination;
};
console.group( "objExtend namespacing tests" );
// define a top-level namespace for usage
var myNS = myNS || {};
// 1\. extend namespace with a "utils" object
extend(myNS, {
utils:{
}
});
console.log( "test 1" , myNS);
// myNS.utils now exists
// 2\. extend with multiple depths (namespace.hello.world.wave)
extend(myNS, {
hello:{
world:{
wave:{
test: function(){
//...
}
}
}
}
});
// test direct assignment works as expected
myNS.hello.test1 = "this is a test";
myNS.hello.world.test2 = "this is another test";
console.log( "test 2", myNS );
// 3\. what if myNS already contains the namespace being added
// (e.g. "library")? we want to ensure no namespaces are being
// overwritten during extension
myNS.library = {
foo:function () {}
};
extend( myNS, {
library:{
bar:function(){
//...
}
}
});
// confirmed that extend is operating safely (as expected)
// myNS now also contains library.foo, library.bar
console.log( "test 3", myNS );
// 4\. what if we wanted easier access to a specific namespace without having
// to type the whole namespace out each time?
var shorterAccess1 = myNS.hello.world;
shorterAccess1.test3 = "hello again";
console.log( "test 4", myNS);
//success, myApp.hello.world.test3 is now "hello again"
console.groupEnd();
~~~
注意: 上面的实现对于所有的对象来说不是跨浏览器的而且只应该被认为是一个概念上的证明. 你可能会觉得前面带下划线的js.extend()方法更简单一些,下面的链接提供了更多的跨浏览器实现,[](http://documentcloud.github.com/underscore/docs/underscore.html#section-67)[http://documentcloud.github.com/underscore/docs/underscore.html#section-67](http://documentcloud.github.com/underscore/docs/underscore.html#section-67)。 另外,从代码中抽取出来的jQuery $.extend() 方法可以在这里找到: [](https://github.com/addyosmani/jquery.parts)[https://github.com/addyosmani/jquery.parts](https://github.com/addyosmani/jquery.parts)。 对于那些将使用jQuery的开发者来说, 可以像下面一样使用$.extend来达到同样的对象命名空间扩展的目的:
~~~
// top-level namespace
var myApp = myApp || {};
// directly assign a nested namespace
myApp.library = {
foo:function(){
//...
}
};
// deep extend/merge this namespace with another
// to make things interesting, let's say it's a namespace
// with the same name but with a different function
// signature: $.extend( deep, target, object1, object2 )
$.extend( true, myApp, {
library:{
bar:function(){
//...
}
}
});
console.log("test", myApp);
// myApp now contains both library.foo() and library.bar() methods
// nothing has been overwritten which is what we're hoping for.
~~~
为了透彻起见,请点击这里 来查看jQuery $.extend来获取跟这一节中其它实现命名空间的过程类似的功能。
## 建议
回顾我们在本部分探讨的命名空间模式,对于大多数更大的应用程序,我个人则是选择嵌入用对象字面值模式为命名空间的对象。我尽可能地用自动嵌入命名空间,当然这只是个人偏好罢了。
IIFEs 和单个全局变量可能只在中小规模的应用程序中运转良好。然而,更大的需要命名空间和深度子命名空间的代码库则需要一个简明的,能提高可读性和规模的解决方案。我认为是这种模式很好地达到了这些目标。我同样推荐你尝试一些拓展命名空间的高级实用的方法,因为它们能长期地节省我们的时间。
jQuery 插件的设计模式
最后更新于:2022-04-01 11:46:56
# jQuery 插件的设计模式
jQuery插件开发在过去几年里进步了很多. 我们写插件的方式不再仅仅只有一种,相反有很多种。现实中,某些插件设计模式在解决某些特殊的问题或者开发组件的时候比其他模式更有效。
有些开发者可能希望使用 jQuery UI 部件工厂; 它对于创建复杂而又灵活的UI组件是很强大的。有些开发者可能不想使用。
有些开发者可能想把它们的插件设计得更像模块(与模块模式相似)或者使用一种更现代化的模块格式。 有些开发者想让他们的插件利用原型继承的特点。有些则希望使用自定义事件或者通过发布/订阅信息来实现插件和他们的其它App之间的通信。
在经过了想创建一刀切的jquery插件样板的数次尝试之后,我开始考虑插件模式。这样的样板在理论上是一个很好的主意,但现实是,我们很少使用固定的方式并且总是使用一种模式来写插件。
让我们假设我们已经为了某个目标去着手尝试编写我们自己的jQuery插件,并且我们可以放心的把一些东西放在一起运作。它是起作用的。它做了它需要去做的,但是也许我们觉得它可以被构造得更好。也许它应该更加灵活或者被设计用来解决更多开发者普遍都碰到过的问题才对。如果这听起来很熟悉,那么你也许会发现这一章是很有用的。在其中,我们将探讨大量的jQuery插件模式,它们在其它开发者的环境中都工作的不错。
> 注意:尽管开头我们将简要回顾一些jQuery插件的基础知识,但这一章是针对中级到高级的开发者的。 如果你觉得对此还没有做足够的准备,我很高兴的建议你去看一看jQuery官方的插件/创作(Plugins/Authoring )指导,Ben Alman的插件类型指导(plugin style guide)和RemySharp的“写得不好的jQuery插件的症候(Signs of a Poorly Written jQuery Plugin)”,作为开始这一节之前的阅读材料。
## 模式
jQuery插件有一些具体的规则,它们是整个社区能够实现这令人难以置信的多样性的原因之一。在最基本的层面上,我们能够编写一个简单地向jQuery的jQuery.fn对象添加一个新的功能属性的插件,像下面这样:
~~~
$.fn.myPluginName = function () {
// our plugin logic
};
~~~
对于紧凑性而言这是很棒的,而下面的代码将会是一个更好的构建基础:
~~~
(function( $ ){
$.fn.myPluginName = function () {
// our plugin logic
};
})( jQuery );
~~~
在这里,我们将我们的插件逻辑封装到一个匿名函数中。为了确保我们使用的$标记作为简写形式不会造成任何jQuery和其它Javascript库之间的冲突,我们简单的将其传入这个闭包中,它会将其映射到美元符号上。这就确保了它能够不被任何范围之外的执行影响到。
编写这种模式的一个可选方式是使用jQuery.extend(),它使得我们能够一次定义多个函数,并且有时能够获得更多的语义上的意义。
~~~
(function( $ ){
$.extend($.fn, {
myplugin: function(){
// your plugin logic
}
});
})( jQuery );
~~~
现在我们已经回顾了一些jQuery插件的基础,但是许多更多的工作可借以更进一步。A Lightweight Start是我们将要探讨的该 设计模式的第一个完整的插件,它涵盖了我们可以在每天的基础的插件开发工作中用到的一些最佳实践, 细数了一些值得推广应用的常见问题描述。
> 注意: 尽管下面大多数的模式都会得到解释,我还是建议大家通过阅读代码里的注释来研究它们,因为这些注释能够提供关于为什么一个具体的最佳实践会被应用这个问题的更深入的理解。 我也应该提醒下,没有前面的工作往后这些没有一样是可能的,它们是来自于jQuery社区的其他成员的输入和建议。我已经将它们列到每一种模式中了,以便诸位可以根据各自的工作方向来阅读相关的内容,如果感兴趣的话。
## A Lightweight Start 模式
让我们用一些遵循了(包括那些在jQuery 插件创作指导中的)最佳实践的基础的东西来开始我们针对插件模式的深入探讨。这一模式对于插件开发的新手和只想要实现一些简单的东西(例如工具插件)的人来说是理想的。A Lightweight Start 使用到了下面这些东西:
* 诸如分号放置在函数调用之前这样一些通用的最佳实践(我们将在下面的注释中解释为什么要这样做)
* window,document,undefined作为参数传入。
* 基本的默认对象。
* 一个简单的针对跟初始化创建和要一起运作的元素的赋值相关的逻辑的插件构造器。
* 扩展默认的选项。
* 围绕构造器的轻量级的封装,它有助于避免诸如实例化多次的问题。
* 坚持最大限度可读性的jQuery核心风格的指导方针。
~~~
/*!
* jQuery lightweight plugin boilerplate
* Original author: @ajpiano
* Further changes, comments: @addyosmani
* Licensed under the MIT license
*/
// the semi-colon before the function invocation is a safety
// net against concatenated scripts and/or other plugins
// that are not closed properly.
;(function ( $, window, document, undefined ) {
// undefined is used here as the undefined global
// variable in ECMAScript 3 and is mutable (i.e. it can
// be changed by someone else). undefined isn't really
// being passed in so we can ensure that its value is
// truly undefined. In ES5, undefined can no longer be
// modified.
// window and document are passed through as local
// variables rather than as globals, because this (slightly)
// quickens the resolution process and can be more
// efficiently minified (especially when both are
// regularly referenced in our plugin).
// Create the defaults once
var pluginName = "defaultPluginName",
defaults = {
propertyName: "value"
};
// The actual plugin constructor
function Plugin( element, options ) {
this.element = element;
// jQuery has an extend method that merges the
// contents of two or more objects, storing the
// result in the first object. The first object
// is generally empty because we don't want to alter
// the default options for future instances of the plugin
this.options = $.extend( {}, defaults, options) ;
this._defaults = defaults;
this._name = pluginName;
this.init();
}
Plugin.prototype.init = function () {
// Place initialization logic here
// We already have access to the DOM element and
// the options via the instance, e.g. this.element
// and this.options
};
// A really lightweight plugin wrapper around the constructor,
// preventing against multiple instantiations
$.fn[pluginName] = function ( options ) {
return this.each(function () {
if ( !$.data(this, "plugin_" + pluginName )) {
$.data( this, "plugin_" + pluginName,
new Plugin( this, options ));
}
});
}
})( jQuery, window, document );
~~~
用例:
~~~
$("#elem").defaultPluginName({
propertyName: "a custom value"
});
~~~
## 完整的 Widget 工厂模式
虽然jQuery插件创作指南是对插件开发的一个很棒的介绍,但它并不能帮助掩盖我们不得不定期处理的常见的插件管道任务。
jQuery UI Widget工厂是这个问题的一种解决方案,能帮助我们基于面向对象原则构建复杂的,具有状态性的插件。它也简化了我们插件实体的通信,也淡化了许多我们在一些基础的插件上工作时必须去编写代码的重复性的工作。
具有状态性的插件帮助我们对它们的当前状态保持跟进,也允许我们在插件被初始化之后改变其属性。 有关Widget工厂最棒的事情之一是大部分的jQuery UI库的实际上都是使用它作为其组件的基础。这意味着如果我们是在寻找超越这一模式的架构的进一步指导,我们将没必要去超越GitHub上的[jQuery UI](https://github.com/jquery/jquery-ui)进行思考。
jQuery UI Widget 工厂模式涵盖了包括事件触发在内几乎所有的默认支持的工厂方法。每一个模式的最后都包含了所有这些方法的使用注释,还在内嵌的注释中给出了更深入的指导。
~~~
/*!
* jQuery UI Widget-factory plugin boilerplate (for 1.8/9+)
* Author: @addyosmani
* Further changes: @peolanha
* Licensed under the MIT license
*/
;(function ( $, window, document, undefined ) {
// define our widget under a namespace of your choice
// with additional parameters e.g.
// $.widget( "namespace.widgetname", (optional) - an
// existing widget prototype to inherit from, an object
// literal to become the widget's prototype );
$.widget( "namespace.widgetname" , {
//Options to be used as defaults
options: {
someValue: null
},
//Setup widget (e.g. element creation, apply theming
// , bind events etc.)
_create: function () {
// _create will automatically run the first time
// this widget is called. Put the initial widget
// setup code here, then we can access the element
// on which the widget was called via this.element.
// The options defined above can be accessed
// via this.options this.element.addStuff();
},
// Destroy an instantiated plugin and clean up
// modifications the widget has made to the DOM
destroy: function () {
// this.element.removeStuff();
// For UI 1.8, destroy must be invoked from the
// base widget
$.Widget.prototype.destroy.call( this );
// For UI 1.9, define _destroy instead and don't
// worry about
// calling the base widget
},
methodB: function ( event ) {
//_trigger dispatches callbacks the plugin user
// can subscribe to
// signature: _trigger( "callbackName" , [eventObject],
// [uiObject] )
// e.g. this._trigger( "hover", e /*where e.type ==
// "mouseenter"*/, { hovered: $(e.target)});
this._trigger( "methodA", event, {
key: value
});
},
methodA: function ( event ) {
this._trigger( "dataChanged", event, {
key: value
});
},
// Respond to any changes the user makes to the
// option method
_setOption: function ( key, value ) {
switch ( key ) {
case "someValue":
// this.options.someValue = doSomethingWith( value );
break;
default:
// this.options[ key ] = value;
break;
}
// For UI 1.8, _setOption must be manually invoked
// from the base widget
$.Widget.prototype._setOption.apply( this, arguments );
// For UI 1.9 the _super method can be used instead
// this._super( "_setOption", key, value );
}
});
})( jQuery, window, document );
~~~
用例:
~~~
var collection = $("#elem").widgetName({
foo: false
});
collection.widgetName("methodB");
~~~
## 嵌套的命名空间插件模式
如我们在本书的前面所述,为我们的代码加入命名空间是避免与其它的全局命名空间中的对象和变量产生冲突的一种方法。它们是很重要的,因为我们想要保护我们的插件的运作不会突然被页面上另外一段使用了同名变量或者插件的脚本所打断。作为全局命名空间的好市民,我们也必须尽我们所能来阻止其他开发者的脚本由于同样的问题而执行起来发生问题。
Javascript并不像其它语言那样真的内置有对命名空间的支持,但它却有可以被用来达到同样效果的对象。雇佣一个顶级对象作为我们命名空间的名称,我们就可以使用相同的名字检查页面上另外一个对象的存在性。如果这样的对象不存在,那么我们就定义它;如果它存在,就简单的用我们的插件对其进行扩展。
对象(或者更确切的说,对象常量)可以被用来创建内嵌的命名空间,namespace.subnamespace.pluginName,诸如此类。而为了保持简单,下面的命名空间样板会向我们展示有关这些概念的入门我们所需要的一切。
~~~
/*!
* jQuery namespaced "Starter" plugin boilerplate
* Author: @dougneiner
* Further changes: @addyosmani
* Licensed under the MIT license
*/
;(function ( $ ) {
if (!$.myNamespace) {
$.myNamespace = {};
};
$.myNamespace.myPluginName = function ( el, myFunctionParam, options ) {
// To avoid scope issues, use "base" instead of "this"
// to reference this class from internal events and functions.
var base = this;
// Access to jQuery and DOM versions of element
base.$el = $( el );
base.el = el;
// Add a reverse reference to the DOM object
base.$el.data( "myNamespace.myPluginName" , base );
base.init = function () {
base.myFunctionParam = myFunctionParam;
base.options = $.extend({},
$.myNamespace.myPluginName.defaultOptions, options);
// Put our initialization code here
};
// Sample Function, Uncomment to use
// base.functionName = function( parameters ){
//
// };
// Run initializer
base.init();
};
$.myNamespace.myPluginName.defaultOptions = {
myDefaultValue: ""
};
$.fn.mynamespace_myPluginName = function
( myFunctionParam, options ) {
return this.each(function () {
(new $.myNamespace.myPluginName( this,
myFunctionParam, options ));
});
};
})( jQuery );
~~~
用例:
~~~
$("#elem").mynamespace_myPluginName({
myDefaultValue: "foobar"
});
~~~
## (使用Widget工厂)自定义事件插件模式
在本书的Javascript设计模式一节,我们讨论了观察者模式,而后继续论述到了jQuery对于自定义事件的支持,其为实现发布/订阅提供了一种类似的解决方案。
这里的基本观点是当我们的应用程序中发生了某些有趣的事情时,页面中的对象能够发布事件通知。其他对象就会订阅(或者侦听)这些事件,并且据此产生回应。我们应用程序的这一逻辑所产生的效果是更加显著的解耦,每一个对象不再需要直接同另外一个对象进行通信。
在接下来的jQuery UI widget工厂模式中,我们将实现一个基本的基于自定义事件的发布/订阅系统,它允许我们的插件向应用程序的其余部分发布事件通知,而这些部分将对此产生回应。
~~~
/*!
* jQuery custom-events plugin boilerplate
* Author: DevPatch
* Further changes: @addyosmani
* Licensed under the MIT license
*/
// In this pattern, we use jQuery's custom events to add
// pub/sub (publish/subscribe) capabilities to widgets.
// Each widget would publish certain events and subscribe
// to others. This approach effectively helps to decouple
// the widgets and enables them to function independently.
;(function ( $, window, document, undefined ) {
$.widget( "ao.eventStatus", {
options: {
},
_create : function() {
var self = this;
//self.element.addClass( "my-widget" );
//subscribe to "myEventStart"
self.element.on( "myEventStart", function( e ) {
console.log( "event start" );
});
//subscribe to "myEventEnd"
self.element.on( "myEventEnd", function( e ) {
console.log( "event end" );
});
//unsubscribe to "myEventStart"
//self.element.off( "myEventStart", function(e){
///console.log( "unsubscribed to this event" );
//});
},
destroy: function(){
$.Widget.prototype.destroy.apply( this, arguments );
},
});
})( jQuery, window , document );
// Publishing event notifications
// $( ".my-widget" ).trigger( "myEventStart");
// $( ".my-widget" ).trigger( "myEventEnd" );
~~~
用例:
~~~
var el = $( "#elem" );
el.eventStatus();
el.eventStatus().trigger( "myEventStart" );
~~~
## 使用DOM-To-Object桥接模式的原型继承
正如前面所介绍的,在Javascript中,我们并不需要那些在其它经典的编程语言中找到的类的传统观念,但我们确实需要原型继承。有了原型继承,对象就可以从其它对象继承而来了。我们可以将此概念应用到jQuery的插件开发中。
Yepnope.js作者Alex Sexton和jQuery团队成员Scott Gonzalez已经瞩目于这个主题的细节。总之,他们发现为了组织模块化的开发,使定义插件逻辑的对象同插件生成过程本身分离是有好处的。
这一好处就是对我们插件代码的测试会变得显著的简单起来,并且我们也能够在不改变任何我们所实现的对象API的方式,这一前提下,适应事物在幕后运作的方式。
在Sexton关于这个主题的文章中,他实现了一个使我们能够将我们的一般的逻辑附加到特定插件的桥,我们已经在下面的模式中将它实现。
这一模式的另外一个优点是我们不需要去不断的重复同样的插件初始化代码,这确保了DRY开发背后的观念得以维持。一些开发者或许也会发现这一模式的代码相比其它更加易读。
~~~
/*!
* jQuery prototypal inheritance plugin boilerplate
* Author: Alex Sexton, Scott Gonzalez
* Further changes: @addyosmani
* Licensed under the MIT license
*/
// myObject - an object representing a concept we wish to model
// (e.g. a car)
var myObject = {
init: function( options, elem ) {
// Mix in the passed-in options with the default options
this.options = $.extend( {}, this.options, options );
// Save the element reference, both as a jQuery
// reference and a normal reference
this.elem = elem;
this.$elem = $( elem );
// Build the DOM's initial structure
this._build();
// return this so that we can chain and use the bridge with less code.
return this;
},
options: {
name: "No name"
},
_build: function(){
//this.$elem.html( "<h1>"+this.options.name+"</h1>" );
},
myMethod: function( msg ){
// We have direct access to the associated and cached
// jQuery element
// this.$elem.append( "<p>"+msg+"</p>" );
}
};
// Object.create support test, and fallback for browsers without it
if ( typeof Object.create !== "function" ) {
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
}
// Create a plugin based on a defined object
$.plugin = function( name, object ) {
$.fn[name] = function( options ) {
return this.each(function() {
if ( ! $.data( this, name ) ) {
$.data( this, name, Object.create( object ).init(
options, this ) );
}
});
};
};
~~~
用例:
~~~
$.plugin( "myobj", myObject );
$("#elem").myobj( {name: "John"} );
var collection = $( "#elem" ).data( "myobj" );
collection.myMethod( "I am a method");
~~~
## jQuery UI Widget 工厂桥接模式
如果你喜欢基于过去的设计模式的对象的生成插件这个主意,那么你也许会对这个在jQuery UI Widget工厂中发现的叫做$.widget.bridge的方法感兴趣。
这座桥基本上是在充当使用$.widget创建的Javascript对象和jQuery核心API之间的中间层,它提供了一种实现基于对象的插件定义的更加内置的解决方案。实际上,我们能够使用自定义的构造器去创建具有状态性的插件。
此外,$.widget.bridge还提供了对许多其它功能的访问,包括下面这些:
* 公共的和私有的方法都如人们在经典的OOP中所希望的方式被处理(例如,公共的方法被暴露出来,而对私有方法的调用则是不可能的)。
* 防止多次初始化的自动保护。
* 传入对象实体的自动生成,而对它们的存储则在内置的$.datacache范围之内。
* 选项可以在初始化后修改。
有关使用这一模式的更多信息,请看看下面内嵌的注释:
~~~
/*!
* jQuery UI Widget factory "bridge" plugin boilerplate
* Author: @erichynds
* Further changes, additional comments: @addyosmani
* Licensed under the MIT license
*/
// a "widgetName" object constructor
// required: this must accept two arguments,
// options: an object of configuration options
// element: the DOM element the instance was created on
var widgetName = function( options, element ){
this.name = "myWidgetName";
this.options = options;
this.element = element;
this._init();
}
// the "widgetName" prototype
widgetName.prototype = {
// _create will automatically run the first time this
// widget is called
_create: function(){
// creation code
},
// required: initialization logic for the plugin goes into _init
// This fires when our instance is first created and when
// attempting to initialize the widget again (by the bridge)
// after it has already been initialized.
_init: function(){
// init code
},
// required: objects to be used with the bridge must contain an
// "option". Post-initialization, the logic for changing options
// goes here.
option: function( key, value ){
// optional: get/change options post initialization
// ignore if you don't require them.
// signature: $("#foo").bar({ cool:false });
if( $.isPlainObject( key ) ){
this.options = $.extend( true, this.options, key );
// signature: $( "#foo" ).option( "cool" ); - getter
} else if ( key && typeof value === "undefined" ){
return this.options[ key ];
// signature: $( "#foo" ).bar("option", "baz", false );
} else {
this.options[ key ] = value;
}
// required: option must return the current instance.
// When re-initializing an instance on elements, option
// is called first and is then chained to the _init method.
return this;
},
// notice no underscore is used for public methods
publicFunction: function(){
console.log( "public function" );
},
// underscores are used for private methods
_privateFunction: function(){
console.log( "private function" );
}
};
~~~
用例:
~~~
// connect the widget obj to jQuery's API under the "foo" namespace
$.widget.bridge( "foo", widgetName );
// create an instance of the widget for use
var instance = $( "#foo" ).foo({
baz: true
});
// our widget instance exists in the elem's data
// Outputs: #elem
console.log(instance.data( "foo" ).element);
// bridge allows us to call public methods
// Outputs: "public method"
instance.foo("publicFunction");
// bridge prevents calls to internal methods
instance.foo("_privateFunction");
~~~
## 使用 Widget 工厂的 jQuery Mobile 小部件
jQuery Mobile 是一个 jQuery 项目框架,为设计同时能运行在主流移动设备和平台以及桌面平台的大多数常见 Web 应用带来便利。我们可以仅编写一次代码,而无需为每种设备或操作系统编写特定的应用,就能使其同时运行在 A、B 和 C 级浏览器。
JQuery mobile 背后的基本原理也可应用于插件和小部件的开发。
接下来介绍的模式令人感兴趣的是,已熟悉使用 jQuery UI Widget Factory 模式的开发者能够很快地编写针对移动设备优化的小部件,即便这会在不同设备中存在细微的差异。
下面为移动优化的widget同前面我们看到的标准UI widget模式相比,有许多有趣的不同之处。
$.mobile.widget 是继承于现有的widget原型的引用。对于标准的widget, 通过任何这样的原型进行基础的开发都是没有必要的,但是使用这种为移动应用定制的jQuery widget 原型,它提供了更多的“选项”格式供内部访问。
在_create()中,教程提供了关于官方的jQuery 移动 widget如何处理元素选择,对于基于角色的能够更好的适应jQM标记的方法的选择。这并不是说标准的选择不被推荐,只是说这种方法也许可以给予jQuery 移动页面的架构更多的意义。
也有以注释形式提供的关于将我们的插件方法应用于页面创建,还有通过数据角色和数据属性选择插件应用程序,这些内容的指导。
~~~
/*!
* (jQuery mobile) jQuery UI Widget-factory plugin boilerplate (for 1.8/9+)
* Author: @scottjehl
* Further changes: @addyosmani
* Licensed under the MIT license
*/
;(function ( $, window, document, undefined ) {
// define a widget under a namespace of our choice
// here "mobile" has been used in the first argument
$.widget( "mobile.widgetName", $.mobile.widget, {
// Options to be used as defaults
options: {
foo: true,
bar: false
},
_create: function() {
// _create will automatically run the first time this
// widget is called. Put the initial widget set-up code
// here, then we can access the element on which
// the widget was called via this.element
// The options defined above can be accessed via
// this.options
// var m = this.element,
// p = m.parents( ":jqmData(role="page")" ),
// c = p.find( ":jqmData(role="content")" )
},
// Private methods/props start with underscores
_dosomething: function(){ ... },
// Public methods like these below can can be called
// externally:
// $("#myelem").foo( "enable", arguments );
enable: function() { ... },
// Destroy an instantiated plugin and clean up modifications
// the widget has made to the DOM
destroy: function () {
// this.element.removeStuff();
// For UI 1.8, destroy must be invoked from the
// base widget
$.Widget.prototype.destroy.call( this );
// For UI 1.9, define _destroy instead and don't
// worry about calling the base widget
},
methodB: function ( event ) {
//_trigger dispatches callbacks the plugin user can
// subscribe to
// signature: _trigger( "callbackName" , [eventObject],
// [uiObject] )
// e.g. this._trigger( "hover", e /*where e.type ==
// "mouseenter"*/, { hovered: $(e.target)});
this._trigger( "methodA", event, {
key: value
});
},
methodA: function ( event ) {
this._trigger( "dataChanged", event, {
key: value
});
},
// Respond to any changes the user makes to the option method
_setOption: function ( key, value ) {
switch ( key ) {
case "someValue":
// this.options.someValue = doSomethingWith( value );
break;
default:
// this.options[ key ] = value;
break;
}
// For UI 1.8, _setOption must be manually invoked from
// the base widget
$.Widget.prototype._setOption.apply(this, arguments);
// For UI 1.9 the _super method can be used instead
// this._super( "_setOption", key, value );
}
});
})( jQuery, window, document );
~~~
用例:
~~~
var instance = $( "#foo" ).widgetName({
foo: false
});
instance.widgetName( "methodB" );
~~~
不论什么时候jQuery Mobile中的一个新页面被创建了,我们也都可以自己初始化这个widget。但一个(通过data-role="page"属性发现的)jQuery Mobile 页面一开始被初始化时, jQuery Mobile的页面插件会自己派发一个创建事件。我们能侦听那个(称作 “pagecreate”的)事件,并且在任何时候只要新的页面一被创建,就自动的让我们的插件运行。
~~~
$(document).on("pagecreate", function ( e ) {
// In here, e.target refers to the page that was created
// (it's the target of the pagecreate event)
// So, we can simply find elements on this page that match a
// selector of our choosing, and call our plugin on them.
// Here's how we'd call our "foo" plugin on any element with a
// data-role attribute of "foo":
$(e.target).find( "[data-role="foo"]" ).foo( options );
// Or, better yet, let's write the selector accounting for the configurable
// data-attribute namespace
$( e.target ).find( ":jqmData(role="foo")" ).foo( options );
});
~~~
现在我们可以在一个页面中简单的引用包含了我们的widget和pagecreate绑定的脚本,而它将像任何其它的jQuery Mobile插件一样自动的运行。
## RequireJS 和 jQuery UI Widget 工厂
如我们在当代模块化设计模式一节所述,RequireJS是一种兼容AMD的脚本装载器,它提供了将应用程序逻辑封装到可管理的模块中,这样一个干净的解决方案。
它能够(通过它的顺序插件)将模块按照正确的顺序加载,简化了借助它优秀的r.js优化器整合脚本的过程,并且提供了在每一个模块的基础上定义动态依赖的方法。
在下面的样板模式中,我们展示了一种兼容AMD的jQuery UI widget(这里是RequireJS)如何能够被定义成做到下面这些事情:
* 允许widget模块依赖的定义,构建在前面早先的jQuery UI Widget 工厂模式之上。
* 展示一种为创建(使用Underscore.js 微模板)模板化的widget传入HTML模板集的方法。
* 包括一种如果我们希望晚一点将其传入到RequireJS优化器,以使我们能够对我们的widget模块做出调整的快速提示。
~~~
/*!
* jQuery UI Widget + RequireJS module boilerplate (for 1.8/9+)
* Authors: @jrburke, @addyosmani
* Licensed under the MIT license
*/
// Note from James:
//
// This assumes we are using the RequireJS+jQuery file, and
// that the following files are all in the same directory:
//
// - require-jquery.js
// - jquery-ui.custom.min.js (custom jQuery UI build with widget factory)
// - templates/
// - asset.html
// - ao.myWidget.js
// Then we can construct the widget as follows:
// ao.myWidget.js file:
define( "ao.myWidget", ["jquery", "text!templates/asset.html", "underscore", "jquery-ui.custom.min"], function ( $, assetHtml, _ ) {
// define our widget under a namespace of our choice
// "ao" is used here as a demonstration
$.widget( "ao.myWidget", {
// Options to be used as defaults
options: {},
// Set up widget (e.g. create element, apply theming,
// bind events, etc.)
_create: function () {
// _create will automatically run the first time
// this widget is called. Put the initial widget
// set-up code here, then we can access the element
// on which the widget was called via this.element.
// The options defined above can be accessed via
// this.options
// this.element.addStuff();
// this.element.addStuff();
// We can then use Underscore templating with
// with the assetHtml that has been pulled in
// var template = _.template( assetHtml );
// this.content.append( template({}) );
},
// Destroy an instantiated plugin and clean up modifications
// that the widget has made to the DOM
destroy: function () {
// this.element.removeStuff();
// For UI 1.8, destroy must be invoked from the base
// widget
$.Widget.prototype.destroy.call( this );
// For UI 1.9, define _destroy instead and don't worry
// about calling the base widget
},
methodB: function ( event ) {
// _trigger dispatches callbacks the plugin user can
// subscribe to
// signature: _trigger( "callbackName" , [eventObject],
// [uiObject] )
this._trigger( "methodA", event, {
key: value
});
},
methodA: function ( event ) {
this._trigger("dataChanged", event, {
key: value
});
},
// Respond to any changes the user makes to the option method
_setOption: function ( key, value ) {
switch (key) {
case "someValue":
// this.options.someValue = doSomethingWith( value );
break;
default:
// this.options[ key ] = value;
break;
}
// For UI 1.8, _setOption must be manually invoked from
// the base widget
$.Widget.prototype._setOption.apply( this, arguments );
// For UI 1.9 the _super method can be used instead
// this._super( "_setOption", key, value );
}
});
});
~~~
用例:
index.html:
~~~
<script data-main="scripts/main" src="http://requirejs.org/docs/release/1.0.1/minified/require.js"></script>
~~~
main.js
~~~
require({
paths: {
"jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min",
"jqueryui": "https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.18/jquery-ui.min",
"boilerplate": "../patterns/jquery.widget-factory.requirejs.boilerplate"
}
}, ["require", "jquery", "jqueryui", "boilerplate"],
function (req, $) {
$(function () {
var instance = $("#elem").myWidget();
instance.myWidget("methodB");
});
});
~~~
## 全局和每次调用的重载选项(最佳调用模式)
对于我们的下一个模式,我们将来看看一种为插件选择默认和手动配置选项的优化了的方法。 定义插件选项,我们大多数人可能熟悉的一种方法是,通过默认的字面上的对象将其传递到$.extend(),如我们在我们基础的插件样板中所展示的。
然而,如果我们正工作在一种带有许多的定制选项,对于这些定制选项我们希望用户在全局和每一次调用的级别都能重载,那样我们就能以更加优化一点的方式构造事物。
相反,通过明确的引用定义在插件命名空间中的一个选项对象(例如,$fn.pluginName.options),还有将此同任何在其最初被调用时传递到插件的选项混合,用户就要对在插件初始化期间传递选项,或者在插件外部重载选项,这两者有所选择(如这里所展示的)。
~~~
/*!
* jQuery "best options" plugin boilerplate
* Author: @cowboy
* Further changes: @addyosmani
* Licensed under the MIT license
*/
;(function ( $, window, document, undefined ) {
$.fn.pluginName = function ( options ) {
// Here's a best practice for overriding "defaults"
// with specified options. Note how, rather than a
// regular defaults object being passed as the second
// parameter, we instead refer to $.fn.pluginName.options
// explicitly, merging it with the options passed directly
// to the plugin. This allows us to override options both
// globally and on a per-call level.
options = $.extend( {}, $.fn.pluginName.options, options );
return this.each(function () {
var elem = $(this);
});
};
// Globally overriding options
// Here are our publicly accessible default plugin options
// that are available in case the user doesn't pass in all
// of the values expected. The user is given a default
// experience but can also override the values as necessary.
// e.g. $fn.pluginName.key ="otherval";
$.fn.pluginName.options = {
key: "value",
myMethod: function ( elem, param ) {
}
};
})( jQuery, window, document );
~~~
用例:
~~~
$("#elem").pluginName({
key: "foobar"
});
~~~
## 高可配置和可变插件模式
在这个模式中,同Alex Sexton的原型继承插件模式类似,我们插件的逻辑并不嵌套在一个jQuery插件自身之中.取而代之我们使用了一个构造器和一种定义在它的原型之上的对象字面值,来定义我们的插件逻辑.jQuery随后被用在插件对象的实际实例中。
通过玩了两个小花样,定制被带到了一个新的层次,其中之一就是我们在前面已经看到的模式:
* 选项不论是全局的还是集合中每一个元素的,都可以被重载。
* 选在可以通过HTML5数据属性(在下面会有展示)在每一个元素的级别被定制.这有利于可以被应用到集合中元素的插件行为,但是会导致在不需要使用一个不同的默认值实例化每一个元素的前提下定制的内联。
在不怎么正规的场合我们不会经常见到这种非常规的选项,但是它能够成为一种重要的清晰方案(只要我们不介意这种内联的方式).如果不知道这个东西在那儿会起作用,那就想象着要为大型的元素集合编写一个可拖动的插件,这种场景.我们可以像下面这样定制它们的选项:
~~~
$( ".item-a" ).draggable( {"defaultPosition":"top-left"} );
$( ".item-b" ).draggable( {"defaultPosition":"bottom-right"} );
$( ".item-c" ).draggable( {"defaultPosition":"bottom-left"} );
//etc
~~~
但是使用我们模式的内联方式,下面这样是可能的。
~~~
$( ".items" ).draggable();
~~~
~~~
html
<li class="item" data-plugin-options="{"defaultPosition":"top-left"}"></div>
<li class="item" data-plugin-options="{"defaultPosition":"bottom-left"}"></div>
~~~
诸如此类.我们也许更加偏好这些方法之一,但它仅仅是我们值得去意识到的另外一个差异。
~~~
/*
* "Highly configurable" mutable plugin boilerplate
* Author: @markdalgleish
* Further changes, comments: @addyosmani
* Licensed under the MIT license
*/
// Note that with this pattern, as per Alex Sexton's, the plugin logic
// hasn't been nested in a jQuery plugin. Instead, we just use
// jQuery for its instantiation.
;(function( $, window, document, undefined ){
// our plugin constructor
var Plugin = function( elem, options ){
this.elem = elem;
this.$elem = $(elem);
this.options = options;
// This next line takes advantage of HTML5 data attributes
// to support customization of the plugin on a per-element
// basis. For example,
// <div class=item" data-plugin-options="{"message":"Goodbye World!"}"></div>
this.metadata = this.$elem.data( "plugin-options" );
};
// the plugin prototype
Plugin.prototype = {
defaults: {
message: "Hello world!"
},
init: function() {
// Introduce defaults that can be extended either
// globally or using an object literal.
this.config = $.extend( {}, this.defaults, this.options,
this.metadata );
// Sample usage:
// Set the message per instance:
// $( "#elem" ).plugin( { message: "Goodbye World!"} );
// or
// var p = new Plugin( document.getElementById( "elem" ),
// { message: "Goodbye World!"}).init()
// or, set the global default message:
// Plugin.defaults.message = "Goodbye World!"
this.sampleMethod();
return this;
},
sampleMethod: function() {
// e.g. show the currently configured message
// console.log(this.config.message);
}
}
Plugin.defaults = Plugin.prototype.defaults;
$.fn.plugin = function( options ) {
return this.each(function() {
new Plugin( this, options ).init();
});
};
// optional: window.Plugin = Plugin;
})( jQuery, window , document );
~~~
用例:
~~~
$("#elem").plugin({
message: "foobar"
});
~~~
## 是什么造就了模式之外的一个优秀插件?
在今天结束之际,设计模式仅仅只是编写可维护的jQuery插件的一个方面。还有大量其它的因素值得考虑,而我也希望分享下对于用第三方插件来解决一些其它的问题,我自己的选择标准。
质量
对于你所写的Javascript和jQuery插件,请坚持遵循最佳实践的做法。是否使用jsHint或者jsLint努力使插件更加厚实了呢?插件是否被优化过了呢?
编码风格
插件是否遵循了诸如jQuery 核心风格指南这样一种一致的风格指南?如果不是,那么你的代码至少是不是相对干净,并且可读的?
兼容性
各个版本的jQuery插件兼容怎么样?通过对编译jQuery-git源码的版本或者最新的稳定版的测试,如果在jQuery 1.6发布之前写的插件,那么它可能存在有问题的属性和特性,因为他们随着新版的发布而改变。
新版本的jQuery为jQuery的项目的提高核心库的使用提供了的改进和环境,虽然偶然出现破损(主要的版本),但我们是朝着更好的方向做的,我看到插件作者在必要时更新自己的代码,至少,测试他们的新版本的插件,以确保一切都如预期般运行。
可靠性
这个插件应该有自己的一套单元测试。做这些不仅是为了证明它确实在按照预期运作,也可以改进设计而无需影响最终用户。我认为单元测试对任何重要的jQuery插件都是必要的,它们对生产环境意义重大,而且它们也不是那么难写。要想获得一个用QUnit实现自动化JavaScript测试的完美指南,你也许会对Jörn Zaefferer的“使用QUnit自动化JavaScript 测试”感兴趣。
性能
如果这个插件需要执行的任务包含有大量的处理语句,或者对DOM的大量处理,那就应该按照基准管理的最佳实践将这个任务最小化。用jsPerf.com 来测试代码段,以实现 a) 在不同的浏览器执行是否良好 以及 b)如果存在的话,找到可以进一步优化的地方。
文档
如果目的是让其他开发者使用插件,那就要保证它具有良好的文档。给API建立文档描述这些插件是如何使用的。这个插件支持什么方法和选项?它有一些用户需要注意的性能和可伸缩性问题吗?如果用户无法理解怎样使用这个插件,他们很可能会找寻一个替代者。评论你的插件代码也是很有益处的。目前这是你能提供给其他开发者的最好的礼物了。如果有人觉得他们可以很好的使用或者改进你的基础代码,那么你就是做了一件漂亮的工作。
维护的可能性
发布插件的时候,估计可能需要多少时间进行维护和支持。我们都喜欢与社区分享我们的插件,但是一个人需要对他回答问题,解决问题和持续改进的能力设置预期。只要前期简单的在README文件中,为维护支持说明项目的意图,就可以做到这一点。
## 结论
在本章里,我们探索了几个节省时间的设计模式,以及一些可以用来改进jQuery插件写法的最佳实践。有些更适宜于特定的使用案例,但我希望总体而言这些模式是有用的。
请记住,当选择一个模式的时候,重要的是联系实际。不要仅仅因为某个插件模式的好处而使用它,而要投入时间理解底层的结构,并搞清楚它能否很好的解决你的问题,或者适应你尝试创建的组件。
建造者模式
最后更新于:2022-04-01 11:46:54
# 建造者模式
处理DOM时,我们常常想要去动态的构建新的元素--这是一个会让我们希望构建的元素最终所包含的标签,属性和参数的复杂性有所增长的过程。
定义复杂的元素时需要特别的小心,特别是如果我们想要在我们元素标签的字面意义上(这可能会乱成一团)拥有足够的灵活性,或者取而代之去获得更多面向对象路线的可读性。我们需要一种为我们构建复杂DOM对象的机制,它独立于为我们提供这种灵活性的对象本身,而这正是建造者模式为我们所提供的。
建造器使得我们仅仅只通过定义对象的类型和内容,就可以去构建复杂的对象,为我们屏蔽了明确创造或者展现对象的过程。
jQuery的美元标记为动态构建新的jQuery(和DOM)对象提供了大量可以让我们这样做的不同的方法,可以通过给一个元素传入完整的标签,也可以是部分标签还有内容,或者使用jQuery来进行构造:
~~~
$( '<div class="foo">bar</div>' );
$( '<p id="test">foo <em>bar</em></p>').appendTo("body");
var newParagraph = $( "<p />" ).text( "Hello world" );
$( "<input />" )
.attr({ "type": "text", "id":"sample"});
.appendTo("#container");
~~~
下面引用自jQuery内部核心的jQuery.protoype方法,它支持从jQuery对象到传入jQuery()选择器的标签的构造。不管是不是使用document.createElement去创建一个新的元素,都会有一个针对这个元素的引用(找到或者被创建)被注入到返回的对象中,因此进一步会有更多的诸如as.attr()的方法在这之后就可以很容易的在其上使用了。
~~~
// HANDLE: $(html) -> $(array)
if ( match[1] ) {
context = context instanceof jQuery ? context[0] : context;
doc = ( context ? context.ownerDocument || context : document );
// If a single string is passed in and it's a single tag
// just do a createElement and skip the rest
ret = rsingleTag.exec( selector );
if ( ret ) {
if ( jQuery.isPlainObject( context ) ) {
selector = [ document.createElement( ret[1] ) ];
jQuery.fn.attr.call( selector, context, true );
} else {
selector = [ doc.createElement( ret[1] ) ];
}
} else {
ret = jQuery.buildFragment( [ match[1] ], [ doc ] );
selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes;
}
return jQuery.merge( this, selector );
~~~
代理模式
最后更新于:2022-04-01 11:46:51
# 代理模式
在我们需要在一个对象后多次进行访问控制访问和上下文,代理模式是非常有用处的。
当实例化一个对象开销很大的时候,它可以帮助我们控制成本,提供更高级的方式去关联和修改对象,就是在上下文中运行一个特别的方法。
在jQuery核心中,一个jQUery.proxy()方法在接受一个函数的输入和返回一个一直具有特殊上下文的新的实体时存在。这确保了它在函数中的值时我们所期待的的值。
一个使用该模式的例子,在点击事件操作时我们利用了定时器。设想我用下面的操作优先于任何添加的定时器:
~~~
$( "button" ).on( "click", function () {
// 在这个函数中,'this'代表了被当前被点击的那个元素对象
$( this ).addClass( "active" );
});
~~~
如果想要在addClass操作之前添加一个延迟,我们可以使用setTiemeout()做到。然而不幸的是这么操作时会有一个小问题:无论这个函数执行了什么在setTimeout()中都会有个一个不同的值在那个函数中。而这个值将会关联window对象替代我们所期望的被触发的对象。
~~~
$( "button" ).on( "click", function () {
setTimeout(function () {
// "this" 无法关联到我们点击的元素
// 而是关联了window对象
$( this ).addClass( "active" );
});
});
~~~
为解决这类问题,我们使用jQuery.proxy()方法来实现一种代理模式。通过调用它在这个函数中,使用这个函数和我们想要分配给它的this,我们将会得到一个包含了我们所期望的上下文中的值。如下所示:
~~~
$( "button" ).on( "click", function () {
setTimeout( $.proxy( function () {
// "this" 现在关联了我们想要的元素
$( this ).addClass( "active" );
}, this), 500);
// 最后的参数'this'代表了我们的dom元素并且传递给了$.proxy()方法
});
~~~
jQuery代理方法的实现如下:
~~~
// Bind a function to a context, optionally partially applying any
// arguments.
proxy: function( fn, context ) {
if ( typeof context === "string" ) {
var tmp = fn[ context ];
context = fn;
fn = tmp;
}
// Quick check to determine if target is callable, in the spec
// this throws a TypeError, but we will just return undefined.
if ( !jQuery.isFunction( fn ) ) {
return undefined;
}
// Simulated bind
var args = slice.call( arguments, 2 ),
proxy = function() {
return fn.apply( context, args.concat( slice.call( arguments ) ) );
};
// Set the guid of unique handler to the same of original handler, so it can be removed
proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;
return proxy;
}
~~~
惰性初始模式
最后更新于:2022-04-01 11:46:49
# 惰性初始模式
延迟初始化 是一种允许我们延迟初始化消耗资源比较大的进程,直到需要他们的时候(才初始化)。这其中的一个例子就是jQuery的.ready()方法,它在DOM节点加载完毕之后会执行一个回调方法。
~~~
$( document ).ready( function () {
//ajax请求不会执行,直到DOM加载完成
var jqxhr = $.ajax({
url: "http://domain.com/api/",
data: "display=latest&order=ascending"
})
.done( function( data ) ){
$(".status").html( "content loaded" );
console.log( "Data output:" + data );
});
});
~~~
jQuery.fn.ready()底层是通过byjQuery.bindReady()来实现的, 如下所示:
~~~
bindReady: function() {
if ( readyList ) {
return;
}
readyList = jQuery.Callbacks( "once memory" );
// Catch cases where $(document).ready() is called after the
// browser event has already occurred.
if ( document.readyState === "complete" ) {
// Handle it asynchronously to allow scripts the opportunity to delay ready
return setTimeout( jQuery.ready, 1 );
}
// Mozilla, Opera and webkit support this event
if ( document.addEventListener ) {
// Use the handy event callback
document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
// A fallback to window.onload, that will always work
window.addEventListener( "load", jQuery.ready, false );
// If IE event model is used
} else if ( document.attachEvent ) {
// ensure firing before onload,
// maybe late but safe also for iframes
document.attachEvent( "onreadystatechange", DOMContentLoaded );
// A fallback to window.onload, that will always work
window.attachEvent( "onload", jQuery.ready );
// If IE and not a frame
// continually check to see if the document is ready
var toplevel = false;
try {
toplevel = window.frameElement == null;
} catch(e) {}
if ( document.documentElement.doScroll && toplevel ) {
doScrollCheck();
}
}
},
~~~
即使不直接在jQuery核心文件中使用,有些开发者通过一些插件也可能熟悉懒加载的概念,延迟加载和揽初始化一样有效,它是一种在需要的时候(比如:当用户浏览到了页面底部的时候)才加载页面数据的技术。最近几年,这种模式已经变得非常显著并且现在可以再Twitter和Facebook的UI里面zhaoda。
迭代器模式
最后更新于:2022-04-01 11:46:47
# 迭代器模式
迭代器模式中,迭代器(允许我们遍历集合中所有元素的对象)顺序迭代一个集合对象中的元素而无需暴漏其底层形式。
迭代器封装了这种特别的迭代操作的内部结构,就jQuery的jQuery.fn.each()迭代器来说,我们实际上可以使用jQuery.each()底层的代码来迭代一个集合,而无需知道或者理解后台提供这种功能的代码是如何实现的。
这种模式可以被理解为门面模式的一种特例,在这里我们只处理与迭代有关的问题。
~~~
$.each( ["john","dave","rick","julian"] , function( index, value ) {
console.log( index + ": "" + value);
});
$( "li" ).each( function ( index ) {
console.log( index + ": " + $( this ).text());
});
~~~
这里我们可以看到jQuery.fn.each()的代码:
~~~
// Execute a callback for every element in the matched set.
each: function( callback, args ) {
return jQuery.each( this, callback, args );
}
~~~
在jQuery.each()方法后面的代码提供了两种迭代对象的方法:
~~~
each: function( object, callback, args ) {
var name, i = 0,
length = object.length,
isObj = length === undefined || jQuery.isFunction( object );
if ( args ) {
if ( isObj ) {
for ( name in object ) {
if ( callback.apply( object[ name ], args ) === false ) {
break;
}
}
} else {
for ( ; i < length; ) {
if ( callback.apply( object[ i++ ], args ) === false ) {
break;
}
}
}
// A special, fast, case for the most common use of each
} else {
if ( isObj ) {
for ( name in object ) {
if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
break;
}
}
} else {
for ( ; i < length; ) {
if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) {
break;
}
}
}
}
return object;
};
~~~
观察者模式
最后更新于:2022-04-01 11:46:45
# 观察者模式
另一个我们之前提到过的模式就是观察者(发布/订阅)模式.这种模式下,系统中的对象可以在关注的事件发生的时候给其他对象发送消息,也可以被其他对象所通知。
jQuery核心库很多年前就已经提供了对于类似于发布/订阅系统的支持,它们称之为定制事件。
jQuery的早期版本中,可以通过使用jQuery.bind()(订阅),jQuery.trigger()(发布),和jQuery.unbind()(取消订阅)来使用这些定制事件,但在近期的版本中,这些都可以通过使用jQuery.on(),jQuery.trigger()和jQuery.off()来完成。
下面我们来看一下实际应用中的一个例子:
~~~
// Equivalent to subscribe(topicName, callback)
$( document ).on( "topicName" , function () {
//..perform some behaviour
});
// Equivalent to publish(topicName)
$( document ).trigger( "topicName" );
// Equivalent to unsubscribe(topicName)
$( document ).off( "topicName" );
~~~
对于jQuery.on()和jQuery.off()的调用最后会经过jQuery的事件系统,与Ajax一样,由于它们的实现代码相对较长,我们只看一下实际上事件处理器是在哪儿以及如何将定制事件加入到系统中的:
~~~
jQuery.event = {
add: function( elem, types, handler, data, selector ) {
var elemData, eventHandle, events,
t, tns, type, namespaces, handleObj,
handleObjIn, quick, handlers, special;
...
// Init the element's event structure and main handler,
//if this is the first
events = elemData.events;
if ( !events ) {
elemData.events = events = {};
}
...
// Handle multiple events separated by a space
// jQuery(...).bind("mouseover mouseout", fn);
types = jQuery.trim( hoverHack(types) ).split( " " );
for ( t = 0; t < types.length; t++ ) {
...
// Init the event handler queue if we're the first
handlers = events[ type ];
if ( !handlers ) {
handlers = events[ type ] = [];
handlers.delegateCount = 0;
// Only use addEventListener/attachEvent if the special
// events handler returns false
if ( !special.setup || special.setup.call( elem, data,
//namespaces, eventHandle ) === false ) {
// Bind the global event handler to the element
if ( elem.addEventListener ) {
elem.addEventListener( type, eventHandle, false );
} else if ( elem.attachEvent ) {
elem.attachEvent( "on" + type, eventHandle );
}
}
}
~~~
对于那些喜欢使用传统的命名方案的人, Ben Alamn对于上面的方法提供了一个简单的包装,然后为我们提供了jQuery.publish(),jQuery.subscribe和jQuery.unscribe方法。我之前在书中提到过,现在我们可以完整的看一下这个包装器。
~~~
(function( $ ) {
var o = $({});
$.subscribe = function() {
o.on.apply(o, arguments);
};
$.unsubscribe = function() {
o.off.apply(o, arguments);
};
$.publish = function() {
o.trigger.apply(o, arguments);
};
}( jQuery ));
~~~
在近期的jQuery版本中,一个多目的的回调对象(jQuery.Callbacks)被提供用来让用户在回调列表的基础上写新的方案。另一个发布/订阅系统就是一个使用这个特性写的方案,它的实现方式如下:
~~~
var topics = {};
jQuery.Topic = function( id ) {
var callbacks,
topic = id && topics[ id ];
if ( !topic ) {
callbacks = jQuery.Callbacks();
topic = {
publish: callbacks.fire,
subscribe: callbacks.add,
unsubscribe: callbacks.remove
};
if ( id ) {
topics[ id ] = topic;
}
}
return topic;
};
~~~
然后可以像下面一样使用:
~~~
// Subscribers
$.Topic( "mailArrived" ).subscribe( fn1 );
$.Topic( "mailArrived" ).subscribe( fn2 );
$.Topic( "mailSent" ).subscribe( fn1 );
// Publisher
$.Topic( "mailArrived" ).publish( "hello world!" );
$.Topic( "mailSent" ).publish( "woo! mail!" );
// Here, "hello world!" gets pushed to fn1 and fn2
// when the "mailArrived" notification is published
// with "woo! mail!" also being pushed to fn1 when
// the "mailSent" notification is published.
// Outputs:
// hello world!
// fn2 says: hello world!
// woo! mail!
~~~
外观模式
最后更新于:2022-04-01 11:46:42
# 外观模式
正如我们早前在书中提过的, 没面模式为一个庞大的(可能更复杂的)代码结构提供了一个更简单的抽象接口。
门面在jQuery库中能够经常见到,它们为开发者处理DOM节点,动画或者令人特别感兴趣的跨域Ajax提供了简单的实现入口。
下面的代码是jQuery $.ajax()方法的门面:
~~~
$.get( url, data, callback, dataType );
$.post( url, data, callback, dataType );
$.getJSON( url, data, callback );
$.getScript( url, callback );
~~~
这些方法背后真正执行的代码是这样的:
~~~
// $.get()
$.ajax({
url: url,
data: data,
dataType: dataType
}).done( callback );
// $.post
$.ajax({
type: "POST",
url: url,
data: data,
dataType: dataType
}).done( callback );
// $.getJSON()
$.ajax({
url: url,
dataType: "json",
data: data,
}).done( callback );
// $.getScript()
$.ajax({
url: url,
dataType: "script",
}).done( callback );
~~~
更有趣的是,上面代码中的门面实际上是它们自身具有的能力,它们隐藏了代码背后很多复杂的操作。
这是因为jQuery.ajax()在jQuery核心代码中的实现是一段不平凡的代码,至少是这样的。至少它规范了XHR(XMLHttpRequest)之间的差异而且让我们能够简单的执行常见的HTTP动作(比如:get、post等),以及处理延迟等等。
由于显示与上面所讲的门面相关的代码将会占据整个章节,这里仅仅给出了jQuery核心代码中规划化XHR的代码:
~~~
// Functions to create xhrs
function createStandardXHR() {
try {
return new window.XMLHttpRequest();
} catch( e ) {}
}
function createActiveXHR() {
try {
return new window.ActiveXObject( "Microsoft.XMLHTTP" );
} catch( e ) {}
}
// Create the request object
jQuery.ajaxSettings.xhr = window.ActiveXObject ?
/* Microsoft failed to properly
* implement the XMLHttpRequest in IE7 (can't request local files),
* so we use the ActiveXObject when it is available
* Additionally XMLHttpRequest can be disabled in IE7/IE8 so
* we need a fallback.
*/
function() {
return !this.isLocal && createStandardXHR() || createActiveXHR();
} :
// For all other browsers, use the standard XMLHttpRequest object
createStandardXHR;
...
~~~
下面的代码也处于实际的jQuery XHR(jqXHR)实现的上层,它是我们实际上经常打交道的方便的门面:
~~~
// Request the remote document
jQuery.ajax({
url: url,
type: type,
dataType: "html",
data: params,
// Complete callback (responseText is used internally)
complete: function( jqXHR, status, responseText ) {
// Store the response as specified by the jqXHR object
responseText = jqXHR.responseText;
// If successful, inject the HTML into all the matched elements
if ( jqXHR.isResolved() ) {
// Get the actual response in case
// a dataFilter is present in ajaxSettings
jqXHR.done(function( r ) {
responseText = r;
});
// See if a selector was specified
self.html( selector ?
// Create a dummy div to hold the results
jQuery("
<div>
")
// inject the contents of the document in, removing the scripts
// to avoid any 'Permission Denied' errors in IE
.append(responseText.replace(rscript, ""))
// Locate the specified elements
.find(selector) :
// If not, just inject the full result
responseText );
}
if ( callback ) {
self.each( callback, [ responseText, status, jqXHR ] );
}
}
});
return this;
}
</div>
~~~
适配器模式
最后更新于:2022-04-01 11:46:40
# 适配器模式
适配器模式 将一个对象或者类的接口翻译成某个指定的系统可以使用的另外一个接口。
适配器基本上允许本来由于接口不兼容而不能一起正常工作的对象或者类能够在一起工作.适配器将对它接口的调用翻译成对原始接口的调用,而实现这样功能的代码通常是最简的。
我们可能已经用过的一个适配器的例子就是jQuery的jQuery.fn.css()方法,这个方法帮助规范了不同浏览器之间样式的应用方式,使我们使用简单的语法,这些语法被适配成为浏览器背后真正支持的语法:
~~~
// Cross browser opacity:
// opacity: 0.9; Chrome 4+, FF2+, Saf3.1+, Opera 9+, IE9, iOS 3.2+, Android 2.1+
// filter: alpha(opacity=90); IE6-IE8
// Setting opacity
$( ".container" ).css( { opacity: .5 } );
// Getting opacity
var currentOpacity = $( ".container" ).css('opacity');
~~~
将上面的代码变得可行的相应的jQuery核心css钩子在下面:
~~~
get: function( elem, computed ) {
// IE uses filters for opacity
return ropacity.test( (
computed && elem.currentStyle ?
elem.currentStyle.filter : elem.style.filter) || "" ) ?
( parseFloat( RegExp.$1 ) / 100 ) + "" :
computed ? "1" : "";
},
set: function( elem, value ) {
var style = elem.style,
currentStyle = elem.currentStyle,
opacity = jQuery.isNumeric( value ) ?
"alpha(opacity=" + value * 100 + ")" : "",
filter = currentStyle && currentStyle.filter || style.filter || "";
// IE has trouble with opacity if it does not have layout
// Force it by setting the zoom level
style.zoom = 1;
// if setting opacity to 1, and no other filters
//exist - attempt to remove filter attribute #6652
if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" ) {
// Setting style.filter to null, "" & " " still leave
// "filter:" in the cssText if "filter:" is present at all,
// clearType is disabled, we want to avoid this style.removeAttribute
// is IE Only, but so apparently is this code path...
style.removeAttribute( "filter" );
// if there there is no filter style applied in a css rule, we are done
if ( currentStyle && !currentStyle.filter ) {
return;
}
}
// otherwise, set new filter values
style.filter = ralpha.test( filter ) ?
filter.replace( ralpha, opacity ) :
filter + " " + opacity;
}
};
~~~
组合模式
最后更新于:2022-04-01 11:46:38
# 组合模式
组合模式 描述了一组对象可像单个对象一样的对待。
这允许我们能统一的处理单个对象或多个对象。这意味着无论是一个对象还是一千个对象我们都能以同样的行为来处理。
在Jquery中,当我们在一个节点或多个节点上应用方法时,我们都能以相同的方式来选择并返回JQuery对象。
下面这个演示我们将使用Jquery的选择器。对单一元素(比如拥有唯一ID的元素)或拥有相同标签或Class的一组元素添加名为active的class,对待它们使用上并无不同:
~~~
// 单一节点
$( "#singleItem" ).addClass( "active" );
$( "#container" ).addClass( "active" );
// 一组节点
$( "div" ).addClass( "active" );
$( ".item" ).addClass( "active" );
$( "input" ).addClass( "active" );
~~~
JQuery的addClass()实现中直接使用原生的for循环、Jquery的JQuery.each()、Jquery.fn.each来迭代一个集合以达到能同时处理一个或一组元素的目的。请看下面的例子:
~~~
addClass: function( value ) {
var classNames, i, l, elem,
setClass, c, cl;
if ( jQuery.isFunction( value ) ) {
return this.each(function( j ) {
jQuery( this ).addClass( value.call(this, j, this.className) );
});
}
if ( value && typeof value === "string" ) {
classNames = value.split( rspace );
for ( i = 0, l = this.length; i < l; i++ ) {
elem = this[ i ];
if ( elem.nodeType === 1 ) {
if ( !elem.className && classNames.length === 1 ) {
elem.className = value;
} else {
setClass = " " + elem.className + " ";
for ( c = 0, cl = classNames.length; c < cl; c++ ) {
if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) {
setClass += classNames[ c ] + " ";
}
}
elem.className = jQuery.trim( setClass );
}
}
}
}
return this;
}
~~~
JQuery 中的设计模式
最后更新于:2022-04-01 11:46:35
# jQuery中的设计模式
jQuery是目前最流行的JavaScript DOM操作库,它提供了一个在安全和跨浏览器的方式下与DOM交互的抽象层。有意思的是,这个库也可以作为一个例子,来展示设计模式如何有效的创建既可读又易用的API。
虽然在很多情况下,撰写jQuery的主要贡献者并没有打算使用特定的模式,但是这些设计模式确实存在,而且对我们学习来说,非常有用。现在让我们看看其中的一些设计模式以及在API中如何使用这些设计模式。
ES Harmony
最后更新于:2022-04-01 11:46:33
# ES Harmony
## 未来的模块
[TC39](http://www.ecma-international.org/memento/TC39.htm),负责讨论ECMAScript语法和语义定义问题和其未来迭代的标准机构,它是由许多的非常聪明的开发者组成的。这些开发者中的一些人(比如Alex Russell)对Javascript在大规模开发中的用例场景在过去几年一直保持者密切的关注,并且敏锐的意识到了人们对于能够使用其编写更加模块化JS的优良的语言特性的需求。 出于这个原因,目前已经有大量激动人心的,包括在客户端和服务器上都能起作用的弹性模块,一个模块加载器以及更多的对语言的改进提议。在这一节中,我们将使用ES.next中为模块提供的语法来探讨代码示例,以使我们能够尝一尝它是什么味道。
> 注意:尽管Harmony仍然处于建设性阶段,我们也已经可以尝试ES.netx的(部分)特性了,而这得感谢Google的Traceur编译器为模块化的Javascript提供的原生支持。为了在短时间内使Traceur启动和运作起来,读一读这份初学指导吧。如果对更深入的了解这个项目感兴趣的话,一个关于它JSConf展示就值得看一看。
## 使用导入和导出的模块
已经通读过AMD和CommonJS章节的话,你也许熟悉模块依赖(导入)和模块导出(或者说是我们允许其它模块使用的公共API/变量)这两个概念。在ES.next中,这些概念已经使用一种同我们预期没太大不同,而开发者将可以在代码示例中往下看到并且能瞬间抓住的用一个export关键字指定依赖的稍微更简洁的方式,被提供了出来。
* import声明绑定了一个以本地变量身份导出的模块,而且可能被重命名以避免名称重复或冲突。
* export声明声明了模块本地绑定的外部可见性,那样其他模块就可能读取到导出但不能修改它们。有趣的是,模块可能导出子模块但不能够导出已经在另外一个地方定义的模块。我们也可以对导出进行重命名以便它们的外部名称同本地名称有所不同。
~~~
module staff{
// specify (public) exports that can be consumed by
// other modules
export var baker = {
bake: function( item ){
console.log( "Woo! I just baked " + item );
}
}
}
module skills{
export var specialty = "baking";
export var experience = "5 years";
}
module cakeFactory{
// specify dependencies
import baker from staff;
// import everything with wildcards
import * from skills;
export var oven = {
makeCupcake: function( toppings ){
baker.bake( "cupcake", toppings );
},
makeMuffin: function( mSize ){
baker.bake( "muffin", size );
}
}
}
~~~
## 从远程来源加载的模块
模块的提案也支持基于远程来源的模块(例如,一个第三方库),这简化了从外部位置载入模块的操作。这里有一个在模块中抽取并使用它的示例:
~~~
module cakeFactory from "http://addyosmani.com/factory/cakes.js";
cakeFactory.oven.makeCupcake( "sprinkles" );
cakeFactory.oven.makeMuffin( "large" );
~~~
## 模块加载API
被提出来的模块加载器描述了一个用于在一个被高度控制的环境中加载模块的动态API。加载器上支持的签名包含load(url, moduleInstance, error)用于加载模块,createModule(object, globalModuleReferences)以及其他的操作。
这里是另外一个我们最初定义的在模块中进行动态加载的示例。注意,并不像我们最近的一个从远程来源拉入一个模块的示例,模块加载器API更加适合于动态环境。
~~~
Loader.load( "http://addyosmani.com/factory/cakes.js" ,
function( cakeFactory ){
cakeFactory.oven.makeCupcake( "chocolate" );
});
~~~
## 针对服务器的CommonJS类似模块
对于那些对服务器环境更加感兴趣的开发者,ES.next提供的模块系统并不仅仅限制只在浏览器中寻找模块。例如在下面,我们能够看到一个CommonJS类似的模块被提供给在服务器上使用。
~~~
// io/File.js
export function open( path ) { ... };
export function close( hnd ) { ... };
~~~
~~~
// compiler/LexicalHandler.js
module file from "io/File";
import { open, close } from file;
export function scan( in ) {
try {
var h = open( in ) ...
}
finally { close( h ) }
}
~~~
~~~
module lexer from "compiler/LexicalHandler";
module stdlib from "@std";
//... scan(cmdline[0]) ...
~~~
## 带有构造器,Get和Set方法的类
类的概念一直都是带有纯粹主义色彩的有争议的问题,而我们目前相对已经回落到关于Javascript原型性质的问题上来,或者通过使用提供在一个表单中使用类定义能力的框架或者抽象,其具有相同原型行为的语法糖。
在Harmony中,为这种语言类已经同构造器和一些(最终)具有某种意义的真正隐晦的东西一起,被提了出来。在下面的示例中,其中的注释提供了用于帮助解释类是如何被构造的问题。
通过阅读,人们也许也会意识到这里“function“世界的缺失。这并不是一个笔误:TC39已经做出有目的的尝试,降低我们在任何事物上对function关键字的滥用,而这其实是希望能够简化我们编写代码的工作。
~~~
class Cake{
// We can define the body of a class" constructor
// function by using the keyword "constructor" followed
// by an argument list of public and private declarations.
constructor( name, toppings, price, cakeSize ){
public name = name;
public cakeSize = cakeSize;
public toppings = toppings;
private price = price;
}
// As a part of ES.next's efforts to decrease the unnecessary
// use of "function" for everything, you'll notice that it's
// dropped for cases such as the following. Here an identifier
// followed by an argument list and a body defines a new method
addTopping( topping ){
public( this ).toppings.push( topping );
}
// Getters can be defined by declaring get before
// an identifier/method name and a curly body.
get allToppings(){
return public( this ).toppings;
}
get qualifiesForDiscount(){
return private( this ).price > 5;
}
// Similar to getters, setters can be defined by using
// the "set" keyword before an identifier
set cakeSize( cSize ){
if( cSize < 0 ){
throw new Error( "Cake must be a valid size -
either small, medium or large" );
}
public( this ).cakeSize = cSize;
}
}
~~~
## ES Harmony 总结
正如我们已经看到的,Harmony带来了一些可以使模块化应用程序的开发工作变得轻松的令人激动附加功能,还为我们处理了诸如依赖管理的问题。
目前,我们在今天的浏览器中使用Harmony语法的最好选择是通过一个转换编译器,比如Google的Traceur或者Esprima。也有诸如Require HM的项目允许使用带有AMD的Harmony模块。在规范定稿以前,我们最好把赌注压在AMD(在浏览器的模块)和CommonJS(对于那些在服务器上的模块)。
## 相关阅读
* [初步了解即将到来的JavaScript模块](http://www.2ality.com/2011/03/first-look-at-upcoming-javascript.html)
* [David Herman关于JavaScript/ES.Next的探讨 (视频)](http://blog.mozilla.com/dherman/2011/02/23/my-js-meetup-talk/)
* [关于 ES Harmony 模块的建议](http://wiki.ecmascript.org/doku.php?id=harmony:modules)
* [ES Harmony 模块语义/结构原理](http://wiki.ecmascript.org/doku.php?id=harmony:modules_rationale)
* [关于 ES Harmony 类的建议](http://wiki.ecmascript.org/doku.php?id=harmony:classes)
## 结论
在这一节中,我们回顾了一些 使用现代化的模块化形式编写模块化JavaScript的选择。
这些形式在利用 模块模式上有众多的优势,包括:避免管理全局变量的必要,更好地支持静态和动态的依赖管理,提高脚本装载机的兼容性,在服务器上以及更多的平台上,能够获得更好的兼容性。
总之,我建议去尝试一下本章所提供的建议,因为这些格式提供了很大的强有力的灵活性,对更好地组织我们的应用程序有着明显的帮助。
CommonJS
最后更新于:2022-04-01 11:46:31
# CommonJS
## 为服务器提供的一种模块形式的优化
CommonJS模块建议指定一个简单的用于声明模块服务器端的API,并且不像AMD那样尝试去广泛的操心诸如io,文件系统,约定以及更多的一揽子问题。
这种形式为CommonJS所建议--它是一个把目标定在设计,原型化和标准化Javascript API的自愿者工作组。迄今为止,他们已经在模块和包方面做出了批复标准的尝试。
## 入门
从架构的角度来看,CommonJS模块是一个可以复用的Javascript块,它出口对任何独立代码都起作用的特定对象。不同于AMD,通常没有针对此模块的功能封装(因此打个比方我们并没有在这里找到定义的相关语句)。
CommonJS模块基本上包括两个基础的部分:一个取名为exports的自由变量,它包含模块希望提供给其他模块的对象,以及模块所需要的可以用来引入和导出其它模块的函数。
理解CommonJS:require()和exports
~~~
// package/lib is a dependency we require
var lib = require( "package/lib" );
// behaviour for our module
function foo(){
lib.log( "hello world!" );
}
// export (expose) foo to other modules
exports.foo = foo;
~~~
exports的基础使用
~~~
// define more behaviour we would like to expose
function foobar(){
this.foo = function(){
console.log( "Hello foo" );
}
this.bar = function(){
console.log( "Hello bar" );
}
}
// expose foobar to other modules
exports.foobar = foobar;
// an application consuming "foobar"
// access the module relative to the path
// where both usage and module files exist
// in the same directory
var foobar = require("./foobar").foobar,
test = new foobar();
// Outputs: "Hello bar"
test.bar();
~~~
等同于AMD的第一个CommonJS示例
~~~
define(function(require){
var lib = require( "package/lib" );
// some behaviour for our module
function foo(){
lib.log( "hello world!" );
}
// export (expose) foo for other modules
return {
foobar: foo
};
});
~~~
这也可以用AMD支持的简化了的CommonJS特定做到。
## 消耗多重依赖
app.js
~~~
var modA = require( "./foo" );
var modB = require( "./bar" );
exports.app = function(){
console.log( "Im an application!" );
}
exports.foo = function(){
return modA.helloWorld();
}
~~~
bar.js
~~~
exports.name = "bar";
~~~
foo.js
~~~
require( "./bar" );
exports.helloWorld = function(){
return "Hello World!!"
}
~~~
加载器和框架对CommonJS提供了什么支持?
在浏览器端:
* [curl.js](http://github.com/unscriptable/curl)
* [SproutCore 1.1](http://sproutcore.com/)
* [PINF]( [http://github.com/pinf/loader-js](http://github.com/pinf/loader-js))
服务器端:
* [Node](http://nodejs.org/)
* [Narwhal](https://github.com/tlrobinson/narwhal)
* [Persevere]( [http://www.persvr.org/](http://www.persvr.org/))
* [Wakanda]( [http://www.wakandasoft.com/](http://www.wakandasoft.com/))
## CommonJS适合浏览器么?
有开发者感觉CommonJS更适合于服务器端的开发,这是如今应该用哪种形式和将要来作为面向未来的备选事实标准,在这一问题上存在一定程度分歧的原因之一。一些争论指摘CommonJS包括许多面向服务器的特性,这些特性很容易可以看出并不能够用Javascript在浏览器级别中实现--例如,io,系统,而且js会被认为是借助于它们功能的性质无法实现的。
那就是说,无论如何了解如何构建CommonJS模块是有用的,那样我们就可以更好的理解它们如何适合于定义可以在任何地方使用的模块了。模块在客户端和服务器端都有包括验证,约定和模板引擎的应用程序。一些开发者趋向于选择那种形式的方式是当一个模块能够在服务器端环境使用时,就选择CommonJS,而如果不是这种场景,就使用AMD。
由于AMD模块具有使用插件的能力,并且能够定义更加精细的像构造器和函数之类的东西,如此是有道理的。
CommonJS模块只能够去定义使用起来会非常繁琐的对象,如果我们尝试从它们那里获取构造器的话。
尽管这超出了本节的讨论范畴,也要注意当论及AMD和CommonJS时,不同类型的“require”方法会被提到。带有类似命名空间的问题理所当然是令人迷惑的,而社区当前对全局的require功能的优点正存在着分歧。这里John Hann的建议是不去叫它“require”,它很可能在告知用户关于全局的和内部的require之间的差别,这一目标上取得失败,将全局加载器方法重新命名为其它什么东西(例如,库的名字)可能更加起作用。正式由于这个原因,像curl.js这样的加载器反对使用require,而使用curl()。
## AMD 与 CommonJS 存在竞争,但都是同样有效的标准
AMD 和 CommonJS 都是有效的模块形式,它们带有不同的最终目标。
AMD采用浏览器先行的方针,它选择了异步的行为方式,并且简化了向后兼容性,但是它并没有任何文件I/O的概念。它支持对象,函数,构造器,字符串,JSON以及许多其它类型的模块,在浏览器进行本地运行。这是令人难以置信的灵活性。
CommonJS 则在另一个方面采用了服务器端先行的方针,承载着同步行为,没有全局的负担并且尝试去迎合(在服务器上的)未来。我们的意思是CommonJS支持无封装的模块,可以感觉到它跟ES.next/Harmony更接近一点,将我们从AMD强制使用的define()封装中解放出来。然而CommonJS仅支持对象作为模块。
## UMD:AMD和兼容CommonJS模块的插件
对于希望创建在浏览器和服务器端环境都能够运作的模块的开发者而言,现有的解决方案感觉可能少了点。为了有助于缓解这个问题,James Burke , 我以及许许多多其他的开发者创造了UMD[(通用模块定义)](https://github.com/umdjs/umd)。
UMD是一种是实验性质的模块形式,允许在编写代码的时候,所有或者大多数流行的实用脚本加载技术对模块的定义在客户端和服务器环境下都能够起作用。另外一种模块格式的想法尽管可能是艰巨的,出于仔细彻底的考虑,我们将简要的概括一下UMD。最开始,我们通过简要的看一看AMD规范中所支持的对于CommonJS的简单封装,来定义UMD。对于希望把模块当做CommonJS模块来编写的开发者,可以应用下面的兼容CommonJS的形式:
基础的AMD混合格式:
~~~
define( function ( require, exports, module ){
var shuffler = require( "lib/shuffle" );
exports.randomize = function( input ){
return shuffler.shuffle( input );
}
});
~~~
然而,注意到如果一个模块并没有包含一个依赖数组,并且定义的函数只包含最少的一个参数,那么它就真的仅仅只是被当做CommonJS模块来对待,这一点是很重要的。这在某些设备(例如PS3)上面也不会正确的工作。如需进一步了解上述的封装,请看看:[](http://requirejs.org/docs/api.html#cjsmodule)[http://requirejs.org/docs/api.html#cjsmodule](http://requirejs.org/docs/api.html#cjsmodule)。
进一步的考虑,我们想要提供许多不同的模式,那不仅仅只是在AMD和CommonJS上起作用,同样也能解决开发者希望使用其它环境开发这样的模块时普遍遇到的问题。
下面我们可以看到这样的变化允许我们使用CommonJS,AMD或者浏览全局的对象创建一个模块。
## 使用 CommonJS,AMD或者浏览器全局对象创建模块
定义一个模块 commonJsStrict,它依赖于另外一个叫做B的模块。模块的名称暗示了文件的名称(,就是说一样的),而让文件名和导出的全局对象的名字一样则是一种最佳实践。
如果模块同时也在浏览器中使用了相同类型的样板,它就会创建一个global.b备用。如果我们不希望对浏览器全局补丁进行支持, 我们可以将root移除,并且把this传递到顶层函数作为其第一个参数。
~~~
(function ( root, factory ) {
if ( typeof exports === 'object' ) {
// CommonJS
factory( exports, require('b') );
} else if ( typeof define === 'function' && define.amd ) {
// AMD. Register as an anonymous module.
define( ['exports', 'b'], factory);
} else {
// Browser globals
factory( (root.commonJsStrict = {}), root.b );
}
}(this, function ( exports, b ) {
//use b in some fashion.
// attach properties to the exports object to define
// the exported module properties.
exports.action = function () {};
}));
~~~
UMD资源库包含了在浏览器中能够最优化运作的涵盖不同的模块,那些对于提供导出非常不错的,那些对于CommonJS的优化还有那些对于定义jQuery插件作用良好的,我们会在接下里看得到。
## 可以在所有环境下面起作用的jQuery插件
UMD提供了两种同jQuery一起工作的模式--一种模式定义了能够同AMD和浏览器全局对象一起工作得很好的插件,而另外一种模式也能够在CommonJS环境中起作用。jQuery并不像是能够运行在大多数CommonJS环境中的,因此除非我们工作在一个能够良好同jQuery一起运作的环境中,那就把这一点牢记于心。
现在我们将定义一个包含一个核心,以及对此核心的一个扩展的插件。核心插件被加载到一个$.core命名空间中,它可以简单的使用借助于命名空间模式的插件扩展进行扩展。通过脚本标签加载的插件会自动填充core下面的一个插件命名空间(比如,$core.plugin.methodName())。
这种模式操作起来相当的棒,因为插件扩展可以访问到底层定义的属性和方法,或者,做一些小小的调整就可以重写行为以便它能够被扩展来做更多的事情。加载器同样也不在需要面面俱到了。
想要了解更多需要做的详细信息,那就请看看下面代码示例中内嵌的注释吧:
usage.html
~~~
<script type="text/javascript" src="jquery-1.7.2.min.js"></script>
<script type="text/javascript" src="pluginCore.js"></script>
<script type="text/javascript" src="pluginExtension.js"></script>
<script type="text/javascript">
$(function(){
// Our plugin "core" is exposed under a core namespace in
// this example, which we first cache
var core = $.core;
// Then use use some of the built-in core functionality to
// highlight all divs in the page yellow
core.highlightAll();
// Access the plugins (extensions) loaded into the "plugin"
// namespace of our core module:
// Set the first div in the page to have a green background.
core.plugin.setGreen( "div:first");
// Here we're making use of the core's "highlight" method
// under the hood from a plugin loaded in after it
// Set the last div to the "errorColor" property defined in
// our core module/plugin. If we review the code further down,
// we can see how easy it is to consume properties and methods
// between the core and other plugins
core.plugin.setRed("div:last");
});
</script>
~~~
pluginCore.js
~~~
// Module/Plugin core
// Note: the wrapper code we see around the module is what enables
// us to support multiple module formats and specifications by
// mapping the arguments defined to what a specific format expects
// to be present. Our actual module functionality is defined lower
// down, where a named module and exports are demonstrated.
//
// Note that dependencies can just as easily be declared if required
// and should work as demonstrated earlier with the AMD module examples.
(function ( name, definition ){
var theModule = definition(),
// this is considered "safe":
hasDefine = typeof define === "function" && define.amd,
// hasDefine = typeof define === "function",
hasExports = typeof module !== "undefined" && module.exports;
if ( hasDefine ){ // AMD Module
define(theModule);
} else if ( hasExports ) { // Node.js Module
module.exports = theModule;
} else { // Assign to common namespaces or simply the global object (window)
( this.jQuery || this.ender || this.$ || this)[name] = theModule;
}
})( "core", function () {
var module = this;
module.plugins = [];
module.highlightColor = "yellow";
module.errorColor = "red";
// define the core module here and return the public API
// This is the highlight method used by the core highlightAll()
// method and all of the plugins highlighting elements different
// colors
module.highlight = function( el,strColor ){
if( this.jQuery ){
jQuery(el).css( "background", strColor );
}
}
return {
highlightAll:function(){
module.highlight("div", module.highlightColor);
}
};
});
~~~
pluginExtension.js
~~~
// Extension to module core
(function ( name, definition ) {
var theModule = definition(),
hasDefine = typeof define === "function",
hasExports = typeof module !== "undefined" && module.exports;
if ( hasDefine ) { // AMD Module
define(theModule);
} else if ( hasExports ) { // Node.js Module
module.exports = theModule;
} else {
// Assign to common namespaces or simply the global object (window)
// account for for flat-file/global module extensions
var obj = null,
namespaces,
scope;
obj = null;
namespaces = name.split(".");
scope = ( this.jQuery || this.ender || this.$ || this );
for ( var i = 0; i < namespaces.length; i++ ) {
var packageName = namespaces[i];
if ( obj && i == namespaces.length - 1 ) {
obj[packageName] = theModule;
} else if ( typeof scope[packageName] === "undefined" ) {
scope[packageName] = {};
}
obj = scope[packageName];
}
}
})( "core.plugin" , function () {
// Define our module here and return the public API.
// This code could be easily adapted with the core to
// allow for methods that overwrite and extend core functionality
// in order to expand the highlight method to do more if we wish.
return {
setGreen: function ( el ) {
highlight(el, "green");
},
setRed: function ( el ) {
highlight(el, errorColor);
}
};
});
~~~
UMD并不企图取代AMD或者CommonJS,而仅仅只是为如今希望让其代码运行在更多的环境下的开发者提供一些补充的援助。想要关于这种实验性质的形式的更多信息或者想要贡献建议,见:[](https://github.com/umdjs/umd)[https://github.com/umdjs/umd](https://github.com/umdjs/umd)。
AMD
最后更新于:2022-04-01 11:46:29
# AMD
## 在浏览器中编写模块化Javascript的格式
AMD (异步模块定义Asynchronous Module Definition)格式的最终目的是提供一个当前开发者能使用的模块化Javascript方案。它出自于Dojo用XHR+eval的实践经验,这种格式的支持者想在以后的项目中避免忍受过去的这些弱点。
AMD模块格式本身是模块定义的一个建议,通过它模块本身和模块之间的引用可以被异步的加载。它有几个明显的优点,包括异步的调用和本身的高扩展性,它实现了解耦,模块在代码中也可通过识别号进行查找。当前许多开发者都喜欢使用它,并且认为它朝ES Harmony提出模块化系统 迈出了坚实的一步。
最开始AMD在CommonJs的列表中是作为模块化格式的一个草案,但是由于它不能达到与模块化完全一致,更进一步的开发被移到了在amdjs组中。
现在,它包含工程Dojo、MooTools、Firebug以及jQuery。尽管有时你会看见CommonJS AMD 格式化术语,但最好的和它相关的是AMD或者是异步模块支持,同样不是所有参与到CommonJS列表的成员都希望与它产生关系。
> 注意:曾有一段时间涉及Transport/C模块的提议规划没有面向已经存在的CommonJS模块,但是对于定义模块来说,它对选择AMD命名空间约定产生了影响。
## 从模块开始
关于AMD值得特别注意的两个概念就是:一个帮助定义模块的define方法和一个处理依赖加载的require方法。define被用来通过下面的方式定义命名的或者未命名的模块:
~~~
define(
module_id /*可选的*/,
[dependencies] /*可选的*/,
definition function /*用来实例化模块或者对象的方法*/
);
~~~
通过代码中的注释我们可以发现,module_id 是可选的,它通常只有在使用非AMD连接工具的时候才是必须的(可能在其它不是特别常见的情况下,它也是有用的)。当不存在module_id参数的时候,我们称这个模块为匿名模块。
当使用匿名模块的时候,模块认定的概念是DRY的,这样使它在避免文件名和代码重复的时候显得很微不足道。因为这样一来代码方便切换,你可以很容易地把它移动到其它地方(或者文件系统的其他位置),而不需要更改代码内容或者它的模块ID。你可以认为模块id跟文件路径的概念是相似的。
注意:开发者们可以将同样的代码放到不同的环境中运行,只要他们使用一个在CommonJS环境下工作的AMD优化器(比如r.js)就可以了。
在回来看define方法签名, dependencies参数代表了我们正在定义的模块需要的dependency数组,第三个参数("definition function" or "factory function") 是用来执行的初始化模块的方法。 一个正常的模块可以像下面那样定义:
Understanding AMD: define()
~~~
// A module_id (myModule) is used here for demonstration purposes only
define( "myModule",
["foo", "bar"],
// module definition function
// dependencies (foo and bar) are mapped to function parameters
function ( foo, bar ) {
// return a value that defines the module export
// (i.e the functionality we want to expose for consumption)
// create your module here
var myModule = {
doStuff:function () {
console.log( "Yay! Stuff" );
}
};
return myModule;
});
// An alternative version could be..
define( "myModule",
["math", "graph"],
function ( math, graph ) {
// Note that this is a slightly different pattern
// With AMD, it's possible to define modules in a few
// different ways due to it's flexibility with
// certain aspects of the syntax
return {
plot: function( x, y ){
return graph.drawPie( math.randomGrid( x, y ) );
}
};
});
~~~
另一方面,require被用来从一个顶级文件或者模块里加载代码,而这是我们原本就希望的动态加载依赖的位置。它的一个用法如下:
理解AMD: require()
~~~
// Consider "foo" and "bar" are two external modules
// In this example, the "exports" from the two modules
// loaded are passed as function arguments to the
// callback (foo and bar) so that they can similarly be accessed
require(["foo", "bar"], function ( foo, bar ) {
// rest of your code here
foo.doSomething();
});
~~~
动态加载依赖
~~~
define(function ( require ) {
var isReady = false, foobar;
// note the inline require within our module definition
require(["foo", "bar"], function ( foo, bar ) {
isReady = true;
foobar = foo() + bar();
});
// we can still return a module
return {
isReady: isReady,
foobar: foobar
};
});
~~~
理解 AMD: 插件
下面是定义一个兼容AMD插件的例子:
~~~
// With AMD, it's possible to load in assets of almost any kind
// including text-files and HTML. This enables us to have template
// dependencies which can be used to skin components either on
// page-load or dynamically.
define( ["./templates", "text!./template.md","css!./template.css" ],
function( templates, template ){
console.log( templates );
// do something with our templates here
}
});
~~~
> 注意:尽管上面的例子中css!被包含在在加载CSS依赖的过程中,要记住,这种方式有一些问题,比如它不完全可能在CSS完全加载的时候建立模块. 取决于我们如何实现创建过程,这也可能导致CSS被作为优化文件中的依赖被包含进来,所以在这些情况下把CSS作为已加载的依赖应该多加小心。如果你对上面的做法感兴趣,我们也可以从这里查看更多@VIISON的RequireJS CSS 插件:[https://github.com/VIISON/RequireCSS](https://github.com/VIISON/RequireCSS)
使用RequireJS加载AMD模块
~~~
require(["app/myModule"],
function( myModule ){
// start the main module which in-turn
// loads other modules
var module = new myModule();
module.doStuff();
});
~~~
这个例子可以简单地看出asrequirejs(“app/myModule”,function(){})已被加载到顶层使用。这就展示了通过AMD的define()函数加载到顶层模块的不同,下面通过一个本地请求allrequire([])示例两种类型的装载机(curl.js和RequireJS)。
使用curl.js加载AMD模块
~~~
curl(["app/myModule.js"],
function( myModule ){
// start the main module which in-turn
// loads other modules
var module = new myModule();
module.doStuff();
});
~~~
延迟依赖模块
~~~
// This could be compatible with jQuery's Deferred implementation,
// futures.js (slightly different syntax) or any one of a number
// of other implementations
define(["lib/Deferred"], function( Deferred ){
var defer = new Deferred();
require(["lib/templates/?index.html","lib/data/?stats"],
function( template, data ){
defer.resolve( { template: template, data:data } );
}
);
return defer.promise();
});
~~~
## 使用Dojo的AMD模块
使用Dojo定义AMD兼容的模块是相当直接的.如上所述,就是在一个数组中定义任何的模块依赖作为第一个参数,并且提供回调函数来执行一次依赖已经被加载进来的模块.例如:
~~~
define(["dijit/Tooltip"], function( Tooltip ){
//Our dijit tooltip is now available for local use
new Tooltip(...);
});
~~~
请注意模块的匿名特性,现在它可以在一个Dojo匿名装载装置中的被处理,RequireJS或者标准的dojo.require()模块装载器。
了解一些有趣的关于模块引用的陷阱是非常有用的.虽然AMD倡导的引用模块的方式宣称它们在一组带有一些匹配参数的依赖列表里面,这在版本更老的Dojo 1.6构建系统中并不被支持--它真的仅仅对AMD兼容的装载器才起作用.例如:
~~~
define(["dojo/cookie", "dijit/Tooltip"], function( cookie, Tooltip ){
var cookieValue = cookie( "cookieName" );
new Tooltip(...);
});
~~~
越过嵌套的命名空间定义方式有许多好处,模块不再需要每一次都直接引用完整的命名空间了--所有我们所需要的是依赖中的"dojo/cookie"路径,它一旦赋给一个作为别名的参数,就可以用变量来引用了.这移除了在我们的应用程序中重复打出"dojo."的必要。
最后需要注意到的难点是,如果我们希望继续使用更老的Dojo构建系统,或者希望将老版本的模块迁移到更新的AMD形式,接下来更详细的版本会使得迁移更加容易.注意dojo和dijit也是作为依赖被引用的:
~~~
define(["dojo", "dijit', "dojo/cookie", "dijit/Tooltip"], function( dojo, dijit ){
var cookieValue = dojo.cookie( "cookieName" );
new dijit.Tooltip(...);
});
~~~
## AMD 模块设计模式 (Dojo)
正如在前面的章节中,设计模式在提高我们的结构化构建的共同开发问题非常有效。 John Hann已经给AMD模块设计模式,涵盖单例,装饰,调解和其他一些优秀的设计模式,如果有机会,我强烈建议参考一下他的 幻灯片。 AMD设计模式的选择可以在下面找到。
一段AMD设计模式可以在下面找到。
修饰设计模式
~~~
// mylib/UpdatableObservable: dojo/store/Observable的一个修饰器
define(["dojo", "dojo/store/Observable"], function ( dojo, Observable ) {
return function UpdatableObservable ( store ) {
var observable = dojo.isFunction( store.notify ) ? store :
new Observable(store);
observable.updated = function( object ) {
dojo.when( object, function ( itemOrArray) {
dojo.forEach( [].concat(itemOrArray), this.notify, this );
});
};
return observable;
};
});
// 修饰器消费者
// mylib/UpdatableObservable的消费者
define(["mylib/UpdatableObservable"], function ( makeUpdatable ) {
var observable,
updatable,
someItem;
// 让observable 储存 updatable
updatable = makeUpdatable( observable ); // `new` 关键字是可选的!
// 如果我们想传递修改过的data,我们要调用.update()
//updatable.updated( updatedItem );
});
~~~
适配器设计模式
~~~
// "mylib/Array" 适配`each`方法来模仿 jQuerys:
define(["dojo/_base/lang", "dojo/_base/array"], function ( lang, array ) {
return lang.delegate( array, {
each: function ( arr, lambda ) {
array.forEach( arr, function ( item, i ) {
lambda.call( item, i, item ); // like jQuery's each
});
}
});
});
// 适配器消费者
// "myapp/my-module":
define(["mylib/Array"], function ( array ) {
array.each( ["uno", "dos", "tres"], function ( i, esp ) {
// here, `this` == item
});
});
~~~
## 使用jQuery的AMD模块
不像Dojo,jQuery真的存在于一个文件中,而是基于插件机制的库,我们可以在下面代码中证明AMD模块是如何直线前进的。
~~~
define(["js/jquery.js","js/jquery.color.js","js/underscore.js"],
function( $, colorPlugin, _ ){
// <span></span>这里,我们通过jQuery中,颜色的插件,并强调没有这些将可在全局范围内访问,但我们可以很容易地在下面引用它们。
// 伪随机一系列的颜色,在改组后的数组中选择的第一个项目
<div>
</div>
var shuffleColor = _.first( _.shuffle( "#666","#333","#111"] ) );
// 在页面上有class为"item" 的元素随机动画改变背景色
$( ".item" ).animate( {"backgroundColor": shuffleColor } );
// 我们的返回可以被其他模块使用
return {};
});
~~~
然而,这个例子中缺失了一些东西,它只是注册的概念。
## 将jQuery当做一个异步兼容的模块注册
jQuery1.7中落实的一个关键特性是支持将jQuery当做一个异步兼容的模块注册。有很多兼容的脚本加载器(包括RequireJS 和 curl)可以使用异步模块形式加载模块,而这意味着在让事物起作用的时候,更少的需要使用取巧的特殊方法。
如果开发者想要使用AMD,并且不想将他们的jQuery的版本泄露到全局空间中,他们就应该在使用了jQuery的顶层模块中调用noConflict方法.另外,由于多个版本的jQuery可能在一个页面上,AMD加载器就必须作出特殊的考虑,以便jQuery只使用那些认识到这些问题的AMD加载器来进行注册,这是使用加载器特殊的define.amd.jQuery来表示的。RequireJS和curl是两个这样做了的加载器。
这个叫做AMD的家伙提供了一种安全的鲁棒的封包,这个封包可以用于绝大多数情况。
~~~
// Account for the existence of more than one global
// instances of jQuery in the document, cater for testing
// .noConflict()
var jQuery = this.jQuery || "jQuery",
$ = this.$ || "$",
originaljQuery = jQuery,
original$ = $;
define(["jquery"] , function ( $ ) {
$( ".items" ).css( "background","green" );
return function () {};
});
~~~
### 为什么AMD是写模块化Javascript代码的好帮手呢?
* 提供了一个清晰的方案,告诉我们如何定义一个可扩展的模块。
* 和我们常用的前面的全局命名空间以及 `<script>` 标签解决方案相比较,非常清晰。有一个清晰的方式用于声明独立的模块,以及它们所依赖的模块。
* 模块定义被封装了,有助于我们避免污染全局命名空间。
* 比其它替代方案能更好的工作(例如CommonJS,后面我们就会看到)。没有跨域问题,局部以及调试问题,不依赖于服务器端工具。大多数AMD加载器支持在浏览器中加载模块,而不需要构建过程。
* 提供一个“透明”的方法用于在单个文件中包含多个模块。其它方式像 CommonJS 要求必须遵循一个传输格式。 再有需要的时候,可以惰性加载脚本。
> 注意:上面的很多说法也可以说做事YUI模块加载策略。
相关阅读
* [The RequireJS Guide To AMD](http://requirejs.org/docs/whyamd.html)
* [What's the fastest way to load AMD modules?](http://unscriptable.com/2011/09/21/what-is-the-fastest-way-to-load-amd-modules/)
* [AMD vs. CommonJS, what's the better format?](http://unscriptable.com/2011/09/30/amd-versus-cjs-whats-the-best-format/)
* [AMD Is Better For The Web Than CommonJS Modules](http://blog.millermedeiros.com/amd-is-better-for-the-web-than-commonjs-modules/)
* [The Future Is Modules Not Frameworks](http://unscriptable.com/code/Modules-Frameworks/#0)
* [AMD No Longer A CommonJS Specification](http://groups.google.com/group/commonjs/browse_thread/thread/96a0963bcb4ca78f/cf73db49ce2)
* [On Inventing JavaScript Module Formats And Script Loaders](http://tagneto.blogspot.com/2011/04/on-inventing-js-module-formats-and.html)
* [The AMD Mailing List](http://groups.google.com/group/amd-implement)
有哪些脚本加载器或者框架支持AMD?
浏览器端:
* [RequireJS](http://requirejs.org/)
* [curl.js](http://github.com/unscriptable/curl)
* [bdLoad](http://bdframework.com/bdLoad)
* [Yabble](http://github.com/jbrantly/yabble)
* [PINF](http://github.com/pinf/loader-js)
(and more)
服务器端:
* [RequireJS](http://requirejs.org/)
* [PINF](http://github.com/pinf/loader-js)
## AMD 总结
在很多项目中使用过AMD,我的结论就是AMD符合了很多条一个构建严肃应用的开发者所想要的一个好的模块的格式要求。不用担心全局,支持命名模块,不需要服务端转换来工作,在依赖管理中也很方便。
同时也是使用Bacbon.js,ember.js 或者其它结构化框架来开发模块时的利器,可以保持项目的组织架构。 在Dojo和CommonJS世界中,AMD已经被讨论了两年了,我们直到它需要时间去逐渐成熟和进化。我们也知道在外面有很多大公司也在实战中使用了AMD用于构建非凡的系统(IBM, BBC iPlayer),如果它不好,那么可能现在它们就已经被丢弃了,但是没有。
但是,AMD依然有很多地方有待改善。使用这些格式一段时间的开发者可能已经感受到了AMD 样板和封装代码很讨厌。尽管我也有这样的忧虑,但是已经存在一些工具例如Volo 可以帮助我们绕过这些问题,同时我也要说整体来看,AMD的优势远远胜过其缺点。
最新的模块化 JavaScript 设计模式
最后更新于:2022-04-01 11:46:26
# 现代模块化的Javascript设计模式
## 对应用程序进行解耦的重要新
在具有可扩展性的Javascript的世界里,当我们说应用程序是模块化的时候,我们的意思常常是它包含着一些高度解耦的各自独立的存储在模块中的功能块。松耦合便于通过移除依赖从而在可能的时候对应用进行维护。当这样的便利性得到了有效实现的时候,就可以相当容易的看到系统的一个部分对其它部分可能产生的影响如何发生改变。
然而不像一些更加普遍的传统的编程语言,JavaScript(ECMA-262)的当前版本并没有使用一种干净,结构化的方式为开发者提供导入此模块的方法。它是直到近几年对于更加结构化的Javascript应用程序的需求变得更加明显,才作为规范需要着重考虑的问题之一。
反过来,现在的开发者只剩下回到带有变异性质的模块或者对象语法模式,这我们已经在本书的前面部分涵盖到了。许多这些用于模块化的脚本使用被描述成为全局对象的命名空间在DOM中串在一起,仍然有可能在我们的架构中产生命名冲突。缺少一些手工的尝试或者第三方插件的帮助,这也不是一种控制依赖管理的干净的方法。
虽然对于这些问题的本地解决方案将会到达ES Harmony(很有可能成为Javascript的下一个版本),好消息是编写模块化的Javascript从来没有变得更加简单,而我们今天开始就可以开始这样做了。
在本节中,我们将看一看编写模块化Javascript的三种形式:**AMD**, **CommonJS**和建议的Javascript的下一个版本,**Harmony**
## 关于脚本加载器的一个需要注意的要点
在没有谈及房间里的大象——脚本加载器之前,要讨论AMD和CommonJS的模块是很困难的。在写这本书的时候,脚本加载意味着一个目标,那个目标就是可以在今天的应用程序中使用的模块化的Javascript——为此,使用与此兼容的脚本加载器,很不幸的说是必需的。为了能尽可能的获取这一节的信息,我建议先对流行的脚本加载工具如何工作有一个基本的理解,以便在本文中对于模块化形式的解释有意思起来。
在AMD和CommonJS形式中有大量用于处理模块加载的加载器,而我个人的选择是RequireJS和curl.js。对于这些工具的完整教程超出了本书的范畴,但是我建议去读John Hann的关于curl.js的文章,和James Burke的RequireJS API文档,以获取更多信息。
对于生产环境而言,使用优化工具(像RequireJS优化器)的来连结脚本,被提倡在这样的模块上工作时用于部署。有趣的是,有Almond AMD垫底,RequireJS并不需要卷入被部署的站点中,而人们可能会考虑的脚本加载器能够简单的在开发工作的外围进行切换。
那就是说,James Burke将可能声称可以在页面加载直到有用武之地后才动态加载脚本,并且RequireJS也能支持这一特性。将这些要点铭记于心了,那就让我们开始吧。
MVVM 模式
最后更新于:2022-04-01 11:46:24
# MVVM
MVVM(Model View ViewModel)是一种基于MVC和MVP的架构模式,它试图将用户界面(UI)从业务逻辑和行为中更加清晰地分离出来。为了这个目的,很多例子使用声明变量绑定来把View层的工作从其他层分离出来。
这促进了UI和开发工作在同一代码库中的同步进行。UI开发者用他们的文档标记(HTML)绑定到ViewModel,在这个地方Model和ViewModel由负责逻辑的开发人员维护。
## 历史
MVVM(如其大名)最初是由微软定义,用于Windows Presentation Foundation(WPF)和Silverlight,在John Grossman2005年的一篇关于Avalon(WPF的代号)的博文中被官方推出。它也作为方便使用MVC的一种可选方案,为Adobe Flex社区积累了一些用户量。
先于微软采用的MVVM名称,在社区中已经有了一场由MVC像MVPM迁移的运动:模型-视图-展现模型。Marton Fowler在2004年为那些对此感兴趣的人写了一篇关于展现模型的文章。展现模型的理念的内容要远远长于这篇文章,然而这篇文章被认为是这一理念的重大突破,并且极大的捧红了它。
在微软推出作为MVPM的可选方案的MVVM后,就出现了许多沸沸扬扬的“alt.net”圈子。其中许多声称这个公司在GUI世界的霸主地位给与了它们将社区统一为整体的机会,出于市场营销的目的,按照它们所高兴的方式对已有的概念重新命名。一个进步的群体也承认MVVM和MVPM其实实在是同样的概念,只是展现出来的是不同的包而已。
在近几年,MVVM已经在Javascript中得到了实现,其构造框架的形式诸如KnockoutJS,Kendo MVVM和Knockback.js,获得了整个社区的积极响应。
现在就让我们来看看组成了MVVM的这三个组件。
## 模型
和其它MV*家族成员一样,MVVM中的模型代表我们的应用用到的领域相关的数据或者信息。一个领域相关的数据的典型例子是用户账号(例如名字,头像,电子邮件)或者音乐唱片(例如唱片名,年代,专辑)。
模型持有信息,但是通常没有操作行为。它们不会格式化信息,也不会影响数据在浏览器中的表现,因为这些不是模型的责任。相反,数据格式化是由视图层处理的,尽管这种行为被认为是业务逻辑,这个逻辑应该被另外一个层封装,这个层和模型交互,这个曾就是视图模型。
这个规则唯一的例外是验证,由模型进行数据验证是被认为可以接受的,这些数据用于定义或者更新现存的模型(例如输入的电子邮件地址是否满足特定的正则表达式要求?)。
在KnockoutJS中,模型遵从上面的定义,但是通常对服务端服务的Ajax调用被做成即可以读取也可以写入模型数据。
如果我们正在构建一个简单的Todo应用,使用KnockoutJS模型来表示一个Todo条目,看起来像下面这个样子:
~~~
var Todo = function ( content, done ) {
this.content = ko.observable(content);
this.done = ko.observable(done);
this.editing = ko.observable(false);
};
~~~
> 注意:在上面小段代码里面,你可能发现了,我们在KnockoutJS的名字空间里面调用observable()方法。在KnockoutJS中,观察者是一类特殊的JavaScript对象,可以将变化通知给订阅者,并且自动检测依赖关系。这个特性使我们在模型值修改之后,可以同步模型和视图模型。
## 视图
使用MVC,视图是应用程序中用户真正与之打交道的唯一一个部分.它们是展现一个视图模型状态的一个可交互UI.此种意义而言,视图是主动的而不是被动的,而这也是真正的MVC和MVP的观点.在MVC,MVP和MVVM中视图也可以是被动的,而这又是什么意思呢?
被动视图仅仅只输出要展示的东西,而不去接受任何用户的输入。
这样一个视图在我们的应用程序中可能也没有真正的模型的概念,而可以被一个代理控制.MVVM的主动视图包含数据绑定,事件和需要能够理解视图模型的行为.尽管这些行为能够被映射到属性,视图仍然处理这来自视图模型的事件。
记住视图在这里并不负责处理状态时很重要的——它使得其与视图模型得以同步。
KnockoutJS视图是简单的一个带有声明链接到视图模型的HTML文档。KnockoutJS视图展示来自视图模型的信息,并且传递命令给他(比如,用户在一个元素上面点击),并且针对视图模型的变化更新状态。而使用来自视图模型的数据来生成标记的模板也能够被用在这个目的上。
未来给出一个简单的初始示例,我们可以看看Javascritpt的MVVM框架KnockoutJS,看它如何允许一个视图模型的定义,还有它在标记中的相关绑定。
视图模型:
~~~
var aViewModel = {
contactName: ko.observable("John")
};
ko.applyBindings(aViewModel);
~~~
视图:
~~~
<p><input id="source" data-bind="value: contactName, valueUpdate: 'keyup'" /></p>
<div data-bind="visible: contactName().length > 10">
You have a really long name!
</div>
<p>Contact name: <strong data-bind="text: contactName"></strong></p>
~~~
我们的text-box输入(源)从contactName获取它的初始值,无论何时contactName发生了改变都会自动更新这个值.由于数据绑定是双向的,像text-box中输入也将据此更新contactName,以此保持值总是同步的。
尽管这个实现特定于KnockoutJS,但是包含着"You have a really long name!"文本的
标签包含有简单的验证(同样是以数据绑定的形式呈现)。如果输入超过10个字符,这个标签就会显示,否则保持隐藏。
让我们看看一个更高级的例子,我们可以看看我们的Todo应用。一个用于这个应用的裁剪后的KnockoutJS的视图,包含有所有必要的数据绑定,这个视图看起来是下面这个样子。
~~~
<div id="todoapp">
<header>
<h1>Todos</h1>
<input id="new-todo" type="text" data-bind="value: current, valueUpdate: 'afterkeydown', enterKey: add"
placeholder="What needs to be done?"/>
</header>
<section id="main" data-bind="block: todos().length">
<input id="toggle-all" type="checkbox" data-bind="checked: allCompleted">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list" data-bind="foreach: todos">
<!-- item -->
<li data-bind="css: { done: done, editing: editing }">
<div class="view" data-bind="event: { dblclick: $root.editItem }">
<input class="toggle" type="checkbox" data-bind="checked: done">
<label data-bind="text: content"></label>
<a class="destroy" href="#" data-bind="click: $root.remove"></a>
</div>
<input class="edit' type="text"
data-bind="value: content, valueUpdate: 'afterkeydown', enterKey: $root.stopEditing, selectAndFocus: editing, event: { blur: $root.stopEditing }"/>
</li>
</ul>
</section>
</div>
~~~
请注意,这个标记的基本布局是相对直观的,包含有一个输入文本框(新的todo)用于增加新条目,用于标记条目完成的开关,以及一个拥有模板的列表(todo列表),这个模板以anli的形式展现Todo条目。
上面标记中绑定的数据可以分成下面几块:
* 输入的文本框new-todo 有一个当前属性的数据绑定,当前要增加的条目的值存储在这里。我们的视图模型(后面就会看到)观察当前属性,并且绑定在添加事件上。当回车键按下的时候,添加事件就被出发了,我们的视图模型就可以处理当前的值按照需要并且将其加入到Todo列表中。
* 输入勾选框可以通过点击标示所有当前条目为完成状态。如果勾选了,触发完成事件,这个事件可以被模型视图观察到。
* 有一类条目是进行中状态。当一个任务被标记为进行中,CSS类也会根据这个状态进行标识。如果双击条目,$root.editItem 回调就会被执行。
* toggle类的勾选框表明当前的进行状态。
* 一个文本标签包含有Todo条目的内容
* 当点击一个移除按钮时可以调用$root.remove 回调函数。
* 编辑模式下的一个输入文本框可以保存Todo条目的内容。回车键事件将会设定编辑属性为真或者假。
## 视图模型
视图模型被认为是一个专门进行数据转换的控制器。它可以把对象信息转换到视图信息,将命令从视图携带到对象。
例如,我们想象我们有一个对象的日期属性是unix格式的(e.g 1333832407),而不是用户视图的所需要的日期格式(e.g 04/07/2012 @ 5:00pm),这时就有必要把unix的日期格式转换为视图需要的格式。我们的对象只简单保存原始的unix数据格式日期,视图模型作为一个中间人角色会格式化原始的unix数据格式转换为视图需要的日期格式。
在这个场景下,视图模型可以被看做一个对象,它处理很多视图显示逻辑。视图模型也对外提供更新视图状态的方法,并通过视图方法和触发事件更新对象。
简单来说,视图模型位于我们UI层后面层。它通过视图发布对象的公共数据,同时它作为视图源提供数据和方法。
KnockoutJS描述视图模型作为数据的表现和操作可以在UI上访问和执行。视图模型并不是一个UI对象,也不是数据持久化对象,而是一个能够为用户提供储存状态及操作的层次对象。Knockout的视图模型实现了JavaScript对象与HTML语言无关性。通过这个实现使开发保持了简单,意味着我们可以在视图层更加简单的管理更多的组合方法。
对于我们的ToDo应用程序的一部分KnockoutJS视图模型可以是像下面这样:
~~~
// our main ViewModel
var ViewModel = function ( todos ) {
var self = this;
// map array of passed in todos to an observableArray of Todo objects
self.todos = ko.observableArray(
ko.utils.arrayMap( todos, function ( todo ) {
return new Todo( todo.content, todo.done );
}));
// store the new todo value being entered
self.current = ko.observable();
// add a new todo, when enter key is pressed
self.add = function ( data, event ) {
var newTodo, current = self.current().trim();
if ( current ) {
newTodo = new Todo( current );
self.todos.push( newTodo );
self.current("");
}
};
// remove a single todo
self.remove = function ( todo ) {
self.todos.remove( todo );
};
// remove all completed todos
self.removeCompleted = function () {
self.todos.remove(function (todo) {
return todo.done();
});
};
// writeable computed observable to handle marking all complete/incomplete
self.allCompleted = ko.computed({
// always return true/false based on the done flag of all todos
read:function () {
return !self.remainingCount();
},
// set all todos to the written value (true/false)
write:function ( newValue ) {
ko.utils.arrayForEach( self.todos(), function ( todo ) {
//set even if value is the same, as subscribers are not notified in that case
todo.done( newValue );
});
}
});
// edit an item
self.editItem = function( item ) {
item.editing( true );
};
..
~~~
上面我们基本上提供了必需的加入、编辑或者移除记录的方法,还有标记所有现存的记录已经被完成的逻辑。注意:唯一真正需要关注的同前面我们的视图模型的示例的不同之处就是观察数组.在KnockoutJS中,如果我们希望监测到并且去回应一个单独的对象发生的改变,我们可以使用观察.然而如果我们希望检测并且去回应一个集合的事物所发生的改变,我们可以换用一个观察数组.如何使用观察数组的一个简单示例就像下面这样:
~~~
// Define an initially an empty array
var myObservableArray = ko.observableArray();
// Add a value to the array and notify our observers
myObservableArray.push( 'A new todo item' );
~~~
> 注意:感兴趣的话,我们在前面所提到的完整的KnockoutJS Todo应用程序可以从 TodoMVC 获取到。
### 扼要重述: 视图和视图模型
视图和视图模型使用数据绑定和事件进行通信。正如我们之前的视图模型例子所见,视图模型不仅仅发布对象属性,它还提供其他的方法和特性,诸如验证。
我们的视图处理自己的用户接口事件,并会把相关事件映射到视图模型。对象和它属性与视图模型是同步的,且通过双向数据绑定进行更新。
触发器(数据触发器)允许我们进一步在视图状态变化后改变我们的对象属性。
### 小结:视图模型和模型
虽然可能会出现在MVVM中视图模型完全对模型负责的情况,这些关系确实有一些值得关注的微妙之处.处于数据绑定的目的,视图模型可以暴露出来一个模型或者模型属性,而且也能够包含获取和操作视图中暴露出来的属性。
#### 优点和缺点
现在,我们完全对MVVM是什么,以及它是如何工作的,有了一个更好的了解.现在就让我们来看看使用这种模式的优点和缺点吧:
优点:
* MVVM更加便于UI和驱动UI的构造块,这两部分的并行开发
* 抽象视图使得背后所需要的业务逻辑(或者粘合剂)的代码数量得以减少
* 视图模型比事件驱动代码更加容易进行单元测试
* 视图模型(比视图更加像是模型)能够在不用担心UI自动化和交互的前提下被测试
缺点:
* 对于更简单的UI而言,MVVM可能矫枉过正了
* 虽然数据绑定可以是声明性质的并且工作得很好,但在我们简单设置断点的地方,它们比当务之急的代码更加难于调试
* 在非凡的应用程序中的数据绑定能够创造许多的账簿.我们也并不希望以绑定比被绑定目标对象更加重量级,这样的境地告终
* 在大型的应用程序中,将视图模型的设计提升到获取足够所需数量的泛化,会变得更加的困难
## MVVM 的低耦合数据绑定
常见到有着MVC或者MVP开发经验的JavaScript程序员评论MVVM的时候在抱怨它会分散他们的关注点。也就是说,他们习惯在一个视图中有相当数量的数据被耦合在了HTML标签中。
我必须承认当我第一次体验实现了MVVM的JavaScript框架后(例如 KnockoutJS, Knockback),我很惊讶很多程序员都想要回到一个难以维护的混淆了逻辑(JavaScript代码)和HTML标签做法的过去。然而现实是使用MVVM会有很多好处(我们之前说过),包括设计师能更容易的通过他们的标记去绑定相关逻辑。
在我们中间的传统程序员,你会很开心知道现在我们能够通过数据绑定这个特性大量减少程序代码的耦合程度,且KnockoutJS从1.3这个版本就开始提供自定义绑定功能。
KnockoutJS 默认有一个数据绑定提供者,这个提供者搜索所有的附属有数据绑定属性的元素,如下面的例子:
~~~
<input id="new-todo" type="text" data-bind="value: current, valueUpdate: 'afterkeydown', enterKey: add" placeholder="What needs to be done?"/>
~~~
当这个提供者定位一个到包含有该属性的元素时,这个工具将会分析该元素,使用当前的数据上下文来将其转化成一个绑定对象。这种方式是 KnockoutJS百分百可以工作的方式,通过这种方式,我们可以采用声明式的方法对元素增加绑定,KnockoutJS之后会在该层上将数据绑定到元素上。
当我们开始构建复杂的视图的时候,我们最终就可能得到大量的元素和属性在标记中绑定数据,这种方式将会变得很难管理。通过自定义的绑定提供者,这就不算个问题。
一个绑定提供者主要关心两件事:
* 给定一个DOM节点,这个节点是否包含任何数据绑定?
* 如果节点回答是YES,那么这个绑定的对象在当前数据上下文中,看起来是什么样的?
绑定提供者实现了两个功能:
* nodeHasBindings:这个有一个DOM的节点参数,这个参数不一定是一个元素
* getBindings:返回一个对象代表当前数据上下文下的要使用的绑定
一个框架绑定提供者看起来如下:
~~~
var ourBindingProvider = {
nodeHasBindings: function( node ) {
// returns true/false
},
getBindings: function( node, bindingContext ) {
// returns a binding object
}
};
~~~
在我们充实这个提供者之前,让我们先简要的讨论一下数据绑定属性中的逻辑。
当使用Knockout的MVVM,我们会对将应用逻辑过度绑定到视图上的这种方法不满。我们可以实现像CSS类一样的东西,将绑定根据名字赋值给元素。Ryan Niemeyer(knockmeout.net上的)之前提出使用数据类用于这个目的,来避免将展示类和数据类混淆,让我们改造我们的nodeHasBindings 函数,来支持这个概念:
~~~
// does an element have any bindings?
function nodeHasBindings( node ) {
return node.getAttribute ? node.getAttribute("data-class") : false;
};
~~~
接下来,我们需要一个敏感的getBindings()函数。既然我们坚持使用CSS类的概念,为什么不考虑一下支持空格分割类呢,这样可以使我们在不同元素之间共享绑定标准。
让我们首先看一下我们的绑定长什么样子。我们建立一个对象用于持有它们,在这些绑定处,我们的属性名需要和我们数据类中使用的关键字相匹配。
> 注意:对于将使用传统数据绑定方式的KnockoutJS应用转化成一个使用自定义绑定提供者的不引人瞩目的绑定方式。我们简单的拉取我们所有的数据绑定属性,使用数据类属性来替换它们,并且像之前做的一样,将我们的绑定放到绑定对象中去。
~~~
var viewModel = new ViewModel( todos || [] ),
bindings = {
newTodo: {
value: viewModel.current,
valueUpdate: "afterkeydown",
enterKey: viewModel.add
},
taskTooltip : {
visible: viewModel.showTooltip
},
checkAllContainer : {
visible: viewModel.todos().length
},
checkAll: {
checked: viewModel.allCompleted
},
todos: {
foreach: viewModel.todos
},
todoListItem: function() {
return {
css: {
editing: this.editing
}
};
},
todoListItemWrapper: function() {
return {
css: {
done: this.done
}
};
},
todoCheckBox: function() {
return {
checked: this.done
};
},
todoContent: function() {
return {
text: this.content,
event: {
dblclick: this.edit
}
};
},
todoDestroy: function() {
return {
click: viewModel.remove
};
},
todoEdit: function() {
return {
value: this.content,
valueUpdate: "afterkeydown",
enterKey: this.stopEditing,
event: {
blur: this.stopEditing
}
};
},
todoCount: {
visible: viewModel.remainingCount
},
remainingCount: {
text: viewModel.remainingCount
},
remainingCountWord: function() {
return {
text: viewModel.getLabel(viewModel.remainingCount)
};
},
todoClear: {
visible: viewModel.completedCount
},
todoClearAll: {
click: viewModel.removeCompleted
},
completedCount: {
text: viewModel.completedCount
},
completedCountWord: function() {
return {
text: viewModel.getLabel(viewModel.completedCount)
};
},
todoInstructions: {
visible: viewModel.todos().length
}
};
....
~~~
上面代码中,我们丢掉了两行,我们仍然需要getBindings函数,这个函数遍历数据类属性中每一个关键字,并从中构建最终对象。如果我们检测到绑定对象是个函数,我们使用当前的数据调用它。我们的完成版自定义绑定提供中,如下:
~~~
// We can now create a bindingProvider that uses
// something different than data-bind attributes
ko.customBindingProvider = function( bindingObject ) {
this.bindingObject = bindingObject;
// determine if an element has any bindings
this.nodeHasBindings = function( node ) {
return node.getAttribute ? node.getAttribute( "data-class" ) : false;
};
};
// return the bindings given a node and the bindingContext
this.getBindings = function( node, bindingContext ) {
var result = {},
classes = node.getAttribute( "data-class" );
if ( classes ) {
classes = classes.split( "" );
//evaluate each class, build a single object to return
for ( var i = 0, j = classes.length; i < j; i++ ) {
var bindingAccessor = this.bindingObject[classes[i]];
if ( bindingAccessor ) {
var binding = typeof bindingAccessor === "function" ? bindingAccessor.call(bindingContext.$data) : bindingAccessor;
ko.utils.extend(result, binding);
}
}
}
return result;
};
};
~~~
我们绑定对象最后的几行,定义如下:
~~~
// set ko's current bindingProvider equal to our new binding provider
ko.bindingProvider.instance = new ko.customBindingProvider( bindings );
// bind a new instance of our ViewModel to the page
ko.applyBindings( viewModel );
})();
~~~
我们在这里所做的是为我们的绑定处理器有效的定义构造器,绑定处理器接受一个我们用来查找绑定的对象(绑定)。然后我们可以使用数据类为我们应用程序视图的重写标记,像下面这样做:
~~~
<div id="create-todo">
<input id="new-todo" data-class="newTodo" placeholder="What needs to be done?" />
<span class="ui-tooltip-top" data-class="taskTooltip" style="display: none;">Press Enter to save this task</span>
</div>
<div id="todos">
<div data-class="checkAllContainer" >
<input id="check-all" class="check" type="checkbox" data-class="checkAll" />
<label for="check-all">Mark all as complete</label>
</div>
<ul id="todo-list" data-class="todos" >
<li data-class="todoListItem" >
<div class="todo" data-class="todoListItemWrapper" >
<div class="display">
<input class="check" type="checkbox" data-class="todoCheckBox" />
<div class="todo-content" data-class="todoContent" style="cursor: pointer;"></div>
<span class="todo-destroy" data-class="todoDestroy"></span>
</div>
<div class="edit'>
<input class="todo-input" data-class="todoEdit'/>
</div>
</div>
</li>
</ul>
</div>
~~~
Nei Kerkin 已经使用上面的方式组合成了一个完整的TodoMVC示例,它可以 从 这里获取到。 虽然上面的解释看起来像是有许多的工作要做,现在我们就有一个一般的getBindingmethod方法要写。比起为了编写我们的KnockoutJS应用程序而严格使用数据绑定,简单的重用和使用数据类更加的琐碎。最终的结果是希望得到一个干净的标记,其中我们的数据绑定会从视图切换到一个绑定对象。
## MVC VS MVP VS MVVM
MVP和MVVM都是MVC的衍生物。它和它的衍生物之间关键的不同之处在于每一层对于其它层的依赖,以及它们相互之间是如何紧密结合在一起的。
在MVC中,视图位于我们架构的顶部,其背后是控制器。模型在控制器后面,而因此我们的视图了解得到我们的控制器,而控制器了解得到模型。这里,我们的视图有对模型的直接访问。然而将整个模型完全暴露给视图可能会有安全和性能损失,这取决于我们应用程序的复杂性。MVVM则尝试去避免这些问题。
在MVP中,控制器的角色被代理器所取代,代理器和视图处于同样的地位,视图和模型的事件都被它侦听着并且接受它的调解。不同于MVVM,没有一个将视图绑定到视图模型的机制,因此我们转而依赖于每一个视图都实现一个允许代理器同视图去交互的接口。
MVVM进一步允许我们创建一个模型的特定视图子集,包含了状态和逻辑信息,避免了将模型完全暴露给视图的必要。不同于MVP的代理器,视图模型并不需要去引用一个视图。视图可以绑定到视图模型的属性上面,视图模型则去将包含在模型中的数据暴露给视图。像我们所提到过的,对视图的抽象意味着其背后的代码需要较少的逻辑。
对此的副作用之一就是视图模型和视图层之间新增的的用于翻译解释的一层会有性能损失。这种解释层的复杂度根据情况也会有所差异——它可能像复制数据一样简单,也可能会像我们希望用视图理解的一种形式去操作它们,那样复杂。由于整个模型是现成可用的,从而这种操作可以被避免掉,所以MVC没有这种问题。
## Backbone.js Vs KnockoutJS
了解MVC,MVP和MVVM之间的细微差别是很重要的,然而基于我们已经了解到的东西,开发者最终会问到是否它们应该考虑使用KnockoutJS而不是Backbone这个问题。下面的一些相关事项对此可能有些帮助:
* 两个库都设计用于不同的目标,它常常不仅仅简单的知识选择MVC或者MVVM的问题。
* 如果数据绑定和双向通信是你主要关注的问题,KnockoutJS绝对是应该选择的方式。实践中任何存储在DOM节点中的值或者属性都能够使用此方法映射到Javascript对象上面。
* Backbone在同RESTful服务的易于整合方面有其过人之处,而KnockoutJS模型就是简单的Javascript对象,而更新模型所需要的代码也必须要由开发者自己来写。
* KnockoutJS专注于自动的UI绑定,如果尝试使用Backbone来做的话则会要求更加细节的自定义代码。由于Backbone自身就意在独立于UI而存在,所以这并不是它的问题。然而Knockback也确实能协助并解决此问题。 使用KnockoutJS,我们能够将我们自己拥有的函数绑定到视图模型观察者上面,任何时候观察者一旦发生了变化,它都会执行。这允许我们能够拥有同在Backbone中发现的一样级别的灵活性。
* Backbone内置有一个坚实的路由解决方案,而KnockoutJS则没有提供路由供我们选择。然而人们如果需要的话,可以很容易的加入这个行为,使用Ben Alman的BBQ插件或者一个像Miller Medeiros优秀的Crossroads就行了。
总结下来,我个人发觉KnockoutJS更适合于小型的应用,而Backbone的特性在任何东西都是无序的场景下面才会是亮点。那就是说,许多开发者两个框架都已经使用过来编写不同复杂度的应用程序,而我建议在一个小范围内两种都尝试一下,在你决定哪一种能更好的为你工作之前。
MVP 模式
最后更新于:2022-04-01 11:46:22
# MVP
模型-视图-展示器(MVP)是MVC设计模式的一个衍生模式,它专注于提升展现逻辑.它来自于上个世纪九十年代早期的一个叫做Taligent的公司,当时他们正工作于一个基于C++ CommonPoint环境的模型.而MVC和MVP的目标都直指对整个多组件关注点的分离,它们之间有一些基础上的不同。
为了要做出总结的目的,我们将专注于最适合于基于Web架构的MVP版本。
## 模型,视图&展示器
MVP中的P代表展示器.它是一个包含视图的用户界面逻辑的组件.不像MVC,来自视图的调用被委派给了展示器,它是从视图中解耦出来的,并且转而通过一个接口来同它进行对话.这允许所有类型的有用的东西,比如在单元测试中模拟视图的调用。
对MVP最通常的实现是使用一个被动视图(Passive View 一种对所有动机和目的保持静默的视图),包含很少甚至与没有任何逻辑.如果MVC和MVP是不同的,那是因为其C和P干了不同的事情.在MVP中,P观察着模型并且当模型发生改变的时候对视图进行更新.P切实的将模型绑定到了视图,这一责任在MVC中被控制器提前持有了。
通过视图发送请求,展示者执行所有和用户请求相关的工作,并且把数据返回给视图。从这个方面来讲,它们获取数据,操作数据,然后决定数据如何在视图上面展示。在一些实现当中,展示者同时和一个服务层交互,用于持久化数据(模型)。模型可以触发事件,但是是由展示者扮演这个角色,用于订阅这些事件,从而来更新视图。在这个被动体系架构下,我们没有直接数据绑定的概念。视图暴露setter ,而展示者使用这些setter 来设置数据。
相较于MVC模式的这个改变所带来的好处是,增强了我们应用的可测试性,并且提供了一个更加干净的视图和模型之间的隔离。但是在这个模式里面伴随着缺乏数据绑定支持的缺陷,这就意味着必须对这个任务做另外的处理。
尽管被动视图实现起来普遍都是为视图和实现一个接口,但在它之上还是有差异的,包括可以更多的把视图从展示器解耦的事件的使用。由于在Javascript中我们并没有接口的构造,我们这里更多的是使用一种约定而不是一个明确的接口。技术上看它仍然是一个接口,而从那个角度对于我们而言把它作为一个接口引用可能更加说得过去一些。
也有一种叫做监督控制器的MVP的变种,它更加接近于MVC和MVVM模式,因为它提供了来自于直接来源于视图的模型的数据绑定。键值观察(KVO)插件(比如Derick Bailey的Backbone.ModelBingding插件)趋向于吧Backbone带出被动视图的范畴,而更多的带入监督控制器和MVVM变异中。
## MVP还是MVC?
MVP一般最常使用在企业级应用程序中,这样的程序中有必要对展现逻辑尽可能的重用。带有非常复杂的逻辑和大量用户交互的应用程序中,我们也许会发现MVC相对来说并不怎么满足需求,因为要解决这个问题可能意味着对多重控制器的重度依赖。在MVP中,所有这些复杂的逻辑能够被封装到一个展示器中,它可以显著的简化维护工作量。
由于MVP的视图是通过一个接口来被定义的,而这个接口在技术上唯一的要点只是系统和视图(展示器除外)之间接触,这一模式也允许开发者不需要等待设计师为应用程序制作出布局和图形,就可以开始编写展现逻辑。
根据其实现,MVP也许MVC更加容易进行自动的单元测试。为此常常被提及的理由是展示器可以被当做用户接口的完全模拟来使用,而因此它能够独立于其它组件接受单元测试。在我的经验中这取决于我们正在实现的MVP所使用的语言(超过一种取代Javascript来实现MVP的可选语言,同Javascript有着相当大的不同,比如说ASP.net)。
在一天的终点,我们对MVC可能会有的底层关注,可能将是保持对MVP的认可,因为它们之间的不同主要是在语义上的。一旦我们对清晰分离的关注被纳入到模型、视图和控制器(或者展示器)中,我们也许会获得大部分同样的好处,而不用去管我们所作出的选择的差异。
## MVC, MVP 和 Backbone.js
很少有,但是如果有任何架构性质的Javascript框架声称用其经典形式实现了MVC或者MVP模式的话,那是因为许多开发者并不认为MVC和MVP是相互冲突的(看到诸如ASP.net或者GWT这样的web框架,我们实际上更加可能会认为MVP被严格的实现了)。这是因为让我们的应用程序有一个附加的展示器/视图逻辑,同时也仍然当其是一种MVC的意味,是有可能的。
Backbone贡献者Irene Ros(位于波士顿的Bocoup)赞同这种想法,当她将视图分离到属于它们自己的单独组件中时,她需要某些东西来实际为她组装它们。这可以是一个控制器路由(比如Backbone.Router,在本书的后面会提到)或者一个对被获取数据做出响应的回调。
这就是说,一些开发者确实感觉Backbone.js更加适合于MVP的描述,相比于MVC。他们的观点是:
* 相比于控制器,MVP中的展示器更好的描述了Backbone.View(视图模板和绑定在视图模板之上的数据之间的中间层)。
* 模型适合Backbone.Model(相较于MVC中的模型并没有很大的不同)。
* 视图最能代表模板(比如 Handlebars/Mustache标记模板)
对此的回应会是视图也可以是一个View(如MVC),因为Backbone对于让它用于多用途有足够的弹性。MVC中的V和MVP中的P都能够通过Backbone.View来完成,因为它们能够达成两个目标:都用来渲染原子组件,还有将那个组件组装起来让其它视图来渲染。
我们也已经看到Backbone中控制器的责任Backbone.View和Backbone.Router都有分享,而在下面的示例中我们能够实际看到那方面实际上都是千真万确的。
在this.model.bind("change",..)一行中,我们的BackbonePhotoView使用了观察者模式来对视图的改变进行“订阅”。它也处理render()方法中的模板,但是并不像一些其它的实现,用户交互也在视图中处理(见events参数)。
~~~
var PhotoView = Backbone.View.extend({
//... is a list tag.
tagName: "li",
// Pass the contents of the photo template through a templating
// function, cache it for a single photo
template: _.template( $("#photo-template").html() ),
// The DOM events specific to an item.
events: {
"click img" : "toggleViewed"
},
// The PhotoView listens for changes to
// its model, re-rendering. Since tHere's
// a one-to-one correspondence between a
// **Photo** and a **PhotoView** in this
// app, we set a direct reference on the model for convenience.
initialize: function() {
this.model.on( "change", this.render, this );
this.model.on( "destroy", this.remove, this );
},
// Re-render the photo entry
render: function() {
$( this.el ).html( this.template(this.model.toJSON() ));
return this;
},
// Toggle the `"viewed"` state of the model.
toggleViewed: function() {
this.model.viewed();
}
});
~~~
另一种(完全不同的)看法是Backbone更加向我们前面考察过的Smalltalk-80MVC靠拢。
定期为Backbone写博客的Derick Bailey之前已经提到过,最终最好不要去强迫Backbone让其适应任何特定的设计模式。设计模式应该考虑指导可能如何被构建的灵活性,而在这一方面,Backbone既不适应MVC,也不适应MVP。相反,它从多个架构模式中借用了一些最好的经验而创造出了一个灵活的框架,并且工作得很好。
而理解这些这些概念源自哪里和为什么源自那里是值得去做的,因此我希望我对于MVC和MVP的阐述对此已经有所帮助。就叫它Backbone方法吧,MV*或带有的其它应用程序架构的意味。大多数结构性Javascript框架自主决定自身采用的经典模式,不管是有意还是无意为之的,最重要的是它们帮助了我们有组织,干净的来开发方便维护的应用程序。
MVC 模式
最后更新于:2022-04-01 11:46:19
# MVC
MVC是一个架构设计模式,它通过分离关注点的方式来支持改进应用组织方式。它促成了业务数据(Models)从用户界面(Views)中分离出来,还有第三个组成部分(Controllers)负责管理传统意义上的业务逻辑和用户输入。该模式最初是由[Trygve Reenskaug](http://en.wikipedia.org/wiki/Trygve_Reenskaug)在研发Smalltalk-80 (1979)期间设计的,当时它起初被称作Model-View-Controller-Editor。在1995年的[“设计模式: 面向对象软件中的可复用元素”](http://www.amazon.co.uk/Design-patterns-elements-reusable-object-oriented/dp/0201633612) (著名的"GoF"的书)中,MVC被进一步深入的描述,该书对MVC的流行使用起到了关键作用。
## Smalltalk-80 MVC
了解一下最初的MVC模式打算解决什么问题是很重要的,因为自从诞生之日起它已经发生了很大的改变。回到70年代,图形用户界面还很稀少,一个被称为分离展示的概念开始被用来清晰的划分下面两种对象:领域对象,它对现实世界里的概念进行建模(比如一张照片,一个人), 还有展示对象,它被渲染到用户屏幕上进行展示。
Smalltalk-80作为MVC的实现,把这一概念进一步发展,产生这样一个观点,即把应用逻辑从用户界面中分离开来。这种想法使得应用的各个部分之间得以解耦,也允许该应用中的其它界面对模型进行复用。关于Smalltalk-80的MVC架构,有几点很有趣,值得注意一下:
* 模型表现了领域特定的数据,并且不用考虑用户界面(视图和控制器).当一个模型有所改变的时候,它会通知它的观察者。
* 视图表现了一个模型的当前状态.观察者模式被用来让视图在任何时候都知晓模型已经被更新了或者被改变了。
* 展现受到视图的照管,但是不仅仅只有一个单独的视图或者控制器——每一个在屏幕上展现的部分或者元素都需要一个视图-控制器对。
* 控制器在这个视图-控制器对中扮演着处理用户交互的角色(比如按键或者点击动作),做出对视图的选择。
开发者有时候会惊奇于他们了解到的观察者模式(如今已经被普遍的作为发布/订阅的变异实现了)已经在几十年以前被作为MVC架构的一部分包含进去了.在Smalltalk-80的 MVC中,视图观察着模型.如上面要点中所提到的,模型在任何时候发生了改变,视图就会做出响应.一个简单的示例就是一个由股票市场数据支撑的应用程序——为了应用程序的实用性,任何对于我们模型中数据的改变都应该导致视图中的结果实时的刷新。
Martin Fowler在过去数年完成了对原生MVC有关问题进行写作的优秀工作,如果对关于Smalltalk-80的MVC的更深入的历史信息感兴趣的话,我建议您读一读他的作品。
## JavaScript 开发者可以使用的 MVC
我们已经回顾了70年代,让我们回到当下回到眼前。现在,MVC模式已经被应用到大范围的编程语言当中,包括与我们关系最近的JavaScript。JavaScript领域现在有一些鼓励支持MVC (或者是它的变种,我们称之为`MV*` 家族)的框架,允许开发者不用付出太多的努力就可以往他们的应用中添加新的结构。
这些框架包括诸如Backbone, Ember.js和AngularJS。考虑到避免出现“意大利面条”式的代码的重要性,该词是指那些由于缺乏结构设计而导致难于阅读和维护的代码,对现代JavaScript开发者来说,了解该模式能够提供什么已经是势在必行。这使得我们可以有效的领会到,这些框架能让我们以不同的方式做哪些事情。
我们知道MVC由三个核心部分组成:
### Models
Models管理一个业务应用的数据。它们既与用户界面无关也与表现层无关,相反的它们代表了一个业务应用所需要的形式唯一的数据。当一个model改变时(比如当它被更新时),它通常会通知它的观察者(比如我们很快会介绍的views)一个改变已经发生了,以便观察者采取相应的反应。
为了更深的理解models,让我们假设我们有一个JavaScript的相册应用。在一个相册中,照片这个概念配得上拥有一个自己的model, 因为它代表了特定领域数据的一个独特类型。这样一个model可以包含一些相关的属性,比如标题,图片来源和额外的元数据。一张特定的照片可以存储到model的一个实例中,而且一个model也可以被复用。下面我们可以看到一个用Backbone实现的被简化的model例子。
~~~
var Photo = Backbone.Model.extend({
// 照片的默认属性
defaults: {
src: "placeholder.jpg",
caption: "A default image",
viewed: false
},
// 确保每一个被创建的照片都有一个`src`.
initialize: function() {
this.set( { "src": this.defaults.src} );
}
});
~~~
不同的框架其内置的模型的能力有所不同,然而他们对于属性验证的支持还是相当普遍的,属性展现了模型的特征,比如一个模型标识符.当在一个真实的世界使用模型的时候,我们一般也希望模型能够持久.持久化允许我们用最近的状态对模型进行编辑和更新,这一状态会存储在内存、用户的本地数据存储区或者一个同步的数据库中。
另外,模型可能也会被多个视图观察着。如果说,我们的照片模型包含了一些元数据,比如它的位置(经纬度),照片中所展现的好友(一个标识符的列表)和一个标签的列表,开发者也许会选择为这三个方面的每一个提供一个单独的视图。
为现代`MVC/MV*`框架提供一种将模型组合到一起的方法(例如,在Backbone中,这些分组作为“集合”被引用)并不常见。管理分组中的模型允许我们基于来自分组中所包含的模型发生改变的通知,来编写应用程序逻辑.这避免了手动设置去观察每一个单独的模型实体的必要。
如下是一个将模型分组成一个简化的Backbone集合的示例:
~~~
var PhotoGallery = Backbone.Collection.extend({
// Reference to this collection's model.
model: Photo,
// Filter down the list of all photos
// that have been viewed
viewed: function() {
return this.filter(function( photo ){
return photo.get( "viewed" );
});
},
// Filter down the list to only photos that
// have not yet been viewed
unviewed: function() {
return this.without.apply( this, this.viewed() );
}
});
~~~
MVC上旧的文本可能也包含了模型管理着应用程序状态的一种概念的引述.Javascript中的应用程序状态有一种不同的意义,通常指的是当前的"状态",即在一个固定点上的用户屏幕上的视图或者子视图(带有特定的数据).状态是一个经常被谈论到的话题,看一看单页面应用程序,其中的状态的概念需要被模拟。
总而言之,模型主要关注的是业务数据。
## 视图
视图是模型的可视化表示,提供了一个当前状态的经过过滤的视图。Smaltalk的视图是关于绘制和操作位图的,而JavaScript的视图是关于构建和操作DOM元素的。
一个视图通常是模型的观察者,当模型改变的时候,视图得到通知,因此使得视图可以更新自身。用设计模式的语言可以称视图为“哑巴”,因为在应用程序中是它们关于模型和控制器的了解是受到限制的。
用户可以和视图进行交互,包括读和编辑模型的能力(例如,获取或者设置模型的属性值)。因为视图是表示层,我们通常以用户友好的方式提供编辑和更新的能力。例如,在之前我们讨论的照片库应用中,模型编辑可以通过“编辑”视图来进行,这个视图里面,用户可以选择一个特定的图片,接着编辑它的元数据。
而实际更新模型的任务落到了控制器上面(我们很快就会讲这个东西)。
让我们使用vanilla JavaScript 实现的例子来更深入的探索一下视图。下面我们可以看到一个函数创建了一个照片视图,使用了模型实例和控制器实例。
我们在视图里定义了一个render()工具,使用一个JavaScript模板引擎来用于渲染照片模型的内容(Underscore的模板),并且更新了我们视图的内容,供照片EI来参考。
照片模型接着将我们的render()函数作为一个其一个订阅者的回调函数,这样通过观察者模式,当模型发生改变的时候,我们就能触发视图的更新。
人们可能会问用户交互如何在这里起作用的。当用户点击视图中的任何元素,不是由视图决定接下来怎么做。而是由控制器为视图做决定。在我们的例子中,通过为photoEI增加一个事件监听器,来达到这个目的,photoEI将会代理处理送往控制器的点击行为,在需要的时候将模型信息和事件一并传递。
这个架构的好处是每个组件在应用工作的时候都扮演着必要的独立的角色。
~~~
var buildPhotoView = function ( photoModel, photoController ) {
var base = document.createElement( "div" ),
photoEl = document.createElement( "div" );
base.appendChild(photoEl);
var render = function () {
// We use a templating library such as Underscore
// templating which generates the HTML for our
// photo entry
photoEl.innerHTML = _.template( "#photoTemplate" , {
src: photoModel.getSrc()
});
};
photoModel.addSubscriber( render );
photoEl.addEventListener( "click", function () {
photoController.handleEvent( "click", photoModel );
});
var show = function () {
photoEl.style.display = "";
};
var hide = function () {
photoEl.style.display = "none";
};
return {
showView: show,
hideView: hide
};
};
~~~
## 模板
在支持`MVC/MV*`的JavaScript框架的下,有必要简略的讨论一下JavaScript的模板以及它们与视图之间的关系,在上一小节,我们已经接触到这种关系了。
历史已经证明在内存中通过字符串拼接来构建大块的HTML标记是一种糟糕的性能实践。开发者这样做,就会深受其害。遍历数据,将其封装成嵌套的div,使用例如document.writeto 这样过时的技术将"模板"注入到DOM中。这样通常意味着校本化的标记将会嵌套在我们标准的标记中,很快就变得很难阅读了,更重要的是,维护这样的代码将是一场灾难,尤其是在构建大型应用的时候。
JavaScript 模板解决方案(例如Handlebars.js 和Mustache)通常用于为视图定义模板作为标记(要么存储在外部,要么存储在脚本标签里面,使用自定义的类型例如text/template),标记中包含有模板变量。变量可以使用变化的语法来分割(例如{{name}}),框架通常也足够只能接受JSON格式的数据(模型可以转化成JSOn格式),这样我们只需要关心如何维护干净的模型和干净的模板。人们遭遇的绝大多数的苦差事都被框架本身所处理了。这样做有大量的好处,尤其选择是将模板存储在外部的时候,这样在构建大型引应用的时候可以是模板按照需要动态加载。
下面我们可以看到两个HTMP模板的例子。一个使用流行的Handlebar.js框架实现,一个使用Underscore模板实现。
### Handlebars.js
~~~
<li class="photo">
<h2>{{caption}}</h2>
<img class="source" src="{{src}}"/>
<div class="meta-data">
{{metadata}}
</div>
</li>
~~~
### Underscore.js Microtemplates
~~~
<li class="photo">
<h2><%= caption %></h2>
<img class="source" src="<%= src %>"/>
<div class="meta-data">
<%= metadata %>
</div>
</li><span style="line-height:1.5;font-family:'sans serif', tahoma, verdana, helvetica;font-size:10pt;"></span>
~~~
请注意模板并不是它们自身的视图,来自于Struts Model 2 架构的开发者可能会感觉模板就是一个视图,但并不是这样的。视图是一个观察着模型的对象,并且让可视的展现保持最新。模板也许是用一种声明的方式指定部分甚至所有的视图对象,因此它可能是从模板定制文档生成的。
在经典的web开发中,在单独的视图之间进行导航需要利用到页面刷新,然而也并不值得这样做。而在单页面Javascript应用程序中,一旦数据通过ajax从服务器端获取到了,并不需要任何这样必要的刷新,就可以简单的在同一个页面渲染出一个新的视图。
这里导航就降级为了“路由”的角色,用来辅助管理应用程序状态(例如,允许用户用书签标记它们已经浏览到的视图)。然而,路由既不是MVC的一部分,也不在每一个类MVC框架中展现出来,在这一节中我将不深入详细的讨论它们。
总而言之,视图是对我们的数据的一种可视化展现。
## 控制器
控制器是模型和视图之间的中介,典型的职责是当用户操作视图的时候同步更新模型。
在我们的照片廊应用程序中,控制器会负责处理用户通过对一个特定照片的视图进行编辑所造成改变,当用户完成编辑后,就更新一个特定的照片模型。
请记住满足了MVC中的一种角色:针对视图的策略模式的基础设施。在策略模式方面,视图在视图的自由载量权方面代表了控制器。因此,那就是测试模式是如何工作的,视图可以代表针对控制器的用户事件,当视图看起来合适的时候。视图也可以代表针对控制器的模型变更事件处理,当视图看起来合适的时候,但这并不是控制器的传统角色。
大多数的Javascript MVC框架都受到了对"MVC"通常认知的影响,而这种认知是和控制器绑定在一起的.出现这种情况的原因各异,但在我的真实想法中,那是由于框架的作者一开始就将从服务器端的角度看待MVC,意识到它并不在客户端进行1:1的翻译,而对MVC中的C进行重新诠释意在他们感觉更加有意义的事情.与此同在的问题在于它是主观的,增加了理解经典MVC模式的复杂度,当然还有控制器在现代框架中的角色。
作为示例,让我们来简要回顾一下当前流行的一种构造框架Backbone.js其架构.Backbone包含了模型和视图(某些东西同我们前面看到的类似),然而它实际上并没有真正的控制器.它的视图和路由行为同控制器有一点点类似,但它们自身实际上都不是控制器。
在这一方面,同官方文档或者博客文章中可能提到的相左,Backbone既不是一个真正的MVC/MVP框架,也不是一个MVVM框架.事实上把它看做是用它自身的方式架构方法的`MV*`家族中的一员,更加合适.当然这没有任何错误的地方,但区分经典MVC和`MV*`是重要的,我们应该依靠前者的经典语法来帮助理解后者。
### Spine.js VS Backbone.js
#### Spine.js
我们现在知道传统的控制器负责当用户更新视图是同步更新模型.值得注意的一个有趣的地方是大多数时下流行的Javascript MVC/MV*框架在编写的时候(Backbone)都没有属于它们自己的明确的控制器的概念。
因此,这对于我们从另一个MVC框架中体会到控制器实现的差异,并更进一步的展现出控制如何扮演着非传统的角色是很有用处的.对于这一点,让我们来看看来自于Spine.js的示例控制器。
在这个示例中,我们会有一个叫做PhotosController的控制器,用来管理应用程序中的个人照片.它将确保当视图更新(例如,一个用户编辑了照片的元数据)时,对应的模型也会更新。
注意:我们并不会花大力气研究Spine.js,而只是对它的控制器能做什么进行一定程度的了解:
~~~
// Controllers in Spine are created by inheriting from Spine.Controller
var PhotosController = Spine.Controller.sub({
init: function () {
this.item.bind( "update" , this.proxy( this.render ));
this.item.bind( "destroy", this.proxy( this.remove ));
},
render: function () {
// Handle templating
this.replace( $( "#photoTemplate" ).tmpl( this.item ) );
return this;
},
remove: function () {
this.el.remove();
this.release();
}
});
~~~
在Spine中,控制器被认为是一个应用程序的粘合剂,对DOM事件进行添加和响应,渲染模板,还有确保视图和模型保持同步(这在我们所知的控制器的上下文中起作用)。
我们在上面的example.js示例中所做的,是使用render()和remove()方法在更新和销毁事件中设置侦听器。当一个照片条目获得更新的时候,我们对视图进行重新渲染,以此反映对元数据的修改。类似的,如果照片从照片集中被删除了,我们也会把它从视图中移除。在render()函数中,我们使用Underscore微模板(通过_.template())来用ID #photoTemplate对一个Javascript模板进行渲染。这样会简单的返回一个编辑了的HTML字符串用来填充photoEL的内容。
这为我们提供了一个非常轻量级的,简单的管理模型和视图之间的变更的方法。
#### Backbone.js
后面的章节我们将会对Backbone和传统MVC之间的区别进行一下重新审视,但现在还是让我们专注于控制器吧。
在Backbone中,控制器的责任一分为二,由Backbone.View和Backbone.Router共享.前段时间Backbone确曾有其属于自己的Backbone.Controller,但是对这一组件的命名对于它所被使用的上下文环境中并没有什么意义,后来它就被重新命名为Router了。
Router比控制器要负担处理着更多一点点的责任,因为它使得为模型绑定事件,以及让我们的视图对DOM事件和渲染产生响应,成为可能.如Tim Branyen(另外一名基于Bocoup的Backbone贡献者)在以前所指出的,为此完全摆脱不使用Backbone.Router是有可能的,因此一种考虑让它使用Router范式的做法可能像下面这样:
~~~
var PhotoRouter = Backbone.Router.extend({
routes: { "photos/:id": "route" },
route: function( id ) {
var item = photoCollection.get( id );
var view = new PhotoView( { model: item } );
$('.content').html( view.render().el );
}
});
~~~
总之,本节的重点是控制器管理着应用程序中模型和视图之间的逻辑和协作。
## MVC给了我们什么?
MVC中关注分离的思想有利于对应用程序中功能进行更加简单的模块化,并且使得:
* 整体的维护更加便利.当需要对应用程序进行更新时,到底这些改变是否是以数据为中心的,意味着对模型的修改还-有可能是控制器,或者仅仅是视觉的,意味着对视图的修改,这一区分是非常清楚的。
* 对模型和视图的解耦意味着为业务逻辑编写单元测试将会是更加直截了当的。
* 对底层模型和控制器的代码解耦(即我们可能会取代使用的)在整个应用程序中被淘汰了。
* 依赖于应用程序的体积和角色的分离,这种模块化允许负责核心逻辑的开发者和工作于用户界面的开发者同时进行工作。
## JavaScript中的Smalltalk-80 MVC
尽管当今主流的JavaScript框架都尝试引入MVC的模式,来更好地面对web应用的开发。由Peter Michaux编写的[Maria.js](https://github.com/petermichaux/maria) ,是一个尝试纯正的Smalltalk-80的框架。其中,Model只是Model,View也只完成View应该做的,controller则只负责控制。然后,一些开发人员认为,MV*架构更值得关注,如果你对纯正的MVC架构的JavaScript实现感兴趣,这将是很好的参考。
## 更加深入的钻研
在这本书的这一点上,我们应该对MVC模式提供了些什么有了一个基础的了解,然而仍然有一些值得去关注的非常美妙的信息。
GoF并不将MVC引述为一种设计模式,而是把它看做是构建一个用户界面的类的集合.按照他们的观点,它实际上是三种经典设计模式的变异组合:观察者模式,策略模式和组件模式.依赖于框架中的MVC如何实现,它也可能会使用工厂和模板模式.GoF Book提到这些模式在使用MVC工作时是非常有用的附加功能。
如我们所讨论的,模型代表应用程序的数据,而视图则是用户在屏幕上看到的被展现出来的东西.如此,MVC它的一些核心的通讯就要依赖于观察者模式(令人惊奇的是,一些相关的内容在许多关于MVC模式的书籍并没有被涵盖到).当模型被改变时,它会通知观察者(视图)一些东西已经被更新了——这也许是MVC中最重要的关系。观察者的这一特性也是实现将多个视图连结到同一个模型的基础。
对于那些对MVC解耦特性想了解更多的开发者(这再一次依赖于特定的实现),这一模式的目标之一就是帮助去实现一个主体(数据对象)和它的观察者之间的一对多关系的定义。当一个主体发生改变的时候,它的观察者也会被更新。视图和控制器有一种稍微不同的关系.控制器协助视图对不同的用户输入做出响应,这也是一个策略模式的例子。
## 总结
回顾完经典的MVC模式以后,我们现在应该理解了它是如何允许我们对一个应用程序中的各个关注点进行清晰地的区分.我们现在也应该感恩于Javascript MVC框架在它们对MVC模式的诠释中是如何的不同,而其对变异也是相当开放的,仍然分享着其原生模式已经提供的其中一些基础概念。
当审视一个新的Javas MVC/MV*框架时,请记住——回过头去考察考察它如何选择相近的架构(特别的,它支持实现了模型,视图,控制器或者其它的一些可选特性)可能会有些用处,因为这样能够更好的帮助我们深入了解这一框架预计需要被如何拿来使用。