使用backbone.js重写spine的contact例子

相对于传统的服务器端动态页面技术,SPA(单页面应用)在浏览器端实现了所有的页面逻辑,避免了页面跳转而造成的操作中断,提供更为平滑的用户体验。要实现SPA,浏览器端必然需要一个javascript库来管理数据模块,用户操作,以及界面刷新,类似于服务器端MVC结构。在这里可以找到目前流行的浏览器端类MVC实现。

前些日子先后学习了BackboneSpine,并参阅了双方的不少例子程序,其中包括Spine实现的Spine.Contacts(demo, source)。为了更好地理解Backbone的原理,就利用空闲时间使用Backbone重写了该地址本应用(demo, source)。

在使用Backbone的过程中,对其的总体感觉是能让页面逻辑层次更清楚,与传统的服务器端MVC结构非常类似:

  • 通过将View的方法绑定到Model/Collection的CRUD事件,可以让数据的变化能自动触发页面的更新;
  • 一个View可以由多个相同或者不同类型的子View聚合而成,所有View都提供render函数供父View的render函数进行调用,所有View对象的聚合就生成了整个页面dom;
  • View通过HTML template,将绑定的Model渲染成dom元素,从某种程度上说,View更像MVC结构里的Controller;
  • Collection是Model的集合,紧密耦合了Underscore的所有方法,相对传统javascript语句,提供了更加便捷的集合数据的运算;
  • Router提供了全局的页面逻辑路由,任何用户的操作,都可以通过Router.navigate进行全局状态的跳转;
  • Backbone内置支持了$函数,如果页面包含有jQuery或者Zepto
  • 用户可以通过Sync函数,来绑定页面Model和服务器端RESTful接口提供的数据;

下面是重写的地址本应用的全部javascript代码,也可以上github查看所有的源代码(演示):

(application.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
(function($) {
  $(document).ready(function(){
    var Contact = Backbone.Model.extend({
      defaults: {
        name: '',
        email: ''
      },
      filter: function(query) {//helper function for user search
        if (typeof(query) === 'undefined' || query === null || query === '') return true;
        query = query.toLowerCase();
        return this.get('name').toLowerCase().indexOf(query) != -1 || this.get('email').toLowerCase().indexOf(query) != -1;
      }
    });

    var Contacts = Backbone.Collection.extend({
      model: Contact,
      localStorage: new Store('my-contacts')
    });

    // contact item view
    var ContactItemView = Backbone.View.extend({
      className: 'item',
      template: _.template($('#tpl-item').html()),
      events: {
        'click': 'select'
      },
      // initialize
      initialize: function() {
        _.bindAll(this, 'select');
        this.model.bind('reset', this.render, this);
        this.model.bind('change', this.render, this);
        this.model.bind('destroy', this.remove, this);
        if (this.model.view) this.model.view.remove();
        this.model.view = this;
      },
      // render the contact item
      render: function() {
        this.$el.html(this.template(this.model.toJSON()));
        return this;
      },
      select: function() {
        appRouter.navigate('contacts/' + this.model.cid, {
          trigger: true
        });
      },
      active: function() {
        this.$el.addClass('active');
      },
      deactive: function() {
        this.$el.removeClass('active');
      }
    });

    // sidebar view
    var SidebarView = Backbone.View.extend({
      className: 'sidebar',
      template: _.template($('#tpl-sidebar').html()),
      events: {
        'click footer button': 'create',
        'click input': 'filter',
        'keyup input': 'filter'
      },
      // initialize
      initialize: function() {
        _.bindAll(this, 'create', 'filter');
        this.model.bind('reset', this.renderAll, this);
        this.model.bind('add', this.add, this);
        this.model.bind('remove', this.remove, this);
      },
      // render the contact item
      render: function() {
        $(this.el).html(this.template());
        this.renderAll();
        return this;
      },
      renderAll: function() {
        this.$(".items").empty();
        this.model.each(this.renderOne, this);
        this.filter();
      },
      renderOne: function(contact) {
        var view = new ContactItemView({
          model: contact
        });
        this.$(".items").append(view.render().el);
      },
      create: function() {
        var contact = new Contact();
        this.model.add(contact);
        appRouter.navigate('contacts/' + contact.cid + '/edit', {
            trigger: true
          });
      },
      filter: function() {
        var query = $('input', this.el).val();
        this.model.each(function(contact) {
          contact.view.$el.toggle(contact.filter(query));
        });
      },
      active: function(item) {
        if (this.activeItem) this.activeItem.view.deactive();
        this.activeItem = item;
        if (this.activeItem) this.activeItem.view.active();
      },
      add: function(contact) {
        this.renderOne(contact);
      },
      remove: function(contact) {
        console.log(contact);
      }
    });
    // show selected contact details
    var ShowView = Backbone.View.extend({
      className: 'show',
      template: _.template($('#tpl-show').html()),
      events: {
        'click .edit': 'edit'
      },
      initialize: function() {
        _.bindAll(this, 'edit');
      },
      render: function() {
        if (this.item) this.$el.html(this.template(this.item.toJSON()));
        return this;
      },
      change: function(item) {
        this.item = item;
        this.render();
      },
      edit: function() {
        if (this.item)
          appRouter.navigate('contacts/' + this.item.cid + '/edit', {
            trigger: true
          });
      }
    });
    // edit selected contact
    var EditView = Backbone.View.extend({
      className: 'edit',
      template: _.template($('#tpl-edit').html()),
      events: {
        'submit form': 'submit',
        'click .save': 'submit',
        'click .delete': 'remove'
      },
      initialize: function() {
        _.bindAll(this, 'submit', 'remove');
      },
      render: function() {
        if (this.item) this.$el.html(this.template(this.item.toJSON()));
        return this;
      },
      change: function(item) {
        this.item = item;
        this.render();
      },
      submit: function() {
        this.item.set(this.form());
        this.item.save();
        appRouter.navigate('contacts/' + this.item.cid, {
          trigger: true
        });
        return false;
      },
      form: function() {
        return {
          name: this.$('form [name="name"]').val(),
          email: this.$('form [name="email"]').val()
        };
      },
      remove: function() {
        this.item.destroy();
        this.item = null;
        appRouter.navigate('', {
          trigger: true
        });
      }
    });
    // main view for show and view
    var MainView = Backbone.View.extend({
      className: 'main stack',
      initialize: function() {
        this.editView = new EditView();
        this.showView = new ShowView();
      },
      render: function() {
        this.$el.append(this.showView.render().el);
        this.$el.append(this.editView.render().el);
        return this;
      },
      edit: function(item) {
        this.showView.$el.removeClass('active');
        this.editView.$el.addClass('active');
        this.editView.change(item);
      },
      show: function(item) {
        this.editView.$el.removeClass('active');
        this.showView.$el.addClass('active');
        this.showView.change(item);
      }
    });

    var AppView = Backbone.View.extend({
      className: 'contacts',
      initialize: function() {
        this.sidebar = new SidebarView({
          model: this.model
        });
        this.main = new MainView();
        this.vdiv = $('<div />').addClass('vdivide');
        this.model.fetch();
        this.render();
      },
      render: function() {
        this.$el.append(this.sidebar.render().el);
        this.$el.append(this.vdiv);
        this.$el.append(this.main.render().el);
        $('#article').append(this.el);
        return this;
      },
      show: function(item) {
        this.sidebar.active(item);
        this.main.show(item);
      },
      edit: function(item) {
        this.sidebar.active(item);
        this.main.edit(item);
      }
    });

    //Router
    var AppRouter = Backbone.Router.extend({
      routes: {
        '': 'show',
        'contacts/:id': 'show',
        'contacts/:id/edit': 'edit'
      },

      show: function(id) {
        appView.show(this.getContact(id));
      },
      edit: function(id) {
        appView.edit(this.getContact(id));
      },
      getContact: function(id) {
        return contacts.getByCid(id);
      }
    });

    var contacts = new Contacts();
    window.appView = new AppView({model: contacts});
    window.appRouter = new AppRouter();
    Backbone.history.start();
  });
})(jQuery);

参考资料:

Comments