一个YUI 3应用配方
2011年4月1日起,在2:52 am由萨蒂扬| 开发 | 18评论锐3已设计围绕模块的应用。 我将不讨论一个模块是什么,因为它已经由Nicholas Zakas在他的演讲可扩展JavaScript的应用程序体系结构描述。 我就坚持到如何建立这些模块。 我会说什么,可以发现在上线文件,连同其他一些替代品,毕竟,这是很好的文档点:告诉你有关的所有可能的方式做事。 这就是为什么这是一个配方,只是当中许多人做的方式。 它还假定一个很小的应用程序,并不像许多层作为尼古拉斯的建议,因为这仅仅是一个文章,而不是一本书。
识别模块
第一步是找出我们需要的模块。 一个好的方法是先切到各个部分的设计应用程序的屏幕:标题栏,菜单栏,内容,侧板或任何其他有可能是有。 然后在库所提供的。 例如,3锐有没有菜单,但有节点的插件MenuNav ,这需要一个基本的菜单嵌套无序的名单<UL>元素结构,并把它们变成积极的菜单。 或者您可能希望检查锐画廊的基本组成部分。 无论如何,最终你会达到的地步,你有一个盒子中,你必须自己填写,布局,让我们的做到这一点。
我建议每个模块放置在其自己的文件和其自己的目录,由同一个名字。 因此, weather模块将weather/weather.js 。 这样做的原因是因为你的模块可能需要一些造型,一些CSS和图像文件,它很容易内置的装载机,如果你把它们在这种情况下,它可以很容易地找到他们,主样式表将在weather/assets/skins/sam/weather.css ,与其他资产,图像等,一起。 这是假设你不使用YUI的生成器已经这样做的事情,无论如何,但那是另一个故事。 文件夹名称的assets和skins有或多或少的不言自明,但sam是不是很明显。 它是skin装载机的财产为默认值,因为这是设计师后,山姆林德名为锐,附带的默认皮肤。 因为这表明,你把你的名字,对自己的皮肤和skin属性允许你告诉锐加载它们,但为了保持简单,就让我们一起去的默认。
模块文件模板
这是我使用更多的时候,文件的结构,在某一时刻,我将描述:
/ * jslint与开发:真实,UNDEF:真实,newcap:真正的,严格的:真实,maxerr:50 * / / *全球锐* / / ** *模块名称模块创建等等等等 * @模块的模块名 * / YUI.add(“模块名称”功能(Y){ “使用严格”; / /方便的常量和模块中使用的快捷键 VAR郎= Y.Lang的, CBX公司='contentBox', 北方新宇='boundingBox的', 名称='XXXX'; / ** *做的XXXX类.... * @类XXXX * @扩展控件 * @使用WidgetParent的 * @构造 {对象} * @ cfg的配置属性 * / Y.Xxxx = Y.Base.create( 名称, Y.Widget, [Y.WidgetParent] { / /实例成员 }, { / /静态成员,特别是: ATTRS:{ } } ); },'0 .99',{ 要求:['部件','小部件的父'], 换肤:真 });
前两行的意见是JSLint ,我真的建议的JavaScript验证工具的好处。 如果你到网络版本 ,有一个框来设置选项。 在该选项框的底部,你可以看到这些选项到文件本身的编码方式。 如果使用YUI的生成器,它会为你运行JSLint,并为您设置这些选项,但你仍然可以覆盖任何个人文件,如果你想。
doc注释为YUI API文档生成器。 这是头痛,如果包括那些API文档的初始模板,最终你会填补他们。 随着应用的增长,你是无法记住这一切,你需要他们。
现在来实际第一线代码, YUI.add()语句。 这是告诉YUI加载信息模块和其他一些作品的名称和内容。 模块名称通常是命名用字与字之间的所有小写字母和连字符。 这些都是你在API文档在左侧最顶端的索引的名称。 你可以看到没有严格遵循公约,一些模块名称没有任何连字符。 无论如何,这主要是给你,只要你使用它始终。
YUI.add()语句的第二个参数是一个函数接受一个参数,习惯地称为Y 。 该功能模块包含的身体和Y是沙盒了YUI的实例,它是在这里你可以找到YUI和其他所有的库模块要求的参考。 跳转到的代码框的底部,你可以看到其余.add()方法的参数,版本( '0.99′或还没有应用),此模块的配置,AA系列中的对象属性。 在这里,我告诉装载机,该模块需要widget和widget-parent ,它有一个皮肤。 上市widget是多余的,因为widget-parent已经要求widget ,但不麻烦,自己的Loader不会两次加载一个模块,并在稍后一点,如果你把一个依赖,你不需要检查其他假设,你可能已经作出:国家所有,它让装载机交易。 你可以找到一个API文档中的所有选项的列表.addModule()方法的装载机。
在函数体中,第一件事就是“use strict”;声明。 这是您的代码以符合ECMAScript的标准,这在这一点上,使你在安全方面,以确保所有的平台,你很可能会在未来遇到的兼容性。 对于年龄较大的口译员,这个宣言是没有什么比一个不分配给任何被忽略的字符串。 “use”声明具有的功能范围,它是安全的放置在函数体内,它比在文件的顶部,所以它不仅影响你定义的模块。 如果你把它在文件的顶部,它也将适用于任何其他JavaScript文件加载之后,其中许多人可能不符合ES5。
随之而来的快捷键和常数,这是只有在使用常数,因为JavaScript有没有常数的概念。 我们称他们在所有的大写字母和下划线,为常数往往是在其他语言。 有两个很好的理由使用常量,特别是字符串常量。 第一是当你写几次相同的字符串,你可能打错其中之一,你只会注意到当一个错误弹出。 如果您使用的常数,JSLint会警告您,当您键入常数的名称是错误的,因为这将是不确定的。 第二个原因是,YUI压缩机可以做得更好,因为常数的名称可以被压缩,而字符串常量不能。 命名常量的好人选配置属性和事件的名称。
如,为Lang Y.Lang Lang捷径都还不错,因为它们允许你键入,解释评估(每个点意味着新的搜索到的对象的成员),他们可以由YUI压缩机压缩。
API文档类的意见后,我们得到实际的声明。 我们有我们的财产申报类Y ,从而Y.Xxxx 。 我的建议是使用Y.Base.create()来创建它,如下所示。 它只能创建派生类Base (和Widget这也将是一个子类的Base ),但将覆盖的模块,您将使用最需要做其他方式,所以它是不寻常的。 第一个参数是模块的名称, NAME属性描述为Base组件。 按照惯例,“ NAME “属性是骆驼的情况下版本的类的名称。 这名被用作前缀事件(前像结肠的一部分“io:success” )所产生的CSS类名, Widget类(即: “yui3-xxxx-content” ),并为默认实现toString()你会经常看到在调试器的痕迹。 在这里,我使用的常量的值的NAME ,定义类的NAME属性。
第二个参数是它扩展的类。 你会经常使用两种Y.Base ,实用模块,这将有没有用户界面, Y.Widget那些将有一个UI, Y.Plugin.Base插件或任何其他类,如任何来自Y.Base你可能已经使用Y.Base.create()创建的。
第三个参数持有将要使用的扩展。 扩展类的属性和方法,你要到类混合。 良好的扩展候选人是ArrayList Base或任何Widget-Xxxx为子模块Widget Attribute , EventTarget和PluginHost生效已混合的Base所以你总是可以指望在这三个有。 扩展非常强大,如果你在看源代码Overlay ,你可以看到有什么,但Widget和扩展。 几个扩展到一个组件可以混合,所以第三个参数是一个数组。
最后,我们得到的实际代码。 第四个和第五个参数是包含类的实例和静态成员对象文字。 实例成员的属性和方法到类prototype ,每个实例将得到一个副本,通常需要通过引用this 。 静态成员是那些将被所有实例共享。
配置属性
这些静态成员的最重要的是ATTRS财产。 这将列出的配置属性将有你的类。 例如,可以说,我们希望有一个配置属性value存放数值,并初步设定为0。 在第五个参数,我们将因此宣告:
ATTRS:{ 值:{ 值:0 验证:Lang.isNumber } }
我们可以列出任何ATTRS属性在属性的数量和每个人都可以有几个选项,其中两个我这里显示配置。 你可以读选项其余addAttr()方法的Attribute 。 可以看出,我用我宣布开始在模块声明Lang快捷。 validator必须是一个函数,它的价值,以验证并返回一个布尔。 的所有Y.Lang.isXxxx方法做到底,这样他们就可以直接使用。 对于更详细的验证,二传手或干将,你需要定义功能。 我建议提供作为字符串的函数的名称, Attribute需要解决实际的函数的函数的名称。 例如,如果我是来定义,也就是说, validCodes可以采取一个有效的代码或一个有效的代码数组的属性,但应该总是返回一个数组,我会做:
ATTRS:{ validCodes:{ 二传手:“_setValidCodes”, } }
我们需要沿其他实例成员申报_setValidCodes在第四个参数的方法Y.Base.create()
_setValidCodes:函数(值){ 如果(!Lang.isArray(值)){ 值= [值]; } 返回值; }
最好是申报的setter,getter和任何但最琐碎的验证作为单独的实例功能,让Attribute解析成实际的函数调用的函数名。
在一般情况下,使用的setter“正常化值,如上图所示,不产生二次影响。 所有配置属性将触发之前和之后能够防止改变属性变化事件与之前的事件。 使用这些变化事件产生任何辅助作用,而不是二传手。 后事件是最好的,因为到那时,你知道,没有阻止,因为其他代码可能已签署前(上)更改事件,并取消它的属性设置。
可以使您的属性或多或少严格取决于你如何定义你的验证和setter。 如果验证程序非常严格,你的属性将是非常严格的,只接受有效的值,在这种情况下,二传手可能是不必要的。 另一方面,你可能无法使用所有的验证,并依靠二传手完全按摩接收到的东西接受任何价值。 例如,你可以有这两个:
验证:Y.Lang.isBoolean,/ /使接受严格的布尔属性 二传手:布尔,/ /属性接受任何价值,并有布尔变成一个
塞特斯也可以作为验证。 制定者应该返回被分配给该属性,但它们也可以返回值Y.Attribute.INVALID_VALUE将离开的属性不变,如果拒绝了一个验证它。
我定义了一个配置属性时,我常常为它定义一个常量,在模块的顶部(沿CBX , BBX的捷径我的地方),例如:
var的值='值', VALID_CODES ='validCodes“;
的机会,我将使用这些配置属性模块内的几次,这会来救我的一些麻烦。 但是,要小心,不要使用该常量在声明属性时,不这样做:
ATTRS:{ / / ***不要这样做*** / / 值:{ 值:0 验证:Lang.isNumber } }
如果你这样做,你会得到属性VALUE而不是的value 。 这是一个JavaScript的问题,不是一个YUI。 此外,要小心,不要对任何超越的配置属性已经声明为基类或任何扩展。 Widget已经有一堆属性的声明(见表格 ),虽然你很难添加boundingBox属性自己,你visible , disabled , height或width已定义可能很容易忘记。 如果你的使用目的符合什么Widget使用他们,一切都会好起来。 这就是说,你可以改变其中任何定义。 Y.Base.create()合并扩展基类的配置属性的定义,所以,如果你要改变,也就是说,默认值现有的属性,你可以这样做再次宣布在你的子类属性。
要小心,如果你的意思是初始化一个数组或对象的属性。 通过参考对象(数组是对象),如果你初始化一个对象的属性,他们可能都结束了指向同一个对象,这样当你改变它的一部分(从数组中删除一个项目或添加属性对象),你结束所有Base改变一次。实例,但是,有一些内部的逻辑,这将允许您安全地初始化对象和数组常量属性。 如果初始化的值是一个对象或数组文本,然后Base将克隆。 使用valueFn选项,或在其他对象初始化类初始化。
其他静态成员
有两个其他的静态成员,您可以定义在第五的说法Y.Base.create() 如果您正在创建一个插件,你绝对必须申报的NS的财产,如果你没有,插件将无法正常工作,静静地失败。 的NS属性必须设置一个将作为属性的名称存储在宿主对象插件,牢记这一点,当你选择这个名字,所以你不要覆盖任何现有的属性的字符串。
如果您正在构建一个部件,你打算支持渐进增强,那么你会使用的HTML_PARSER静态属性。 这是设置为一个对象,其中包含配置属性后命名的属性设置,从分析现有的HTML,要么CSS3选择器或功能,会产生自己的价值观。 渐进增强 Widget的用户指南。
您可能还需要提供要使用你的类的开发人员使用的值。 在文件的顶部声明的常量是完全从模块本身以外的无形。 如果你想向公众提供的常数,这是做它的地方。 这样的例子是HEADER , BODY和FOOTER的,常量WidgetStdMod (使用它们,你必须使用完全合格的名称: Y.WidgetStdMod.BODY等)。
实例成员
第四个参数Y.Base.create()是将进入prototype创建的类的属性和方法。 通常情况下,我们申报购买物业第一和方法。 我没有任何理由,为了实际上是无关紧要的,既不是JavaScript的也不YUI的需要你这样做,但它可以更容易地找到源文件中的东西。 虽然飞在初始创建的实例的属性可以,我建议明确宣布他们和初始化。 每个属性之前,应先通过一个API文档注释。
属性通常是私人的,它的名字前缀一个下划线。 这是最好的,如果通过配置属性,而不是属性暴露该对象的公共接口。 属性是非常愚蠢的,可配置属性验证,类型转换(通过setter),并产生二次效应(通过更改事件),直到你找到了你想要的所有功能,它往往是不长。
与配置属性,不初始化属性对象或数组,他们最终都会指向同一个对象,你遇到了麻烦。 这是更好地设置属性是持有对象null null 。 另外,不要离开属性没有设置,如果你不知道自己的价值,将它们设置为null代替。 后来,在调试时,属性设置为undefined点错误,通常是一个错字。
基本实例方法
您可能已经注意到,我们没有宣布任何构造,为我们的子类。 Base模块的初始化,然后调用称为方法initializer ,如果它存在,相同的参数,它已经收到这样实例化时,所有目的而言,你可以考虑, initializer是你的构造。 从派生类Base通常需要一个参数被创建时,一个对象,其中包含配置属性。 Base (或Widget ,因为它是一类的Base )读取这个参数,并设置配置属性之前调用initializer 。 Widget Widget ,如果有一个HTML_PARSER财产,它也已被处理和读标记的属性值将设置以及。
initializer方法有几个任务。 首先,它应该设置任何属性,需要初始化对象或数组。 然后,它会发布这个类会产生的所有事件。 EventTarget将允许你触发一个事件尚未公布首次使用默认设置的事件,但即使在这种情况下,我建议你无论如何宣布他们。 这是一个好地方,增加对这些事件的API文档意见,即使它看起来有点古怪,在一个函数声明的身体,但有没有更好的地方,这样做的。
将已收到的参数initializer处理,届时,但有时你需要一些额外的选项,用于初始化,你不在乎,保持他们的实际属性。 例如, Base接受的属性on after , bubbleTargets和plugins ( Base ),虽然它对于那些没有配置属性。 ,同样WidgetParent需要一个children在初始化属性,但有没有这个名字的配置属性。 initializer方法是一个处理它们。 因此,尽管你的类最终会实例上只有一个参数,这个单一的参数可以携带您可能需要的所有信息。
JavaScript有没有析构函数的概念。 Base补偿这允许你声明一个destructor方法,在那里你可以把代码释放你的对象可能采取的资源。 这只是一个部分解决方案,JavaScript解释器并不调用它时自动删除一个对象,所以你仍然负责销毁对象之前抛弃它,但至少你知道将有一个析构函数。
您的类的用户将不会直接调用initializer和destructor 。 Base将打电话给他们需要的时候。 initializer的对象被实例化时,将被称为destructor将被调用时,您的类的用户调用其destroy方法。
,往往产生内存泄漏的事情之一是留下的事件监听器。 Widget尝试以分离的边界框元素中包含的用户界面元素的所有听众,但它可以不分离的任何其他。 Base可以不分离任何在所有的事件监听器。 这是我使用的代码,以帮助我。 沿其他私人,实例的属性,我宣布_eventHandles财产:
_eventHandles:空, 然后,在initializer方法,我将它设置一个数组:
初始化函数(CFG){ this._eventHandles = []; / / ...... },
在相同的initializer (也bindUI如果它是Widget Widget ),我会再附上做听众:
,this._eventHandles.push(this.after(“someAttributeChange”,this._afterSomeAttributeChange,本)); 然后,在destructor ,我有:
析构函数(){ y.each(this._eventHandles,功能(处理){ handle.detach(); }); },
正是在这里,在initializer ,产生二次效应(你可能会有所不同的属性,应该挂钩的事件监听器bindUI如果这一次要作用,处理与UI)。 正如我刚才所说,属性setter函数应该只处理属性值正常化。 该设置应该超越存储的值产生任何影响,这些应该由事件侦听器处理。 在上述例子中,我已经设置方法_afterSomeAttributeChange听为任何变化someAttribute属性。 事件监听器将接收一个参数,我通常所说的ev事件门面,几个属性,其中之一, newVal包含的值被设置的对象。
Widget实例属性
两个重要的属性Widget用途是BOUNDING_TEMPLATE和CONTENT_TEMPLATE 。 最初都设置为“<div></div>”这产生了两个集装箱内的其他大部分部件使用的标准结构。 然而,这可能不适合所有部件,例如, Button Button部件可以更好地服务由<span>内锚(元素<a> )元素,而不是两个的嵌套<div>小号。 事实上,你可能不在乎有contentBox所有Widget不需要你。 这两个实例的属性,你可以设置你想要的任何标记。 例如,对于Button类Button ,我可能有:
BOUNDING_TEMPLATE:“<A>” CONTENT_TEMPLATE:空,
有CONTENT_TEMPLATE设置为null null会告诉你不想Widget contentBox在所有Widget 。 在这种情况下contentBox配置属性将指向相同的元素,作为boundingBox配置属性。
你不应该把这些模板把整个部件的HTML,使这两个简单的HTML元素和创建任何通过代码的额外的标记renderUI (我们稍后将看到)。
Widget将添加id id属性和它使用的标准类你想要的任何标记,如yui3-xxxx , yui3-xxxx-visible或yui3-xxxx-disabled ,其中xxxx是成小写NAME打开属性的值。
Widget实例方法
Widget拆分在几个步骤,其初始化。 除了initializer调用的对象被实例化时,和destructor ,被称为destroy ,这两种方法所处理Base , Widget增加, renderUI bindUI和syncUI Widget render renderUI syncUI建设阶段,将在序列时调用Widget的render方法是调用。
renderUI方法需要照顾生产的部件的基本HTML。 同时boundingBox和contentBox已经呈现在这一点上。 如果使用渐进增强, renderUI首先要检查是否已经存在的元素在页面上。 如果我们使用的HTML_PARSER财产,那么引用这些元素的配置属性将被设置,届时,如果没有,我们需要创建它们。
要做到这一点,最简单的方法(假设没有渐进增强)是使用Y.Node.create ,这样的:
renderUI:函数(){ CBX = this.get(CBX公司); cbx.append(Y.Node.create(Y.substitute(Y.Xxxx.TEMPLATE,CLASS_NAMES))); },
这是假设了很多东西,我将马上解释。 首先,我CBX不断宣布在第一个代码中所示,在这篇文章中。 然后,它假定Node被载入,哪些Widget使用,所以它是安全的,但它也假定的Y.substitute是存在的,它是可选的。 你必须添加'substitute'的requires为您的模块列表。 然后,预计是在一个静态变量所谓的TEMPLATE这是由你来定义以及其他静态类成员(,权利ATTRS等)部件的模板。 最后,它假定有一个恒定的CLASS_NAMES宣布某处。
通常,我宣布CLASS_NAMES在我的模块定义,以及北方新宇和CBX公司(在这篇文章中看到的第一个代码框),像这样:
北方新宇='boundingBox的“, CBX公司='contentBox', NAME ='按钮', / /其他常量和快捷.... 永进= Y.ClassNameManager.getClassName, getClassName =函数(){ VAR ARGS = Y.Array(参数); args.unshift(姓名); 返回YCM.apply(此参数)与toLowerCase(); }, 标签='标签', 按下='压', 图标='图标', CLASS_NAMES = { getClassName(按下),按下: 图标:getClassName(图标), 标签:getClassName(LABEL), noLabel:getClassName('不',标签) };
届时ClassNameManager Widget CLASS_NAMES恒定对象ClassNameManager (其中还包括Widget )与所创建的属性。 在上述代码中,我首先创建快捷YCM以使以后访问更快,然后我创建的函数的的getClassName ,这是一个私有函数只能在模块定义访问。 函数的工作原理很像Widget widget的名称相同的方法,但我可以用它来 进一步定义的静态值,它是一个静态函数。 这是我做什么以后,当我创造与生成的类名作为自己的财产为对象的CLASS_NAMES 。 这让我写一个TEMPLATE ,如字符串:
模板:的“<label class="{label}"> <input/>” 这是非常愚蠢至今。 我也想融入这个模板值从其他来源,特别是配置属性。 这是我是怎么做的:
this.get(CBX公司)。追加(Y.Node.create(Y.substitute(模板,CLASS_NAMES,Y.bind(功能(关键,建议中,arg){ 回报(===关键“_”this.get(阿根廷):建议); }))));
我添加了第三个参数到Y.substitute ,功能。 通常情况下,占位符Y.substitute括在大括号之间的字符,但是,如果有一个空间,它会分成两个占位符,部分空间是关键,第二个可选参数。 这都得心应手,当第三个参数是一个功能,比如这里。 函数将接收三个参数,第一个是关键,二是更换对象中发现的价值,这里CLASS_NAMES ,如果有的话,第三个是可选参数。 因此,在上述声明中,我可以使用这样的模板:
模板:'<label class="{label} for="{_ id}"/> <input id="{_ id}" value="{_ value}" />“ Y.substitute会找到{label}它在CLASS_NAMES搜索。 它会找到它,并得到的'yui3-button-label' 。 然后,它会调用带参数的替换功能'label' , 'yui3-button-label'和undefined 。 的key是不等于'_'将在第二个参数,原始的类的名称返回值。 当它到达{_ id}有没有财产的价值,称为_在CLASS_NAMES ,所以它会调用带参数的替换功能'_' undefined和'id' 。 与平等的key '_'该函数将去获取的价值'id'属性。 它将再次做同样的{_ value}占位符。
隐藏在顶部的所有声明的常量从任何代码以外的模块,但你可能想使他们中的一些可见,如CLASS_NAMES 。 做静态成员,一节中,最后一个参数Y.Base.create ,你可以有:
CLASS_NAMES:CLASS_NAMES 然后将所有的类名对象为可见Y.MyWidget.CLASS_NAMES 。
我建议你做尽可能多的格式,你可以用HTML字符串,这将使部件的内容。 JavaScript中的字符串操作是速度远远超过访问DOM,所以,你打电话之前Y.Node.create该字符串,速度越快你会得到它。
bindUI任何部件被称为是下一个实例方法。 这是你附加的事件侦听器所创建的任何元素renderUI ,例如,在值中的任何变化监听器<input>框TEMPLATE上面。 TextBox和配置属性的值应该始终保持同步。 可以通过代码或用户打字输入框的value属性可以改变。 如果从外部代码,文本框应该被刷新,如果从TextBox,它不应该,否则你可能会进入一个无限循环:在TextBox的变化设定value属性,然后设置value在TextBox然后变化和套value属性等。 让我们看看如何处理这种情况。 我们设置的合成上输入框valueChange事件侦听器。 要做到这一点,我们需要添加的的event-valuechange模块requires这个模块列表。
,this._eventHandles.push(this._inputEl.after(“valueChange”,this._afterInputChange,本)); 我们假设对象有一个参考_inputEl中保存的文本框。 听者:
_afterInputChange:功能(EV){ this.set(值,ev.target.get(值){来源:界面}); },
在这里,我们假设我们有常量的VALUE和UI宣布为'value'和'ui'分别。 我们只需设置属性value输入框读出的值。 然而,我们增加了第三个参数的设置方法: {source:UI} 。 set方法,可以采取第三个参数,一个对象,其属性将混合的属性更改事件的事件门面。 就是这样,我们可以告诉正在从TextBox或从外部代码设置值之间的差异。 在bindUI我们将不得不设置这个监听器:
this._eventHandles.push(this.after(valueChange,this._afterValueChange)); 这是在变化的监听对象的value属性,其他的价值变化是<input>盒,他们被称为相同的,毕竟,他们都听取被称为有价值的东西的变化,但不是一回事。 通常情况下,在initializer设置属性变化的听众,但是,因为这会影响UI元素,我们把它bindUI让我们知道文本框将在那里。 听众将有:
_afterValueChange:功能(EV){ (ev.source ===用户界面){ 返回; } this._inputEl.set(值,ev.newVal)的; },
我们做的第一件事就是检查source事件。 如果从UI然后我们忽略它。 Both the property name, source and its value, UI are arbitrary, those are the ones I used when setting the value attribute so those are the ones I check for in the listener, but any name/value would do just as well. Actually, Widget provides a constant for that, Y.Widget.UI_SRC , but it is kind of long so I would probably use a shortcut anyway.
Another tidbit: you can set attributes declared as read-only by using _set instead of set . The _set method is meant to be protected, to be used internally but, as we know, JavaScript knows nothing about security so _set is open to any but, at least, we try by declaring the attribute with readOnly:true and documenting it as such in the API docs.
Finally we declare syncUI . While the first two, renderUI and bindUI are going to be called once and only once, syncUI will be called at least once by Widget itself and you might call it several times afterwards. Its purpose is to refresh the UI to reflect the current state of the object. Since the state might change, the UI might need to be refreshed over time. However, I can't provide a simple recipe for handling this. For a simple UI, syncUI might refresh everything in the screen and be called every time anything changes. For more complex UIs refreshing the whole UI might take time and cause flickering so you might want to refresh only the bits and pieces you need. If so, you will have separate methods to refresh each of these parts and syncUI will call each of them just once. Moreover, as I've shown in the example for renderUI , I set the value of the textbox right there, though that should be done in syncUI .
In the more general case, you will have a function for each UI element that can be set separately. That function will be called once from syncUI , when initializing, and any number of times from the after attribute change event listener. For example, we could have:
_valueUIRefresh: function (value) {
this._inputEl.set(VALUE, value);
} Which could be called from syncUI along other similar setters:
syncUI: function () {
this._valueUIRefresh(this.get(VALUE));
// other such refreshers
}, and by the after listener:
_afterValueChange: function (ev) {
if (ev.source === UI) {
return;
}
this._valueUIRefresh(ev.newVal);
}, Communicating with others
Once you have the logic of one of your modules finished, you want it to interact with other modules on your page. If you've seen Nicholas Zakas video, you already know what tight and loose coupling is. Calling methods and setting attributes from one module to another means having those modules tightly coupled and it is the traditional way, so I won't talk about it since you know how to do it. The other way to do it is to fire custom events. Base already includes everything you need to do that.
First, in initializer , you publish the custom events you want everybody to find out about.
initializer: function (cfg) {
this.publish('eventName', { /*… options … */});
}, Normally, the name of the event will come from a constant, since you will use that same name every time you fire it and you don't want typos there.
Normally, when you have a reference to an object, such as:
var myWidget = new Y.MyWidget({ /* .. attributes … */ }); you can listen to its events by doing:
myWidget.after('eventName', this._eventNameListener, this); However, to do this, you need to have a reference to myWidget , which is not as tightly coupled as calling its methods directly but it is still quite tight: at least one module knows about the other or, perhaps, a supervisor module knows about both and sets the links in between them. Two options are important to get modules to communicate in between themselves, broadcast and emitFacade .
The first, broadcast , lets you set listeners for that event in other modules. When broadcast is left at 0, the default, you have to do as shown above. If you want the event to be listened to elsewhere, you will want broadcast set to 1, so events are broadcast within the same sandbox and sometimes 2, so they can go across sandboxes. In this context, a sandbox is what you get when you call:
YUI().use( 'module1′, …, 'moduleN', function (Y) {
// this is your sandbox
}); You can have several such sandboxes in your page:
YUI().use( 'module1′, …, 'moduleN', function (Y) {
// this is your sandbox
});
YUI().use( 'moduleX-1′, …, 'moduleX-N', function (Z) {
// this is another sandbox
}); If you set broadcast to 2, then an object in the second sandbox can listen to an event when fired in the first. You can see the details in the Event user guide . Lets just stick to the simple sandbox case.
To listen to an event fired from another module within the same sandbox you need to know the value of the NAME static property of that module and the name of the event. Remember, Y.Base.create takes, as its first argument, the value that it will use for its NAME property, thus, if you created a module in this way:
Y.MyWidget = Y.Base.create(
'xxxx',
Y.Widget,
// … and so on and then, in the initializer you published the 'help' event like this:
initializer: function (config) {
this.publish('help', {
broadcast: 1,
emitFacade: true
});
}, To listen to that event in any other module within the same sandbox, you do:
Y.after('xxxx:help', function (ev) { … }, this); Here, I am calling Y.after , not myWidget.after , I don't need to have a reference to the module firing the event. This is the same method used to listen to DOM events or other synthetic events such as 'valueChange' the only difference being the prefix, the part before the colon. Base already takes care to prefix all events with the value of the NAME property so you don't have to take care of that when publishing them. You can do so, you can even use something else as a prefix; if one such prefix is there, Base will respect it, but usually you just want the default, which Base provides.
You also want to set emitFacade because you will want to have a reference to the instance that fired the event, which the event facade provides in ev.target . But wait, if the listener module gets a reference to the firing module, don't they become tightly coupled once again? Not quite, as long as you don't preserve that reference in the listening module, the coupling will be volatile. Still, we can do better.
When firing the event we may add all the information the listener needs in the facade, like this:
this.fire('help', {helpTopic: 'Event Broadcasting'}); Method fire takes the name of the event being fired (which Base will further prefix with the NAME of the class) and an object containing any number of properties which will be merged into the event facade. The listener then doesn't need to query the firing module for any information, all that might be needed is there. This is as loose as it gets. The listener simply knows that some module, and there may be many such modules, is asking for help on 'Event Broadcasting' and that is really all it needs to know. It doesn't even care which module asked for it. New modules may be added later and the help system will also work for them.
Events and Default Behaviors
The usual solution to changing the behavior of a class is to sub-class it so you can override one of its functions and do whatever it is you want to do instead. You can still do that. You can use Y.Base.create to define a module based on, say Y.Widget and then use Y.Base.create again using your new module as the base to change a particular behavior. For example, I might have:
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
}
); and then:
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 improves over MySimpleWidget by adding some bells and whistles. This might be too much of a trouble in some cases, you might plan for a base class more flexible and easier to change. Custom events can help with that.
Imagine you have a class that has a sort function. The sort function takes a key and direction argument and is declared like this:
sort: function (key, direction) {
// sorting happens here
}, If you know that the behavior of that function might be changed in some circumstances, you might do the following. In the initializer method, you can have:
initializer: function (config) {
// amongst many other things:
this.publish(SORT, {defaultFn: this._defSortFn});
}, Where SORT is a constant containing 'sort' . Then, you declare the sort function like this:
sort: function(key, direction) {
this.fire(SORT, {key:key, direction:direction});
}, The sort function simply transforms the standard function call into a fired event containing the same arguments. Though this is meant to provide alternatives, you still want the class to sort somehow, you do that through the default sort function:
_defSortFn: function (ev) {
var key = ev.key, direction = ev.direction;
// same code as the original sort function
}, The class will do sort as before, the body of _defSortFn might be just the same as the original one, once you have read the key and direction arguments from the event facade, but any other piece of code can set a listener for that same sort event and change it, for example:
myObjectThatSorts.on('sort', function (ev) {
var key = ev.key, direction = ev.direction;
ev.preventDefault();
// now do your own sort
}); By calling preventDefault I tell myObjectThatSorts not to call _defSortFn . I could do this conditionally and decide, based on whatever I want, whether I may leave the original sort go ahead or unconditionally stop it, as I did here. I might not even care to stop it ever, I might listen to the after event and simply flip an arrow somewhere in the UI to signal which way the sort went.
I may also alter the event facade. There is only one copy of the event facade that gets built when the event is fired and it is propagated through all before (on) listeners, to the default function and then to the after listeners until finally it is dropped. You can change the values of its properties at any point. Of course, it hardly matters any changes you might do after the default function is called but any changes done in the before (on) listeners will reach the default function, for example:
myObjectThatSorts.on('sort', function (ev) {
ev.direction = (ev.direction==='desc'?'asc':'desc');
}); This would get the sort done upside down.
YUI_config
The easiest way to get your module on your page is to include it in its own <script> tag or in a script tag pointing to a combo URL (via creating a file on the server that is a manual concatenation of files or a combo service is the server supports one). Integrating custom modules into the Loader is a more advanced option, though it might improve performance. The important point in this case is to make sure the YUI.add() includes the requires: [...] in the last parameter, so use() will apply the module and its dependencies in the proper order.
For small applications, you will probably have everything loaded from the start as outlined above. However, for larger applications, you might not want everything loaded from the start since it can take too long. You can call use() more than once to request extra functionality as needed. However, having the Loader find out about each module's dependencies when it loads each is time consuming since it might take several sequential requests until it finally gets everything it needs. Instead, you can forewarn the Loader of your modules and their dependencies so, when the time comes, it knows how to deal with them and can load them all in parallel.
To do so, you need to add the module description and requirements to the tables that the YUI Loader uses to fetch modules. The easiest way is to build a yui_config.js file (or whatever you want to call it) that contains all those definitions. That file will look like this:
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
}
}; You include this file in a regular <script> tag in your HTML file before you issue the first YUI().use() statement. They replace those options you would otherwise place as the first argument to YUI().use() , as if you did YUI(YUI_config).use() , but YUI does it for you. You can use any of the options listed here .
The filter option can be set to 'min' for production code (the default so you would usually comment out), 'debug' for the fully expanded with log statements (which might overwhelm your console) and 'raw' for fully expanded without log statements, the last two used only in development. Likewise with the combine option, only used when you have really tough bugs and you want to find out what is going on and get lost in those huge combos. Then you put your gallery option, if you use any gallery modules, to freeze your gallery modules to a version you know it works.
The groups option is where you start describing your own modules. The first name, in this case js , can be anything, whatever you want to call your group of files. You could create one such group for each family of files in a common location. The first declaration in each group is the base location of the group of files relative to the home page or an absolute path. That is, basically, the criteria for grouping files, however, there are several more options, listed here .
Finally, in the modules section you start listing your modules. The key for each entry is the module name, the very same name that you have used as the first argument in the YUI.add in your file and the same that you will use in the module list when you issue the YUI().use() call in your application. Then you specify the location of the module file, relative to the previous base or the fullpath if located elsewhere, and the rest of the options that where at the very end of the YUI.add declaration and are listed here . The requires list can list YUI modules, gallery modules or modules of your own either within the same group or from other groups in your config file. Skins will be loaded automatically by setting skinnable:true if you locate them as I recommended at the beginning of this article.
To simplify things for myself, I created a Windows script file that builds the YUI_config options for me. It basically scans the folder with the module files and reads each of them and extracts the information from each YUI.add call by defining a fake YUI.add function that extracts the arguments for me. It makes plenty of quite simplistic assumptions but it works for me as it is, you use it at your own risk.
结论
YUI3 is very flexible and you can build your modules in many ways. This is no more than one way to do that; I don't always do it this way, sometimes, not often, I don't need all of what Base provides so Y.Base.create is of no use, but this works most of the time.
共享和扩展: 书签del.icio.us Digg它! | reddit!
18 Comments
很抱歉,评论已被封闭,在这个时候。


Thanks a lot for this article. It is difficult to find informations about skins in the official doc. It is now much clearer for me.
Comment by plv — April 1, 2011 #
Awesome article!
Thank you, Satyam!
Comment by John — April 1, 2011 #
Wonderful article!
Comment by Daniel Stockman — April 1, 2011 #
“This is a JavaScript issue, not a YUI one.”
That is NOT issue, but pretty normal behavior. :)
Comment by John — April 3, 2011 #
Thanks for the article! Cleared a lot of doubts that I had.
I have one question though :) I don't quite understand the use of syncUI method. You have stated its use as “Its purpose is to refresh the UI to reflect the current state of the object”. Aren't you doing that in bindUI() method when you register afterValueChange events for widget's attributes to refresh UI?
Comment by Vignesh — April 5, 2011 #
i am just start to learn YUI3, i think this article will help me to understand it deeply.
Comment by lily — April 5, 2011 #
[...] mobile support, a “Simple” theme, a new kitchen-sink-like Widget Browser, and more A Recipe for a YUI 3 Application – Satyen Desai of the YUI team goes into detail about how to organize a YUI application WebGL [...]
Pingback by JavaScript Magazine Blog for JSMag » Blog Archive » News roundup: iOS viewport fixes, Mobile Boilerplate, CommunityJS, Ender.js — April 8, 2011 #
I've put it into Chinese.
http://ued.taobao.com/blog/2011/04/14/a-recipe-for-a-yui-3-application/
Comment by jayli — April 14, 2011 #
@Vignesh:
Regarding the syncUI method, the listeners you add in bindUI dont get fired during sync, because the object state at the time you bind the listeners has already been set, so there's no change in the state.
I'll use pseudo code to help explain:
var obj = new Y.Widget({attr1: 55});
//attr1 state is now 55
obj.render()
So during render, since nothing changes, no change event is fired, hence why you may need to call the syncUI method.
Ideally, you should have a _uiSetAttr1 method that is just called by the after change event, and can be called during sync that simply handles updating the ui in response to that attr changing.
There are also properties that help make this glue wiring easier. Off the top of my head I don't remember if they're private still, but if you set them (I think it's either BIND_UI or BIND_UI_ATTRS and same for SYNC) all you have to do is set a method called _uiSetAttrname and list the ATTRS in that object, and it will automatically do all of that wiring for you.
I hope that helps :)
Comment by Nate Cavanaugh — April 16, 2011 #
[...] Recipe for a YUI Application Building Reusable Widgets with YUI3 Alloy UI [...]
Pingback by Build A Chrome App with YUI – (1) « Triptych — April 17, 2011 #
Nate
You are referring to property _UI_ATTRS which is an object with two properties,
BINDandSYNC. As its initial underscore signals and the documentation states, they are private and unless you really know what you are doing, it is better keep your hands off them. (I mean 'you' in generic terms)Each of these has an array of attribute names which are already initialized by Widget so any changes need to be done carefully not to destroy their initial values.
For those listed in the
_UI_ATTRS.BINDarray, an “after change” listener will be attached before ourbindUImethod which will call a method named like_uiSetXxxx, wherexxxxis the name of the attribute.Likewise, configuration attributes named in
_UI_ATTRS.SYNCwill have this same_uiSetXxxxcalled right before our ownsyncUImethod. Each of these_uiSetXxxxmethods will receive the value of the attribute, either the initial value or the changed value.Indeed, this is a clever mechanism and it would be great if it was made public, or an alternative public equivalent were to be provided and thus supported in the long term. A formal statement by the YUI team regarding the long-term support for this mechanism would be welcome.
Nevertheless, I should have mentioned it or suggested a similar custom approach. So, since the Pandora's box is open, let me explain it.
Say you have an attribute “myAttr” which has an effect on the UI. You need to provide a method called _uiSetMyAttr (note the M in myAttr turned uppercase) which receives the new value to set and a second argument which might either be
undefined(not there) or set to Widget.UI_SRC and affects the UI when the second argument is not set. You would define it in the instance member section like this:_uiSetMyAttr: function (value, src) {if (src === Y.Widget.UI_SRC) { return; }// set the UI element}To get the UI set initially, right before your own
syncUIis called, push the name of the attribute into_UI_ATTRS.SYNC, usually in theinitializer, like this:this._UI_ATTRS.SYNC.push("myAttr");To have it further called after any change in the attribute, push it also into
_UI_ATTRS.BIND, like this:this._UI_ATTRS.BIND.push("myAttr");感谢您的小费。
Comment by Satyam — April 17, 2011 #
Here's the enhancement request:
http://yuilibrary.com/projects/yui3/ticket/2529439
Comment by Satyen Desai — April 18, 2011 #
Heya Satyam, thanks for the detailed writeup. We actually support this as a public attribute in our Alloy Component gallery module since it's so useful, but we allow it to be defined on the class level and handle automatically copying of the arrays so that if you pass in a custom set it is merged with a copy of the defaults instead of having to create a new array and concat it (or doing it in the initializer).
The only downside I've found to the API (and it's a small one at that) are the times when I want other data from the event passed in (maybe the second or third arg would be good to have the event object being passed in).
Btw, great article, thanks for consolidating this all here :)
Comment by Nate Cavanaugh — April 21, 2011 #
[...] Recipe for a YUI 3 Application 原文地址:http://www.yuiblog.com/blog/2011/04/01/a-recipe-for-a-yui-3-application/ 译文:使用YUI [...]
Pingback by [译]使用YUI 3开发Web应用的诀窍 « Uedmagazine — April 22, 2011 #
A correction to my previous comment :
The extra attributes to monitor should be added to the
_UI_ATTRSarrays via method concat() not push():this._UI_ATTRS.SYNC.concat("myAttr");this._UI_ATTRS.BIND.concat("myAttr");Using push() will add the new attribute to an array which is shared by Widget and all its subclasses while concat() will create a new copy for this particular subclass
BTW: concat() allows several values to be concatenated at once:
this._UI_ATTRS.BIND.concat("myAttr1","myAttr2","myAttr3");Comment by Satyam — May 14, 2011 #
Sorry I missed once again in my correction . The way to add an attribute to be handled by Widget is this:
this._UI_ATTRS = {BIND: this._UI_ATTRS.BIND.concat("myAttr"),SYNC: this._UI_ATTRS.SYNC.concat("myAttr")};Method concat() does not modify the array it applies to so the code in the previous comment does nothing. Sorry about the confusion.
Comment by Satyam — May 14, 2011 #
Hi Satyam. 伟大的文章! I'm in doubt with something and hope you could clarify this issue for me. You wrote: “Base cannot detach any event listeners at all”. But Base's destroy() method calls detachAll(), inherited from EventTarget, which according to the documentation, it should remove all listeners. 我错过什么?
Comment by alejandroci — May 25, 2011 #
alejandroci ,
You are right, the wording of that phrase is not right, it should have been 'Base cannot remove all events'. Indeed, it does remove some via
detachAll(), which it inherits from EventTarget which comes along Attribute. Event Target is capable of removing all events created by itself viathis.onandthis.after.It cannot detach those subscribed to via
Y.on,Y.afteron events broadcast by other objects (subscribed via the" publisher : event "notation) or via a reference to some other object (myPublisher.on()).Widget can only detach events picked by delegation to its bounding box.
So, YUI tries to keep it safe as much as possible with what it knows but it can't do it all, not with any reasonable amount of code. If you forget, YUI will cope as best as it can, but it is better if you do it yourself.
Comment by Satyam — May 26, 2011 #