[译]使用YUI 3开发Web应用的诀窍

标签: 前端技术 | 发表时间:2011-04-14 22:25 | 作者:拔赤 小伟
出处:http://ued.taobao.com/blog

导语:这篇“基于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中,这样做的原因是,有可能你的组件会依赖一些样式,包括cssimg等,将这些样式和资源文件放到和js同一个目录下,YUILoader就会很方便的找到他们。这样,样式文件就可以放在weather/assets/skins/sam/weather.css,同样,其他图片和样式也可以按这种方式放置,当然我们假设你没有使用YUI Builder来打包你的项目,这就有点说来话长了。assets目录和skin目录的含义不言自明,但sam目录就搞不懂啥意思了,其实samYUI配置项中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 BuilderJSLint会自动去读取注释中的配置,来依此对你的代码进行语法检查。

接下来的注释是为了生成YUIDoc,这会为你省去不少麻烦,文档有固定格式的模板,你只要通过注释语法填充内容就可以了。当项目越来越大,你就会越来越依赖这种项目文档。

现在开始真正的代码段,YUI.add()语句。我们用YUI.add()来告知Loader模块名称、模块内容、以及其他的一些信息。模块名通常使用小写字母加中横线组成,这些模块名称就是你在YUIAPI文档左侧看到的名字列表。当然你也看到有很多不是小写字母加中横线的命名,毕竟这种命名方法并非强制,你可以根据你的习惯做模块命名。

YUI.add()的第二个参数是一个带入YUI实例的函数,这个实例通常被命名为Y,这个回调中的逻辑即是模块的主体部分,而你需要依赖的模块的功能方法都会被挂载到这个Y上。跳到代码的结尾,你会看到YUI.add()剩下的参数,版本号(也可以不需要)和模块配置项,模块配置项是一个对象,这里代码的含义就是告诉Loader这个模块依赖了widgetwidget-parent,并包含皮肤。当然,在这个例子中widget是多余的,因为widget-parent依赖了widget,这时即便你写上widgetLoader也不会重复加载widget。而且你根本不必在意模块的顺序,Loader会计算他们的依赖关系并做好排序。你也会注意到YUI APIdocLoader包含一个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的子模块、EventTargetPluginHost(这两个参元类已经被Base继承),YUI中的继承机制是如此强大,以至于你看Overlay的源码仅仅是一句从Widget的继承。子类可以继承自多个父类,因此第三个参数是一个数组。

最后是实际的代码实现部分。第四个和第五个参数都是对象,它们包含了实例,和类中的静态成员。实例属性通过深拷贝挂载到类的原型(prototy)上,通过this来访问他们,其他静态成员则是可公开访问的。

配置属性

静态成员中最重要的就是ATTRS,其中列出了你的类所需的所有配置参数。例如你需要一个名为value的成员,这个成员用于保存一个数字,默认初始值为0,可以这样传入第五个参数。

ATTRS: {
    value: {
        value: 0,
        validator: Lang.isNumber
    }
}

ATTRS中当然可以写入多个成员,每个成员都下辖各自的配置项,通过阅读addAttr()来了解更多参数配置。我们看到,validator使用了之前代码中定义的缩写Lang,验证函数得到一个实参的输入,返回一个布尔值,所有的Y.Lang.isXxxx都可以作为校验函数,当然,为了更严格细致的校验,你可能需要定义新的validatorsettergetter,这里推荐使用字符串作为句柄赋值给validatorsetter或者getterAttribute会将字符串对应到具体的函数。比如要定义一个validCodes属性,可以接收单个值或者一个数组,但应当统一返回一个数组:

ATTRS: {
    validCodes: {
        setter: ‘_setValidCodes’
    }
}

我们需要在Y.Base.create()的第四个参数中声明_setValidCodes方法,其他的成员也可以在这里追加声明。

_setValidCodes: function (value) {
    if (!Lang.isArray(value)) {
        value = [ value ];
    }
    return value;
}

除非函数足够简单,最好单独重写settersettervalidator,传入他们的函数名作为实参,让Attribute将函数名对应到真实的函数。

总之,使用setter作为标准输入,这样会为组件带来一些非常有用的特性。比如配置项值的改变会触发beforeChangeafterChange事件,如果在beforeChange事件中停止事件,则可以阻止属性的更改,因此组件实例生成后,请善用自定义事件。比如你如果绑定了afterChange事件,如果触发了回调,那么一定是值被改变了,而不是即便试图更改属性值却在beforeChange中被阻止了。

validatorsetter的校验强度是你来决定的,如果你的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不会发生改变。

当我定义成员的时候,通常会指定这个常量,并放在模块的顶部(和CBXBBX这些缩写声明一起),例如:

var VALUE = ‘value’,
    VALID_CODES = ‘validCodes’;

这样做的原因是,在一个模块内可能会多次用到这个值,使用常量更加方便。但是,请千万别在属性声明中使用这些常量。

ATTRS: {
    // *** Don’t do this *** //
    VALUE: {
        value: 0,
        validator: Lang.isNumber
    }
}

这样书写会生成一个名为VALUE而不是value的属性成员,同时记住,不要覆盖基类中的属性成员,Widget中已经有许多属性成员(见表格),如boundingBoxvisibledisabledheightwidth等,你也可以修改他们,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()的第五个参数传入一个bojectobject的成员都会被当作静态成员挂载在类本身上。例如WidgetStdMod中的HEADERBODYFOOTER常量就是用这种方法声明的静态常量,不过你需要通过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_PARSERWidget来说, Attribute中的属性是从HTML“标签”中获取,而不是直接从参数中读的。

Initialize方法干了这么几件事情,首先是将所有需要初始化的属性设置为objectarray,然后注册(publish)自定义事件,虽然EventTarget可以直接在没有注册事件的情况下fire事件,但我还是建议先注册事件类型(为了事件可以冒泡),同样,类似先声明后赋值的原则,你可以在注册事件的地方补充APIDoc注释,乍一看似乎在函数体内写APIDoc注释很奇怪,但你也找不到更好的写APIDoc注释的方式。

接下来就是处理Initializer函数接受的参数,你也可以在参数中增加配置属性中没有的内容,比如我们将on,after, buggleTargetspluginsBase原本没有的属性(或方法)挂到Base上(参见Base),也可以给Widget父类挂载新的子属性,尽管你在实例化一个对象的时候只传入一个参数,但这个参数所包含的内容不止与此。

Javascript没有析构函数的概念。Base允许你声明一个名为destructor的函数来释放内存资源,依此来模拟析构函数。但这只是一个折衷方案,在删除一个对象时,javascript编译器不会自动回收其内存,因此对于没用的对象是需要手动销毁的,即调用一下析构函数。

开发者在使用你的类的时候,不需要直接调用initializerdestructorBase会在必要的时候调自动用他们。Initializer会在对象实例化时调用,destuctor会在用户调用destroy方法销毁对象时调用。

绑定的事件没有被解绑常常会造成内存泄漏。Widget会自动解除组件内部绑定的事件,其他的手动绑定的事件不会被自动解除。Base也不能解除所有事件监听,我通常使用这段代码来解除事件绑定,先在声明私有成员_eventHandlers:

_eventHandles: null,

然后在initializer方法内将_eventhandlers设为数组

initializer: function (cfg) {
    this._eventHandles = [];
    // … …
},

同样在initializer 函数(或者在WidgetbindUI函数)中,通过这段代码来绑定监听:

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会给所有你指定的标签加上idclass,形式诸如yui3-xxxx, yui3-xxxx-visible yui3-xxxx-disabled,其中xxxx是小写的NAME属性。

Widget实例方法

Widget的初始化有多个步骤,除了会在对象实例化时调用initializer(构造函数)外,在销毁时也会调用destructor函数(析构函数),这两个方法都继承自BaseWidget又增加了renderUIbindUIsyncUI三个函数,在widgetrender方法被调用时,这三个函数会依次执行。

renderUI负责生成widget最基本的HTML骨架,包括boundingBoxcontentBox,如果考虑到渐进增强, 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之前会自动加载其依赖的NodeY.substitute是可选模块,如果需要则要把’substitute’ 加入到requires中。此外还要有一个作为widget内容模板的名为TEMPLATE的静态变量,你可以和其它静态成员一起定义(通过ATTRS定义)它。最后常量CLASS_NAMES应当也已经事先定义好了。

我通常在模块开头,和BBXCBX一起定义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来访问。

我建议尽可能将widgetHTML字符串都调整好再执行Y.Node.create。因为在Javascript中,字符串操作的速度比DOM操作快很多。在Y.Node.create调用前做的越多,代码执行的速度就越快。

所有widget还都会用到的方法就是bindUI。你可以在这个函数创建元素绑定事件,例如你可以给上面TEMPLATE中的<input>绑定valueChange事件,使textbox中值和配置属性中的对应的值始终保持一致。代码和用户都有可能去修改input中的值。如果是来自内部代码的更新值,textbox要刷新以显示新值,如果改变来自用户输入则不用刷新。如不这样,就会陷入死循环:textboxvalue属性发生改变,触发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});
},

这里假设我们事先已定义了表示valueui的常量VALUEUI。我们简单的将配置属性中的值设置为input中的值,我还给函数加了第三个参数{source:UI}set函数可以接受第三个参数,这个参数是一个object对象,它的属性可以加入attribute change事件的事件对象(event facade)中,由此就可以区别textboxvalueChange事件是来自内部代码还是用户输入。在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。前两个方法renderUIbindUI会且仅会执行一次,但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。但是这在某些情况下会有些问题,所以你需要设计一个更灵活,更容易改变的基类。自定义事件会对你有所帮助。

假设有个排序类,拥有keydirection两个参数,声明如下:

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类的函数体与原始的方法一模一样,达到相同的排序目的。但是你可以从事件对象中知道keydirection参数,只要一段简单的代码段就可以设置一个监听器,并且改变排序方法。

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”:产品代码(去除注释后的最简版本);

debugbebug版本(带有一些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。

相关 [yui 开发 web] 推荐:

[译]使用YUI 3开发Web应用的诀窍

- 小伟 - Taobao UED Team
导语:这篇“基于YUI3开发web应用的诀窍”是比较经典的介绍 YUI3 工作机制的文章,文章发布在yuiblog上,总体难度适中,比较适合初学者认识、了解 YUI3. 以此纠集了三名应届同学来翻译这篇文章:函谷、郝黎、张勉,并希望能对正在学习YUI3的同学有所帮助和启发. 原文标题:A Recipe for a YUI 3 Application.

雅虎宣布停止开发YUI

- - Solidot
雅虎官方博客宣布终止开发开源的JavaScript工具库Yahoo User Interface library (YUI). 雅虎开发者解释说,行业趋势发生了改变. 过去几年,Web平台经历了激烈变革,相比以前如今的JavaScript几乎是无处不在. Node.JS的出现允许JavaScript在服务器端使用, 以及新的包管理器如npm,构建工具如Grunt,应用程序框架,测试工具等的出现,使得YUI之类的大型JavaScript工具库不再像以前那样受到社区的关注.

Web开发入门(转载)

- linchanx - Starming星光社最新更新
Web应用的竞争异常激烈,开发难度也是入门容易做好很难,所以第一次开发的应用不成功是很正常的事情. 不过这正是一个积累的过程,反正你需要的只是电脑和少量服务器经费,所以多磨练几次,水平自然会提高. 2, 习惯阅读及查阅英文资料. 前沿信息基本源自美国,翻译的东西不及时,不全,很多水平不高,再加之中文原创资料毕竟很有限,因此是否能熟练地查阅英文资料决定了你获取信息的 及时性和质量.

Spring MVC 与 web开发

- - 码蜂笔记
项目组用了 Spring MVC 进行开发,觉得对里面的使用方式不是很满意,就想,如果是我来搭建开发环境,我会怎么做. 下面就是我的想法,只关注于 MVC 的 View 层. 现在基本上都是用 ajax 来调用后台接口,拿到 json格式的数据再展示,有的人直接返回数据,却没有考虑异常的情况,我觉得返回的报文里必须包含表示可能的异常信息的数据和业务响应数据.

web开发利器之grunt

- - CSDN博客Web前端推荐文章
grunt不难,它主要依赖的是nodeJS的npm包管理器,和一个JSON及一个JS文件,先说说npm包管理器,玩过nodeJS的对它应该都很熟悉,在这里我们只需要安装nodeJS即可(新版的nodeJS基本都集成了npm),至于nodeJS的安装可以 点这里,这这篇文章就不做详细介绍,安装完后打开命令管理器(nodeJS安装完后的终端)输入:.

Web开发者必备:Web应用检查清单

- - ITeye博客
想做一个高质量的Web应用,前前后后要做的事情非常多. 国外开发者 Ata Sasmaz 为 Web 开发者制作分享了一份检查清单,包括应用开发、性能、安全、分析、可用性、可靠性、转换策略、竞争策略这些方面需要注意的事项. 清单内容可能不全面,欢迎大家在评论中补充. JavaScript 允许捕获异常.

超近距离接触-YUI浦东机场接机实录

- 龙 - 和邪社
上周通过和邪社微博还有YUI吧征集了这次YUI到上海来的接机活动,今天终于到了实战的日子. 这还是Jimmy第一参加接机活动,之前比如下川美娜啊、LiSA啊Jimmy还都没有自己跑去接待过,这次YUI酱呢Jimmy成为了一个召集人所以当然要亲临现场,不过和其他前往浦东国际机场的同学比我算是伪饭,感谢馄饨同学拍摄的照片,同时也感谢所有现场接机的朋友,期待以后还有更多这样歌迷与偶像的互动活动.

Web应用程序的开发步骤

- xxg - 月光博客
  如今已进入了web2.0高速发展的互联网时代,各种互联网的Web应用程序如雨后春笋般出现. 那么作为一名Web开发人员,怎样去开发一款优秀的Web应用程序呢. 这个问题没有一个简单的答案,甚至那些教育机构都未必能清楚的知道. 所以,像大多数在这个领域里的web开发人员一样,我们只是通过去做,去实验才学会了这些.

Web开发人员速查卡

- abcd - 酷壳 - CoolShell.cn
无论你是多牛的程序员,你都无法记住所有的东西. 而很多时候,查找某些知识又比较费事. 所以,网上有很多Cheat Sheets,翻译成小抄也好 ,速查卡也好,总之就是帮你节省 时间的. 之前给大家介绍过Web设计的速查卡、25个jQuery的编程小抄,还有程序员小抄大全,今天转一篇开发人员的速查卡,源文在这里.

平台是Web开发的未来吗?

- iyuan - 伯乐在线 -博客
  导读:本文是Arjun Khanna关于平台的出现以及它们如何简化Web开发的个人分析,也分析了平台的缺点和它们能够继续存在的因素.   即便现在大部分网站开发人员所构建的网站在结构上非常相似,或是至少在布局方面会有一些根本的共同之处,如果你问一下,他们大多会说他们还有一大堆的苦差使要头疼呢. 虽然客户几乎都要求顶上有横幅,导航条在左边,页面布局不超过三列,但是这并不使他们的工作更简单.