Writing Maintainable JavaScript

5,020 views

Published on

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.

Published in: Technology

Writing Maintainable JavaScript

  1. 1. WritingMaintainable JavaScript Andrew Dupont http://andrewdupont.net
  2. 2. I help maintain these.I write ugly JavaScript all the time.
  3. 3. I work here.We write ugly JavaScript all the time.
  4. 4. “What’s the problem?”
  5. 5. A JavaScript codebasegets uglier as it grows.
  6. 6. Day 1$("p.neat").addClass("ohmy").show("slow");
  7. 7. Day 31var 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);
  8. 8. Day 90options = 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);});
  9. 9. Ugliness of Code over Time (Source: gut feeling)
  10. 10. design patterns recipes ideas
  11. 11. The solution:Use existing so ware principles to make your codebase more maintainable.
  12. 12. Wishes:
  13. 13. WISH #1:Code that accomplishes a single taskshould all live together in one place.
  14. 14. WISH #2:We should be able to rewrite a component without affecting things elsewhere.
  15. 15. WISH #3:Troubleshooting should be somewhat easy even if you’re unfamiliar with the code.
  16. 16. Plan of attack
  17. 17. WISH: Code that accomplishes a single task should all live together in one place. THEREFORE:Divide your codebase into components, placing each in its own file.
  18. 18. “What’s a component?”
  19. 19. WISH: We should be able to rewrite a component without breaking things elsewhere. THEREFORE: A component should be whatever size isnecessary to isolate its details from other code.
  20. 20. A “component” is something you could rewrite from scratchwithout affecting other stuff.
  21. 21. Law of Demeter:“Each unit should haveonly limited knowledge about other units.”
  22. 22. The fewer “friends” a component has,the less it will be affected by changes elsewhere.
  23. 23. Gowalla.Locationhandles all client-side geolocation. Gowalla.Location.getLocation(); //=> [30.26800, -97.74283] Gowalla.Location.getLocality(); //=> "Austin, TX"
  24. 24. Gowalla.ActivityFeedhandles all feeds of user activity.
  25. 25. Gowalla.Flash handles the display of transient status messages.Gowalla.Flash.success("Your settings were updated.");
  26. 26. Gowalla.Maphandles all interaction with Google Maps.
  27. 27. Example: Gowalla.Mapfunction addSpotsToMap(spots) { Gowalla.Map.clearSpots(); $.each(spots, function(i, spot) { Gowalla.Map.addSpot(spot); });}
  28. 28. Example: Gowalla.Mapfunction addSpotsToMap(spots) { Gowalla.Map.clearSpots(); $.each(spots, function(i, spot) { Gowalla.Map.addSpot(spot, { infoWindow: true }); });}
  29. 29. WISH:We should be able to rewrite a component without breaking things elsewhere. THEREFORE: We should standardize the way components talk to one another.
  30. 30. Have components communicate through a central message bus. (“custom events”)
  31. 31. Publisher and subscriber don’t need to know about one another.
  32. 32. Instead, they only know about a central event broker.
  33. 33. WISH:Troubleshooting should be somewhat easy even if you’re unfamiliar with the code. THEREFORE: Embrace conventions.
  34. 34. “Files are named according to their module names.”
  35. 35. “Componets have astandard way of initializing.”
  36. 36. “Why custom events?”
  37. 37. Every major framework has them:
  38. 38. jQuery$(document).bind(customevent, function(event, data) { // stuff});$(#troz).trigger(customevent, [someAssociatedData]);
  39. 39. Prototype$(document).observe(custom:event, function(event) { var customData = event.memo; // stuff});$(troz).fire(custom:event, { foo: "bar" });
  40. 40. Dojo (“pub-sub”)dojo.subscribe(some-event, function(data) { // stuff});dojo.publish(some-event, someData);
  41. 41. A custom event is an interface thatpublisher and subscriber adhere to.
  42. 42. As long as the interfaceremains the same, either part can be safely rewritten.
  43. 43. “So I should replaceall my method callswith custom events? Fat chance.”
  44. 44. A consistent public API is also an interface.
  45. 45. It’s OK for a subscriberto call methods on a broadcaster, but not vice-versa.
  46. 46. Example: script.aculo.us 2.0
  47. 47. The auto-completer knows about the menu… var menu = new S2.UI.Menu(); menu.addChoice("Foo"); menu.addChoice("Bar"); someElement.insert(menu); menu.open();
  48. 48. …but the menu doesn’t know about the auto-completermenu.observe(ui:menu:selected, function(event) { console.log(user clicked on:, event.memo.element);});
  49. 49. “What does a rewrite look like?”
  50. 50. Instead of: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()); } });}
  51. 51. Do this:function getNearbySpotsFromServer(lat, lng) { $.ajax({ url: /spots, params: { lat: lat, lng: lng }, success: function(spots) { $(document).trigger(nearby-spots-received, [spots]); } });}
  52. 52. And this: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);
  53. 53. 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());}
  54. 54. Intra-module organization(divide code up according to job)
  55. 55. A formal “contract”
  56. 56. Easier testingfunction testNearbySpotsRendering() { renderNearbySpots(Fixtures.NEARBY_SPOTS); assertEqual($(#spot_menu > li).length, 3);}
  57. 57. “What if it’s not enough?”
  58. 58. More complex web apps might need desktop-like architectures.
  59. 59. “Single-page apps” havea few common characteristics:
  60. 60. maintaining data objects onthe client side, instead of expecting the server to do all the work;
  61. 61. creating views on the client sideand mapping them to data objects;
  62. 62. use of the URL hash for routing/permalinking (or HTML5 history management).
  63. 63. Is this MVC? Perhaps.
  64. 64. Backbonehttp://documentcloud.github.com/backbone/
  65. 65. Models define a model class window.Todo = Backbone.Model.extend({ EMPTY: "new todo...",property access wrapped in set/get methods initialize: function() { if (!this.get(content)) this.set({ content: this.EMPTY }); }, toggle: function() { this.set({ done: !this.get(done) }); }, triggered when the object is saved validate: function(attributes) { if (!attributes.content.test(/S/)) return "content cant be empty"; }, // ... });
  66. 66. Views define a view class window.Todo.View = Backbone.View.extend({ tagName: li, bind events to pieces of the view events: { dblclick div.todo-content : edit, keypress .todo-input : updateOnEnter }, initialize: function() {map to a model object; re-render when it changes this.model.bind(change, this.render); }, set the view’s contents render: function() { // ... }, // ... });
  67. 67. Synchronization Backbone.sync = function(method, model, yes, no) {determine the HTTP verb to use for this action var type = methodMap[method]; serialize the object to JSON var json = JSON.stringify(model.toJSON()); send the data to the server $.ajax({ url: getUrl(model), type: type, data: json, processData: false, contentType: application/json, dataType: json, success: yes, error: no }); };
  68. 68. Other options: SproutCore (http://sproutcore.com/) Cappuccino (http://cappuccino.org/) JavaScriptMVC(http://javascriptmvc.com/)
  69. 69. “Great. How do I start?”
  70. 70. Don’t do aGrand Rewrite™
  71. 71. One strategy:Write new code to conform to your architecture.Improve old code little by little as you revisit it.
  72. 72. Maintainabilityis not all-or-nothing.
  73. 73. Questions?✍ PLEASE FILL OUT AN EVALUATION FORM Andrew Dupont http://andrewdupont.net

×