使用YUI 3开发Web应用的诀窍
this._eventHandles.push(this.after(‘valueChange’,this._afterValueChange));
这是一个监听value变化的事件监听,上一个例子也是对<input>的value的变化作监听。两个事件名称都一样,实际上,它们都是对一个叫做value的值的变化进行监听,但实际却不一样。通常,对属性变化的监听会放在initializer里,而此例涉及改变UI元素,所以把它放在bindUI中,也提醒我们这个事件响应涉及textbox。事件响应函数如下:
_afterValueChange: function (ev) { if (ev.source === UI) { return; } this._inputEl.set(VALUE, ev.newVal); },
首先我们检查事件对象的source属性。如果事件来自UI,我们直接忽略。在这里,属性名UI和它的值都是任意的,你可以根据自己喜好定义。我在设定value的属性值时定义了UI和它的值,所以在这里我就可以访问UI这一属性,你也可以用其它的键值对。实际上,widget也提供了一个相同功能的常数Y.Widget.UI_SRC,只是名字有点长,所以我宁愿自己定义。
一个小技巧:你可以使用_set代替set来改变只读属性的值。_set方法本来是作为受保护方法,只能在类及其子类中访问的,但是javascript中对象成员都是公有的,所以_set实际上是个公共方法,外部也能访问。即使这样,我们还是会给只读属性声明readOnly:true,并且在API文档里也将这一属性标为只读。
最后一个实例方法是syncUI。前两个方法renderUI和bindUI会且仅会执行一次,但syncUI则至少被widget自身调用一次,你也可以在后面的程序中多次调用这个方法。syncUI的作用是根据对象的状态刷新其外观。对象的状态可能一直在变化, 界面也会跟着变化。不过,如何编写这个方法不能一概而论,要根据具体情况。对于简单的用户界面,syncUI可以在每次有变化发生时都重绘界面中的全部内容。而对于复杂的用户界面,重绘整个界面费时且会造成图像闪烁,所以你最好只重绘发生变化的部分,这样的话,你就需要将重绘不同部分的代码分别放在不同的函数中,syncUI会将每一部分只调用一次。还有,在先前的renderUI的例子中,我改变了textbox的值,而只有在syncUI执行之后,这一变化才能在屏幕上显示出来。
更常见的使用方法是给每个UI元素设置单独的重绘函数。这个函数会在初始化时被syncUI调用一次,之后会在配置属性的发生变化后,通过事件响应函数调用。例如
_valueUIRefresh: function (value) { this._inputEl.set(VALUE, value); }
这一函数和其它相似功能的重绘函数会在syncUI中被调用:
syncUI: function () { this._valueUIRefresh(this.get(VALUE)); // other such refreshers },
在after事件响应函数中的代码如下:
_afterValueChange: function (ev) { if (ev.source === UI) { return; } this._valueUIRefresh(ev.newVal); },
与其他模块的通信
在实现一个模块之后,它会和其他模块进行交互。传统的方法是紧耦合(通过Nicholas Zakas的视频中我们可以了解什么是紧耦合什么是松耦合),也就是通过方法调用和属性赋值来将这些模块紧密联系在一起,在这儿就不赘述了。下面我们介绍另一种方法――自定义事件,Y.Base里面包含了你所需要的全部方法。
首先,在initializer中,你要发布这个自定义事件,让大家都知道它:
initializer: function (cfg) { this.publish(‘eventName’, { /*… options … */}); },
需要注意的是,事件名称最好是一个常见单词,因为在后面你会经常使用它,常见单词可以避免出现拼写错误。然后,假设你拥有一个对象,例如:
var myWidget = new Y.MyWidget({ /* .. attributes … */ });
此时,你可以为它绑定事件:
myWidget.after(‘eventName’, this._eventNameListener, this);
然而,这样做虽然不像直接的方法调用那样联系紧密,但是由于必须有一个myWidget的实例,所以其实质还是紧耦合。也就是说,在两个模块的通信中,一个模块必须知道另一个的细节,或者存在一个监视模块为它们建立连接。在这个过程中,有两个配置项是非常重要的,broadcast和emitFacade。
第一个,broadcast,可以让你在其他的模块中为这个事件设置监听器。broadcast默认值为0,此时只能用前面所示的那个方法。如果希望事件可以在任何地方被监听,你需要改变broadcast的值。如果只是在沙箱内,broadcast值为1,如果需要在各个沙箱间,则broadcast值为2。一个沙箱如下所示:
YUI().use( ‘module1′, …, ‘moduleN’, function (Y) { // this is your sandbox });
在页面中可以有多个这样的沙箱:
YUI().use( ‘module1′, …, ‘moduleN’, function (Y) { // this is your sandbox }); YUI().use( ‘moduleX-1′, …, ‘moduleX-N’, function (Z) { // this is another sandbox });
如果你设置broadcast值为2,你就可以在沙箱2中监听在沙箱1中发布的事件,具体细节请看Event user guide。我们继续讨论简单沙箱的情况。
要在一个沙箱内监听另一个模块中发布的事件,必须知道的是这个模块的静态属性NAME的值和事件名称。回想下,Y.Base.create方法所带的第一个参数的值,就是NAME属性的值。因此,如果你创建了这样一个模块:
Y.MyWidget = Y.Base.create( ‘xxxx’, Y.Widget, // … and so on
然后在initializer发布了一个’help’事件:
initializer: function (config) { this.publish(‘help’, { broadcast: 1, emitFacade: true }); },
那么,要在其他模块的沙箱内监听这个事件,就可以这样做:
Y.after(‘xxxx:help’, function (ev) { … }, this);
在这里调用了Y.after,而不是myWidget.after,所以我不再需要一个实例才能触发这个事件。你也可以用同样的方法来监听DOM事件或者其他的自定义事件,比如’valueChange’等,不同的仅仅是引号前面的前缀。你也可以使用别的东西作为前缀,Y.base会接受这个值,但是通常情况下,Y.base提供的默认值已经足够了。你还需要设置emitFacade值,因为需要一个对象来触发事件,从而为ev.target提供门面值(facade value)。也许你会想,如果监听器所在的模块获得了注册事件模块的对象,那不是重新成为紧耦合了么。但事实并非如此,只要你在监听器模块中不保留这个对象,耦合就不复存在。此外,我们还有更好的办法。
在发布事件时,我们可以添加所有在事件对象中监听器所需要的信息,例如:
this.fire(‘help’, {helpTopic: ‘Event Broadcasting’});
fire()方法的两个参数分别为发布事件的名称(也就是Y.Base为它增加前缀的类的名称)和包含一些特性的对象(这些特性需要复制给事件对象)。这样监听器就不需要为了获取一些信息而遍历注册事件模块,从而达到了松耦合的目的。监听器通过“事件广播”知道有这样的一些模块,甚至可能有很多这样的模块会响应help事件,但并不需要关注是哪一个模块正在响应它。这种方法也简化了日后新模块的添加。
事件和默认行为
要改变一个类的行为,通常的办法是建立一个子类,然后重写它的方法。YUI也可以完成这些工作。你可以用Y.Base.create来创建一个基类Y.Widget,然后用Y.Base.create来创建一个新的类来作为基类的扩展,并给予其一些特殊的行为。例如,先创建基类:
Y.MySimpleWidget = Y.Base.create( ‘simpleWidget’, Y.Widget, [], { // instance members here, amongst them: renderUI: function () { this.get(CBX).append(Y.Node.create(‘ … whatever goes into the widget … ‘ )); } }, { ATTRS: { // configuration attributes } // other static members } );
然后创建子类:
Y.MyFancyWidget = Y.Base.create( ‘fancyWidget’, Y.MySimpleWidget, [], { renderUI: function () { Y.MyFancyWidget.superclass.renderUI.apply(this, arguments); this.get(CBX).append(Y.Node.create(‘ … add some bells and whistles … ‘ )); } // Presumably the fancy version does not need any further static members so I skip the last argument );
我们可以看到,MyFancyWidget通过添加一些细节改进了MySimpleWidget。但是这在某些情况下会有些问题,所以你需要设计一个更灵活,更容易改变的基类。自定义事件会对你有所帮助。
假设有个排序类,拥有key和direction两个参数,声明如下:
sort: function (key, direction) { // sorting happens here },
如果这个函数的行为在某些情况下需要更改,您可以这么做,在initializer方法中添加自定义事件:
initializer: function (config) { // amongst many other things: this.publish(SORT, {defaultFn: this._defSortFn}); },
若SORT是一个具有排序功能的实例,你可以这样声明它的sort函数:
sort: function(key, direction) { this.fire(SORT, {key:key, direction:direction}); },
这样子,排序函数就转换为一个具有相同参数的事件触发函数。这样只是提供了一个转换接口,你仍然需要通过原始的排序函数来设计一个类:
_defSortFn: function (ev) { var key = ev.key, direction = ev.direction; // same code as the original sort function },
这个_defSortFn类的函数体与原始的方法一模一样,达到相同的排序目的。但是你可以从事件对象中知道key和direction参数,只要一段简单的代码段就可以设置一个监听器,并且改变排序方法。
myObjectThatSorts.on(‘sort’, function (ev) { var key = ev.key, direction = ev.direction; ev.preventDefault(); // now do your own sort });
通过preventDefault我让myObjectThatSorts不再执行_defSortFn中的排序方法,从而可以根据我需要的结果做任何事,无论原来的排序是什么样。我甚至不用关心它是否停止,只要监听after事件来翻转UI上用来显示排序方向的箭头。
我也可以改变事件对象。当事件触发时,我们得到的是根据事件对象复制的一个对象,它从before监听器开始传播,通过默认的函数,到达after监听器,然后被丢弃。你可以在过程中改变它的一些属性的值。当然,在默认方法执行后再做任何改变是没有影响的,在执行之前改变事件对象才能对方法有所影响。例如。
myObjectThatSorts.on(‘sort’, function (ev) { ev.direction = (ev.direction===’desc’?'asc’:'desc’); });
这样将得到倒置的排序结果。
YUI_config
在页面中调用一个模块的最简单的方法是通过scirpt标签来引用脚本,或者是将它放在script标签的url指向的combo文件中(combo可以通过手动连接或者支持combo的服务器自动完成)。而将自定义模块集成到YUI Loader中是一个更好的办法,可以极大的改进性能。这种方法很重要的一点是在使用YUI.add()插入模块时引入依赖文件(通过requires的最后一个参数),这样在调用use()时就可以按照正确的顺序调用它们。
对于小的web应用,你可以在页面load时加载所有内容,但是对于大型应用,这样是很不合理的,因为会花费太长的时间。你可以使用很多次use()方法去请求各个模块所需要的文件,然而这种让Loader在模块加载时去查找本模块的依赖文件是非常耗时的,它可能需要建立很多个请求,直到获得它需要的文件为止。相反,你可以预先告诉Loader各个模块和它的依赖文件,这样,当遇到这个模块时,加载器就可以并行的对它们进行处理。
为此,你需要为YUI Lodaer添加模块说明和要求来建立模块依赖关系,最简单的方法就是建立一个包含这些信息的yui_config.js文件(可以改为其他名字),如下所示:
YUI_config = { filter:’raw’, //combine:false, gallery: ‘gallery-2011.02.18-23-10′, groups: { js: { base: ‘build/’, modules: { ‘myWidget’: { path: ‘myWidget/myWidget.js’, requires: ['widget', 'widget-parent', 'widget-child', 'widget-stdmod', 'transition'], skinnable: true }, // other modules here } } // other groups here } };
将这段代码放在常规的<script>标签内,并放在第一个YUI().use()代码段之前。它用来配置(YUI)库加载前需要的一些全局属性。,就像以前你必须放在YUI().use()的第一个参数一样,现在YUI可以代替你做这些。你可以使用这儿所列的任何配置项。
filter:“min”:产品代码(去除注释后的最简版本);
debug:bebug版本(带有一些log语句,包含console组件);
raw:非最简版本(不带log,含有注释);其中后两者通常只应用于开发环境中。
combine:这个配置项仅仅应用在combo后一些难解决的bug的查找时。
gallery:如果你使用了gallery模块,填上它的版本号。
group:这个属性用来描述你的模块。
首先是组的名称,这里叫‘js’,也可以是其他名称。 你可以为放置在相同位置的一系列文件创建一个这样的组,每个组的第一个参数用来声明文件的根路径(可以是相对路径或者绝对路径)。除base之外,还有一些组的基本属性,具体请参照这儿。
最后是modules属性,需要在这列举这个组的所有模块。调用每个模块的关键词是模块的名称,也就是你在YUI().add()和YUI().use()的第一个参数。path是模块相关文件的位置,可以是在base基础上的相对路径,如果文件放在其他地方,也可以用全路径。其他的属性可以在这儿找到,和放在YUI.add()结尾的一样。requires属性里面可以是YUI modules, gallery modules或者你自己创建的modules,无论是这个组的还是其他组的。此外,如果设置skinnable:true的话,皮肤会被自动加载,就像我在文章开头所讲的那样。
为了简化这些工作,我创建了一个Windows脚本文件,可以为我自动配置YUI_config。它会扫描并阅读每一个模块文件,并提取出每一个YUI.add中的参数。对于我来说,它很有用,但并不一定适合你。
结语
YUI3非常灵活,你可以通过很多方法建立你的模块。比如通过Y.Base来派生,其实我也不是经常这么做,只是偶尔会用到,但在复杂系统中,依然非常推荐使用Y.Base。
建议继续学习:
- JQuery,Extjs,YUI,Prototype,Dojo 等JS框架的区别和应用场景简述 (阅读:3067)
- CSS实现HTML元素透明的那些事 (阅读:2843)
- YUI 还是 jQuery? (阅读:2744)
- 动态加载JavaScript的小实践 (阅读:2249)
- The Deferred Evaluation of YUI 3 (阅读:1999)
- 使用minify合并YUI请求 (阅读:1972)
- YUI3设计中的激进和妥协 (阅读:1651)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:拔赤 来源: Taobao UED Team
- 标签: YUI
- 发布时间:2011-06-02 22:46:38