首页 > 代码库 > 翻译 - 【Dojo Tutorials】Application Controller

翻译 - 【Dojo Tutorials】Application Controller

原文:Application Controller

一个页面级别的控制器就像胶水,通过将模块化的功能黏在一起来构造一个鲜活的应用。我们将实现配置与一个明确的生命周期,通过松耦合的架构组合一个单页面应用的多个部分。

介绍

作为一个模块化的工具包,很多Dojo的文档都是在讲解单独的组件如何使用。但是当你需要组合它们来创建一个应用的时候,你需要一个框架来将它们灵活的组织起来。

问题

最佳实践建议保持关注点分离,维护组成应用的模块。所以,如何管理各个组件的加载与初始化,如何将它们与数据结合起来,用户界面处理是否灵活与模块化?

解决方案

一个页面级别的控制器是一个对象,它有管理页面或应用的职责。它假定控制应用的生命周期与各部分的加载。它按正确的顺序初始化与连接那些部件,并能掌控大局。

big picture

讨论

Dojo并没有建议我们该如何将它所提供的组件组合成一个应用。它有所有的瓶瓶罐罐,就是没有蓝图。作为一个工具包,就是这么设计的。你可以在你的静态页面使用一些Dojo组件来点缀,或者使用它来构建一个纯GUI的应用,使用哪种设计模式与实现方式取决与你的选择。对于这份教程,我们取个折中,构建这个实现有些关键需求:

  • 利用Dojo的包系统来帮助模块加载,通过build脚本来优化
  • 模块化维护——避免把一些应用的特殊内容编写进组件中去
  • 保持关注点的分离——UI应该与数据分离

开始

我们的需求是构建一个应用允许用户搜索Flickr上的照片,按缩略图展示结果,点击每个缩略图查看对应的大图。这种主—祥(我取的)模式,在很多应用中都有。在本教程中我们专注于组合——如何把分离的部分组合在一起——所以我们先大致预览一个它们各个部分。

存储

应用的数据层由dojox/data/FlickrStore来处理。这是一个开箱即用的组件,实现了dojo/data的读取API,发送请求到Flickr的API服务器。

我们使用标准的fetch方法来传递查询,将转化成到Flickr服务器的JSONP请求,响应并触发onComplete回调。其他组件应该多少知道一点Flickr的东西。任何特殊的需求都应该限制在store的实例中实现,通过提供的配置——也就是我们应用提供的。

UI布局

我们使用在布局教程中有讲过的基于BorderContainer的布局。每个搜索结果将会有它自己的tab在TabContainer中,占居中心的区域。

表单

用户在顶部的输入框中输入搜索关键词。他们可以点击搜索按钮,或按下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: true14         }]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: terms12         }),13         count: 10,14         onItem: function(item) {15             // first assign and record an id16             // render the items into the <ul> node17             var node = renderItem(item, listNode);18         },19         onComplete: endLoading20     });21 },

 

第4步 搜索结果

要处理从store返回的结果,我需要创建renderItem方法。注意,流程没有变,标记没有变,如何获取数据与如何渲染数据依然是分离的。

为了有助于渲染我们为应用控制器添加一些属性——元素内容模版,和一些Flickr返回的用于查找url的对象路径。

1 var itemTemplate = ‘<img src="http://www.mamicode.com/${thumbnail}">${title}‘;2 var itemClass = "item";3 var itemsById = {};4 5 var largeImageProperty = "media.l"; // path to the large image url in the store item6 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="http://www.mamicode.com/${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: originNode103             });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().length111             });112 113             var panel = new ContentPane({114                 title: term,115                 content: listNode,116                 closable: true117             });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
  • 我们可以是用自拥有对象来呈现用户界面——控制器与“整体挂件”打交道。

 

翻译 - 【Dojo Tutorials】Application Controller