Writing Maintainable JavaScript

Post on 15-Jan-2015

4.264 views 0 download

Tags:

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

WritingMaintainable

JavaScript

Andrew Duponthttp://andrewdupont.net

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

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

“What’s the problem?”

A JavaScript codebasegets uglier as it grows.

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

Day 1

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

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

Ugliness of Code over Time

(Source: gut feeling)

design patternsrecipes

ideas

The solution:Use existing so!ware principles

to make your codebasemore maintainable.

Wishes:

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

WISH #1:

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

WISH #2:

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

WISH #3:

Plan of attack

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:

“What’s a component?”

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.

A “component” is something you couldrewrite from scratch

without affecting other stuff.

“Each unit should haveonly limited knowledge

about other units.”

Law of Demeter:

The fewer “friends”a component has,

the less it will be affectedby changes elsewhere.

Gowalla.Locationhandles all client-side geolocation.

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

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

Gowalla.ActivityFeedhandles all feeds of user activity.

Gowalla.Flashhandles the display of

transient status messages.

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

Gowalla.Maphandles all interaction

with Google Maps.

Example: Gowalla.Map

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

Example: Gowalla.Map

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

We should standardize the waycomponents talk to one another.

THEREFORE:

WISH:We should be able to rewrite a component

without breaking things elsewhere.

Have components communicate through a central message bus.

(“custom events”)

Publisher and subscriberdon’t need to knowabout one another.

Instead, they only know abouta central event broker.

Embrace conventions.THEREFORE:

WISH:Troubleshooting should be somewhat easy

even if you’re unfamiliar with the code.

“Files are named according totheir module names.”

“Componets have astandard way of initializing.”

“Why custom events?”

Every major frameworkhas them:

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

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

jQuery

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

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

Prototype

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

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

Dojo(“pub-sub”)

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

As long as the interfaceremains the same, either part

can be safely rewritten.

“So I should replaceall my method calls

with custom events?Fat chance.”

A consistent public APIis also an interface.

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

but not vice-versa.

Example: script.aculo.us 2.0

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

The auto-completer knowsabout the menu…

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

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

“What does a rewritelook like?”

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:

Do this:

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

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:

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(''));}

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

A formal “contract”

Easier testing

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

“What if it’s not enough?”

More complex web apps might need desktop-like architectures.

“Single-page apps” havea few common characteristics:

maintaining data objects onthe client side, instead of expecting

the server to do all the work;

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

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

Is this MVC?Perhaps.

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

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

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

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

Other options:

SproutCore(http://sproutcore.com/)

Cappuccino(http://cappuccino.org/)

JavaScriptMVC(http://javascriptmvc.com/)

“Great. How do I start?”

Don’t do aGrand Rewrite™

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

Improve old code little by little as you revisit it.

Maintainabilityis not all-or-nothing.

✍ PLEASE FILL OUTAN EVALUATION FORM

Questions?

Andrew Duponthttp://andrewdupont.net