writing maintainable javascript

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

Upload: andrew-dupont

Post on 15-Jan-2015

4.264 views

Category:

Technology


0 download

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

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