使用Backbone.js在Tizen平台上创建MVC应用程序

简介

如果不用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”,用来在页和页之间进行导航。 这个搜索页面有点像谷歌的搜索页面。 下面是该应用程序的截图。

 

 

 

 

图1:应用程序截图   

 

 

 

在讲解代码之前,我们先来浏览一下谷歌的Custom Search API。 为了更好的讲解这个应用程序,我们创建了一个谷歌账号,并做了必要的设置。 您可以用这个账号来测试这个应用是怎样工作的。 一天最多只能进行100次免费搜索,多余100次就要付费了。 可能当您正在测试这个应用的时候,其他人超出了这个限制。 这时,浏览器不会返回任何的搜索结果,那您就必须再等24小时或者您重新创建账号。

 

 

 

设置Custom Search API

这里我将简要介绍一下Custom Search API的设置。 想要了解更加详细的信息,最好是阅读谷歌的Custom Search文档,并研究一下谷歌API的控制台程序。 我们需要两样东西:Custom Search标示符和API关键字。

  1. 首先,您需要创建一个谷歌账户
  2. 然后登录到Custom Search
  3. 您需要进入到搜索页面来建立一个Custom Search。 您只是使用谷歌所有的搜索页面,所以这儿您可以输入任何域名。
    选择名字和语言。
  4. 现在进入了浏览器列表,对您刚才创建的浏览器进行编辑。 删除上一步中您进入的页面,选择搜索整个网络。 如果需要的话,选择语言类型为All languages。
  5. 然后,通过点击对应名字的按钮来拷贝搜索标示符。 这个标示符将会在API中用到。
  6. 打开谷歌控制台应用程序,并创建一个新的工程,激活Custom Search API选项。 选择同意,这样就准备就绪了。
  7. 最后,您需要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模型,并能减少应用程序创建的时间。

文件附件: 
List
SDK Version Since: 
2.3.1