Backbone實例todos分析

源碼來自:http://todomvc.com/examples/backbone/css

這是一個用Backbone.js完成的待辦事項實例,精簡但完善,能夠幫助很好的幫助理解Backbone的API,MVC框架和寫webApp的基本思路。html

下面讓咱們一步步分析,jquery

準備工做:

首先是html頁面,新建index.html,並保存如下代碼:git

<!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文件,咱們將深刻分析。github

先簡單介紹一下這些js框架和咱們頁面的關係:web

1.jquery.js:咱們將利用jquery操做dom,緩存

2.underscore.js:backbone的依賴庫,提供一下方便的方法,和模板的操做。服務器

3.backbone.js:咱們主要使用的MVC框架mvc

4.backbone.localStorage.js:backbone的拓展庫,用來操做HTML5新加入的本地存儲app

頁面的樣式表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();
});

初始化真個程序。

相關文章
相關標籤/搜索