Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Structuring complex Backbone.js apps

Structuring complex Backbone.js apps

Building a more complex JavaScript app can easily get out of hand if no effort is put into architecturing the application. I’m overviewing some of the most common higher level architectural patterns and concerns that you should consider when building modular single page apps using Backbone.js.

Mikko Lehtinen

October 31, 2012
Tweet

Other Decks in Technology

Transcript

  1. •faster development •maintainability •performance •reusability •testability good design vs. bad

    design == clean code •tedious to add feats •changes cause bugs •slow UX •code repetition •impossible to test ==
  2. •modules & dependency loading •bindings •inter-object communication •template & DOM

    automation •advanced view management •easy memory management missing from backbone
  3. •how to structure your code into multiple files? •improve code

    organization/modularity, reduce need for globals/namespacing •AMD vs CommonJS vs yourown •development vs. build process modules
  4. AMD example define([ 'base/view', 'views/modals/confirmation_modal_view', 'text!templates/layout/messages.hbs' ], function(View, ConfirmationModalView, template)

    { 'use strict'; var MessagesView = View.extend({ template: template, id: 'messages-view', container: '#messages-container', autoRender: true, showErrorModal: function(errorInfo) { var confirmationModal = new ConfirmationModalView({ info: errorInfo, modalEl: '#confirmation-modal', eventNamespace: 'confirmation-modal', callback: function() {} }); } }); return MessagesView; });
  5. •avoid strong coupling between application modules •observer pattern e.g. Publish/Subscribe:

    • uses an event channel between the subscribers and the publisher • to achieve decoupling you need a mediator between the publisher and subscribers inter-object communication
  6. Pub/Sub usage var AccountSettingsView = View.extend({ initialize: function() { this.constructor.__super__.initialize.apply(this);

    // subscribing to DOM events this.delegate('click', '#save-personal-info-btn', this.savePersonalInfo); // subscribing to mediator events this.subscribeEvent('account:showSettingsModal', this.showSettingsModal); }, savePersonalInfo: function(e) { //... }, showSettingsModal: function() { //... } }); var AnotherView = View.extend({ // ... doSomething: function(e) { this.publishEvent('account:showSettingsModal'); } });
  7. Mediator implementation example from Chaplin // Mediator is just wrapping

    Backbone.Events var mediator = {}; mediator.subscribe = Backbone.Events.on; mediator.publish = Backbone.Events.trigger; // View's event broker uses mediator: subscribeEvent: function(type, handler) { mediator.unsubscribe(type, handler, this); return mediator.subscribe(type, handler, this); } publishEvent: function() { var args, type; type = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; return mediator.publish.apply(mediator,[type].concat(__slice.call(args))); }
  8. •keep properties between two objects in sync, and propagate changes

    in either direction •building markup from model •moving data between views and models •validation; dealing with errors and rendering appropriate error messages data bindings
  9. Repeating this in each view gets tedious... saveModel: function() {

    var name = this.$('.name').val(); var clickThroughUrl = this.$('.clickThroughUrl').val(); var startDate = this.$('.startDate').val() + 'T' + this.$('.startDateHours').val() + ':00:00.000Z'; var picture = this.$('#pictureInput').val(); var largePicture = this.$('#largePictureInput').val(); var backgroundPicture = this.$('#backgroundPictureInput').val(); var trailer = this.$('#trailerInput').val(); var type = this.$('.type').val(); var model = this.model.set({ 'name': name, 'clickUrl': clickThroughUrl, 'startDate': startDate, 'picture': picture, 'largePicture': largePicture, 'backgroundPicture':backgroundPicture, 'trailer':trailer, 'tiers': tiers }); }
  10. Simple example for more complex needs check libs: https://github.com/theironcook/Backbone.ModelBinder https://github.com/mikeric/rivets

    var AccountView = View.extend({ bindings: { 'given_name': '#account-first-name', 'family_name': '#account-last-name', 'email': '#account-email' }, initialize: function() { this.model.on('change', this.render, this); }, updateBindings: function() { _.each(this.bindings, function(selector, attr) { var val = self.$(selector).val(); self.model.set(attr, val, {silent: true}); }); } });
  11. •goals: maintainability, speed •features to watch • syntax - is

    it readable & maintainable • speed - precompilation, caching, rendering speed • extensibility - partials, loops, conditionals, i18n templates
  12. Too much logic makes your templates unmaintainable <tr> ! <td

    class="info-text">Select categories</td> ! <td class="bold-text"> ! ! <div class="controls"> ! ! ! <% var key; for (key in Client.categories) { %> ! ! ! ! <label class="checkbox"> ! ! ! ! ! <input type="checkbox" <% if(_.indexOf(blockedGenres, Number(key)) !== -1) { %> checked="checked" <% } %> ! ! ! ! ! class="categoriesCheckbox" value="<%= key %>"><%= Client.categories[key] %> ! ! ! ! </label> ! ! ! <% } %> ! ! </div> ! </td> </tr>
  13. DRY & Keep logic in JS when possible <ul class="members

    unstyled pull-right hidden-phone"> {{#each membersToShow}} {{>avatar_partial}} {{/each}} </ul> <div class="clear"> <span class="pull-right small show-comments"> {{#if comments}} {{trans i18n.commentsTrans comments}}, {{/if}} </span> </div> Handlebars.registerHelper('partial', function(partialName, options) { return new Handlebars.SafeString(Handlebars.VM.invokePartial( Handlebars.partials[partialName], partialName, options.hash) ); }); Handlebars.registerPartial('avatar_partial', avatar_partial);
  14. •memory-safe lifecycle management • loading/disposing • doing stuff after view

    is added to DOM •event bindings •subview mgmt •rendering, placing in UI, populating data for template view helpers needed
  15. Example: doing stuff after rendering View var AccountView = View.extend({

    afterRender: function() { this.constructor.__super__.afterRender.apply(this); // initialize plugins tied to DOM elements this.$('[rel=tooltip]').tooltip(); this.$('.datepicker-input').data('datepicker', ''); this.$('.datepicker-input').datepicker({format: 'yyyy-mm-dd', autoclose: true}); // render subviews this.renderSubview('addButton', '#add-button-container'); }, }); View.prototype.render = function() { var templateFunc = this.getTemplateFunction(); if (typeof templateFunc === 'function') { var html = templateFunc(this.getTemplateData()); this.$el.empty().append(html); } this.afterRender(); };
  16. •memory leaks are common in single-page apps e.g. due to

    forgetting to unbind events and/or binding events multiple times •clean up after your objects to let garbage collector do its job •implement automation for memory management e.g. in the lifecycle methods memory management
  17. Example: cleaning up a View var BaseView = View.extend({ dispose:

    function() { // clean up sub-objects _.each(this.subviews, function(subview) { subview.dispose(); }); // unsubscribe from events this.unsubscribeAllEvents(); this.modelUnbindAll(); this.off(); // cleanup DOM this.$el.remove(); } });
  18. •backbone.js is not easy to scale to complex apps •learn

    from other frameworks: ember, knockout, batman •reuse and build on top of other’s libraries •use opinionated backbone application frameworks: Backbone.Marionette, Chaplin, Thorax don’t reinvent the wheel