准备

开始本教程之前,你需要了解以下几点:

  • alexyoung / dailyjs-backbone-tutorial提交到了0953c5d版本
  • 第二部分中的API key
  • 第二部分中的“Client ID”
  • 更新app/js/config.js成你自己的key(如果你是检出的代码)

要检出源码,请运行以下命令(或用Git GUI工具):

git clone git@github.com:alexyoung/dailyjs-backbone-tutorial.git
cd dailyjs-backbone-tutorial
git reset --hard 0953c5d

任务项CRUD

这部分将涵盖以下内容:

  • 新建单个任务项视图
  • 新建多个任务项的视图
  • 添加任务集合
  • 利用Google’s API获取任务

这里需要处理一个父视图和子视图之间的关系,Backbone并没有提供模型和视图之间关系图,这个例子中,我们理解的关系应该是任务项(tasks)属于列表(lists),任务视图(task views)属于列表视图(list views),虽然没有这个关系没有具体的呈现形式,但是在Backbone/Underscore已经又相应的代码去解决这个。

架子

开始之前,我们先新建好目录:

$ mkdir app/js/views/tasks
$ mkdir app/js/templates/tasks

app/js/collections/tasks.js中添加新集合:

define(['models/task'], function(Task) {
  var Tasks = Backbone.Collection.extend({
    model: Task,
    url: 'tasks'
  });

  return Tasks;
});

Tasks还没有做任何事,现在我们需要通过Google’s API请求一个tasklist来获取任务项,在调用fetch的时候还需要一个额外的参数:

collection.fetch({ data: { tasklist: this.model.get('id') }, // ...

我们处理获取得到的TaskLists,就和项目中已经存在的{ userId: '@me' }数据类似。

我们还需要一个包含新建任务项表单和任务项列表容器的任务项视图模板,被保存到app/js/templates/index.js:

<div class="span6">
  <div id="add-task">
    <form class="well row form-inline add-task">
      <input type="text" class="pull-left" placeholder="Enter a new task's title and press return" name="title">
      <button type="submit" class="pull-right btn"><i class="icon-plus"></i></button>
    </form>
  </div>
  <ul id="task-list"></ul>
</div>
<div class="span6">
  <div id="selected-task"></div>
  <div class="alert" id="warning-no-task-selected">
    <strong>Note:</strong> Select a task to edit or delete it.
  </div>
</div>

我们使用了Bootstrap得布局一些样式类,在app/js/templates/tasks/task.htmlTaskView视图中,有title的span元素、notes的span元素和切换任务的状态的checkbox复选框:

<input type="checkbox" data-task-id="" name="task_check_" class="check-task" value="t">
<span class="title "></span>
<span class="notes"></span>

视图

TasksIndexView通过Tasks聚合加载任务项,用TaskView视图来渲染展示任务,具体的TasksIndexView代码在app/js/views/tasks/index.js中:

define(['text!templates/tasks/index.html', 'views/tasks/task', 'collections/tasks'], function(template, TaskView, Tasks) {
  var TasksIndexView = Backbone.View.extend({
    tagName: 'div',
    className: 'row-fluid',

    template: _.template(template),

    events: {
      'submit .add-task': 'addTask'
    },

    initialize: function() {
      this.children = [];
    },

    addTask: function() {
    },

    render: function() {
      this.$el.html(this.template());

      var $el = this.$el.find('#task-list')
        , self = this;

      this.collection = new Tasks();
      this.collection.fetch({ data: { tasklist: this.model.get('id') }, success: function() {
        self.collection.each(function(task) {
          var item = new TaskView({ model: task, parentView: self });
          $el.append(item.render().el);
          self.children.push(item);
        });
      }});

      return this;
    }
  });

  return TasksIndexView;
});

使用collection.fetch获取任务项,然后给每个任务项追加一个TaskView,这个TaskView如下:

define(['text!templates/tasks/task.html'], function(template) {
  var TaskView = Backbone.View.extend({
    tagName: 'li',
    className: 'controls well task row',

    template: _.template(template),

    events: {
      'click': 'open'
    },

    initialize: function(options) {
      this.parentView = options.parentView;
    },

    render: function(e) {
      var $el = $(this.el);
      $el.data('taskId', this.model.get('id'));
      $el.html(this.template(this.model.toJSON()));
      $el.find('.check-task').attr('checked', this.model.get('status') === 'completed');

      return this;
    },

    open: function(e) {
      if (this.parentView.activeTaskView) {
        this.parentView.activeTaskView.close();
      }
      this.$el.addClass('active');
      this.parentView.activeTaskView = this;
    },

    close: function(e) {
      this.$el.removeClass('active');
    }
  });

  return TaskView;
});

父视图会判断确定open是否执行,是看是不是其他任务被点击和被关闭(删除active),实现这个又很多方式:可以遍历视图看看是不是关闭状态(关闭状态会通过$('selector').removeClass('active')删除相关的classactive)或者在模型中通过触发事件。我觉得应该在视图中处理视图相关的代码,模型和集合也应该是类似。

接下来我们需要把TasksIndexView添加app/js/views/lists/menuitem.jsdefine中,修改open方法,让他去实例化一个TasksIndexView视图:

open: function() {
  if (bTask.views.activeListMenuItem) {
    bTask.views.activeListMenuItem.$el.removeClass('active');
  }

  bTask.views.activeListMenuItem = this;
  this.$el.addClass('active');

  // Render the tasks
  if (bTask.views.tasksIndexView) {
    bTask.views.tasksIndexView.remove();
  }

  bTask.views.tasksIndexView = new TasksIndexView({ collection: bTask.collections.tasks, model: this.model });
  bTask.views.app.$el.find('#tasks-container').html(bTask.views.tasksIndexView.render().el);

  return false;
}

app/js/models/task.js中添加一个默认的模型Task:

define(function() {
  var Task = Backbone.Model.extend({
    url: 'tasks',
    defaults: { title: '', notes: '' }
  });

  return Task;
});

我们给它了一个默认值{ title: '', notes: '' },添加默认值的原因—主要是避免我们通过Google Tasks获取任务为空时出现错误

有了这些模板,视图和修改,我们现在可以选择列表,看到自己的任务,也可以选择任务了

样式

enter image description here

如图,我们应用程序没有太多的视觉元素, Bootstrap完全可以满足需求—我们只去Bootstrap官网下载图片和样式文件放到我们项目的app/imgapp/css目录下,然后在我们的页面app/index.html引入css/bootstrap.min.css

我们再给应用的任务面板加一些自定义样式,这样就看起来和Things界面差不多。

Backbone 0.9.10

我已经将Backbone升级到了0.9.10版本,app/js/gapi.js中当调用options.successBackbone.sync方法有点区别,修改下:

options.success(model, result, request);

总结

教程中完整的代码可以在这里找到:alexyoung / dailyjs-backbone-tutorial, commit 0491ad