Post on 15-Jan-2015
description
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