源碼來自: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>
這是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; } }
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(); });
初始化真個程序。