Backbone实例todos分析

源码来自:http://todomvc.com/examples/backbone/

这是一个用Backbone.js完成的待办事项实例,精简但完善,可以帮助很好的帮助理解Backbone的API,MVC框架和写webApp的基本思路。

下面让我们一步步分析,

准备工作:

首先是html页面,新建index.html,并保存以下代码:

<!doctype html>

<html lang="en" data-framework="backbonejs">

    <head>

        <meta charset="utf-8">

        <title>Backbone.js • TodoMVC</title>

        <link rel="stylesheet" href="bower_components/todomvc-common/base.css">

    </head>

    <body>

        <section id="todoapp">

            <header id="header">

                <h1>todos</h1>

                <input id="new-todo" placeholder="What needs to be done?" autofocus>

            </header>

            <section id="main">

                <input id="toggle-all" type="checkbox">

                <label for="toggle-all">Mark all as complete</label>

                <ul id="todo-list"></ul>

            </section>

            <footer id="footer"></footer>

        </section>

        <footer id="info">

            <p>Double-click to edit a todo</p>

            <p>Written by <a href="https://github.com/addyosmani">Addy Osmani</a></p>

            <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>

        </footer>

        <!-- Templates -->

        <script type="text/template" id="item-template">

            <div class="view">

                <input class="toggle" type="checkbox" <%= completed ? 'checked' : '' %>>

                <label><%- title %></label>

                <button class="destroy"></button>

            </div>

            <input class="edit" value="<%- title %>">

        </script>

        <script type="text/template" id="stats-template">

            <span id="todo-count"><strong><%= remaining %></strong> <%= remaining === 1 ? 'item' : 'items' %> left</span>

            <ul id="filters">

                <li>

                    <a class="selected" href="#/">All</a>

                </li>

                <li>

                    <a href="#/active">Active</a>

                </li>

                <li>

                    <a href="#/completed">Completed</a>

                </li>

            </ul>

            <% if (completed) { %>

            <button id="clear-completed">Clear completed (<%= completed %>)</button>

            <% } %>

        </script>

        <script src="bower_components/jquery/jquery.js"></script>

        <script src="bower_components/underscore/underscore.js"></script>

        <script src="bower_components/backbone/backbone.js"></script>

        <script src="bower_components/backbone.localStorage/backbone.localStorage.js"></script>

        <script src="js/models/todo.js"></script>

        <script src="js/collections/todos.js"></script>

        <script src="js/views/todo-view.js"></script>

        <script src="js/views/app-view.js"></script>

        <script src="js/routers/router.js"></script>

        <script src="js/app.js"></script>

    </body>

</html>
View Code

这是webapp的主页面,它引入的文件有样式表base.css;js框架jquery.js,underscore.js,backbone.js,backbone.localStorage.js,请大家自行引入。然后是即将利用backbone编写的js代码.为了方便理解,原作者将这些JS代码分成了5个JS文件,我们将深入分析。

先简单介绍一下这些js框架和我们页面的关系:

1.jquery.js:我们将利用jquery操作dom,

2.underscore.js:backbone的依赖库,提供一下方便的方法,和模板的操作。

3.backbone.js:我们主要使用的MVC框架

4.backbone.localStorage.js:backbone的拓展库,用来操作HTML5新加入的本地存储

页面的样式表base.css

html,

body {

    margin: 0;

    padding: 0;

}



button {

    margin: 0;

    padding: 0;

    border: 0;

    background: none;

    font-size: 100%;

    vertical-align: baseline;

    font-family: inherit;

    color: inherit;

    -webkit-appearance: none;

    -ms-appearance: none;

    -o-appearance: none;

    appearance: none;

}



body {

    font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;

    line-height: 1.4em;

    background: #eaeaea url('bg.png');

    color: #4d4d4d;

    width: 550px;

    margin: 0 auto;

    -webkit-font-smoothing: antialiased;

    -moz-font-smoothing: antialiased;

    -ms-font-smoothing: antialiased;

    -o-font-smoothing: antialiased;

    font-smoothing: antialiased;

}



button,

input[type="checkbox"] {

  outline: none;

}



#todoapp {

    background: #fff;

    background: rgba(255, 255, 255, 0.9);

    margin: 130px 0 40px 0;

    border: 1px solid #ccc;

    position: relative;

    border-top-left-radius: 2px;

    border-top-right-radius: 2px;

    box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),

                0 25px 50px 0 rgba(0, 0, 0, 0.15);

}



#todoapp:before {

    content: '';

    border-left: 1px solid #f5d6d6;

    border-right: 1px solid #f5d6d6;

    width: 2px;

    position: absolute;

    top: 0;

    left: 40px;

    height: 100%;

}



#todoapp input::-webkit-input-placeholder {

    font-style: italic;

}



#todoapp input::-moz-placeholder {

    font-style: italic;

    color: #a9a9a9;

}



#todoapp h1 {

    position: absolute;

    top: -120px;

    width: 100%;

    font-size: 70px;

    font-weight: bold;

    text-align: center;

    color: #b3b3b3;

    color: rgba(255, 255, 255, 0.3);

    text-shadow: -1px -1px rgba(0, 0, 0, 0.2);

    -webkit-text-rendering: optimizeLegibility;

    -moz-text-rendering: optimizeLegibility;

    -ms-text-rendering: optimizeLegibility;

    -o-text-rendering: optimizeLegibility;

    text-rendering: optimizeLegibility;

}



#header {

    padding-top: 15px;

    border-radius: inherit;

}



#header:before {

    content: '';

    position: absolute;

    top: 0;

    right: 0;

    left: 0;

    height: 15px;

    z-index: 2;

    border-bottom: 1px solid #6c615c;

    background: #8d7d77;

    background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));

    background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));

    background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));

    filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');

    border-top-left-radius: 1px;

    border-top-right-radius: 1px;

}



#new-todo,

.edit {

    position: relative;

    margin: 0;

    width: 100%;

    font-size: 24px;

    font-family: inherit;

    line-height: 1.4em;

    border: 0;

    outline: none;

    color: inherit;

    padding: 6px;

    border: 1px solid #999;

    box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);

    -moz-box-sizing: border-box;

    -ms-box-sizing: border-box;

    -o-box-sizing: border-box;

    box-sizing: border-box;

    -webkit-font-smoothing: antialiased;

    -moz-font-smoothing: antialiased;

    -ms-font-smoothing: antialiased;

    -o-font-smoothing: antialiased;

    font-smoothing: antialiased;

}



#new-todo {

    padding: 16px 16px 16px 60px;

    border: none;

    background: rgba(0, 0, 0, 0.02);

    z-index: 2;

    box-shadow: none;

}



#main {

    position: relative;

    z-index: 2;

    border-top: 1px dotted #adadad;

}



label[for='toggle-all'] {

    display: none;

}



#toggle-all {

    position: absolute;

    top: -42px;

    left: -4px;

    width: 40px;

    text-align: center;

    /* Mobile Safari */

    border: none;

}



#toggle-all:before {

    content: '»';

    font-size: 28px;

    color: #d9d9d9;

    padding: 0 25px 7px;

}



#toggle-all:checked:before {

    color: #737373;

}



#todo-list {

    margin: 0;

    padding: 0;

    list-style: none;

}



#todo-list li {

    position: relative;

    font-size: 24px;

    border-bottom: 1px dotted #ccc;

}



#todo-list li:last-child {

    border-bottom: none;

}



#todo-list li.editing {

    border-bottom: none;

    padding: 0;

}



#todo-list li.editing .edit {

    display: block;

    width: 506px;

    padding: 13px 17px 12px 17px;

    margin: 0 0 0 43px;

}



#todo-list li.editing .view {

    display: none;

}



#todo-list li .toggle {

    text-align: center;

    width: 40px;

    /* auto, since non-WebKit browsers doesn't support input styling */

    height: auto;

    position: absolute;

    top: 0;

    bottom: 0;

    margin: auto 0;

    /* Mobile Safari */

    border: none;

    -webkit-appearance: none;

    -ms-appearance: none;

    -o-appearance: none;

    appearance: none;

}



#todo-list li .toggle:after {

    content: '✔';

    /* 40 + a couple of pixels visual adjustment */

    line-height: 43px;

    font-size: 20px;

    color: #d9d9d9;

    text-shadow: 0 -1px 0 #bfbfbf;

}



#todo-list li .toggle:checked:after {

    color: #85ada7;

    text-shadow: 0 1px 0 #669991;

    bottom: 1px;

    position: relative;

}



#todo-list li label {

    white-space: pre;

    word-break: break-word;

    padding: 15px 60px 15px 15px;

    margin-left: 45px;

    display: block;

    line-height: 1.2;

    -webkit-transition: color 0.4s;

    transition: color 0.4s;

}



#todo-list li.completed label {

    color: #a9a9a9;

    text-decoration: line-through;

}



#todo-list li .destroy {

    display: none;

    position: absolute;

    top: 0;

    right: 10px;

    bottom: 0;

    width: 40px;

    height: 40px;

    margin: auto 0;

    font-size: 22px;

    color: #a88a8a;

    -webkit-transition: all 0.2s;

    transition: all 0.2s;

}



#todo-list li .destroy:hover {

    text-shadow: 0 0 1px #000,

                 0 0 10px rgba(199, 107, 107, 0.8);

    -webkit-transform: scale(1.3);

    transform: scale(1.3);

}



#todo-list li .destroy:after {

    content: '✖';

}



#todo-list li:hover .destroy {

    display: block;

}



#todo-list li .edit {

    display: none;

}



#todo-list li.editing:last-child {

    margin-bottom: -1px;

}



#footer {

    color: #777;

    padding: 0 15px;

    position: absolute;

    right: 0;

    bottom: -31px;

    left: 0;

    height: 20px;

    z-index: 1;

    text-align: center;

}



#footer:before {

    content: '';

    position: absolute;

    right: 0;

    bottom: 31px;

    left: 0;

    height: 50px;

    z-index: -1;

    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),

                0 6px 0 -3px rgba(255, 255, 255, 0.8),

                0 7px 1px -3px rgba(0, 0, 0, 0.3),

                0 43px 0 -6px rgba(255, 255, 255, 0.8),

                0 44px 2px -6px rgba(0, 0, 0, 0.2);

}



#todo-count {

    float: left;

    text-align: left;

}



#filters {

    margin: 0;

    padding: 0;

    list-style: none;

    position: absolute;

    right: 0;

    left: 0;

}



#filters li {

    display: inline;

}



#filters li a {

    color: #83756f;

    margin: 2px;

    text-decoration: none;

}



#filters li a.selected {

    font-weight: bold;

}



#clear-completed {

    float: right;

    position: relative;

    line-height: 20px;

    text-decoration: none;

    background: rgba(0, 0, 0, 0.1);

    font-size: 11px;

    padding: 0 10px;

    border-radius: 3px;

    box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);

}



#clear-completed:hover {

    background: rgba(0, 0, 0, 0.15);

    box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);

}



#info {

    margin: 65px auto 0;

    color: #a6a6a6;

    font-size: 12px;

    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);

    text-align: center;

}



#info a {

    color: inherit;

}



/*

    Hack to remove background from Mobile Safari.

    Can't use it globally since it destroys checkboxes in Firefox and Opera

*/



@media screen and (-webkit-min-device-pixel-ratio:0) {

    #toggle-all,

    #todo-list li .toggle {

        background: none;

    }



    #todo-list li .toggle {

        height: 40px;

    }



    #toggle-all {

        top: -56px;

        left: -15px;

        width: 65px;

        height: 41px;

        -webkit-transform: rotate(90deg);

        transform: rotate(90deg);

        -webkit-appearance: none;

        appearance: none;

    }

}



.hidden {

    display: none;

}



hr {

    margin: 20px 0;

    border: 0;

    border-top: 1px dashed #C5C5C5;

    border-bottom: 1px dashed #F7F7F7;

}



.learn a {

    font-weight: normal;

    text-decoration: none;

    color: #b83f45;

}



.learn a:hover {

    text-decoration: underline;

    color: #787e7e;

}



.learn h3,

.learn h4,

.learn h5 {

    margin: 10px 0;

    font-weight: 500;

    line-height: 1.2;

    color: #000;

}



.learn h3 {

    font-size: 24px;

}



.learn h4 {

    font-size: 18px;

}



.learn h5 {

    margin-bottom: 0;

    font-size: 14px;

}



.learn ul {

    padding: 0;

    margin: 0 0 30px 25px;

}



.learn li {

    line-height: 20px;

}



.learn p {

    font-size: 15px;

    font-weight: 300;

    line-height: 1.3;

    margin-top: 0;

    margin-bottom: 0;

}



.quote {

    border: none;

    margin: 20px 0 60px 0;

}



.quote p {

    font-style: italic;

}



.quote p:before {

    content: '“';

    font-size: 50px;

    opacity: .15;

    position: absolute;

    top: -20px;

    left: 3px;

}



.quote p:after {

    content: '”';

    font-size: 50px;

    opacity: .15;

    position: absolute;

    bottom: -42px;

    right: 3px;

}



.quote footer {

    position: absolute;

    bottom: -40px;

    right: 0;

}



.quote footer img {

    border-radius: 3px;

}



.quote footer a {

    margin-left: 5px;

    vertical-align: middle;

}



.speech-bubble {

    position: relative;

    padding: 10px;

    background: rgba(0, 0, 0, .04);

    border-radius: 5px;

}



.speech-bubble:after {

    content: '';

    position: absolute;

    top: 100%;

    right: 30px;

    border: 13px solid transparent;

    border-top-color: rgba(0, 0, 0, .04);

}



.learn-bar > .learn {

    position: absolute;

    width: 272px;

    top: 8px;

    left: -300px;

    padding: 10px;

    border-radius: 5px;

    background-color: rgba(255, 255, 255, .6);

    -webkit-transition-property: left;

    transition-property: left;

    -webkit-transition-duration: 500ms;

    transition-duration: 500ms;

}



@media (min-width: 899px) {

    .learn-bar {

        width: auto;

        margin: 0 0 0 300px;

    }



    .learn-bar > .learn {

        left: 8px;

    }



    .learn-bar #todoapp {

        width: 550px;

        margin: 130px auto 40px auto;

    }

}
View Code

代码分析:

todo.js

//app对象

var app = app || {};



(function () {

    'use strict';



    // Todo 模型

    // ----------



    

    //我们基本的Todo模型有`title`,`order`,`completed`,属性

    app.Todo = Backbone.Model.extend({

        //todo默认属性,确保每个对象都拥有`title` and `completed`.

        defaults: {

            title: '',

            completed: false

        },



        //改变todo中`completed`属性为反向状态 

        toggle: function () {

            this.save({

                completed: !this.get('completed')

            });

        }

    });

})();

在todo app我们实现要实现的功能中,一个基本待办事项就是最基本的数据模型,它拥有title,completed,order,三个属性,分别表示显示的标题,是否完成和顺序,其中title,completed为默认属性。

模型有一个改变状态的方法。save方法除了将数据保存到模型中,还将向服务器发送request请求,如果成功,触发sync事件。简言之,与数据存储方面配合,利用save不单是将数据存储到我们定义的模型里,还将保存到服务器上。

todos.js

//app对象

var app = app || {};



(function () {

    'use strict';



    // Todo 集合

    // ---------------



    // 这里数据集合将存储到本地存储,替代服务器存储

    var Todos = Backbone.Collection.extend({

        //引用这个集合的模型 

        model: app.Todo,



        // 集合中所有的todo 模型都将存在本地

        localStorage: new Backbone.LocalStorage('todos-backbone'),



        // 过滤为所有完成的todo模型

        completed: function () {

            return this.where({completed: true});

        },



        // 过滤为所有为完成的todo模型

        remaining: function () {

            return this.where({completed: false});

        },



        // 我们将保持Todos有序,不管是不是被无序存储

        // 生成下个todo模型的顺序ID

        nextOrder: function () {

            return this.length ? this.last().get('order') + 1 : 1;

        },



        // 所有的todo将以最初插入的顺序保存到Todos当中。

        comparator: 'order'

    });



    // 创建一个todos集合。

    app.todos = new Todos();

})();

这是数据模型的集合。

todo-view.js

var app = app || {};



(function ($) {

    'use strict';



    // todo 单个模型的视图

    // --------------



    // 一个todo模型的DOM元素

    app.TodoView = Backbone.View.extend({

        // 整个视图在一个li标签中

        tagName:  'li',



        // 缓存单个模型的模板函数

        template: _.template($('#item-template').html()),



        // 定义单个模型页面视图中指定DOM元素触发事件时调用的函数

        events: {

            'click .toggle': 'toggleCompleted',

            'dblclick label': 'edit',

            'click .destroy': 'clear',

            'keypress .edit': 'updateOnEnter',

            'keydown .edit': 'revertOnEscape',

            'blur .edit': 'close'

        },



        // 监听其对应模型的事件,这是一对一的,这个视图,监听其模型。

        initialize: function () {

            this.listenTo(this.model, 'change', this.render);

            this.listenTo(this.model, 'destroy', this.remove);

            this.listenTo(this.model, 'visible', this.toggleVisible);

        },



        // 生成模型的标题。

        render: function () {

            //Backbone LocalStorage保存模型时,会增加一个ID属性,这将引起render事件再次发生。我们想过滤由此引起的第二次render,因此增加了一段功能。

            // 这是localStorage.js的一个BUG,相关信息可以查看 https://github.com/tastejs/todomvc/issues/469

            if (this.model.changed.id !== undefined) {

                return;

            }



            this.$el.html(this.template(this.model.toJSON()));

            this.$el.toggleClass('completed', this.model.get('completed'));

            this.toggleVisible();

            this.$input = this.$('.edit');

            return this;

        },



        toggleVisible: function () {

            this.$el.toggleClass('hidden', this.isHidden());

        },



        isHidden: function () {

            return this.model.get('completed') ?

                app.TodoFilter === 'active' :

                app.TodoFilter === 'completed';

        },



        // 改变模型completed状态

        toggleCompleted: function () {

            this.model.toggle();

        },



        // 转到编辑状态

        edit: function () {

            this.$el.addClass('editing');

            this.$input.focus();

        },



        // 关闭编辑模式,存储对模型的改变

        close: function () {

            var value = this.$input.val();

            var trimmedValue = value.trim();



            // 在非编辑状态下直接返回

            if (!this.$el.hasClass('editing')) {

                return;

            }



            if (trimmedValue) {

                this.model.save({ title: trimmedValue });



                if (value !== trimmedValue) {

                    // 只增加了两边的空格的更改并不会存入服务器中,在此情况下,我们重新渲染视图,

                    // 达到显示和后台统一的目的。

                    this.model.trigger('change');

                }

            } else {

                this.clear();

            }



            this.$el.removeClass('editing');

        },



        // 如果你按下回车键,将跳到编辑状态

        updateOnEnter: function (e) {

            if (e.which === ENTER_KEY) {

                this.close();

            }

        },



        // 如果你按下esc键,将恢复视图为模型的数据,并离开编辑状态

        revertOnEscape: function (e) {

            if (e.which === ESC_KEY) {

                this.$el.removeClass('editing');

                // 重置输入框为模型中的数据

                this.$input.val(this.model.get('title'));

            }

        },



        // 删除条目,并从localStorage中销毁,并删除视图(事件监听destroy)

        clear: function () {

            this.model.destroy();

        }

    });

})(jQuery);

 这里是单个条目的视图,它主要负责对单个条目的操作,如删除,更改等每个条目都有的功能。

app-view.js

var app = app || {};



(function ($) {

    'use strict';



    // 程序

    // ---------------



    // 整体的程序视图

    app.AppView = Backbone.View.extend({



        // 视图绑定为已经存在的页面节点

        el: '#todoapp',



        // 缓存状态模板

        statsTemplate: _.template($('#stats-template').html()),



        events: {

            'keypress #new-todo': 'createOnEnter',

            'click #clear-completed': 'clearCompleted',

            'click #toggle-all': 'toggleAllComplete'

        },



        // 在初始化时,我们绑定相关的事件到集合实例上,

        // 重置时,重新添加所有模型。

        initialize: function () {

            this.allCheckbox = this.$('#toggle-all')[0];

            this.$input = this.$('#new-todo');

            this.$footer = this.$('#footer');

            this.$main = this.$('#main');

            this.$list = $('#todo-list');



            this.listenTo(app.todos, 'add', this.addOne);

            this.listenTo(app.todos, 'reset', this.addAll);

            this.listenTo(app.todos, 'change:completed', this.filterOne);

            this.listenTo(app.todos, 'filter', this.filterAll);

            this.listenTo(app.todos, 'all', this.render);



            // 抑制由于每个add事件引起的视图生成。从本地存储获取到模型时,触发reset事件,添加所有的模型到集合中。

            app.todos.fetch({reset: true});

        },



        // reset并不会引起视图的生成。

        render: function () {

            var completed = app.todos.completed().length;

            var remaining = app.todos.remaining().length;



            if (app.todos.length) {

                this.$main.show();

                this.$footer.show();



                this.$footer.html(this.statsTemplate({

                    completed: completed,

                    remaining: remaining

                }));



                this.$('#filters li a')

                    .removeClass('selected')

                    .filter('[href="#/' + (app.TodoFilter || '') + '"]')

                    .addClass('selected');

            } else {

                this.$main.hide();

                this.$footer.hide();

            }



            this.allCheckbox.checked = !remaining;

        },



        // 为新增的模型创建视图,添加到页面中去

        addOne: function (todo) {

            var view = new app.TodoView({ model: todo });

            this.$list.append(view.render().el);

        },



        // 一次添加所有模型视图

        addAll: function () {

            this.$list.html('');

            app.todos.each(this.addOne, this);

        },



        filterOne: function (todo) {

            todo.trigger('visible');

        },



        filterAll: function () {

            app.todos.each(this.filterOne, this);

        },



        // 生成新模型的属性

        newAttributes: function () {

            return {

                title: this.$input.val().trim(),

                order: app.todos.nextOrder(),

                completed: false

            };

        },



        // 回车键保存模型,且保存到localStorage

        createOnEnter: function (e) {

            if (e.which === ENTER_KEY && this.$input.val().trim()) {

                app.todos.create(this.newAttributes());

                this.$input.val('');

            }

        },



        // 清除所有完成的模型,

        clearCompleted: function () {

            _.invoke(app.todos.completed(), 'destroy');

            return false;

        },



        toggleAllComplete: function () {

            var completed = this.allCheckbox.checked;



            app.todos.each(function (todo) {

                todo.save({

                    completed: completed

                });

            });

        }

    });

})(jQuery);

主视图,主程序,前面所有的模型,集合,视图,都由他来管理。渲染整个视图,监听新增事件,添加条目到集合中等全面的问题处理。

route.js

var app = app || {};



(function () {

    'use strict';



    // Todo 路由

    // ----------

    var TodoRouter = Backbone.Router.extend({

        routes: {

            '*filter': 'setFilter'

        },



        setFilter: function (param) {

            // 存储目前的过滤参数。

            app.TodoFilter = param || '';



            // 触发过滤事件,改变视图的隐藏显示。

            app.todos.trigger('filter');

        }

    });



    app.TodoRouter = new TodoRouter();

    Backbone.history.start();

})();

利用route,保存参数到变量中,供程序使用

app.js

var app = app || {};

var ENTER_KEY = 13;

var ESC_KEY = 27;



$(function () {

    'use strict';



    // 创建App实例,初始化所有功能

    new app.AppView();

});

初始化真个程序。

你可能感兴趣的:(backbone)