原文:Application Controllerhtml
一個頁面級別的控制器就像膠水,經過將模塊化的功能黏在一塊兒來構造一個鮮活的應用。咱們將實現配置與一個明確的生命週期,經過鬆耦合的架構組合一個單頁面應用的多個部分。node
做爲一個模塊化的工具包,不少Dojo的文檔都是在講解單獨的組件如何使用。可是當你須要組合它們來建立一個應用的時候,你須要一個框架來將它們靈活的組織起來。設計模式
最佳實踐建議保持關注點分離,維護組成應用的模塊。因此,如何管理各個組件的加載與初始化,如何將它們與數據結合起來,用戶界面處理是否靈活與模塊化?api
一個頁面級別的控制器是一個對象,它有管理頁面或應用的職責。它假定控制應用的生命週期與各部分的加載。它按正確的順序初始化與鏈接那些部件,並能掌控大局。服務器
Dojo並無建議咱們該如何將它所提供的組件組合成一個應用。它有全部的瓶瓶罐罐,就是沒有藍圖。做爲一個工具包,就是這麼設計的。你能夠在你的靜態頁面使用一些Dojo組件來點綴,或者使用它來構建一個純GUI的應用,使用哪一種設計模式與實現方式取決與你的選擇。對於這份教程,咱們取個折中,構建這個實現有些關鍵需求:架構
咱們的需求是構建一個應用容許用戶搜索Flickr上的照片,按縮略圖展現結果,點擊每一個縮略圖查看對應的大圖。這種主—祥(我取的)模式,在不少應用中都有。在本教程中咱們專一於組合——如何把分離的部分組合在一塊兒——因此咱們先大體預覽一個它們各個部分。app
應用的數據層由dojox/data/FlickrStore來處理。這是一個開箱即用的組件,實現了dojo/data的讀取API,發送請求到Flickr的API服務器。框架
咱們使用標準的fetch方法來傳遞查詢,將轉化成到Flickr服務器的JSONP請求,響應並觸發onComplete回調。其餘組件應該多少知道一點Flickr的東西。任何特殊的需求都應該限制在store的實例中實現,經過提供的配置——也就是咱們應用提供的。dom
咱們使用在佈局教程中有講過的基於BorderContainer的佈局。每一個搜索結果將會有它本身的tab在TabContainer中,佔居中心的區域。async
用戶在頂部的輸入框中輸入搜索關鍵詞。他們能夠點擊搜索按鈕,或按下Enter鍵來提交搜索。Wiring up event handlers and their actions is the domain of our application controller in this example.
咱們能夠爲咱們的應用建立一個自定義的掛件來提供高層的接口,可是這麼簡單的需求不必定要這麼作。
咱們應用的renderItem方法渲染結果並建立一個新的tab面板。
咱們經過事件委託技術來註冊一個點擊事件監聽器,那樣在選中列表項的時候就會展現其響應的大圖。這裏,咱們也能夠建立一個自定義的掛件來渲染這些項目,可是在應用層面流程與職責沒有太大改變。
咱們把大圖放在一個幻燈片樣式的彈出框裏面。咱們能夠實例化一個dojox/image/LightboxNano掛件來顯示圖片。
咱們一對簡單的startLoading與endLoading方法來增減加載遮罩。加載遮罩是應用級別頁面關心的事情,因此遮罩的顯隱放在應用的控制器中來管理。
在這個應用中,咱們使用聲明式的UI建立方法。應用的主佈局在頁面的標記中陳述,使用適當的data-dojo-type與data-dojo-props屬性來配置咱們的掛件。
關鍵詞輸入字段是一個純HTML的文本輸入框,搜索的提交按鈕是一個純HTML的按鈕。Dijit的BorderContainer管理頂部與中心區域的位置與尺寸,讓搜索欄固定,搜索結果高度自適應。
滾動操做由分開的tab面板來處理——咱們使用了dijit/layout/ContentPane。
1 <script> 2 require([ 3 "dijit/layout/BorderContainer", 4 "dijit/layout/TabContainer", 5 "dijit/layout/ContentPane", 6 "dojo/domReady!" 7 ]); 8 </script>
咱們爲初始化佈局所須要的模塊以下:
1 <body class="claro"> 2 <div id="appLayout" class="demoLayout" data-dojo-type="dijit/layout/BorderContainer" data-dojo-props="design:'headline'"> 3 <div class="centerPanel" id="tabs" data-dojo-type="dijit/layoutTabContainer" data-dojo-props="region:'center',tabPosition:'bottom'"> 4 <div data-dojo-type="dijit/layout/ContentPane" data-dojo-props="title:'About'"> 5 <h2>Flickr keyword photo search</h2> 6 <p>Each search creates a new tab with the results as thumbnail</p> 7 <p>Click on any thumbnail to view the larger image</p> 8 </div> 9 </div> 10 <div class="edgePanel" data-dojo-type="dijit/layout/ContentPane" data-dojo-props="region:'top'"> 11 <div class="searchInputColumn"> 12 <div class="searchInputColumInner"> 13 <input id="searchTerms" placeholer="search terms"/> 14 </div> 15 </div> 16 <div class="searchButtonColumn"> 17 <button id="searchBtn">Search</button> 18 </div> 19 </div> 20 </div> 21 </body>
一切都很好,每樣東西都在它們應在的位置,可是沒有功能,咱們須要把功能放在什麼地方。固然就是應用控制器中啦。
咱們爲應用控制器建立一個新的模塊。
1 define([ 2 "dojo/_base/config", 3 "dojox/data/FlickrRestStore", 4 "dojox/image/LightboxNano" 5 ], function(config, FlickrRestStore, LightboxNano) { 6 var store = null, 7 flickrQuery = config.flickrRequest || {}, 8 9 startup = function() { 10 // 建立數據存儲 11 store = new FlickrRestStore(); 12 initUi(); 13 }, 14 15 initUi = function() { 16 lightbox = new LightboxNano({}); 17 }, 18 19 doSearch = function() { 20 21 }, 22 23 renderItem = function(item, refNode, posn) { 24 25 }; 26 27 return { 28 init: function() { 29 startup(); 30 } 31 }; 32 });
demo/app模塊得到查詢詳情,它將最終經過Flickr存儲從Dojo的配置對象。在模塊以外保持不少種細節也許改變在從測試,開發到產品之間。dojoConfig聲明以下:
1 dojoConfig = { 2 async: true, 3 isDebug: true, 4 parseOnLoad: true, 5 packages: [{ 6 name: "demo", 7 location: "/documentation/tutoials/1.10/recipes/app_controller/" 8 }], 9 flickrRequest: { 10 apikey: "YOURAPIKEYHERE", 11 sort:[{ 12 attribute: "datetaken", 13 descending: true 14 }] 15 } 16 };
關於dojo/_base/config更多信息,查看教程與參考指南。
demo/app模塊是咱們將要保存數據存儲引用的對方,與查詢信息一塊兒,咱們在每一個Flickr請求中使用。
咱們定義一個init方法做爲主入口。視覺交互在initUi方法中完成,也就是全部的掛件與DOM依賴的步驟都放在這裏。
主要交互動做就是經過doSearch方法發送搜索關鍵字。
控制器有建立請求的能力。它經過調用doSearch方法將事件與搜索欄關聯起來,他組合請求對象並調用存儲的fetch方法。
當搜索成功後,咱們沒有在這裏直接處理結果,而是經過renderItem方法來處理每一個結果,幫組咱們實現關注分離。
1 doSearch= function() { 2 // summary: 3 // initiate a search for the given keywords 4 var terms = dom.byId("searchTerms").value; 5 if(!terms.match(/\w+/)) { 6 return; 7 } 8 var listNode = createTab(terms); 9 var results = store.fetch({ 10 query: lang.delegate(flickrQuery, { 11 text: terms 12 }), 13 count: 10, 14 onItem: function(item) { 15 // first assign and record an id 16 // render the items into the <ul> node 17 var node = renderItem(item, listNode); 18 }, 19 onComplete: endLoading 20 }); 21 },
第4步 搜索結果
要處理從store返回的結果,我須要建立renderItem方法。注意,流程沒有變,標記沒有變,如何獲取數據與如何渲染數據依然是分離的。
爲了有助於渲染咱們爲應用控制器添加一些屬性——元素內容模版,和一些Flickr返回的用於查找url的對象路徑。
1 var itemTemplate = '<img src="${thumbnail}">${title}'; 2 var itemClass = "item"; 3 var itemsById = {}; 4 5 var largeImageProperty = "media.l"; // path to the large image url in the store item 6 var thumbnailImageProperty = "media.t"; // path to the thumb url in the store item
如此以來renderItem就能夠工做了:
第5步 查看大圖
第6步 加載遮罩
第7步 交錯加載
第8步 進一步改進
最終代碼
demo/app的代碼看起來以下:
1 define([ 2 "dojo/dom", 3 "dojo/dom-style", 4 "dojo/dom-class", 5 "dojo/dom-construct", 6 "dojo/dom-geometry", 7 "dojo/string", 8 "dojo/on", 9 "dojo/aspect", 10 "dojo/keys", 11 "dojo/_base/config", 12 "dojo/_base/lang", 13 "dojo/_base/fx", 14 "dijit/registry", 15 "dojo/parser", 16 "dijit/layout/ContentPane", 17 "dojox/data/FlickrRestStore", 18 "dojox/image/LightboxNano", 19 "demo/module" 20 ], function(dom, domStyle, domClass, domConstruct, domGeometry, string, on, aspect, keys, config, lang, baseFx, registry, parser, ContentPane, FlickrRestStore, LightboxNano) { 21 var store = null, 22 preloadDelay = 500, 23 flickrQuery = config.flickrRequest || {}, 24 25 itemTemplate = '<img src="${thumbnail}">${title}', 26 itemClass = 'item', 27 itemsById = {}, 28 29 largeImageProperty = "media.l", 30 thumbnailImageProperty = "media.t", 31 32 startup = function() { 33 store = new FlickrRestStore(); 34 initUi(); 35 aspect.before(store, "fetch", function() { 36 startLoading(registry.byId("tabs").domNode); 37 }); 38 }, 39 40 endLoading = function() { 41 baseFx.fadeOut({ 42 node: dom.byId("loadingOverlay"), 43 onEnd: function(node) { 44 domStyle.set(node, "display", "none"); 45 } 46 }).play(); 47 }, 48 49 startLoading = function(targetNode) { 50 var overlayNode = dom.byId("loadingOverlay"); 51 if("none" == domStyle.get(overlayNode, "display")) { 52 var coords = domGeometry.getMarginBox(targetNode || document.body); 53 domGeometry.setMarginBox(overlayNode, coords); 54 55 domStyle.set(dom.byId("loadingOverlay"), { 56 display: "block", 57 opacity: 1 58 }); 59 } 60 }, 61 62 initUi = function() { 63 lightbox = new LightboxNano({}); 64 65 on(dom.byId("searchTerms"), "keydown", function(event) { 66 if(event.keyCode == keys.ENTER) { 67 event.preventDefault(); 68 doSearch(); 69 } 70 }); 71 72 on(dom.byId("searchBtn"), "click", doSearch); 73 74 endLoading(); 75 }, 76 77 78 doSearch = function() { 79 var terms = dom.byId("searchTerms").value; 80 if(!terms.match(/\w+/)) { 81 return; 82 } 83 var listNode = createTab(terms); 84 var results = store.fetch({ 85 query: lang.delegate(flickrQuery, { 86 text: terms 87 }), 88 count: 10, 89 onItem: function(item) { 90 itemsById[item.id] = item; 91 var node = renderItem(item); 92 node.id = listNode.id + '_' + item.id; 93 listNode.appendChild(node); 94 }, 95 onComplete: endLoading 96 }); 97 }, 98 99 showImage = function(url, originNode) { 100 lightbox.show({ 101 href: url, 102 origin: originNode 103 }); 104 }, 105 106 createTab = function(term, items) { 107 var contr = registry.byId("tabs"); 108 var listNode = domConstruct.create("ul", { 109 "class": "demoImageList", 110 "id": "panel" + contr.getChildren().length 111 }); 112 113 var panel = new ContentPane({ 114 title: term, 115 content: listNode, 116 closable: true 117 }); 118 contr.addChild(panel); 119 contr.selectChild(panel); 120 121 var hdl = on(listNode, "click", onListClick); 122 return listNode; 123 }, 124 125 showItemById = function(id, originNode) { 126 var item = itemsById[id]; 127 if(item) { 128 showImage(lang.getObject(largeImageProperty, false, item), originNode); 129 } 130 }, 131 132 onListClick = function(event) { 133 var node = event.target, 134 containerNode = registry.byId("tabs").containerNode; 135 136 for(var node = event.target; (node && node !== containerNode); node = node.parentNode) { 137 if(domClass.contains(node, itemClass)) { 138 showItemById(node.id.substring(node.id.indexOf("_") + 1), node); 139 break; 140 } 141 } 142 }, 143 144 renderItem = function(item, refNode, posn) { 145 itemsById[item.id] = item; 146 var props = lang.delegate(item, { 147 thumbnail: lang.getObject(thumbnailImageProperty, false, item) 148 }); 149 150 return domConstruct.create("li", { 151 "class": itemClass, 152 innerHTML: string.substitute(itemTemplate, props) 153 }, refNode, posn); 154 }; 155 156 return { 157 init: function() { 158 startLoading(); 159 startup(); 160 } 161 }; 162 });
總結
在咱們構建這個應用的時候按着這種方式作了不少決策。在任什麼時候候答案都是不同的,不一樣的需求或預設。如: