一個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");Thanks for the tip.
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. Great article! 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. What am I missing?
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 #