源码来自: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>
这是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- 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(); });
初始化真个程序。