I’ve been asked by a few users of JS.Class to explain how I use it to organize projects. I’ve been meaning to write this up for quite a while, ever since we adopted it at Songkick for managing our client-side codebase. Specifically, we use JS.Packages to organize our code, and JS.Test to test it, and I’m mostly going to talk about JS.Packages here.
JS.Packages is my personal hat-throw into the ring of JavaScript module
loaders. It’s designed to separate dependency metadata from source code, and
be capable of loading just about anything as efficiently as possible. It works
at a more abstract level than most script loaders: users specify objects they
want to use, rather than scripts they want to load, allowing JS.Packages to
optimize downloads for them and load modules that have their own loading
strategies, all through a single interface, the JS.require()
function.
As an example, I’m going to show how we at Songkick use JS.Packages within our
main Rails app. We manage our JavaScript and CSS by doing as much as possible
in those languages, and finding simple ways to integrate with the Rails stack.
JS.Packages lets us specify where our scripts live and how they depend on each
other in pure JavaScript, making this information portable. We use
JS.require()
to load our codebase onto static pages for running unit tests
without the Rails stack, and we use jsbuild
and AssetHat to package
it for deployment. Nowhere in our setup do we need to manage lists of script
tags or worry about load order.
The first rule of our codebase is: every class/module lives in its own file, much like how we organize our Ruby code. And this means every namespace: even if a namespace has no methods of its own but just contains other classes, we give it a file so that other files don’t have to guess whether the namespace is defined or not. For example a file containing a UI widget class might look like this:
// public/javascripts/songkick/ui/widget.js
Songkick.UI.Widget = function() {
// ...
};
This file does not have to check whether Songkick
or Songkick.UI
is defined,
it just assumes they are. The namespaces are each defined in their own file:
// public/javascripts/songkick.js
Songkick = {};
// public/javascripts/songkick/ui.js
Songkick.UI = {};
Notice how each major class or namespace lives in a file named after the module
it contains; this makes it easier to find things while hacking and lets us take
advantage of the autoload()
feature in JS.Packages to keep our dependency
data small. It looks redundant at first, but it helps maintain predictability as
the codebase grows. It results in more files, but we bundle everything for
production so we keep our code browsable without sacrificing performance. I’ll
cover bundling later on.
To drive out the implementation of our UI widget, we use JS.Test to write a spec for it. I’m just going to give it some random behaviour for now to demonstrate how we get everything wired up.
// test/js/songkick/ui/widget_spec.js
Songkick.UI.WidgetSpec = JS.Test.describe("Songkick.UI.Widget", function() { with(this) {
before(function() { with(this) {
this.widget = new Songkick.UI.Widget("foo")
}})
it("returns its attributes", function() { with(this) {
assertEqual( {name: "foo"}, widget.getAttributes() )
}})
}})
So now we’ve got a test and some skeleton source code, how do we run the tests? First, we need a static page to load up the JS.Packages loader, our manifest (which we’ll get to in a second) and a script that runs the tests:
// test/js/browser.html
<!doctype html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>JavaScript tests</title>
</head>
<body>
<script type="text/javascript">ROOT = '../..'</script>
<script type="text/javascript" src="../../vendor/jsclass/min/loader.js"></script>
<script type="text/javascript" src="../../public/javascripts/manifest.js"></script>
<script type="text/javascript" src="./runner.js"></script>
</body>
</html>
The file runner.js
should be very simple: ideally we just want to load
Songkick.UI.WidgetSpec
and run it:
// test/js/runner.js
// Don't cache files during tests
JS.cacheBust = true;
JS.require('JS.Test', function() {
JS.require(
'Songkick.UI.WidgetSpec',
// more specs as the app grows...
function() { JS.Test.autorun() });
});
The final missing piece is the manifest, the file that says where our files are
stored and how they depend on each other. Let’s start with a manifest that uses
autoload()
to specify all our scripts’ locations; I’ll present the code and
explain what each line does.
// public/javascripts/manifest.js
JS.Packages(function() { with(this) {
var ROOT = JS.ENV.ROOT || '.'
autoload(/^(.*)Spec$/, {from: ROOT + '/test/js', require: '$1'});
autoload(/^(.*)\.[^\.]+$/, {from: ROOT + '/public/javascripts', require: '$1'});
autoload(/^(.*)$/, {from: ROOT + '/public/javascripts'});
}});
The ROOT
setting simply lets us override root directory for the manifest, as
we do on our test page. After that, we have three autoload()
statements. When
you call JS.require()
with an object that’s not been explicitly configured,
the autoload()
rules are examined in order until a match for the name is
found.
The first rule says that object names matching /^(.*)Spec$/
(that is, test
files) should be loaded from the test/js
directory. For example,
Songkick.UI.WidgetSpec
should be found in
test/js/songkick/ui/widget_spec.js
. The require: '$1'
means that the object
depends on the object captured by the regex, so Songkick.UI.WidgetSpec
requires Songkick.UI.Widget
to be loaded first, as you’d expect.
The second rule makes sure that the containing namespace for any object is
loaded before the object itself. For example, it makes sure Songkick.UI
is
loaded before Songkick.UI.Widget
, and Songkick
before Songkick.UI
. The
regex captures everything up to the final .
in the name, and makes sure it’s
loaded using require: '$1'
.
The third rule is a catch-all: any object not matched by the above rules should
be loaded from public/javascripts
. Because of the preceeding rule, this only
matches root objects, i.e. it matches Songkick
but not Songkick.UI
. Taken
together, these rules say: load all objects from public/javascripts
, and make
sure any containing namespaces are loaded first.
Let’s implement the code needed to make the test pass. We’re going to use jQuery to do some trivial operation; the details aren’t important but it causes a dependency problem that I’ll illustrate next.
// public/javascripts/songkick/ui/widget.js
Songkick.UI.Widget = function(name) {
this._name = name;
};
Songkick.UI.Widget.prototype.getAttributes = function() {
return jQuery.extend({}, {name: this._name});
};
If you open the page test/js/browser.html
, you’ll see an error:
The test doesn’t work because jQuery is not loaded; this means part of our
codebase depends on it but JS.Packages doesn’t know that. Remember runner.js
just requires Songkick.UI.WidgetSpec
? We can use jsbuild
to see which files
get loaded when we require this object. (jsbuild
is a command-line tool I
wrote after an internal project at Amazon, that was using JS.Class, decided they
needed to pre-compile their code for static analysis rather than loading it
dynamically at runtime. You can install it by running npm install -g jsclass
.)
$ jsbuild -m public/javascripts/manifest.js -o paths Songkick.UI.WidgetSpec
public/javascripts/songkick.js
public/javascripts/songkick/ui.js
public/javascripts/songkick/ui/widget.js
test/js/songkick/ui/widget_spec.js
As expected, it loads the containing namespaces, the Widget
class, and the
spec, in that order. But the Widget
class depends on jQuery, so we need to
tell JS.Packages about this. However, rather than adding it as a dependency to
every UI module in our application, we can use a naming convention trick: all
our UI modules require Songkick.UI
to be loaded first, so we can make
everything in that namespace depend on jQuery but making the namespace itself
depend on jQuery. We update our manifest like so:
// public/javascripts/manifest.js
JS.Packages(function() { with(this) {
var ROOT = JS.ENV.ROOT || '.';
file('https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js')
.provides('jQuery', '$');
autoload(/^(.*)Spec$/, {from: ROOT + '/test/js', require: '$1'});
autoload(/^(.*)\.[^\.]+$/, {from: ROOT + '/public/javascripts', require: '$1'});
autoload(/^(.*)$/, {from: ROOT + '/public/javascripts'});
pkg('Songkick.UI').requires('jQuery');
}});
Running jsbuild
again shows jQuery will be loaded, and if you reload the tests
now they will pass:
$ jsbuild -m public/javascripts/manifest.js -o paths Songkick.UI.WidgetSpec
public/javascripts/songkick.js
https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js
public/javascripts/songkick/ui.js
public/javascripts/songkick/ui/widget.js
test/js/songkick/ui/widget_spec.js
So we’ve now got a working UI widget, and we can use exactly the same approach
to load it in our Rails app: load the JS.Packages library and our manifest, and
call JS.require('Songkick.UI.Widget')
. But in production, we’d rather not be
downloading all those tiny little files one at a time, it’s much more efficient
to bundle them into one file.
To bundle our JavaScript and CSS for Rails, we use AssetHat, or rather a fork we made to tweak a few things. Our fork notwithstanding, AssetHat is the closest of the handful of Rails packaging solutions we tried that did everything we needed, and I highly recommend it.
AssetHat uses a file called config/assets.yml
, in which you list all the
bundles you want and which files should go in each section. But I’d rather
specify which objects I want in each bundle; we already have tooling that
figures out which files we need and in what order so I’d rather not duplicate
that information. But fortunately, AssetHat lets you put ERB in your config, and
we use this to shell out to jsbuild
to construct our bundles for us.
First, we write a jsbuild
bundles file that says which objects our application
needs. We exclude jQuery from the bundle because we’ll probably load that from
Google’s CDN.
// config/bundles.json
{
"app" : {
"exclude" : [ "jQuery" ],
"include" : [
"Songkick.UI.Widget"
]
}
}
This is a minimal format that’s close to what the application developer works
with: objects. It’s easy to figure out which objects your app needs, less simple
to make sure you only load the files you need and get them in the right order,
in both your test pages and your application code. We can use jsbuild
to tell
us which files will go into this bundle:
$ jsbuild -m public/javascripts/manifest.js -b config/bundles.json -o paths app
public/javascripts/songkick.js
public/javascripts/songkick/ui.js
public/javascripts/songkick/ui/widget.js
Now all we need to do is pipe this information into AssetHat. This is easily done with a little ERB magic:
// config/assets.yml
# ...
js:
<% def js_bundles
JSON.parse(File.read('config/bundles.json')).keys
end
def paths_for_js_bundle(name)
jsbuild = 'jsbuild -m public/javascripts/manifest.js -b config/bundles.json'
`#{jsbuild} -o paths -d public/javascripts #{name}`.split("\n")
end
%>
bundles:
<% js_bundles.each do |name| %>
<%= name %>:
<% paths_for_js_bundle(name).each do |path| %>
- <%= path %>
<% end %>
<% end %>
Running the minification task takes the bundles we’ve defined in bundles.json
and packages them for us:
$ rake asset_hat:minify
Minifying CSS/JS...
Wrote JS bundle: public/javascripts/bundles/app.min.js
contains: public/javascripts/songkick.js
contains: public/javascripts/songkick/ui.js
contains: public/javascripts/songkick/ui/widget.js
MINIFIED: 14.4% (Engine: jsmin)
This bundle can now be loaded in your Rails views very easily:
<%= include_js :bundle => 'app' %>
This will render script tags for each individual file in the bundle during development, and a single script tag containing all the code in production. (You may have to disable the asset pipeline in recent Rails versions to make this work.)
So that’s our JavaScript strategy. As I said earlier, the core concern is to
express dependency information in one place, away from the source code, in a
portable format that can be used just as easily in a static web page as in your
production web framework. Using autoload()
and some simple naming conventions,
you can get all these benefits while keeping the configuration very small
indeed.
But wait, there’s more!
As a demonstration of how valuable it is to have portable dependency data and tests, consider the situation where we now want to run tests from the command line, or during our CI process. We can load the exact same files we load in the browser, plus a little stubbing of the jQuery API, and make our tests run on Node:
// test/js/node.js
require('jsclass');
require('../../public/javascripts/manifest');
JS.ENV.jQuery = {
extend: function(a, b) {
for (var k in b) a[k] = b[k];
return a;
}
};
JS.ENV.$ = JS.ENV.jQuery;
require('./runner');
And lo and behold, our tests run:
$ node test/js/node.js
Loaded suite Songkick.UI.Widget
Started
.
Finished in 0.003 seconds
1 tests, 1 assertions, 0 failures, 0 errors
Similarly, we can write a quick PhantomJS script to parse the log messages that JS.Test emits:
// test/js/phantom.js
var page = new WebPage();
page.onConsoleMessage = function(message) {
try {
var result = JSON.parse(message).jstest;
if ('total' in result && 'fail' in result) {
console.log(message);
var status = (!result.fail && !result.error) ? 0 : 1;
phantom.exit(status);
}
} catch (e) {}
};
page.open('test/js/browser.html');
We can now run our tests on a real WebKit instance from the command line:
$ phantomjs test/js/phantom.js
{"jstest":{"fail":0,"error":0,"total":1}}
One nice side-effect of doing as much of this as possible in JavaScript is that it improves your API design and makes you decouple your JS from your server-side stack; if it can’t be done through HTML and JavaScript, your code doesn’t do it. This makes it easy to keep your code portable, making it easier to reuse across applications with different server-side stacks.