Writing Maintainable JavaScript

74
Writing Maintainable JavaScript Andrew Dupont http://andrewdupont.net

description

Is your web app drowning in a sea of JavaScript? Has your client-side codebase grown from "a snippet here and there" to "more JavaScript than HTML"? Do you find yourself writing one-off snippets instead of generalized components? You're not the only one. Learn about a handful of strategies you can use to keep your JavaScript codebase lean, modular, and flexible. We'll cover all the major pain points — MVC, templates, persisting state, namespacing, graceful error handling, client/server communication, and separation of concerns. And we'll cover how to do all this incrementally so that you don't have to redo everything from scratch.

Transcript of Writing Maintainable JavaScript

Page 1: Writing Maintainable JavaScript

WritingMaintainable

JavaScript

Andrew Duponthttp://andrewdupont.net

Page 2: Writing Maintainable JavaScript

I help maintain these.I write ugly JavaScript all the time.

Page 3: Writing Maintainable JavaScript

I work here.We write ugly JavaScript all the time.

Page 4: Writing Maintainable JavaScript

“What’s the problem?”

Page 5: Writing Maintainable JavaScript

A JavaScript codebasegets uglier as it grows.

Page 6: Writing Maintainable JavaScript

$("p.neat").addClass("ohmy").show("slow");

Day 1

Page 7: Writing Maintainable JavaScript

var trip = Gowalla.trip;$.each(trip.spots, function(i, spot) { var marker = new GMarker( new GLatLng(spot.lat, spot.lng), { icon: Gowalla.createLetterIcon(i), title: h(spot.name) } ); GEvent.addListener(marker, "click", function() { marker.openInfoWindowHtml('<div class="map-bubble"><img src="' + spot.image_url + '" width="50" height="50" /><b><a href="' + spot.url + '" style="color: #37451e;">' + h(spot.name) + '</a></b></div>'); return false; }); Gowalla.map.addOverlay(marker);});Gowalla.zoomAndCenter(trip.spots);

Day 31

Page 8: Writing Maintainable JavaScript

options = options || {};var params = this.getSearchParams(options);Paginator.currentPage = 1;Paginator.handler = Gowalla.displaySpots;Paginator.paginate('/spots', params);if (Gowalla.filterOptions["l"] || Gowalla.filterOptions["sw"] || Gowalla.filterOptions["lat"]) { $('#map-wrapper').show(); $('#spots_search_l').removeClass('off'); if (options.l) $('#spots_search_l').val(unescape(options.l));} else { $('#map-wrapper').hide();}if (Gowalla.mapVisible()) $('#map-placeholder').show();$('#heading').hide();$('#featured_spots').hide();$('#new_spots').hide();$.getJSON('/spots', this.getSearchParams(options), function(spots) { if (spots.length > 0) { $('.paging').show(); $('#filter').show(); $('#results').show(); $('#map-placeholder').hide(); if (Gowalla.mapVisible() && !Gowalla.map) { $('#map-placeholder').addClass("transparent"); Gowalla.createMap(); GEvent.addListener(Gowalla.map, "dragend", function() { var sw = this.getBounds().getSouthWest().toString(); var ne = this.getBounds().getNorthEast().toString(); Gowalla.searchSpots({sw:sw, ne:ne, limit:'150'}); }); } } Gowalla.displaySpots(spots);});

Day 90

Page 9: Writing Maintainable JavaScript

Ugliness of Code over Time

(Source: gut feeling)

Page 10: Writing Maintainable JavaScript

design patternsrecipes

ideas

Page 11: Writing Maintainable JavaScript

The solution:Use existing so!ware principles

to make your codebasemore maintainable.

Page 12: Writing Maintainable JavaScript

Wishes:

Page 13: Writing Maintainable JavaScript

Code that accomplishes a single taskshould all live together in one place.

WISH #1:

Page 14: Writing Maintainable JavaScript

We should be able to rewrite a componentwithout affecting things elsewhere.

WISH #2:

Page 15: Writing Maintainable JavaScript

Troubleshooting should be somewhat easyeven if you’re unfamiliar with the code.

WISH #3:

Page 16: Writing Maintainable JavaScript

Plan of attack

Page 17: Writing Maintainable JavaScript

Code that accomplishes a single taskshould all live together in one place.

Divide your codebase into components,placing each in its own file.

THEREFORE:

WISH:

Page 18: Writing Maintainable JavaScript

“What’s a component?”

Page 19: Writing Maintainable JavaScript

A component should be whatever size is necessary to isolate its details from other code.

THEREFORE:

WISH:We should be able to rewrite a component

without breaking things elsewhere.

Page 20: Writing Maintainable JavaScript

A “component” is something you couldrewrite from scratch

without affecting other stuff.

Page 21: Writing Maintainable JavaScript

“Each unit should haveonly limited knowledge

about other units.”

Law of Demeter:

Page 22: Writing Maintainable JavaScript

The fewer “friends”a component has,

the less it will be affectedby changes elsewhere.

Page 23: Writing Maintainable JavaScript

Gowalla.Locationhandles all client-side geolocation.

Gowalla.Location.getLocation();//=> [30.26800, -97.74283]

Gowalla.Location.getLocality();//=> "Austin, TX"

Page 24: Writing Maintainable JavaScript

Gowalla.ActivityFeedhandles all feeds of user activity.

Page 25: Writing Maintainable JavaScript

Gowalla.Flashhandles the display of

transient status messages.

Gowalla.Flash.success("Your settings were updated.");

Page 26: Writing Maintainable JavaScript

Gowalla.Maphandles all interaction

with Google Maps.

Page 27: Writing Maintainable JavaScript

Example: Gowalla.Map

function addSpotsToMap(spots) { Gowalla.Map.clearSpots(); $.each(spots, function(i, spot) { Gowalla.Map.addSpot(spot); });}

Page 28: Writing Maintainable JavaScript

Example: Gowalla.Map

function addSpotsToMap(spots) { Gowalla.Map.clearSpots(); $.each(spots, function(i, spot) { Gowalla.Map.addSpot(spot, { infoWindow: true }); });}

Page 29: Writing Maintainable JavaScript

We should standardize the waycomponents talk to one another.

THEREFORE:

WISH:We should be able to rewrite a component

without breaking things elsewhere.

Page 30: Writing Maintainable JavaScript

Have components communicate through a central message bus.

(“custom events”)

Page 31: Writing Maintainable JavaScript

Publisher and subscriberdon’t need to knowabout one another.

Page 32: Writing Maintainable JavaScript

Instead, they only know abouta central event broker.

Page 33: Writing Maintainable JavaScript

Embrace conventions.THEREFORE:

WISH:Troubleshooting should be somewhat easy

even if you’re unfamiliar with the code.

Page 34: Writing Maintainable JavaScript

“Files are named according totheir module names.”

Page 35: Writing Maintainable JavaScript

“Componets have astandard way of initializing.”

Page 36: Writing Maintainable JavaScript

“Why custom events?”

Page 37: Writing Maintainable JavaScript

Every major frameworkhas them:

Page 38: Writing Maintainable JavaScript

$(document).bind('customevent', function(event, data) { // stuff});

$('#troz').trigger('customevent', [someAssociatedData]);

jQuery

Page 39: Writing Maintainable JavaScript

$(document).observe('custom:event', function(event) { var customData = event.memo; // stuff});

$('troz').fire('custom:event', { foo: "bar" });

Prototype

Page 40: Writing Maintainable JavaScript

dojo.subscribe('some-event', function(data) { // stuff});

dojo.publish('some-event', someData);

Dojo(“pub-sub”)

Page 41: Writing Maintainable JavaScript

A custom event is an interface that publisher and subscriber adhere to.

Page 42: Writing Maintainable JavaScript

As long as the interfaceremains the same, either part

can be safely rewritten.

Page 43: Writing Maintainable JavaScript

“So I should replaceall my method calls

with custom events?Fat chance.”

Page 44: Writing Maintainable JavaScript

A consistent public APIis also an interface.

Page 45: Writing Maintainable JavaScript

It’s OK for a subscriberto call methods on a broadcaster,

but not vice-versa.

Page 46: Writing Maintainable JavaScript

Example: script.aculo.us 2.0

Page 47: Writing Maintainable JavaScript
Page 48: Writing Maintainable JavaScript

var menu = new S2.UI.Menu();menu.addChoice("Foo");menu.addChoice("Bar");someElement.insert(menu);menu.open();

The auto-completer knowsabout the menu…

Page 49: Writing Maintainable JavaScript

…but the menu doesn’t knowabout the auto-completer

menu.observe('ui:menu:selected', function(event) { console.log('user clicked on:', event.memo.element);});

Page 50: Writing Maintainable JavaScript

“What does a rewritelook like?”

Page 51: Writing Maintainable JavaScript

function showNearbySpotsInMenu() { $.ajax({ url: '/spots', params: { lat: someLat, lng: someLng }, success: function(spots) { var html = $.map(spots, function(spot) { return '<li id="spot-"' + spot.id + '>' + spot.name + '</li>'; }); $('#spot_menu').html(html.join('')); } });}

Instead of:

Page 52: Writing Maintainable JavaScript

Do this:

function getNearbySpotsFromServer(lat, lng) { $.ajax({ url: '/spots', params: { lat: lat, lng: lng }, success: function(spots) { $(document).trigger('nearby-spots-received', [spots]); } });}

Page 53: Writing Maintainable JavaScript

function renderNearbySpots(event, spots) { var html = $.map(spots, function(spot) { return '<li id="spot-"' + spot.id + '>' + spot.name + '</li>'; }); $('#spot_menu').html(html.join(''));}

$(document).bind('nearby-spots-received', renderNearbySpots);

And this:

Page 54: Writing Maintainable JavaScript

Or, if you prefer…function getNearbySpotsFromServer(lat, lng) { $.ajax({ url: '/spots', params: { lat: lat, lng: lng }, success: function(spots) { renderNearbySpots(spots); } });} function renderNearbySpots(spots) { var html = $.map(spots, function(spot) { return '<li id="spot-"' + spot.id + '>' + spot.name + '</li>'; }); $('#spot_menu').html(html.join(''));}

Page 55: Writing Maintainable JavaScript

Intra-module organization(divide code up according to job)

Page 56: Writing Maintainable JavaScript

A formal “contract”

Page 57: Writing Maintainable JavaScript

Easier testing

function testNearbySpotsRendering() { renderNearbySpots(Fixtures.NEARBY_SPOTS); assertEqual($('#spot_menu > li').length, 3);}

Page 58: Writing Maintainable JavaScript

“What if it’s not enough?”

Page 59: Writing Maintainable JavaScript

More complex web apps might need desktop-like architectures.

Page 60: Writing Maintainable JavaScript

“Single-page apps” havea few common characteristics:

Page 61: Writing Maintainable JavaScript

maintaining data objects onthe client side, instead of expecting

the server to do all the work;

Page 62: Writing Maintainable JavaScript

creating views on the client sideand mapping them to data objects;

Page 63: Writing Maintainable JavaScript

use of the URL hash for routing/permalinking(or HTML5 history management).

Page 64: Writing Maintainable JavaScript

Is this MVC?Perhaps.

Page 65: Writing Maintainable JavaScript

Backbonehttp://documentcloud.github.com/backbone/

Page 66: Writing Maintainable JavaScript

window.Todo = Backbone.Model.extend({ EMPTY: "new todo...",

initialize: function() { if (!this.get('content')) this.set({ 'content': this.EMPTY }); },

toggle: function() { this.set({ done: !this.get('done') }); },

validate: function(attributes) { if (!attributes.content.test(/\S/)) return "content can't be empty"; },

// ... });

define a model class ⇒

property access wrapped in set/get methods ⇒

triggered when the object is saved ⇒

Models

Page 67: Writing Maintainable JavaScript

window.Todo.View = Backbone.View.extend({ tagName: 'li',

events: { 'dblclick div.todo-content' : 'edit', 'keypress .todo-input' : 'updateOnEnter' },

initialize: function() { this.model.bind('change', this.render); },

render: function() { // ... },

// ...});

define a view class ⇒

bind events to pieces of the view ⇒

set the view’s contents ⇒

map to a model object; re-render when it changes ⇒

Views

Page 68: Writing Maintainable JavaScript

determine the HTTP verb to use for this action ⇒

serialize the object to JSON ⇒

Backbone.sync = function(method, model, yes, no) { var type = methodMap[method];

var json = JSON.stringify(model.toJSON());

$.ajax({ url: getUrl(model), type: type, data: json, processData: false, contentType: 'application/json', dataType: 'json', success: yes, error: no });};

send the data to the server ⇒

Synchronization

Page 69: Writing Maintainable JavaScript

Other options:

SproutCore(http://sproutcore.com/)

Cappuccino(http://cappuccino.org/)

JavaScriptMVC(http://javascriptmvc.com/)

Page 70: Writing Maintainable JavaScript

“Great. How do I start?”

Page 71: Writing Maintainable JavaScript

Don’t do aGrand Rewrite™

Page 72: Writing Maintainable JavaScript

One strategy:Write new code to conform to your architecture.

Improve old code little by little as you revisit it.

Page 73: Writing Maintainable JavaScript

Maintainabilityis not all-or-nothing.

Page 74: Writing Maintainable JavaScript

✍ PLEASE FILL OUTAN EVALUATION FORM

Questions?

Andrew Duponthttp://andrewdupont.net