Following on from my talk on cross-platform JavaScript testing, I got a question on loading platform-specific code without wasting bandwidth. In the example, I went through building a Twitter API client that works in browsers and in Node, and ended up factoring toward a design that looks like this:
Twitter = new JS.Class('Twitter', {
search: function(query, callback) {
var resource = 'http://search.twitter.com' +
'/search.json?q=' + query
Net.getJSON(resource, callback)
}
})
where Net
would encapsulate the logic for how to do HTTP on different
platforms. That is, it would look something like this:
Net = {
getJSON: function(url, callback) {
if (typeof document === 'object')
// make call using JSONP
else
// make call using Node HTTP library
}
}
The problem with this is that, if you’re using such a library in a browser, you don’t want to waste bandwidth on loading code that isn’t applicable to the current environment. This is true for server-side code, and for code for other browsers. For example you don’t want to load a lot of code for dealing with Internet Explorer if your application is running on a phone.
So a common refactoring we can use is to split this module into two objects with an identical interface but different implementations. We can put each implementation in its own file so it can be loaded separately.
// source/net/jsonp.js
Net = {
getJSON: function(url, callback) {
// make call using JSONP
}
}
// source/net/node.js
Net = {
getJSON: function(url, callback) {
// make call using Node HTTP
}
}
This has the added benefit of removing the overhead of an if
statement from
the code, and makes it easier to maintain each implementation independently.
With the code split out like this, we can use JS.Packages to pick the right implementation to load:
JS.Packages(function() { with(this) {
file('source/twitter.js')
.provides('Twitter')
.requires('Net')
var netpath = (typeof document === 'object')
? 'source/net/jsonp.js'
: 'source/net/node.js'
file(netpath).provides('Net')
}})
This is one advantage of basing a package system on object names: the package manager can figure out the best way to satisfy an object requirement so this logic doesn’t clutter your application code.
Also, because JS.Packages uses object detection to figure out whether an object
needs to be loaded, we can use it to simply fill in missing browser APIs. For
example, I have a toy project called Pathology, which is a bug-ridden slow
implementation of half of the document.evaluate()
API for Internet Explorer,
letting you use XPath to query the DOM. Most other browsers have this built-in,
but loading an implementation just for IE is as simple as:
JS.Packages(function() { with(this) {
file('/lib/pathology.js').provides('document.evaluate')
}})
Then, when you JS.require('document.evaluate')
, no extra files will be loaded
in browsers that already support this interface natively. In browsers without
this API, the external file will be loaded to fill in the functionality.
Whether you use conditional code to pick a file path to load, or use a more complex loader function is a question of taste and of the complexity of detecting which components you need to load on your target platforms. Either way, this system is a nice example of why in many cases I prefer executable code with a simple DSL over static data for configuration tasks.