Thursday, 3 January 2013

A tidy way to organise your JavaScript

Last year I was lucky enough to work on several projects with some very good UI developers. Doing so enabled to greatly improve my own skills in that area, and one of the biggest 'take-aways' from these projects was the way in which these guys preferred to manage the JavaScript in a project. In this post I will outline how they did it, and why they did it this way.

Note: I really like this approach because it's solved (and neatly so) a problem I never seemed to sort out in a satisfactory manner on my own. I am sure there are many other good ways of doing this - I'm just particularly fond of this solution. I welcome discussion on the matter!

The basic idea behind how the JS should be organised can probably be summed up in three simple points:

  • Everything is namespaced (and thus organised accordingly).
  • Every page that requires JS has a single 'page class' that creates and initialises JS components that work on that particular page. A 'page class' should not do anything except initialisation.
  • All JS code that provides functionality to a page should is organised into logical components.
I'll address each bullet point in order, but before doing so let's have a quick way at how the JS files are organised in the web project (or on disk, for that matter). Note that the code examples and screen shots are from a ASP.NET MVC4 project in VS2012.

As you can see from the picture on the right I have modified the default project structure somewhat. In the "Content" folder I have created a "Scripts" folder (with multiple sub-folders) and also a "Styles" folder. In this post I'll only talk about the "~/Content/Scripts/MyApp" folder and its files/sub-folders. I'll mention here, though, that the "~/Content/Styles" folder contains the "site.css" stylesheet and the "themes" folder from the original project. The "~/Content/Scripts/Plugins" folder contains all the scripts that were originally placed in the "~/Scripts/" folder, and the "~/Content/Scripts/Jasmine/" folder will hold all Jasmine files and tests.

Now, back to the bullet points. We'll address them in order, starting with:

Namespaces

The "~/Content/Scripts/MyApp" folder should be named in accordance with whatever your application is called. Inside this folder, place a single JS file named in accordance with whatever the root namespace of you application's JS should be. I usually use the application name as the root namespace, hence I name the root namespace file MyApp.js.

Inside this file I simply declare the root namespace like this:
  
  var MyApp = {};

You'll notice that inside the "~/Content/Scripts/MyApp/" folder there is also a "Pages" folder. This is the folder that will hold all the JS files that define the previously mentioned page classes. The "Pages" folder has two files: MyApp.Pages.js, which defines the MyApp.Pages namespace...
  
  MyApp.Pages = {};

... and also MyApp.Pages.Home.js, which defines the "Home" page class. We will get onto the details of this particular file in a second.

Although we've only talked about two folders and three files, I've hinted at a pattern with regard to namespaces. Every folder under "~/Scripts/Content/MyApp/" represents and is named in accordance with a namespace. That namespace is defined in a JS file named exactly the same as the full namespace, e.g. MyApp.Pages.js or MyApp.MappingComponents.js.

In this example there are only two namespaces, and that's fine to start with. I keep all components in the root namespace unless some are completely page specific (thus not really reusable) or unless there are too many of them so that it becomes useful to group them.

Page classes

All page classes are defined under the "MyApp.Pages" namespace. The purpose of a page class is to create and initialise all JS (components) required by a specific page in your web application. In this example we have the MyApp.Pages.Home class. Let's take a closer look:

  $(function () {
    MyApp.Pages.Home = new function () {
      this.CountryMap = new MyApp.GoogleMap($('#map_canvas')[0]);
      this.CountryMap.init(66.196009, 14.143799, 4);
    }
  });

All this page class does is create a GoogleMap component, and initialise it. On creation the component is passed a reference to the div that we want to render the map within, and then it is initialised by calling the init method of the component and passing in the latitude, longitude, and map zoom level.

This is a contrived example, so I've hardcoded the initialisation of the map instance. In a 'real world' scenario you'd probably have your page class either read the lat/lng from hidden form fields on the page or perhaps request the lat/lng by making an ajax call. The point is, beyond doing the necessary work for creating and initialising components, the page class is pretty simple.

Components

All JavaScript for a page should be encapsulated in (reusable) components, and each component should be defined in its own, separate file. There are two reasons for this: It makes your JS more manageable because each component is defined in one place and has one purpose. It also makes your JS easier to test, because each component is discrete. The latter point is still subject to how you choose to build your components, though, and I'm not going to go into testing of JS here.

Let's have a quick look at the GoogleMap component that's used by the Home page class:

  MyApp.GoogleMap = function (canvas) {
    this.canvas = canvas;
  };

  MyApp.GoogleMap.prototype.generateMap = function (lat, lng, zoomLevel) {
    var canvasElement = this.canvas[0];
    var mapOptions = {
        center: new google.maps.LatLng(lat, lng),
        zoom: zoomLevel,
        mapTypeId: google.maps.MapTypeId.ROADMAP,
    };

    this.map = new google.maps.Map(canvasElement, mapOptions);
  };

  MyApp.GoogleMap.prototype.init = function (lat, lng, zoomLevel) {
    this.generateMap(lat, lng, zoomLevel);
  };

This is a very simple component that creates a Google map instance on a div in your HTML markup. As you can see, when an instance of this component is created you pass it a reference to the div that should be used as the canvas for the map. This reference is then stored so that when the init() method is called the map can be drawn within the given div.

Putting it together

In order to make all of this work we just have to do the following:
  • Add a reference to the Google Maps API within the tag of _Layout.cshtml
  • Add references to "~/Content/Scripts/MyApp/MyApp.js" and "~/Content/Scripts/MyApp/Pages/MyApp.Pages.js" within _Layout.cshtml
  • Add references to "~/Content/Scripts/MyApp/MyApp.GoogleMap.js" and "~/Content/Scripts/MyApp/Pages/MyApp.Pages.Home.js" within the "~/Views/Home/Index.cshtml" view.
  • Add a div with the id "map_canvas" within the "~/Views/Home/Index.cshtml" view. Oh - and you should style it, too, to the desired size.
With all this in place, the home page should look something like this:



Points to note

While I really like this solution, there are a couple of things to beware of before you start using it yourself. The most important thing to realise is that as your site grows, so will the number of JavaScript files. If you reference each file separately the web browser will potentially have to make a lot of requests in order to render a single page, and this is bad for performance. Therefore you should utilise bundling so that multiple files can be combined into a single downloadable unit.

While bundling is really helpful for the above scenario, you should also exercise some restraint when defining these. Because of the number of JS files you might have in a large project it can be tempting to "bundle everything" into one big, eh, bundle and reference that everywhere. That's ultimately counter productive because the web browser will have to download more than what's required for a single page, and it will probably lead to a lax attitude towards the boundaries that you've defined within your JS, too.


No comments: