翻譯 - 【Dojo Tutorials】Application Controller

原文:Application Controllerhtml

一個頁面級別的控制器就像膠水,經過將模塊化的功能黏在一塊兒來構造一個鮮活的應用。咱們將實現配置與一個明確的生命週期,經過鬆耦合的架構組合一個單頁面應用的多個部分。node

介紹

做爲一個模塊化的工具包,不少Dojo的文檔都是在講解單獨的組件如何使用。可是當你須要組合它們來建立一個應用的時候,你須要一個框架來將它們靈活的組織起來。設計模式

問題

最佳實踐建議保持關注點分離,維護組成應用的模塊。因此,如何管理各個組件的加載與初始化,如何將它們與數據結合起來,用戶界面處理是否靈活與模塊化?api

解決方案

一個頁面級別的控制器是一個對象,它有管理頁面或應用的職責。它假定控制應用的生命週期與各部分的加載。它按正確的順序初始化與鏈接那些部件,並能掌控大局。服務器

big picture

討論

Dojo並無建議咱們該如何將它所提供的組件組合成一個應用。它有全部的瓶瓶罐罐,就是沒有藍圖。做爲一個工具包,就是這麼設計的。你能夠在你的靜態頁面使用一些Dojo組件來點綴,或者使用它來構建一個純GUI的應用,使用哪一種設計模式與實現方式取決與你的選擇。對於這份教程,咱們取個折中,構建這個實現有些關鍵需求:架構

  • 利用Dojo的包系統來幫助模塊加載,經過build腳原本優化
  • 模塊化維護——避免把一些應用的特殊內容編寫進組件中去
  • 保持關注點的分離——UI應該與數據分離

開始

咱們的需求是構建一個應用容許用戶搜索Flickr上的照片,按縮略圖展現結果,點擊每一個縮略圖查看對應的大圖。這種主—祥(我取的)模式,在不少應用中都有。在本教程中咱們專一於組合——如何把分離的部分組合在一塊兒——因此咱們先大體預覽一個它們各個部分。app

存儲

應用的數據層由dojox/data/FlickrStore來處理。這是一個開箱即用的組件,實現了dojo/data的讀取API,發送請求到Flickr的API服務器。框架

咱們使用標準的fetch方法來傳遞查詢,將轉化成到Flickr服務器的JSONP請求,響應並觸發onComplete回調。其餘組件應該多少知道一點Flickr的東西。任何特殊的需求都應該限制在store的實例中實現,經過提供的配置——也就是咱們應用提供的。dom

UI佈局

咱們使用在佈局教程中有講過的基於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方法來增減加載遮罩。加載遮罩是應用級別頁面關心的事情,因此遮罩的顯隱放在應用的控制器中來管理。

第1步 佈局

在這個應用中,咱們使用聲明式的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>

一切都很好,每樣東西都在它們應在的位置,可是沒有功能,咱們須要把功能放在什麼地方。固然就是應用控制器中啦。

第2步 應用控制器

咱們爲應用控制器建立一個新的模塊。

 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方法發送搜索關鍵字。

第3步 搜索鉤子

控制器有建立請求的能力。它經過調用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 });

 

總結

在咱們構建這個應用的時候按着這種方式作了不少決策。在任什麼時候候答案都是不同的,不一樣的需求或預設。如:

  • 咱們固然能夠更整潔地建立自定義掛件來封裝結果列表
  • 控制器能夠派生自一個類
  • 咱們可使用通用的數據存儲,甚至是較新的dojo/store API
  • 咱們能夠是用自擁有對象來呈現用戶界面——控制器與「總體掛件」打交道。
相關文章
相關標籤/搜索