Backbone Todo 逐行解释版

483 阅读6分钟

参考文档

addyosmani.com/backbone-fu…

Step01

虽然文章中要求下载所有外库到本地,但这里还是选择了比较快的引用外链,引用外链有挺多缺点的:多个HTTP请求影响性能,第三方库的安全性考虑等等,但这里为了方便用引用外链的方式。更多关于第三方库的探究在这里。同时还需要添加CSS文件

<!doctype html>
<html lang="en">
<body>
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.0/backbone-min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/backbone-localstorage.js/1.1.16/backbone.localStorage-min.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>

Step02

定义HTML部分,输入框输入每一个事项,然后由列表显示,列表id="todo-list"

<!doctype html>
<html lang="en">
<body>
    <section id="todoapp">
        <header id="header">
            <input id="new-todo" placeholder="What needs to be done?">
        </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>
    </section>

</body>
</html>  

Step03 添加模版

<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>
  

<%= <%-是来自underscore的模版 也可以自定义 接下来还需要定义#stats-template

<script type="text/template" id="stats-template">
  <span class="todo-count"><strong><%= remaining %></strong> <%= remaining === 1 ? 'item' : 'items' %> left</span>
  <ul class="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 class="clear-completed">Clear completed</button>
  <% } %>
</script>

Step04

模型(model)包括两个默认属性:标题和完成状态

此外还包括一个开关方法toggle

// js/models/todo.js
var app = app || {};

app.Todo = Backbone.View.extend({
    default: {
        title: '',
        completed: false
    },
    toggle: function() {
        this.save({
            completed: !this.get('completed')
        });
    }

});

Model.save方法是通过代理Backbone.sync的方式,把一个模型保存到数据库中(或者是其他的存储数据层,这儿是浏览器的localStorage)。如果验证成功,它返回一个jqXHR对象,否则返回false。
可传递的参数包括两个:

model.save([attributes], [options])  

attributes 是你需要修改的属性值,没有列出来的属性不会被修改,但是整个完整对象会一起传到服务端。

options可以接收success 和 error的回调处理:

model.save("author", "F.D.R.", {error: function(){ ... }}); 

Model.get方法则从model中读取相应的属性值。
所以这里当调用开关(toggle)方法时,我们读取完成的状态,然后取反,最后再把它保存。

Step05

待办事项集合(Todo collection) 是模型的集合。我们使用LocalStorage adapter将我们的数据存储在浏览器本地(localStorage)

// js/collections/todos.js
var app = app || {};

var TodoList = Backbone.Collection.extend({
    model: app.Todo,
    localStorage: new Backbone.LocalStorage('todos-backbone'),
    completed: function() {
        return this.filter(function (todo) {
            return todo.get('completed');
        });
    },
    remaining: function () {
        return this.without.apply(this, this.completed());
    },
    nextOrder: function () {
        if (!this.length) { return 1; }
        return this.last().get('order') + 1;
    },
    comparator: function (todo) {
        return todo.get('order');
    }
});

app.Todos = new TodoList();  

集合的completed() 和 remaining()方法分别返回已完成的和未完成的事项。
下一个nextOrder方法按照顺序生成下一个事项。
comparator则按照添加顺序给它们排列。
P.S this.filter, this.withoutthis.last 都是underscore的方法。

Step06

视图(View)处理应用的逻辑并且负责展示。我们的应用包含两个视图:AppView负责首次渲染列表并且处理新增加的事项,TodoView则负责具体每一条事项的编辑更新和删除。

var app = app || {};

app.AppView = Backbone.View.extend({
    el: '#todoapp',
    statsTemplate: _.template($('#stats-template').html()),
    initialize: function () {
        this.listenTo(app.Todos, 'add', this.addOne);
        this.listenTo(app.Todos, 'reset', this.addAll);
    },
    addOne: function(todo){
        var view = new app.TodoView({model: todo});
        $('#todo-list').append(view.render().el);
    },
    addAll: function(){
        this.$('#todo-list').html('');
        app.Todos.each(this.addOne, this);
    }
});  

AppView里面的逻辑比较多,所以我们分两步。第一步先添加statsTemplate和initialize方法,addOne和addAll方法。
el (element) 属性连接DOM结构里的id="todoapp"。
_.template是使用underscore的模版。
initialize方法里面: 当add事件被触发时,会调用addOne方法。addOne方法创建一个新的TodoView实例,然后把它添加到列表 id="todo-list"

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

View.render方法 默认是no-op,我们用自己的逻辑重写覆盖它。 View.el可以连接DOM选择器字符串或者一个元素,其他时候会根据标签名类名ID或属性进行匹配。

Step07

光有这两个方法是不够的,我们还要再添加更多事件和方法。 事件包括:

    events: {
        'keypress #new-todo': 'createOnEnter',
        'click #clear-completed': 'clearCompleted',
        'click #toggle-all': 'toggleAllComplete'
    },

当用户在输入框里点击回车按钮调用createOnEnter,此方法会新建一个model并且在把它保存在浏览器本地。app.Todos在集合文件里(js/collections/todos.js)被赋值:

app.Todos = new TodoList();  

create方法是集合上新建一个model实例的便捷方式。

    newAttributes: function(){
        return {
            title: this.$input.val().trim(),
            order: app.Todos.nextOrder(),
            completed: false
        };
    },
    createOnEnter: function( event ) {
      if ( event.which !== ENTER_KEY || !this.$input.val().trim() ) {
        return;
      }

      app.Todos.create( this.newAttributes() );
      this.$input.val('');
    },  

clearCompleted方法会隐藏所有已完成的事项。这个按钮选项是模版通过#stats-template渲染出来。 toggleAllComplete方法允许用户一次性将所有事项标记为完成。

    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
            });
        });
    }  

在初始化initialize方法里,我们给事件绑定一些回调方法:

this.listenTo(app.Todos, 'changed:completed', this.filterOne);
this.listenTo(app.Todos, 'filter', this.filterAll);
this.listenTo(app.Todos, 'all', this.render);  

当我们单独过滤一个事项,我们调用filterOne方法;全部事项一起过滤时,我们调用filterAll方法。还有特殊的all事件绑定在事项集合上,下面会更详细讨论。

    filterOne: function(){
        todo.trigger('visible');
    },  
    filterAll: function(){
        app.Todos.each(this.filterOne, this);
    },

初始化方法最后一步是从浏览器存储中读取之前保存的数据。

 app.Todos.fetch();  

在渲染方法里,同样做了很多逻辑处理:

    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.$('#filter li a')
               .removeClass('selected')
               .filter('[href="#/' + (app.TodoFilter || '') + '"]')
               .addClass('selected');
       } else {
           this.$main.hide();
           this.$footer.hide();
       }
       this.allCheckbox.checked = !remaining;
   },  

#main和#footer部分会根据是否有事项列表来显示。我们会在路由中设置 app.TodoFilter的值,该值会决定selected这个类,进而影响样式的显示。

Step08 TodoView

var app = app || {};

app.TodoView = Backbone.View.extend({
   tagName: 'li',
   template: _.template($('#item-template').html()),
   events: {
       'dbclick label': 'edit',
       'keypress .edit': 'updateOnEnter',
       'blur .edit': 'close'
   },
   initialize: function(){
       this.listenTo(this.model, 'change', this.render);
   },
   render: function(){
       this.$el.html(this.template(this.model.attributes));
       this.$input = this.$('.edit');
       return this;
   },
   edit: function(){
       this.$el.addClass('editing');
       this.$input.focus();
   },
   close: function(){
       var value = this.$input.val().trim();

       if(value){
         this.model.save({title: value});
       }

       this.$el.removeClass('editing');
   },
   updateOnEnter: function(e){
       if (e.which === ENTER_KEY){
           this.close();
       }
   }
});  

在initialize构造函数里,我们监听事项的变化。每当事项一更新,应用都会重新渲染并且在视图上表现出来。
在render() 方法里面, 我们使用 Underscore.js #item-template渲染。

我们的事件包括三个回调方法: edit将当前模式调整为编辑模式,允许用户修改事项的内容。 updateOnEnter检查用户是否按了回车键。 close会清理输入框中的内容,如果是有效信息,我们保存此前的修改。

Step09 Startup

现在我们有里两个视图 AppView 和 TodoView,我们要借助jQuery’s ready() 方法把 AppView 在app.js 文件中执行。 至此,一个初步的应用就已经出来了。我们可以在浏览器打开index.html页面来验证,在控制台执行一条添加指令。

app.Todos.create({ title: 'My first Todo item'});  


如果没有什么差错,这条指令会在页面添加一条事项。同时它会保存在浏览器本地存储,页面刷新后也依然存在。

app.Todos.create()会执行集合上的create方法,它会初始化一个model事项。

  // js/collections/todos.js

  var TodoList = Backbone.Collection.extend({

      model: app.Todo // collection.create() 用来创造的模型
      ...
  )};

我们通过以下指令来验证新创造的模型是app.Todo 的实例:

var secondTodo = app.Todos.create({ title: 'My second Todo item'});
secondTodo instanceof app.Todo // returns true  

Step10 继续完善

这一步我们要做到是,完成了某项任务和删除任务。 我们会添加两个方法togglecompleted() clear() 和相应的事件触发。 这两个事件都是针对具体的每一个事项,所以我们在TodoView里作相应添加。
首先添加两个事件:

    events: {
        'click .toggle': 'toggleCompleted',
        'click .destroy': 'clear',
    },  

初始化方法中添加监听:

    initialize: function(){
        ...
        this.listenTo(this.model, 'destroy', this.remove);
        this.listenTo(this.model, 'visible', this.toggleVisible);
    },   

render方法中需要根据完成状态显示不同的列表:

    render: function(){
        this.$el.toggleClass('completed', this.model.get('completed'));
        this.toggleVisible();
        ...
    }  

toggleVisible方法调用isHidden来判断任务是否完成,是否显示。
当我们点击了任务的叉按钮时,clear 方法会调用 destroy, 任务就会从本地存储中被删除。

    toggleVisible: function(){
        this.$el.toggleClass('hidden', this.isHidden());
    },
    isHidden: function(){
        var isCompleted = this.model.get('completed');
        return (
            (!isCompleted && app.TodoFilter === 'completed')
            || (isCompleted && app.TodoFilter === 'active')
        );
    },
    togglecompleted: function(){
        this.model.toggle();
    },
    clear: function(){
        this.model.destroy();
    }  

Step11 Routing

最后我们添加路由,根据路由可以快速过滤出我们想要的列表:

#/ (all - default)
#/active
#/completed  
var app = app || {};

var Workspace = Backbone.Router.extend({
    routes: {
        '*filter': 'setFilter'
    },

    setFilter: function(param){
        if (param) {
            param = param.trim();
        }
        app.TodoFilter = param || '';
        app.Todos.trigger('filter');
    }
});

app.TodoRouter = new Workspace();
Backbone.history.start();