使用Backbone.js在Tizen平台上创建MVC应用程序
PUBLISHED
简介
如果不用MVC(Model View Controller)架构,就不能创建出复杂的应用程序。 简单的应用程序可以在文件中用代码来实现,但这在实际中并不可取。 本文中,我们将使用BACKBONE JS库来创建一个搜索应用程序,该应用程序是基于谷歌的Custom Search API的。 我们将讨论以下几个问题:开发者怎样管理应用程序数据,怎样显示模板,怎样建立层与层之间的联系。 首先,我们将讨论backbone.js是怎样来管理model,view和controller这三个层的,然后我们将研究具体的应用程序实现过程。
Model
Model用来保持应用程序数据的独立性。 有了backbone.js,我们可以使设置在对象上所有数据值生效,可以通过其他对象的属性计算当前属性的属性值,转换属性,还可以控制对属性的访问权限。 首先,我们看看怎样创建一个model。
var User = Backbone.Model.extend({ /* Constructor */ initialize: function () { }, /* Methods */ getFullName: function () { return this.get('name') +' '+ this.get('surname'); }, /* Default attributes' values */ defaults: { name: 'John', surname: 'Smith' } });
从上面的代码中可以看到,用Backbone.Model.extend()可以创建一个默认的model。 我们不需要声明属性。 它们可以动态的加载到创建的对象上。 您可以像代码中那样定义默认的属性值。 我们设置默认的名称为”name“和”surname“。 我们同时还定义了一个自定义的函数getFullName()和初始化函数initialize()。 为了对给定的model对象进行实例化,我们使用了JavaScript的new标示符。
var john = new User({surname: 'Johnson'});
我们给初始化函数传递两个对象。 第一个对象是定义了属性的对象,第二个对象是带有property的对象。 想了解Backbone.js,请参考相关的文档。 初始化函数的两个入参对象都是可选的。
设置和获取对象的属性
每个对象都有多种方式来对它的属性进行操作。 首先,我们可以通过has()函数来检查对象是否有属性,has()函数的参数是属性的名称。
user.has('name');
我们通过属性的名称来设置或获取属性。 设置属性时,函数的第一个参数是属性名称,第二个参数是属性的值。 获取属性值时,我们只传递属性的名称给get()函数。
user.set('name', 'John'); user.get('name'); // 'John'
- escape() - 返回属性值的转义版本。 对每个属性进行转义是防止HTML代码受XSS攻击的有效方法。
- clear() - 删除所有的对象属性,
- unset() - 通过名称来删除对应的属性。
继承
Backbone.Model有个extend()函数,这个函数是从Backbone.Model的延伸对象继承过来的。 通过这个函数我们可以实现多级继承。
var Fruit = Backbone.Model.extend({ someMethod: function() {} }); var Apple = Fruit.extend({ /* Override method */ someMethod: function() {} });
没有简单的方法来调用父model的函数, 你必须直接调用父model的函数。 调用方式如下面的代码所示。
var Apple = Fruit.extend({ /* Override method */ someMethod: function() { /* Call overridden function from the parent */ Fruit.prototype.someMethod.apply(this, arguments); } });
想要了解更多关于继承性和extend()函数的信息,请参考这个网页。
从server保存,删除,获取对象
REST应用程序最重要的部分就是在client和server之间进行通信。 client可以通过server提供的REST API调用URL来访问server的资源。 server上的URL具有类似于”user/5“这种形式,它的含义是ID是5的用户对象。 Backbone.js会发送具有某种操作方法的HTTP请求:要获取数据的时候,会发送GET方法,更新或创建的时候会发送POST方法,删除的时候会发送DELETE方法。 默认的URL模式为”[collection.url]/[id]”,你也可以重载model定义中的url属性。 这个属性可以是一个字符串或者是一个返回值为字符串的函数。
要保存server上的对象,直接调用指定对象的save()函数就行了。 save()函数会将要存储的属性作为第一个参数列表。 第二个参数是调用成功或失败是触发的回调函数。 如果server上的对象存在,save()函数仅仅是更新该对象的属性。
要从server上删除对象,需要调用destroy()函数。 destroy()函数会发送一个DELETE方式的HTTP请求。 我们也可以选择性的用回调函数作为destroy()函数的参数。
使用fetch()函数从server上获取数据。 fetch()函数会获取到数据,并将数据存放到对应的model对象上。 前面我们说过,这儿您不需要指定model的属性。 在取数据的时候,对应的model所没有的属性就会被创建。
要了解更多信息,请参考相关的文档。
View
Backbone的view不会指定您所使用的模板系统。 view只是提供一些约定,这些约定会通过对象数据传递,绑定对象变化事件来帮助您管理view的渲染过程。 用Backbone.View.extend()函数来创建view,这个函数的参数是我们所指定的view的属性。
view适合DOM单元一一对应的,DOM单元存储在view的“el”属性中。 如果不指定任何的element,就会创建一个名称为div的默认element,但是您必须手动将这个element插入到DOM树中。 如果您指定了一些属性,比如className,tagName,id,attributes等,那么在创建DOM element时,这些属性都会生效。
var MyView = Backbone.View.extend({ tagName: 'div', el: '#element', /* ... */ });
initialize()函数可以对view对象进行一些初始化的操作。 最常用的方法就是调用listenTo()函数,该函数将对象的变化绑定到view上。
initialize: function() { /* Listen to changes of the model and if any change occure call view's render function. */ this.listenTo(this.model, 'change', this.render); }
render()函数是用来做模板渲染的。 要是用这个函数,您必须要有一个模板,可能需要对该模板进行编译,并把对象数据传递给该模板。 通过HTML的代码,您可以将该模板存放到view的el单元中。 view含有一个特殊的属性叫$el,这个属性是el单元通过jQuery对象封装得到的。 在讲述示例应用程序的时候,我会讲一下render()函数的实例代码。
事件授权
另一个值得注意的问题是事件授权。 您可以绑定将鼠标或键盘事件绑定到模板元素上,并注册一些回调函数来进行需要的操作。 events属性是用来定义事件授权的。 事件用下面的格式定义。
{'event selector': 'callback'}
事件授权用jQuery的on()函数来匹配选择器,并将函数绑定到事件上。 callback字符串是事件触发的回调函数的名称。
使用listenTo()函数会自动绑定一些事件的回调函数。 一方面,model变化的时候会自动刷新模板,另一方面,您可以根据用户的操作来对model做出变化。 所有的这些操作都是自动完成的,这些代码都在MVC model中,这样代码就很易读。
Controller
实际上,事件授权是MVC模型中controller层的一部分,但我们还是将事件授权放在了view层中。 当然,您也可以将事件授权的定义放到其他的文件中。 在创建controller的时候,您可以使用view对象的delegateEvents()函数将时间定义传递给view。 代码如下:
var Controller = (function() { var _home = function () {}; var _contact = function () {}; var _exit = function () { /* Delegate events. */ MyView.delegateEvents({ 'click .exit': function () { /* Close Tizen application when element with 'exit' class was clicked. */ tizen.application.getCurrentApplication.exit(); } }); }; /* Return public module interface. */ return { home : _home, contact : _contact, exit : _exit }; }());
MVC模型中的变量很多,怎样组织代码由您自己决定。 编写代码的时候遵循一些常用的规则对写代码是很有好处的。
Routing
Routing用来匹配URL和显示的网页。 数据库中有很多条记录,每条记录都有一个唯一的ID,我们通过将这个ID写到URL中,并以URL地址的形式来访问这些记录,URL地址的形式为:record/ID。 有时,通过URL访问应用程序的页面是可以满足需求的,比如:
- About – ‘/about’
- Home – ‘/’
- Send – ‘/send’
当然,我们不使用routing也能实现页面变化和记录的展现,但是,如果应用程序不只是想部署在Tizen平台上,还想部署在web上的话,建议使用routing的方法,这样的应用程序具有SEO(Search Engine Optimization)的特性。
在Backbone.js中有一个特殊的对象来管理routing - “Backbone.Router”。 我们需要创建router,定义路径和函数,以便在route和URL匹配是做相应的操作。
var Router = Backbone.Router.extend({ routes: { '': 'home', 'record/:id': 'record' /* Route with ID parameter. */ }, home: function () { Controller.home(); }, /* Parameter are passed as function's arguments. */ record: function(id) { } });
每个route都有一个对应的函数,该函数在URL和route匹配时被触发。 稍后,我们将会将句柄传递给controller的函数,如果代码不是太多的话,也可以直接在router对象内部进行处理。
Routes可以带参数。 参数以冒号开始传递给函数作为入参。 我们也可以用正则表达式来匹配URL。 您可以在官网上找到详细的描述。
示例应用程序
这个叫“Search”的示例应用程序是一个浏览器应用,它是用谷歌的Custom Search API实现的。 这个应用程序很简单,就只有一个搜索栏和一个查找按钮。 一页能显示10条搜索结果。 如果搜索结果多余10条,则会在搜索栏的下方显示导航信息。 在导航信息中有两个链接按钮,“Previous page”和“Next page”,用来在页和页之间进行导航。 这个搜索页面有点像谷歌的搜索页面。 下面是该应用程序的截图。
在讲解代码之前,我们先来浏览一下谷歌的Custom Search API。 为了更好的讲解这个应用程序,我们创建了一个谷歌账号,并做了必要的设置。 您可以用这个账号来测试这个应用是怎样工作的。 一天最多只能进行100次免费搜索,多余100次就要付费了。 可能当您正在测试这个应用的时候,其他人超出了这个限制。 这时,浏览器不会返回任何的搜索结果,那您就必须再等24小时或者您重新创建账号。
设置Custom Search API
这里我将简要介绍一下Custom Search API的设置。 想要了解更加详细的信息,最好是阅读谷歌的Custom Search文档,并研究一下谷歌API的控制台程序。 我们需要两样东西:Custom Search标示符和API关键字。
- 首先,您需要创建一个谷歌账户。
- 然后登录到Custom Search。
- 您需要进入到搜索页面来建立一个Custom Search。 您只是使用谷歌所有的搜索页面,所以这儿您可以输入任何域名。
选择名字和语言。 - 现在进入了浏览器列表,对您刚才创建的浏览器进行编辑。 删除上一步中您进入的页面,选择搜索整个网络。 如果需要的话,选择语言类型为All languages。
- 然后,通过点击对应名字的按钮来拷贝搜索标示符。 这个标示符将会在API中用到。
- 打开谷歌控制台应用程序,并创建一个新的工程,激活Custom Search API选项。 选择同意,这样就准备就绪了。
- 最后,您需要API关键字,这个关键字可以在谷歌API控制台程序的API Access页面中找到。
配置和初始化
由于谷歌Custom Search API需要访问谷歌服务器,所以我们要给应用程序对URL的访问权限。 通过在config.xml文件中添加域名来控制对URL的访问权限。
<access origin="https://www.googleapis.com" subdomains="true"/>
应用程序的入口是“app.js”文件,这个文件做了所有的初始化工作。 API关键字和Custom Search标示符存储在“_config”变量中,并可以通过公共接口“app.config.KEY”和“app.config.CX”来使用。
_config = { KEY: 'AIzaSyA9BValsidKl37iNJhNOaFbiDD_VOVQmDM', CX: '017789846576532612536:voiha7bulha' };
在初始化函数“_initialize”函数内部会对model,view和controller进行初始化。 我们在研究应用程序的MVC层是会对它进行详细的讨论。
依赖
Backbone.js的稳定版本和开发版本都可以从官网上下载。 为了能使Backbone正常运行,您必须包含Underscore.js,这个库提供了大概80个JavaScript的扩展功能。 这个库将帮助我们管理array,object和collection。 您可以从官网上了解更多信息。 jQuery库也必须被包含。
示例应用程序使用的库版本为:Backbone.js 1.0.0,Underscore.js 1.4.4,jQuery 2.0.0。
<script type="text/javascript" src="js/libs/jquery-2.0.0.js"></script> <script type="text/javascript" src="js/libs/underscore-1.4.4.js"></script> <script type="text/javascript" src="js/libs/backbone-1.0.0.js"></script>
Model
这儿我将应用程序分成两部分,第一部分是查询,第二部分是搜索结果。 相关的model已经被创建。
“SearchQuery” model只包含一个“query”属性。
var SearchQuery = Backbone.Model.extend({ defaults: { query: null } });
“SearchResult” model是应用程序的核心。 它负责对谷歌服务器发起数据请求。 它有两个属性,“query”和“start”。 第一个属性用来存储需要搜索的问题,第二个用来标示从哪个条目开始搜索。 当然,这些属性也用来分页。
只要query被设置,我们就会重载“fetch()”函数来获取数据。 这样就会减少对服务器的询问数量。 如果query没有被设置,就会返回空。 父类的“fetch()”函数是值得讨论的。 对于Backbone.js,没有函数可以帮助调用父类的方法,所以我们就需要明确地调用父类的函数,在前面的继承章节中我们也提到过这一点。
/* Override the fetch method to retrieve data only if the query was set. */ fetch: function(options) { if (this.has('query')) { /* Call parent method. */ return Backbone.Model.prototype.fetch.call(this, options); } return; }
前面提到过,任何一个model都有一个“url”属性用来标示资源。 它可以是一个字符串,也可以用函数动态产生。 这儿,我们用API关键字,Custom Search标示符,query和start这些属性了创建URL。 创建的query字符串遵循这样的模式: ‘https://www.googleapis.com/customsearch/v1?key={API_KEY}&cx={CUSTOM_SEARCH_ID}&q={QUERY}&start={START}。
url: function () { var url, params; params = { key: app.config.KEY, cx: app.config.CX }; if (this.has('query')) { params.q = this.get('query'); } if (this.has('start')) { params.start = this.get('start'); } url = 'https://www.googleapis.com/customsearch/v1'; url += '?'+ jQuery.param(params); return url; }
View
每个model都有一个对应的view,SearchQuery model对应于SearchQueryView,SearchResult model对应于SearchResultView。 我使用Handlebar模板系统来渲染view。 您可以从官网上下载Handlebar模板系统。 它必须被包含在“index.html”文件的HEAD部分。
Handlebar是一个很简单的语义模板系统,它和Mustache模板系统兼容。 为了将模板放在“index.html”文件的HEAD部分, 必须要用SCRIPT标签对模板进行封装。
<script id="search-query-template" type="text/x-handlebars-template"> <header>Search Application<span class="close">x</span></header> <form action="#" class="search-query"> <div class="search-input-padding"><input id="query" name="query" type="text" value="{{query}}" autocomplete="off" /></div><input id="search" type="submit" value="Search" /> </form> </script> <script id="search-result-template" type="text/x-handlebars-template"> <ul class="search-result"> {{#if searchInformation}} <li class="result-stats">Around {{searchInformation.formattedTotalResults}} results ({{searchInformation.formattedSearchTime}} s)</li> {{/if}} {{#if items}} {{#each items}} <li class="search-item"> <h3><a href="{{link}}">{{{htmlTitle}}}</a></h3> <div class="url">{{{htmlFormattedUrl}}}</div> <div class="snippet">{{{htmlSnippet}}}</div> </li> {{/each}} {{/if}} <li class="pagination"> <ul> {{#if queries.previousPage}} <li><a href="#query/{{queries.request.0.searchTerms}}/start/{{queries.previousPage.0.startIndex}}">Previous page</a></li> {{/if}} {{#if queries.nextPage}} <li><a href="#query/{{queries.request.0.searchTerms}}/start/{{queries.nextPage.0.startIndex}}">Next page</a></li> {{/if}} </ul> </li> </ul> </script>
在渲染时,每个SCRIPT标签都有一个ID来标示。 “type”属性要设置为“text/x-handlebars-template”。 Handlebar其实就是花括号中的表达式。 我们可以使用预定义的表达式,也可以创建自己的表达式。 在示例应用中,我只是用了三个表达式。 您可以在官网上了解更多关于创建模板和表达式工作流程的文章。 在示例应用中,模板被直接包含在index.html文件中,它们也可以进行预编译,并以外部的JavaScript文件形式包含到index.html文件中。 点击这儿,您可以了解更多。
“#search-query-holder”是一个选择器,用来选择两个DIV中的某一个,我们将“SearchQueryView”的“el”属性设置为“#search-query-holder”。 第二个DIV的ID是“#search-result-holder”,顾名思义,保存了搜索的结果。
对于“events”属性,定义了两个动作。 一个动作是当用户点击“X”时,关闭应用程序,第二个就是提交窗体。
events: { 'click .close': 'close', 'submit form': 'submit' },
当窗体被提交后,“submit()”函数就会被执行。 该函数执行后,就能从窗体中获取到需要搜索的疑问,并形成一个新的URL地址,并对该地址进行设置。
submit: function (e) { var query; e.preventDefault(); query = this.$el.find('#query').val(); if (query) { document.location = '#query/' + query; } else { document.location = '#'; } }
“initialize()”函数只做一件事情, 就是监听model的变化。 在这种情况下,model是“SearchQuery”类的实例,它在“app.js”文件中定义,并作为“SearchQueryView”构造函数的第一个参数。
/* js/view/SearchQueryView.js */ initialize: function () { this.listenTo(this.model, 'change', this.render); } /* js/app.js */ /* Create objects. */ app.searchQuery = new SearchQuery(); app.searchResult = new SearchResult(); /* Create view. */ app.searchQueryView = new SearchQueryView({model: app.searchQuery}); app.searchResultView = new SearchResultView({model: app.searchResult});
“SearchQueryView”的最后一个功能就是“render()”,该功能能获取模板的代码,并对代码进行编译,参数传递和渲染等操作。 渲染好的模板就会被放到前面提到的拥有“#seach-query-holder” ID属性的DIV单元中。
render: function () { var source; if (!this.template) { this.template = Handlebars.compile($('#search-query-template').html()); } this.$el.html(this.template(this.model.attributes)); }
现在我们来研究“SearchResultView”。 它比较简单。 它也有一个“el”属性,定义在DIV单元中。 它也监听model的变化,但是这个model对象是SearchResult的实例。 渲染模板的过程和上面提到的是一样的,唯一的不同点就是代码不一样。
var SearchResultView = Backbone.View.extend({ tagName: 'div', el: '#search-result-holder', initialize: function () { this.listenTo(this.model, 'change', this.render); }, render: function () { var source; if (!this.template) { this.template = Handlebars.compile($('#search-result-template').html()); } this.$el.html(this.template(this.model.attributes)); } });
Controller
在示例应用程序中,controller和它的本意有点不一样。 代码很简单,大部分的逻辑和model与view也差不多。 但是,这儿需要有一个routing系统,所以我创建了一个Router对象。 这个Router对象定义了三个route。 第一个route为应用程序主界面服务,第二个为搜索结果的界面服务,最后一个为用户的导航服务。
routes: { '': 'search', 'query/:query': 'search', 'query/:query/start/:start': 'search' }
每个route都执行同一个“search()”函数。 这个函数做的第一件事就是清除“app.searchResult”对象,同时也清除了前一次搜索的搜索结果。 然后,根据router传递给“search()”函数的参数来设置“app.searchQuery”和“app.searchResult”对象的属性。 最后调用“fetch()”函数从谷歌服务器上获取搜索数据。
search: function (query, start) { app.searchResult.clear(); if (query !== undefined) { app.searchResult.set('query', query); app.searchQuery.set('query', query); } if (start !== undefined) { app.searchResult.set('start', start); } app.searchResult.fetch(); }
router首先要被初始化。 我们在“app”模块的“_initialize()”函数中对router进行了初始化。
/* Initialize router. */ app.router = new Router(); Backbone.history.start();
最后就是在应用程序启动时,渲染空模板,这样这个流程就能工作了。 如果不渲染,屏幕上就不会有任何的显示。
app.searchQueryView.render(); app.searchResultView.render();
总结
希望本文能帮助您理解Backbone的工作流程,并能通过本文学会创建动态的应用程序。 Backbone框架是学会创建Tizen Web Applications的基础。 它遵循REST模式和MVC模型,并能减少应用程序创建的时间。