[译]使用YUI 3开发Web应用的诀窍
导语:这篇“基于YUI3开发web应用的诀窍”是比较经典的介绍 YUI3 工作机制的文章,文章发布在yuiblog上,总体难度适中,比较适合初学者认识、了解 YUI3。以此纠集了三名应届同学来翻译这篇文章:函谷、郝黎、张勉,并希望能对正在学习YUI3的同学有所帮助和启发。
原文标题:A Recipe for a YUI 3 Application
原文地址:http://www.yuiblog.com/blog/2011/04/01/a-recipe-for-a-yui-3-application/
译文:使用YUI 3开发Web应用的诀窍
我们知道,YUI3的设计始终围绕着“模块”展开。今天我不想过多解释什么是模块,因为Nicholas Zakas在他的文章”构建可伸缩的前端架构“中已经做了详尽说明。在这里我将着重阐述如何构建这些模块。文章中的大部分内容都可以从在线文档查阅,并有其他可代替的方法。当然在线API文档的内容大而全。本文只是介绍其中的精华的部分——基于YUI3开发web应用的诀窍,这里的“诀窍”更针对短小精悍的程序场景,不像Nicholas Zakas所指的架构级场景,毕竟仅凭本文的篇幅无法全部展开讲述YUI3。
模块的定义
首先我们要对模块进行定义,一种常见的方法是将页面可视区域的不同部分做切分,导航、菜单、正文、边栏面板等等。然后查一下YUI是否已经提供了这些模块,比如YUI3就没有提供“菜单”组件,但提供了Node-MenuNav插件,这个插件可以将结构化好的html代码(ul>li)渲染成具有交互行为的菜单。或者你可以直接去YUI Gallery中去找基础组件。不管怎样,你总会找到一种容器或者布局,可以让你往里填充你需要的东西,ok,让我们从这里开始。
我建议将每个模块都封装进一个文件,放在和文件名同名的目录中,比如weather模块应当放在/weather/weather.js中,这样做的原因是,有可能你的组件会依赖一些样式,包括css和img等,将这些样式和资源文件放到和js同一个目录下,YUILoader就会很方便的找到他们。这样,样式文件就可以放在weather/assets/skins/sam/weather.css,同样,其他图片和样式也可以按这种方式放置,当然我们假设你没有使用YUI Builder来打包你的项目,这就有点说来话长了。assets目录和skin目录的含义不言自明,但sam目录就搞不懂啥意思了,其实sam是YUI配置项中skin的默认值,指代YUI内嵌组件的默认样式,sam取名自其设计师Sam Lind。因此你也可以使用你的昵称作为你的组件皮肤名称,当然这需要你在YUI全局配置中传入skin参数,简单起见,我们这里只使用默认皮肤。
模块文件模板
这里是一段最常用的模块定义的代码:
/*jslint devel: true, undef: true, newcap: true, strict: true, maxerr: 50 */ /*global YUI*/ /** * The module-name module creates the blah blah * @module module-name */ YUI.add(‘module-name’, function (Y) { "use strict"; // handy constants and shortcuts used in the module var Lang = Y.Lang, CBX = ‘contentBox’, BBX = ‘boundingBox’, NAME = ‘xxxx’; /** * The Xxxx class does …. * @class Xxxx * @extends Widget * @uses WidgetParent * @constructor * @cfg {object} configuration attributes */ Y.Xxxx = Y.Base.create( NAME, Y.Widget, [Y.WidgetParent], { // Instance members here }, { // Static members here, specially: ATTRS: { } } ); }, ’0.99′, { requires: ['widget','widget-parent'], skinnable: true });
前两行是供JSLint过滤这段代码用的,JSLint是我非常推荐的一个javascript语法检查工具。在JSLint网页版本中,会有一个选项区来设置选项,这排checkbox在页面的底部,如果你使用YUI Builder,JSLint会自动去读取注释中的配置,来依此对你的代码进行语法检查。
接下来的注释是为了生成YUIDoc,这会为你省去不少麻烦,文档有固定格式的模板,你只要通过注释语法填充内容就可以了。当项目越来越大,你就会越来越依赖这种项目文档。
现在开始真正的代码段,YUI.add()语句。我们用YUI.add()来告知Loader模块名称、模块内容、以及其他的一些信息。模块名通常使用小写字母加中横线组成,这些模块名称就是你在YUIAPI文档左侧看到的名字列表。当然你也看到有很多不是小写字母加中横线的命名,毕竟这种命名方法并非强制,你可以根据你的习惯做模块命名。
YUI.add()的第二个参数是一个带入YUI实例的函数,这个实例通常被命名为Y,这个回调中的逻辑即是模块的主体部分,而你需要依赖的模块的功能方法都会被挂载到这个Y上。跳到代码的结尾,你会看到YUI.add()剩下的参数,版本号(也可以不需要)和模块配置项,模块配置项是一个对象,这里代码的含义就是告诉Loader这个模块依赖了widget和widget-parent,并包含皮肤。当然,在这个例子中widget是多余的,因为widget-parent依赖了widget,这时即便你写上widget,Loader也不会重复加载widget。而且你根本不必在意模块的顺序,Loader会计算他们的依赖关系并做好排序。你也会注意到YUI APIdoc中Loader包含一个addModule()方法,YUI.add的功能就和它很类似。
在函数体内,首先是“use strict”的声明,这个声明源自ECMAScript5,这使你的代码对于未来的解释器也具备足够好的适应能力,对于旧的解释器也只相当于定义了一个孤零零的字符串值而已,不妨碍程序运行。“use”声明指定了一个函数作用域,因此将这句声明放在YUI.add()内比放在文件最顶部要更安全一些,因为如果将其放在全局,这回影响后续加载的所有JS逻辑。
然后是定义常量和变量的缩写,JavaScript本身没有常量的概念,这里的“常量”只是一种约定,和在其他编程语言中一样,常量通常使用大写字母和下划线来命名。这样做的好处有两个,特别是字符串常量,一是当你多次写一个字符串的时候,如果字符串拼错则只能在运行时报错,当改为常量形式如果出错在JSLint语法检查时就能发现。二是让YUICompressor工作效率更高,使用常量可以节省不少字节数。通常,我们更多的把配置属性和事件名称定义为常量。
缩写,比如用Lang代替Y.Lang,写的更少,解释器执行更快(直接使用引用而不是逐级查找对象成员)。
在API docs所需的文档注释后是实际的代码,我们需要将自己的模块对象挂载在Y上, 比如 Y.Xxxx。我建议采用Y.Base.create()来创建模块,这样能创建Base的派生类(Widget即派生自Base),Base模板基本能满足大多数的模块功能需求,因此不必在用其他方式来代替Y.Base.create。其中的第一个参数是模块名称,NAME属性是Base的一个描述,通常用驼峰命名法,这个名字会用于事件的前缀(例如”io:success”),抑或是组件容器的class命名前缀(例如:”yui3-xxxx-content”),此外,NAME还是执行“实例.toString()”时的默认返回值,在debugger模式下经常看到很多类似的log。这里我们使用常量NAME来标示类模块的名称。
create()的第二个参数是要派生的父类。比如,你会使用Y.Base作为无UI组件的父类、使用Y.Widget作为带UI组件的父类、Y.Plugin.Base作为插件的父类、或者使用任意派生自Y.Base的子类作为父类,也就是说,任意通过Y.Base.create()创建的类都可以再次通过create()被继承。
第三个参数是一个参元类,摻元类的方法都将挂载到你的类上,常用的参元类包括从Base派生来的ArrayList、从Widget.Attribute派生来的类似Widget-Xxxx的子模块、EventTarget和PluginHost(这两个参元类已经被Base继承),YUI中的继承机制是如此强大,以至于你看Overlay的源码仅仅是一句从Widget的继承。子类可以继承自多个父类,因此第三个参数是一个数组。
最后是实际的代码实现部分。第四个和第五个参数都是对象,它们包含了实例,和类中的静态成员。实例属性通过深拷贝挂载到类的原型(prototy)上,通过this来访问他们,其他静态成员则是可公开访问的。
配置属性
静态成员中最重要的就是ATTRS,其中列出了你的类所需的所有配置参数。例如你需要一个名为value的成员,这个成员用于保存一个数字,默认初始值为0,可以这样传入第五个参数。
ATTRS: { value: { value: 0, validator: Lang.isNumber } }
ATTRS中当然可以写入多个成员,每个成员都下辖各自的配置项,通过阅读addAttr()来了解更多参数配置。我们看到,validator使用了之前代码中定义的缩写Lang,验证函数得到一个实参的输入,返回一个布尔值,所有的Y.Lang.isXxxx都可以作为校验函数,当然,为了更严格细致的校验,你可能需要定义新的validator、setter和getter,这里推荐使用字符串作为句柄赋值给validator、setter或者getter,Attribute会将字符串对应到具体的函数。比如要定义一个validCodes属性,可以接收单个值或者一个数组,但应当统一返回一个数组:
ATTRS: { validCodes: { setter: ‘_setValidCodes’ } }
我们需要在Y.Base.create()的第四个参数中声明_setValidCodes方法,其他的成员也可以在这里追加声明。
_setValidCodes: function (value) { if (!Lang.isArray(value)) { value = [ value ]; } return value; }
除非函数足够简单,最好单独重写setter、setter和validator,传入他们的函数名作为实参,让Attribute将函数名对应到真实的函数。
总之,使用setter作为标准输入,这样会为组件带来一些非常有用的特性。比如配置项值的改变会触发beforeChange和afterChange事件,如果在beforeChange事件中停止事件,则可以阻止属性的更改,因此组件实例生成后,请善用自定义事件。比如你如果绑定了afterChange事件,如果触发了回调,那么一定是值被改变了,而不是即便试图更改属性值却在beforeChange中被阻止了。
validator和setter的校验强度是你来决定的,如果你的validator的校验逻辑很严格,就不用再在setter中添加严格的校验,甚至不需要setter,反之亦然,setter校验很严格的话也可以缺省validator。比如,下面两句代码,你可以任选其一:
validator: Y.Lang.isBoolean, // to make the attribute accepts strictly a Boolean setter: Boolean, // to make the attribute accept any value and have Boolean turn it into one
Setter也可以作为校验器来使用。如果输入的值不能转换成attribute可接受的值,setter就会返回Y.Attribute.INVALID_VALUE。这种情况下,attribute不会发生改变。
当我定义成员的时候,通常会指定这个常量,并放在模块的顶部(和CBX, BBX这些缩写声明一起),例如:
var VALUE = ‘value’, VALID_CODES = ‘validCodes’;
这样做的原因是,在一个模块内可能会多次用到这个值,使用常量更加方便。但是,请千万别在属性声明中使用这些常量。
ATTRS: { // *** Don’t do this *** // VALUE: { value: 0, validator: Lang.isNumber } }
这样书写会生成一个名为VALUE而不是value的属性成员,同时记住,不要覆盖基类中的属性成员,Widget中已经有许多属性成员(见表格),如boundingBox、visible、disabled、height、width等,你也可以修改他们,Y.Base.create()会将父类和基类(Base)中的属性成员合并,所以要重载已有的属性成员,你只要在派生类中重复定义即可。
当你要定义的属性成员是数组或者对象的时候要尤为小心,对象(数组)是一个引用值,如果初始化的属性中含有object,他们(所有实例的这个成员)指向的会是同一个object。当你更改这个object或者删除的时候,所有的实例也都跟着改变(prototye永远指向父类的prototye),所以,Base提供了克隆的机制,在你传入object属性成员的时候克隆一份,这会避免引用重复的问题,其他objects的初始化,请使用valueFn参数,或者直接在类的构造函数(initialize)中构建他们。
其他静态成员
另外,你需要给Y.Base.create()的第五个参数上挂载两个静态成员,如果你开发一个插件(plugin),你需要传入NS(命名空间Namespace的缩写)参数,否则插件就无法工作,NS是一个字符串,作为在宿主对象中的一个属性,插件将挂载其上,不过你要特别小心,选择NS命名的时候不要覆盖已有的插件命名。
如果你开发一个需要渐进增强的组件(widget),你需要传入HTML_PARSER参数,HTML_PARSER是一个object,其成员可以是从配置参数传入的已有的HTML片段获取,也可以来自于一个(CSS3)选择器或者一个getter函数。请参考Widget使用指南中的Progressive Enhancement来了解更多。
当然,你有时会提供一些类静态成员(属性/方法),并暴露给开发者去调用,而类中的常量由于闭包的保护对外并不可见,如果你想对外暴露他们,则需要通过Y.Base.create()的第五个参数传入一个boject,object的成员都会被当作静态成员挂载在类本身上。例如WidgetStdMod中的HEADER,BODY和FOOTER常量就是用这种方法声明的静态常量,不过你需要通过Y.WidgetStdMod.BODY来访问。
实例成员
Y.Base.create()的第四个参数(object)会mix到类原型上,通常我们先声明属性后声明方法,没有原因一定要这样,只是惯例,声明的顺序也不影响程序的执行。这样约定是为了更工整的组织你的代码,尽管你在类构造器中也可以创建成员,但还是强烈建议先声明后赋值,声明的位置用来为APIdoc补充注释,这样的coding方式大大增强源码的可读性。
私有属性通常带有下划线前缀, 类的公共接口最好通过Attribute传入,而不是直接挂载实例属性,直接挂载属性很不安全,毕竟它不像通过Attribute可以带入校验器、适配参数类型(通过setter函数),并且可以支持AOP的一些特性诸如触发valuechange事件等,你会发现通过Attribute传参的好处不胜枚举。
在使用Attribute的时候,注意不要将成员初值设为对象或数组,因为它们都会指向同一个object而使程序出错,最好将那些object成员初值设为null,也不要将属性留空,如果实在不知道成员的数据类型,统统设置为null,因为在调试程序的时候,引用那些没有初值的成员会报一个“类型错误”。
Base中的实例方法
你可能会注意到,在子类中并没有定义类构造器,因为在构建实例的时候,Base会通过调用initializer方法(如果存在的话)对实例进行初始化,Initializer使用的参数即是实例的构造参数,所以可以把initializer看做构造器,所有派生自Base的类在创建之时先要被Base“扩充”(Argument)一下,Base这里被称之为“摻元类”,参元类在调用initializer之前通过Attribute来传入参数,对于设置了HTML_PARSER的Widget来说, Attribute中的属性是从HTML“标签”中获取,而不是直接从参数中读的。
Initialize方法干了这么几件事情,首先是将所有需要初始化的属性设置为object或array,然后注册(publish)自定义事件,虽然EventTarget可以直接在没有注册事件的情况下fire事件,但我还是建议先注册事件类型(为了事件可以冒泡),同样,类似先声明后赋值的原则,你可以在注册事件的地方补充APIDoc注释,乍一看似乎在函数体内写APIDoc注释很奇怪,但你也找不到更好的写APIDoc注释的方式。
接下来就是处理Initializer函数接受的参数,你也可以在参数中增加配置属性中没有的内容,比如我们将on,after, buggleTargets和plugins等Base原本没有的属性(或方法)挂到Base上(参见Base),也可以给Widget父类挂载新的子属性,尽管你在实例化一个对象的时候只传入一个参数,但这个参数所包含的内容不止与此。
Javascript没有析构函数的概念。Base允许你声明一个名为destructor的函数来释放内存资源,依此来模拟析构函数。但这只是一个折衷方案,在删除一个对象时,javascript编译器不会自动回收其内存,因此对于没用的对象是需要手动销毁的,即调用一下析构函数。
开发者在使用你的类的时候,不需要直接调用initializer和destructor,Base会在必要的时候调自动用他们。Initializer会在对象实例化时调用,destuctor会在用户调用destroy方法销毁对象时调用。
绑定的事件没有被解绑常常会造成内存泄漏。Widget会自动解除组件内部绑定的事件,其他的手动绑定的事件不会被自动解除。Base也不能解除所有事件监听,我通常使用这段代码来解除事件绑定,先在声明私有成员_eventHandlers:
_eventHandles: null,
然后在initializer方法内将_eventhandlers设为数组
initializer: function (cfg) { this._eventHandles = []; // … … },
同样在initializer 函数(或者在Widget的bindUI函数)中,通过这段代码来绑定监听:
this._eventHandles.push(this.after(‘someAttributeChange’, this._afterSomeAttributeChange, this));
在destructor方法内:
destructor: function () { Y.each(this._eventHandles, function (handle) { handle.detach(); }); },
这里在initializer中,有可能绑定了自定义事件(除了UI元素上的事件),就像上文提到的,Attribute是处理参数的标准方式。任何非数据的存储都由事件处理逻辑完成。在上面的例子中,我使用_afterSomeAttributeChange来监听someAttribut的属性变化。事件发生会触发回调,并带入即事件对象(我通常以ev表示),事件对象包含几个与事件相关的属性,其中一个属性newVal表示变化后的值。
Widget的实例属性
Widget有两个重要属性BOUNDING_TEMPLATE 和 CONTENT_TEMPLATE。初始值都是”<div></div>”,这为多数widget提供一个标准html结构,包含双层嵌套的div。但并不是所有的widget都适用,比如按钮使用<span> <a></a></span> 而不是<div><div></div></div>结构,其实Widget不强制要求使用一个大容器(contentBox),这两个属性可以使用任意html标签,比如我用a来描述按钮
BOUNDING_TEMPLATE: ‘<a>’, CONTENT_TEMPLATE: null,</a>
在你不需要contentBox的时候就将其设置为null,这时contentBox的配置会继承自boundingBox。不要将widget的所有HTML都放在这两个属性中,这两个属性中的元素要尽量简单,在renderUI中使用代码创建其余的html标签。Widget会给所有你指定的标签加上id和class,形式诸如yui3-xxxx, yui3-xxxx-visible 或 yui3-xxxx-disabled,其中xxxx是小写的NAME属性。
Widget实例方法
Widget的初始化有多个步骤,除了会在对象实例化时调用initializer(构造函数)外,在销毁时也会调用destructor函数(析构函数),这两个方法都继承自Base。Widget又增加了renderUI, bindUI, syncUI三个函数,在widget的render方法被调用时,这三个函数会依次执行。
renderUI负责生成widget最基本的HTML骨架,包括boundingBox和contentBox,如果考虑到渐进增强, renderUI会先检查元素是否已存在于DOM中,这是因为如果设置HTML_PARSER属性,配置属性中定义的元素会被事先生成好。如果元素没有事先生成,我们才需要通过renderUI来创建。
最简单的生成HTML的方法(假设没有使用渐进增强)就是使用Y.Node.create方法:
renderUI: function () { var cbx = this.get(CBX); cbx.append(Y.Node.create(Y.substitute(Y.Xxxx.TEMPLATE, CLASS_NAMES))); },
这段代码的运行需要特定的上下文,首先,要声明常量CBX(见本篇文章第一个代码块),其次,widget所需的模块都加载完全,加载widget之前会自动加载其依赖的Node, Y.substitute是可选模块,如果需要则要把’substitute’ 加入到requires中。此外还要有一个作为widget内容模板的名为TEMPLATE的静态变量,你可以和其它静态成员一起定义(通过ATTRS定义)它。最后常量CLASS_NAMES应当也已经事先定义好了。
我通常在模块开头,和BBX,CBX一起定义CLASS_NAMES(见本文第一个代码块):
var BBX = ‘boundingBox’, CBX = ‘contentBox’, NAME = ‘button’, // other constants and shortcuts …. YCM = Y.ClassNameManager.getClassName, getClassName = function () { var args = Y.Array(arguments); args.unshift(NAME); return YCM.apply(this, args).toLowerCase(); }, LABEL = ‘label’, PRESSED = ‘pressed’, ICON = ‘icon’, CLASS_NAMES = { pressed: getClassName(PRESSED), icon: getClassName(ICON), label: getClassName(LABEL), noLabel: getClassName(‘no’, LABEL) };
CLASS_NAME为一个object常量,其中的属性值由ClassNameManager(包含在Widget中)生成。在以上代码中,我先声明一个指向Y.ClassNameManager.getClassName的缩写YCM,然后声明一个只能在模块内部访问的私有函数getClassName,这个函数的功能与Widget中的同名函数getClassName的功能很类似,只是widget中的getClassName是个静态函数,可以用来生成静态变量。CLASS_NAMES中表示样式名的属性值就是用它生成的。现在,我就可以这样定义TEMPLATE中的样式名了:
TEMPLATE: ‘<label>’,</label>
这还不够,我还想将其它的值比如配置属性中的值加入模板中。可以这样做:
this.get(CBX).append(Y.Node.create(Y.substitute(TEMPLATE , CLASS_NAMES, Y.bind(function (key, suggested, arg) { return (key === ‘_’?this.get(arg):suggested); },this))));
我给Y.substitute增加了第三个参数,一个函数。通常,Y.substitute中的占位符是由一对花括号包围的字符串,如果其中有空格,占位符就会被一分为二。空格前面的部分为key,空格后的部分为可选参数。当Y.substitute的第三个参数是一个函数时,这个特性十分有用。比如在此例中,第一个参数是key,第二个参数是用于替换的内容,此例中是CLASS_NAME,第三个是可选参数。所以在上面的语句之后,我可以这样使用模板:
TEMPLATE: ‘<label for="”{_" />’,
Y.substitute在执行时先碰到{label},开始在CLASS_NAME中搜索,并找到对应值‘yui3-button-label’。然后以‘label’, ‘yui3-button-label’ 和 undefined为参数调用替换函数。因为key不等于’_’,所以函数返回第二个参数中值,即原始的样式名。当遇到{_ id}时,因为在CLASS_NAMES中没有’_’属性,所以会以传入参数‘_’, undefined 和 ‘id’调用替换函数,函数会从配置属性中读取’id’的值。在遇到占位符{_ value}时函数将执行同样的操作。
所有在模块的代码顶部声明的常量在模块外都是不可见的,但你可能想使一些常量可见,比如CLASS_NAMES。此时,你只需在Y.Base.create的最后一个参数中,即声明静态成员的区域声明:
CLASS_NAMES: CLASS_NAMES
这样这一对象及其包含的所有属性都可通过Y.MyWidget.CLASS_NAMES来访问。
我建议尽可能将widget的HTML字符串都调整好再执行Y.Node.create。因为在Javascript中,字符串操作的速度比DOM操作快很多。在Y.Node.create调用前做的越多,代码执行的速度就越快。
所有widget还都会用到的方法就是bindUI。你可以在这个函数创建元素绑定事件,例如你可以给上面TEMPLATE中的<input>绑定valueChange事件,使textbox中值和配置属性中的对应的值始终保持一致。代码和用户都有可能去修改input中的值。如果是来自内部代码的更新值,textbox要刷新以显示新值,如果改变来自用户输入则不用刷新。如不这样,就会陷入死循环:textbox的value属性发生改变,触发change事件,响应函数又改变textbox的值,这一改变又触发了change事件……让我们看看如何处理这一情况。我们在input上绑定自定义的valueChange事件,当然要先在模块的requires中加入event-valuechange模块。
this._eventHandles.push(this._inputEl.after(‘valueChange’, this._afterInputChange, this));
我们假设_inputEl对象指向textbox。事件响应函数如下:
_afterInputChange: function (ev) { this.set(VALUE, ev.target.get(VALUE),{source:UI}); },
这里假设我们事先已定义了表示value和ui的常量VALUE和UI。我们简单的将配置属性中的值设置为input中的值,我还给函数加了第三个参数{source:UI}。set函数可以接受第三个参数,这个参数是一个object对象,它的属性可以加入attribute change事件的事件对象(event facade)中,由此就可以区别textbox的valueChange事件是来自内部代码还是用户输入。在bindUI中,我们可能已经这样设置了事件监听:
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。