翻译 - 【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: 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
  • 我们可以是用自拥有对象来呈现用户界面——控制器与“整体挂件”打交道。

 

posted on 2014-12-15 11:02  古道倦马  阅读(302)  评论(0编辑  收藏  举报

导航