Refactor Large applications with Backbone

64
From Mess to Success, Refactoring Large Applications with Backbone dev.Objective() May 14, 2015 Stacy London @stacylondoner McWay Falls, Julia Pfeiffer Burns State Park - Big Sur, CA - 2015 by Stacy London

Transcript of Refactor Large applications with Backbone

From Mess to Success, Refactoring Large Applications with Backbone

dev.Objective() May 14, 2015 Stacy London

@stacylondoner

McWay Falls, Julia Pfeiffer Burns State Park - Big Sur, CA - 2015 by Stacy London

About Me

I’ve been making things for the web since 1998.

Currently I’m a Senior Application Architect focused on Front-End Engineering at Northwestern Mutual.

@stacylondoner

Northwestern Mutual

Northwestern Mutual has been helping families and businesses achieve financial security for nearly 160 years. Our financial representatives build relationships with clients through a distinctive planning approach that integrates risk management with wealth accumulation, preservation and distribution. With more than $230 billion in assets, $27 billion in revenues, nearly $90 billion in assets under management and more than $1.5 trillion worth of life insurance protection in force, Northwestern Mutual delivers financial security to more than 4.3 million people who rely on us for insurance and investment solutions, including life, disability and long-term care insurance; annuities; trust services; mutual funds; and investment advisory products and services.

Northwestern Mutual is the marketing name for The Northwestern Mutual Life Insurance Company, Milwaukee, WI, and its subsidiaries. Northwestern Mutual and its subsidiaries offer a comprehensive approach to financial security solutions including: life insurance, long-term care insurance, disability income insurance, annuities, investment products, and advisory products and services. Subsidiaries include Northwestern Mutual Investment Services, LLC, broker-dealer, registered investment adviser, member FINRA and SIPC; the Northwestern Mutual Wealth Management Company, limited purpose federal savings bank; and Northwestern Long Term Care Insurance Company.

What do I mean by large application?

• a multi-page web application (MPA) built with ASP.NET MVC

• 1500+ files | 343,000+ lines of code

• UI (CSS / JS)

• 230+ files | 42,000+ lines of code

Development Culture

• JEE/Java shop turned .NET/C#

• view JS as a bit of a “toy” language

• as JS was being added it was just being added without much thought to maintainability, extensibility, testability, patterns, etc.

• app was going to be heavy with JS yet initially on a platform that wasn’t optimized for this kind of app dev.

• path of least resistance was to just put everything in $(document).ready()

https://www.youtube.com/watch?v=J---aiyznGQ

http://starecat.com/kermit-typing-on-a-typewriter-like-crazy-animation/

$(document).ready(function() {

});

http://www.wikihow.com/Make-a-Quick-Italian-Spaghetti

Problems

• one large JavaScript file in the <head> (all code included on every page regardless if it needed it)

• all JavaScript global and in $(document).ready()

• some pages had inline JavaScript

• not commented

• no unit tests

• unclear if a function / event was used on multiple pages

Iterative Refactoring

• big codebase

• large team working on 2 week sprints that couldn’t be slowed down

• refactors had to fit into a sprint

Refactor, Iteration 1: Break Apart JS

• figure out what JS belongs with each page and move that JS into a separate file for that page

• include that JS with the partial HTML

Refactor, Iteration 1: Start Adding Unit Tests

• start adding unit tests for original code then make sure it passed after refactoring as a safety/confidence mechanism

• to prepare for this journey of breaking code into smaller, more testable chunks it was good to start early with creating a test bed

• help make subsequent refactors less stressful

Refactor, Iteration 1: Start Adding Unit Tests

• picked Jasmine (BDD style) - http://jasmine.github.io/

• a lot of the JS tied to DOM manipulation so not in an ideal state for unit testing with Jasmine out of the box

• needed DOM testing helper so added jquery-jasmine library to add fixtures (HTML snippets to run tests against)

• no external UI Regression tools (e.g. Selenium) in a state to do this validation throughout the refactor

Refactor, Iteration 2: Namespacing & Object Literal

• create an app namespace to get custom code out of the global namespace (window) to avoid collisions with external libs/frameworks

• introduce an Object Literal notation style in preparation for Backbone (which uses this pattern)

• helps to organize the code and parameters logically “an object containing a collection of key:value pairs with a colon separating each pair of keys and values where keys can also represent new namespaces”

• add closures with Instantly Invoked Function Expressions (IIFEs)

https://gist.github.com/stacylondon

;var myApp = myApp || {};

(function($, myApp) {

'use strict';

$.extend(myApp, {

init: function() { this.commonStuff(); },

commonStuff: function() { // common code across many or all pages }

});

})(window.jQuery, window.myApp);

$(document).ready(function() {

myApp.init();

});

my-app.js

my-app.js with comments

// prefix with semi-colon as safety net against concatenated // scripts and/or other plugins that are not closed properly;// check for existence of myApp in the global namespace var myApp = myApp || {};// Use IIFE to:// * encapsulate app logic to protect it from global namespace // * pass in namespace so can be modified locally and isn't// overwritten outside of our function context// * ensure $ only refers to window.jQuery (defensive programming)// * $ passed through as local var rather than as globals and this // (slightly) quickens the resolution process and can be more // efficiently minified (especially if regularly referenced)(function($, myApp) {

my-app.js with comments

(function($, myApp) {

// Strict mode makes several changes to normal JavaScript semantics: // * eliminates some JS silent errors by changing them to throw errors. // * fixes mistakes that make it difficult for JavaScript engines to // perform optimizations: strict mode code can sometimes be made to // run faster than identical code that's not strict mode. Add inside // the IIFE so it's defined for just the functions defined within and // doesn't flip concatenating/minified code to strict inadvertently 'use strict';

// extend the namespace with more functionality $.extend(myApp, {

;var myApp = myApp || {};myApp.pageOne = myApp.pageOne || {};

(function($, myApp) {

'use strict';

$.extend(myApp.pageOne, {

init: function() { this.pageSpecificStuff(); },

pageSpecificStuff: function() { // code specific to this page }

});

})(window.jQuery, window.myApp);

$(document).ready(function() {

myApp.pageOne.init();

});

page-one.js

Better but not great

• code is aligned with the screen to which is pertains

• pages now have mid-page script includes which isn’t good for performance / rendering

• still hand-wiring Ajax calls

• entire-page-JS is not very modular

• want to break screens down into smaller features and keep events neatly associated

Backbone to the rescue

• wanted something that provided/enforced structure but in a lightweight way

• it’s a MPA not a SPA so full-featured SPA frameworks didn’t make sense (e.g. Angular, Ember)

• wanted to be able to use just a small feature of the library/framework and add more full integration over time (refactor in multiple iterations)

• for these reasons Backbone.js made sense - http://backbonejs.org/

Refactor, Iteration 3: Page Level Backbone Views

• reducing boilerplate code

• views enforce organization of events

• views enforce an Object Literal notation pattern

• views helped developers think about encapsulating pieces of the screen

• starting with just Views gave the team time to start planning for refactoring back-end data provided by ASP.NET MVC Controllers into more RESTful web services (Web API) which is necessary to take full advantage of Backbone.js Models/Collections

Refactor, Iteration 3: Page Level Backbone Views

• my-app.js - is now responsible for instantiating the app namespace and setting up a super light-weight view manager for Backbone.js

• instantiate all available views on the page upon DOM ready.

• page-one-view.js - no longer namespaced individually since being added to the views object of the app namespace

my-app.jsvar myApp = myApp || {};

(function($, myApp) { 'use strict'; $.extend(myApp, { init: function() { this.initializeViews(); },

// view manager code });})(window.jQuery, window.myApp);

$(document).ready(function() { myApp.init();});

my-app.js view manager

views: {}, initializeViews: function() { for (var view in this.views) { if (typeof this.views[view] === 'function') { this.views[view.substring(0, 1).toLowerCase() + view.substring(1, view.length)] = new this.views[view](); } } }, addView: function(key, view) { if (this.views.hasOwnProperty(key)) { throw (new Error('A view with that key already exists.')); } this.views[key] = view; }, getView: function(key) { if (!this.views.hasOwnProperty(key)) { throw new Error('View does not exist in views collection.'); } return this.views[key]; }

page-one-view.js;(function($, _, Backbone, myApp) {

'use strict';

var PageOneView = Backbone.View.extend({ el: '#pageOneMainContainer',

events: { 'click .something': 'doSomething' }, initialize: function() { this.setupValidation(); }, setupValidation: function() {

}, doSomething: function() {

} });

myApp.addView('PageOneView', PageOneView);

}(window.jQuery, window._, window.Backbone, window.myApp));

Refactor, Iteration 4: Sub-Page Backbone Views

• start breaking the large page views into sub-page views so that you can get truly modular

• share modules/code if a module exists on more than one page

Refactor, Iteration 5: Backbone Models, Collections

• move code that is getting data and doing any business logic out of the Views and into Models and Collections

• e.g. move phone number formatting out of View and into the Model

• remove hand written Ajax, use BB API (e.g. fetch)

• make sure server side controllers were written in a RESTful way

• still not complete across the application

var PageOneView = Backbone.View.extend({

el: '#pageOneMainContainer',

initialize: function() { this.getSomeData(); },

getSomeData: function() { var self = this; $.ajax({ type: 'GET', url: 'SomeEndpoint', data: formData, dataType: 'json', success: function(data, textStatus, jqXHR) { self.displayResults(data); }, error: function(jqXHR, textStatus, errorThrown) { self.displayError(); } }); },

displayResults: function(data) { },

displayError: function() { }

});

Hand-wiring Ajax

Use Backbone.js API to get data

// initialize viewvar pageOneView = new PageOneView({ collection: new PageOneCollection([]),});

var PageOneView = Backbone.View.extend({

el: '#pageOneMainContainer',

template: _.template($('#page-one-template').html()),

initialize: function() {},

render: function() { this.$el.html(this.template({'collection': this.collection})); // maintain chainability return this; }

});

var PageOneCollection = Backbone.Collection.extend({

initialize: function(models, options) {},

model: PageOneModel,

// RESTful web service URL url: '/SomeEndpoint'

});

Refactor, Iteration 6: Mini-SPA

• treat each page of the multi-page app (MPA) like it’s a miniature single page app (SPA)

• each page has a single entry point (main.js)

• this will setup the code for a module loader / build system so that we can finally move the JS to the bottom of the page and remove mid-page scripts

• improve performance (time to first paint - how long it takes between a user entering a URL into the browser and when he/she sees visual activity on screen)

Refactor, Iteration 7: Modules

• current version of JavaScript (ECMA-262) doesn’t provide a way to import modules of code like more traditional programming languages do

• modules are proposed for the next version of JS (ES6/ES2015/Harmony)

• to use modules and manage dependencies with the current version of JS you can use community driven methodologies

• there are two popular styles with associated script loaders / build systems

Refactor, Iteration 7: Modules - AMD

• AMD - Asynchronous Module Definition“The Asynchronous Module Definition (AMD) API specifies a mechanism for defining modules such that the module and its dependencies can be asynchronously loaded. This is particularly well suited for the browser environment where synchronous loading of modules incurs performance, usability, debugging, and cross-domain access problems.”https://github.com/amdjs/amdjs-api/wiki/AMD

Refactor, Iteration 7: Modules - CommonJS

• CommonJS’s module format was made popular for server-side JavaScript development (namely for Node.js/NPM)

• it’s synchronous

• syntax is a bit easier as it frees you from the define() wrapper that AMD enforces

• requires a build in a JS runtime

Refactor, Iteration 7: Modules - RequireJS

• popular script loader written by James Burke that helps you load multiple script files and define modules with or without dependencies

• use RequireJS as first pass at module loading since async nature means no build step during dev time (least disruption to the dev team which is important because there is a learning curve)

• use almond (AMD API shim) so don’t have to add a special script tag to load RequireJS and change any HTML

• this will move the code away from IIFEs to modules with dependency management

my-app.js// requireJS simplified commonJS wrapper// do this so can use the commonJS style & // make switch to browserify easierdefine('app', function(require, exports, module) {

'use strict';

var $ = require('jquery'); var _ = require('underscore'); var Backbone = require('backbone'); var Modernizr = require('modernizr'); var HeaderController = require('./header-controller'); var FooterController = require('./footer-controller');

var app = new Backbone.Application();

module.exports = app;

});

(function() {

var $ = require('jquery'); var app = require('app');

// dom ready $(function() { app.start(); });

}());

;(function() {

'use strict';

var $ = require('jquery'); var PageOneController = require('./page-one-controller'); var app = require('app');

// dom ready $(function() { app.start({ ScreenController: PageOneController }); });

}());

page-one-main.js

define('page-one-controller', function(require, exports, module) {

'use strict';

var Backbone = require('backbone'); var PageOneView = require('PageOneView');

var PageOneController = Backbone.Controller.extend({

initialize: function(options) { var pageOneView = new PageOneView(); }

});

module.exports = PageOneController;

});

page-one-controller.js

define('page-one-view', function(require, exports, module) {

'use strict';

var Backbone = require('backbone');

var PageOneView = Backbone.View.extend({

});

module.exports = PageOneView;

});

page-one-view.js

Refactor, Iteration 7: Modules - Browserify

• Browserify lets you require('modules') in the browser by bundling up all of your dependencies during a build step

• it’s the end-state module loader because it allows for easy bundle splitting

• figure out common/shared JS and create a common bundle then a bundle for the unique code on each page

• by having a main.js for each screen this will act as an entry point so Browserify can find all the modules required and create a screen-specific bundle

Refactor, Iteration 7: Modules - Browserify

• means build during dev but we were already doing that for our CSS with LESS

• “It’s great the dev community has embraced compilation because it’s inevitable.” - Brendan Eich at Fluent 2015

• the code will become very clean

'use strict';

var Backbone = require('backbone');

var PageOneView = Backbone.View.extend({

el: '#pageOneMainContainer',

initialize: function() {}

});

module.exports = PageOneView;

page-one-view.js

Summary

• the JavaScript is now modular and easier to maintain

• Backbone.js helps devs keep consistent with coding patterns and organization (Object Literal notation, events)

• events are scoped to the smallest part of the page to which they matter (Backbone.js)

• namespacing/IIFEs and then later module loader/build removes the possibility of collisions with other frameworks/libs

• unit tests mean you can feel more confident to change things

• only sending JS to the browser that is necessary is good for performance (think mobile)

http://ak-static.scoopon.com.au/scpn/deals/main/50000/50693_2.jpg

"The secret to building large apps is never build large apps. Break your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big

application"

- Justin Meyer, author JavaScriptMVC

Team Shout Out

• This was over the course of several years and I worked with two other fantastic front-end engineers:

• Ryan Anklam ( @bittersweetryan )

• Zeek Chentnik ( http://ezekielchentnik.com )

JavaScript References

• “Patterns For Large-Scale JavaScript Application Architecture” by Addy Osmani http://addyosmani.com/largescalejavascript/

• “Learning JavaScript Design Patterns” by Addy Osmani http://addyosmani.com/resources/essentialjsdesignpatterns/book/

• “Using Objects to Organize Your Code” by Rebecca Murphey http://rmurphey.com/blog/2009/10/15/using-objects-to-organize-your-code/

• “It’s time to start using JavaScript strict mode” by Nicholas Zakas http://www.nczonline.net/blog/2012/03/13/its-time-to-start-using-javascript-strict-mode/

• “Writing Modular JavaScript with AMD, CommonJS & ES Harmony” by Addy Osmanihttp://addyosmani.com/writing-modular-js/

Backbone.js References

• “Developing Backbone.js Applications” by Addy Osmanihttp://addyosmani.github.io/backbone-fundamentals/

• “Communicating Between Views in Client-Side Apps” by Rebecca Murphey http://bocoup.com/weblog/communicating-between-views-in-client-side-apps/

• Talks from past Backbone Conferences are free/online: http://backboneconf.com/http://backboneconf.com/2013/

Thank You!

@stacylondoner

Code Samples:https://gist.github.com/stacylondon