Todo – Backbone vs. Spine
Min tanke är att använda mig utav Addy Osmanis TodoMVC projekt som är en Todo-applikation skriven i många olika ramverk och tekniker inom JavaScript. Du kan kika på projektet här. Det är fler utvecklare inblandade i projektet men Addy är lead.
Tanken är helt enkelt att jämför de olika implementationerna mellan hur applikationen är implementerad i Backbone och Spine. Jag kommer inte fördjupa mig någonting i Hur Spine används utan enkelt försöka förklara skillnaderna i varje Modell, Vy och Controller på en grundläggande nivå.
Eftersom SpineJS är CoffeScript så passar det bra att nämna min grundläggande artikel om CoffeScript.
Modell
Vi börjar med att kika på vad det är för skillnad på modellerna i Backbone respektive Spine.
Model i Backbone
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
| var Todo = Backbone.Model.extend({
// Default attributes for the todo.
defaults: {
content: "empty todo...",
done: false
},
// Ensure that each todo created has `content`.
initialize: function() {
if (!this.get("content")) {
this.set({"content": this.defaults.content});
}
},
// Toggle the `done` state of this todo item.
toggle: function() {
this.save({done: !this.get("done")});
},
// Remove this Todo from *localStorage* and delete its view.
clear: function() {
this.destroy();
}
}); |
Model i Spine (Tyvärr så har itne WP-Syntax stöd för CoffeScript)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class window.Task extends Spine.Model
@configure 'Task', 'name', 'done'
@extend Spine.Model.Local
validate: ->
'Task name is required' unless $.trim(@name)
@active: ->
@select (task) -> !task.done
@done: ->
@select (task) -> !!task.done
@destroyDone: ->
task.destroy() for task in @done() |
Backbone modellen ser ganska identisk ut med hur vi har gjort under kursen med våra egna applikationer så där behöver väl jag inte nämna någonting speciellt, förutom ett annat sätt att sätta default-värden på modellerna vilket jag inte visste att man kunde göra på detta vis. Vi kan även notera här att @ används istället för att this (t.ex. this.egenskap => @egenskap). Även om syntaxen verkar vara lite klurig att läsa/förstå så går det enkelt när man har läst på om hur CoffeScript fungerar och dess syntax.
Router/Controller
Vad jag kan se i Backbone implementationen av Todo applikationen så finns det ingen router, så denna kan jag inte göra någon jämförelse mot, däremot kan jag nämna att SpineJS använder sig utav controllers.
app.coffe – Huvudcontroller för applikationen
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
| class TaskApp extends Spine.Controller
elements:
'.items': 'tasks'
'.countVal': 'counter'
'a.clear': 'clearCompleted'
'form#new-task input[name=name]': 'newTaskName'
events:
'submit form#new-task': 'new'
'click a.clear': 'clearCompleted'
constructor: ->
super
Task.bind 'create', @renderNew
Task.bind 'refresh', @renderAll
Task.bind 'refresh change', @renderCounter
Task.bind 'refresh change', @toggleClearCompleted
Task.fetch()
new: (e) ->
e.preventDefault()
Task.fromForm('form#new-task').save()
@newTaskName.val('')
renderNew: (task) =>
view = new Tasks(task: task)
@tasks.append view.render().el
renderAll: =>
Task.each @renderNew
renderCounter: =>
@counter.text Task.active().length
toggleClearCompleted: =>
if Task.done().length
@clearCompleted.show()
else
@clearCompleted.hide()
clearCompleted: ->
Task.destroyDone()
$ ->
new TaskApp(el: $('#tasks')) |
Efter att ha kikat på koden en snabbis så framgår det att upplägget för applikationen i både Backbone och Spine är väldigt likt. Controllern börjar med att definera vilka element som den ska använda sig utav, definerar events och har en constructor (motsvarighet till Backbones initialize) där han binder event för Task-objektet (De verkar inte finns någon motsvarighet till Backbone collections i Spine). Sedan är det helt vanliga funktioner enligt vanlig Backbone standard kan man säga. Vad jag kan se så händer det inget här som man inte kan förstå. Det ända jag kan notera i controllern är att den faktiskt mer liknar en Backbone vy än en Backbone router.
Vyer
Vi vet alla hur en vy i Backbone ser ut, men för att förenkla jämförelsen så kommer AppView:n här.
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
| // Our overall **AppView** is the top-level piece of UI.
var AppView = Backbone.View.extend({
// Instead of generating a new element, bind to the existing skeleton of
// the App already present in the HTML.
el: $("#todoapp"),
// Our template for the line of statistics at the bottom of the app.
statsTemplate: _.template($('#stats-template').html()),
// Delegated events for creating new items, and clearing completed ones.
events: {
"keypress #new-todo": "createOnEnter",
"keyup #new-todo": "showTooltip",
"click .todo-clear a": "clearCompleted",
"click .mark-all-done": "toggleAllComplete"
},
// At initialization we bind to the relevant events on the `Todos`
// collection, when items are added or changed. Kick things off by
// loading any preexisting todos that might be saved in *localStorage*.
initialize: function() {
_.bindAll(this, 'addOne', 'addAll', 'render', 'toggleAllComplete');
this.input = this.$("#new-todo");
this.allCheckbox = this.$(".mark-all-done")[0];
Todos.bind('add', this.addOne);
Todos.bind('reset', this.addAll);
Todos.bind('all', this.render);
Todos.fetch();
},
// Re-rendering the App just means refreshing the statistics -- the rest
// of the app doesn't change.
render: function() {
var done = Todos.done().length;
var remaining = Todos.remaining().length;
this.$('#todo-stats').html(this.statsTemplate({
total: Todos.length,
done: done,
remaining: remaining
}));
this.allCheckbox.checked = !remaining;
},
// Add a single todo item to the list by creating a view for it, and
// appending its element to the `<ul>`.
addOne: function(todo) {
var view = new TodoView({model: todo});
this.$("#todo-list").append(view.render().el);
},
// Add all items in the **Todos** collection at once.
addAll: function() {
Todos.each(this.addOne);
},
// Generate the attributes for a new Todo item.
newAttributes: function() {
return {
content: this.input.val(),
order: Todos.nextOrder(),
done: false
};
},
// If you hit return in the main input field, create new **Todo** model,
// persisting it to *localStorage*.
createOnEnter: function(e) {
if (e.keyCode != 13) return;
Todos.create(this.newAttributes());
this.input.val('');
},
// Clear all done todo items, destroying their models.
clearCompleted: function() {
_.each(Todos.done(), function(todo){ todo.clear(); });
return false;
},
// Lazily show the tooltip that tells you to press `enter` to save
// a new todo item, after one second.
showTooltip: function(e) {
var tooltip = this.$(".ui-tooltip-top");
var val = this.input.val();
tooltip.fadeOut();
if (this.tooltipTimeout) clearTimeout(this.tooltipTimeout);
if (val == '' || val == this.input.attr('placeholder')) return;
var show = function(){ tooltip.show().fadeIn(); };
this.tooltipTimeout = _.delay(show, 1000);
},
toggleAllComplete: function () {
var done = this.allCheckbox.checked;
Todos.each(function (todo) { todo.save({'done': done}); });
}
}); |
En helt vanlig Backbone vy.
Nu blir det lite repetition här men det är för att (som jag nämnde ovan) att controllers i Spine verkar fungera som vyer också. Nedan kommer samma controller som ovan:
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
| class TaskApp extends Spine.Controller
elements:
'.items': 'tasks'
'.countVal': 'counter'
'a.clear': 'clearCompleted'
'form#new-task input[name=name]': 'newTaskName'
events:
'submit form#new-task': 'new'
'click a.clear': 'clearCompleted'
constructor: ->
super
Task.bind 'create', @renderNew
Task.bind 'refresh', @renderAll
Task.bind 'refresh change', @renderCounter
Task.bind 'refresh change', @toggleClearCompleted
Task.fetch()
new: (e) ->
e.preventDefault()
Task.fromForm('form#new-task').save()
@newTaskName.val('')
renderNew: (task) =>
view = new Tasks(task: task)
@tasks.append view.render().el
renderAll: =>
Task.each @renderNew
renderCounter: =>
@counter.text Task.active().length
toggleClearCompleted: =>
if Task.done().length
@clearCompleted.show()
else
@clearCompleted.hide()
clearCompleted: ->
Task.destroyDone()
$ ->
new TaskApp(el: $('#tasks')) |
Det finns även en till controller i Spine applikationen som definerar en controller för varje task-object, som fasktiskt verkar användas som en vy här innuti renderNew funtionen. Jag hade nog mer valt att kalla den för en sorts partiel vy, men det är nog för att jag kommer från Backbone och hade säkerligen tyckt att Backbone gör det konstigt om jag kommit från Spine.
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
| class window.Tasks extends Spine.Controller
ENTER_KEY = 13
ESCAPE_KEY = 27
elements:
'form.edit': 'form'
events:
'click a.destroy': 'remove'
'click input[type=checkbox]': 'toggleStatus'
'dblclick .view': 'edit'
'keypress input[type=text]': 'finishEditOnEnter'
'blur input[type=text]': 'finishEdit'
constructor: ->
super
@task.bind 'update', @render
@task.bind 'destroy', @release
render: =>
@replace $('#task-template').tmpl(@task)
this
remove: ->
@task.destroy()
toggleStatus: ->
@task.updateAttribute 'done', !@task.done
edit: ->
@el.addClass('editing')
@$('input[name=name]').focus()
finishEdit: ->
@el.removeClass('editing')
@task.fromForm(@form).save()
finishEditOnEnter: (e) ->
if e.keyCode in [ENTER_KEY, ESCAPE_KEY] then @finishEdit() |
Här känner jag även att, med utgångspunkt från min Todo-applikation, att det här borde finnas direkt på modellen och inte på en controller/vy. Men allt beror självklart på hur man väljer att implementera sin applikation.
Slutsats
Efter att ha läst på, och skrivit en artikel om grunderna i CoffeScript, och sedan kikat igenom koden för Backbone respektive Spine så känner jag att implementationerna är nästan identiska med varandra. Detta kanske beror på att de är skriva av samma person/personer men jag trodde faktiskt först att det skulle vara större skillnad syntax och struktur mässigt än vad det faktiskt var.
Jag känner att man blir lite små förvirrad över vad som egentligen är en vy och vad som är en controller i Spine, kanske faller det på valet av hur applikationen är implementerad men jag blev lite förvirrad just när det gäller dessa två typer av objekt. Enligt den här implementationen så framgår det ganska klart och tydligt att controller är en typ controller och vy, och att det inte direkt finns någon uppdelning av dessa.
Med en del Backbone kunskap och en grundläggande överblick över hur CoffeScript fungerar så kan jag klart och tydligt se att Spine implementationen klart är mycket mer lättläst och enklare att förstå. Detta beror antagligen mest på skillnaden i hur syntax ser ut i CoffeScript som Spine är skrivet i, det ger klara fördelar om man kan syntaxen, framför allt så sparar det en hel del tråkiga klammrar och långa filer.
Jag kommer nog definitiv att kika mer på Spine/CoffeScript och börja skriva applikationer i detta språk, men först måste jag få upp en fungerande Node server som inte riktigt gick som jag tänkt förra gången jag försökte mig på detta.