Migrating MVC to theFront-end usingBackbone JS.
Planbox is a single-page web application for Agile project management. It was built using the traditional MVC stack with CodeIgniter (PHP) and jQuery (Javascript). AJAX was heavily used to update DOM elements to offer a dynamic user experience. UX logic code quickly became spread across Javascript and PHP. The application code base quickly became unmanageable and scaling functionality became difficult. Things had to change.
A decision was made to change architecture: bring all the UX logic in the front-end, and turn the back-end into an engine in charge of business logic.
This talk is about this experience. How we moved the MVC stack from the back-end to the front-end. How we used Backbone JS as the foundation of our front-end framework and built on top. How the backend became a black-box with a Restful API. What lessons we learned, what benefits we gained, and what reflections we made about the future of MVC in Javascript.
9. AGILE PROJECT MANAGEMENT
Client http://www.planbox.com/plan
Web Page
HTML
CSS
Was architected 1 Javascript
on Back-end Server 4
5
Controller View
MVC 2
Model
3
Database
Engine
Martin Drapeau Feb 2012
11. MVC in the Back-end
HTML • HTML generated in PHP
HTML
HTML • AJAX to update page
fragments
• Javascript is the duck-tape
HTML
Feature development
Bottleneck!
Martin Drapeau Feb 2012
12. MVC in the Back-end
A
B
C
Change of A requires update of B and C.
Why does the back-end have to do that?
Martin Drapeau Feb 2012
13. MVC in the Back-end
does not scale for
Dynamic Web Apps
WHY?
• Where do I write that code? PHP or JS?
• Can we push to production? No, back-end has
changed!
• Developers must be trained on PHP and JS.
• Etc...
Martin Drapeau Feb 2012
15. Move MVC to Front-end
Let’s write our code in Javascript!
Martin Drapeau Feb 2012
16. Ground Rules
Back-end
• Becomes a REST API. Talks JSON.
Front-end
• Handles UI interactions.
• Replicates data models.
• Replicates business logic only when necessary.
Must be fast and lightweight!
Martin Drapeau Feb 2012
17. Choose a Framework
Requirements:
• Lightweight - fast to load & mobile friendly
• Extendible
• jQuery Friendly
• Open Source
• Can be integrated gradually
Martin Drapeau Feb 2012
18. Contenders (back in Nov 2010)
• SproutCore
A widget framework, too big and constraining!
• Javascript MVC
Too much stuff in there! Constraining and awkward.
• Knockout
MVVM?? I don’t want to couple views and models!
• Backbone JS
Small, has collections, HTML templates, etc. Natural fit.
A recent source of comparisons can be found here:
http://codebrief.com/2012/01/the-top-10-javascript-mvc-frameworks-reviewed/
Martin Drapeau Feb 2012
20. Backbone.js gives structure to web applications
by providing models with key-value binding and
custom events, collections with a rich API of
enumerable functions, views with declarative
event handling, and connects it all to your
Jeremy Ashkenas,
existing API over a RESTful JSON interface. Author of
Backbone JS ,
Underscore JS, and
CoffeeScript
•Open Source
•5.3kb, Packed and gzipped
•87 contributors
•6,500 watchers
Martin Drapeau Feb 2012
21. Backbone’s Take on MVC
• Data and presentation are decoupled:
– “stop tying your data to the DOM”
• Models use the Pub/Sub pattern:
– Views subscribe to model change events to re-render
• Collections are used to group models:
– Events bubble up to collections. Subscribe to the add event on the collection
to get notified.
• Models are synchronized with your back-end:
– Via AJAX to your RESTful API. CRUD by default.
• Views control user interactions and trigger model updates:
– HTML templates, jQuery or Zepto DOM manipulation and AJAX
Where is the C in MVC? The Controller is in the View.
Martin Drapeau Feb 2012
22. Model – attributes and events
var story = new Backbone.Model({
id:1,
name:"As a coder I want MVC in the front-end",
points:21
});
story.bind("change:points", function(model, points) {
console.log("Story #"+model.id+" points changed from "+
model.previous("points")+" to "+points);
});
story.get("points"); // Returns 21
story.set({points:99});
Console
>>> Story #1 points changed from 21 to 99
Martin Drapeau Feb 2012
23. Collection – model IDs and Underscore JS
var stories = new Backbone.Collection([{
id:1,
name:"As a coder I want MVC in the front-end",
points:21
}, {
id:2,
name:"As I PM, I want visibility",
points:33
}]);
var story = stories.get(2); // Return model #2
stories.pluck('points'); // Returns [21, 33]
Martin Drapeau Feb 2012
24. Collection – event bubbling
stories.bind("add", function(collection, model) {
console.log("Story #"+model.id+" added to collection");
});
stories.add({
id:3,
name:"As a driver, I want a reliable car"
points:5
});
stories.pluck('points'); // Returns [21, 33, 5]
Console
>>> Story #3 added to collection
Martin Drapeau Feb 2012
25. Collection – ordered or unordered
stories.pluck('points'); // Returns [21, 33, 5]
stories.comparator = function(modelA, modelB) {
var a = modelA.get('points');
var b = modelB.get('points');
return a < b ? -1 : a > b ? 1 : 0;
}
stories.sort();
stories.pluck('points'); // Returns [5, 21, 33]
Martin Drapeau Feb 2012
26. Collection – fetch from server with AJAX
stories.url = "/api/get_stories"; // REST API
// Preprocess JSON before creating models
stories.parse = function(object) {
if (object && object.content) return object.content;
};
stories.fetch({
data: {product_id:1}, // jQuery $.ajax arguments
dataType: 'JSONP',
success: function(collection) {
console.log('Loaded '+collection.length+' stories');
},
error: function(collection, error) {
console.log(error);
}
});
>>> Loaded 87 stories Console
Martin Drapeau Feb 2012
27. View – Render a model
Use of extend to create a new Class.
StoryView = Backbone.View.extend({
className: "story",
render: function() {
var view = this;
var model = this.model;
$(this.el).text(model.get('name'));
return view; // Always return view for chaining
}
});
var view = new StoryView({model: story});
view.render();
$(view.el).appendTo('body');
Martin Drapeau Feb 2012
28. View – HTML template (Underscore JS)
<script type="text/template" id="story_template">
<label>#<%=id%></label>
<input type="text" value="<%=name%>" />
<button>Delete</button>
</script>
StoryView = Backbone.View.extend({
className: "story",
template: _.template($("#story_template").html()),
render: function() {
var view = this;
var model = this.model;
$(this.el).html(view.template(model.toJSON()));
return view;
}
});
Martin Drapeau Feb 2012
29. View – User interaction
initialize: function() {
var view = this, model = view.model;
model.bind("change", view.render, view);
model.bind("remove", view.remove, view);
return view;
},
events: { // Delegate events
"change input": "saveName",
"click button": "del"
},
saveName: function() {
var view = this, model = view.model;
var new_name = $(view.el).children("input").val();
model.save({name:new_name}); // Call server then model.set
return view;
},
del: function() {
var view = this, model = view.model;
model.destroy(); // Call server then collection remove
return view;
}
Martin Drapeau Feb 2012
30. Client/Server Sync Loop
1. Push changes to server
2. Receive acknowledgement
3. Set attribute change on data model
4. View captures change event
5. View renders (maybe)
Martin Drapeau Feb 2012
31. Event Payload – options argument
The options argument can be used as an event payload.
Use it not to render unnecessarily.
saveName: function() {
var view = this, model = view.model;
var new_name = $(view.el).children("input").val();
model.save({name:new_name}, {caller:view});
return view;
}
render: function(options) {
options || (options = {});
if (options.caller == this) return this;
var view = this;
var model = this.model;
$(this.el).html(view.template(model.toJSON()));
return view;
}
Martin Drapeau Feb 2012
32. View – Render a collection
StoriesView = Backbone.View.extend({
tagName: "ul",
className: "stories",
initialize: function() {
var view = this;
var collection = view.collection;
collection.bind('add', view.addOne, view);
collection.bind('reset', view.addAll, view);
},
addOne: function(model) {
var view = this;
var story_view = new StoryView({model: model});
story_view.render();
$(view.el).append(story_view.el);
return view;
},
addAll: function() {
var view = this, collection = view.collection;
collection.each(function(model) {
view.addOne(model);
});
return view;
}
});
Martin Drapeau Feb 2012
33. Other Backbone Artefacts
• Sync
– Handles client/server communication
– Uses jQuery (or Zepto) $.ajax
– Follows CRUD (Model.url and Collection.url)
• Router & History
– Handle URL changes and that Back button
– Wraps HTML5 History API
– Graceful polling fallback
Martin Drapeau Feb 2012
34. Backbone JS
got
Planboxed
Martin Drapeau Feb 2012
35. No CRUD - Rich verbs mapping to API
• A story model has:
– save www.planbox.com/api/update_story
– move www.planbox.com/api/move_story
– duplicate www.planbox.com/api/duplicate_story
– etc...
• Did not use Sync
– Overwrote methods fetch, save, etc.
– Used jQuery $.ajax of course
Martin Drapeau Feb 2012
36. Local Persistence in URL
• State of page is saved in URL’s hash tag using
JSON (quotes are stripped)
Opened tab in Story
Selected Story
Pane
http://planbox.com/nik#{product_id:1,story_pane_tab:tasks,filter_id:1,story_id:199486}
Top level project
selected Selected Sprint
https://github.com/martindrapeau/JSON-in-URL-Hash
Martin Drapeau Feb 2012
37. Model Selection
NikModel = Backbone.Model.extend({
selected: false,
select: function(options) {
options || (options = {});
if (this.selected) return this;
this.selected = true;
if (!options.silent)
this.trigger('select', this, options);
return this;
},
unselect: function(options) {
options || (options = {});
if (!this.selected) return this;
this.selected = false;
if (!options.silent)
this.trigger('unselect', this, options);
return this;
}
Martin Drapeau Feb 2012
39. Collection Filtering
// Argument filters is a hash of attributes and their
// allowed values. For example: filters.type = ['bug']
// Property Model.filtered is a hash of failed conditions.
// For example is Model.type = 'feature', then
// Model.filtered = ['bug'].
Model.filter(filters, options)
Model.filtered // Array of filtered out attributes
Collection.filter() // Runs filtering on the collection
Collection.filtered()
Collection.not_filtered()
Martin Drapeau Feb 2012
40. Sort DOM Against Collection
DOM Collection
#199834
#190293
#201293
#150932
#188456
#178545
#190293
• Stories are ordered by priority (same for everyone)
• Need to sync order of stories (i.e. after drag and drop)
• Helper function to re-order DOM elements minimizing DOM
manipulations
Martin Drapeau Feb 2012
41. Cascading Events – Model bound to Collection
• Summary Model listens to changes on Stories Collection
• Keeps sums of points, resources, estimate-hours, etc...
• Views tied to it update in real time!
Martin Drapeau Feb 2012
42. Custom Widgets
• All widgets are custom
– Ligthweight and flexible
– Fast to load
– No jQuery UI with large CSS and JS footprint
– Use of Backbone Models and Collections
• Types of widgets
– Dialogs
– Drop-downs
– Date picker
– Auto-complete
– Input
Martin Drapeau Feb 2012
44. Real Time – No page reload
• Polling server at 15 second interval
(Tried COMET but was overkill)
• Change history Collection
– Last transaction ID is kept in client
– Every 15 seconds, ask server for newer updates
– Server sends updated Models
Martin Drapeau Feb 2012
45. Deployment
Build procedure
– Javascript files are minified and collated
– Version is automatically incremented
– ?v=3.1234 is added to all CSS/JS files (prevent caching issues)
Planbox - Minifying Javascript Files...
Version: 3.019
3rd.js 70397 bytes hom.js 9758 bytes
jwysiwyg/jquery.wysiwyg.min.js 36278 bytes acc.js 14505 bytes
zeroclipboard/zeroclipboard.min.js 6747 bytes rep.js 84129 bytes
adw.js 32136 bytes table.js 42311 bytes
common.js 195872 bytes mobile_3rd.js 74594 bytes
nik.js 230008 bytes mobile_common.js 71371 bytes
mag.js 53275 bytes mobile.js 116873 bytes
Martin Drapeau Feb 2012
48. Benefits of MVC in front-end
• Decouple back-end and front-end
– The API becomes the back-end contract (and
performance too I hope).
– Back-end and front-end teams are decoupled
• Bandwidth reduction
– Data and presentation loaded once
– Subsequently, only Model deltas are transferred
• Reduce server load
– All UX processing is performed client side
Martin Drapeau Feb 2012
49. Benefits of MVC in front-end
• Release anytime
– Majority of code changes are in UI
– Changes happen faster in front-end than back-end
– Concurrent versions of the client can exist. Hit F5 to get the
latest and greatest.
– Back-end doesn’t change – no risk of conflicts in browser
• Faster development
– Know and master one language
– Use the browser as your development and testing bed (yeah
Firebug!)
– Release often
– Don’t accumulate ‘corrected’ defects in development – push
them out as they are fixed
Martin Drapeau Feb 2012