Prequel: Professional JavaScript Build System
It occurs to me after reading my previous post that perhaps I wasn’t clear why I needed to have a “services container” to provide services to my scripts. So I thought I would put together this prequel – a brief tour of how I got to thinking that my the method for service abstraction I outlined in that post was worth the extra glue-code it required.
Like everyone else, I started with hard-coding my services into the objects that use them. So in the Headshot example, I simply appended the absolute path to the relative path like:
1: dojo.declare("medryx.widgets.Headshot", [dijit._Widget, dijit._Templated], {
2: templatePath: dojo.moduleUrl("medryx.widgets", "templates/Headshot.html"),
3: pathToPhotos: "http://blog.medryx.org/photos",
4: person: null,
5: postMixInProperties: function() {
6: this.imagePath = this.pathToPhotos + this.person.imagePath
7: }
8: });
Pros: a reasonable default pathToPhotos, that is easily over-ridden by any page. Cons: My widgets start looking very messy in the markup when they have so many attributes that have to be specified; I have to change my web page attribute for pathToPhotos depending on my environment. This is probably the most common way to handle changing environments.
My second stab was to create a set of scripts that aliased the service I needed (or the constant I needed) to a common object. For example:
dojo.provide("medryx.environment.Static");
dojo.mixin(medryx.environment, {
pathToPhotos: "http://blog.medryx.org/images"
});
Then I could create a seperate file as:
dojo.provide("medryx.environment.DynamicLocal");
dojo.mixin(medryx.environment, {
pathToPhotos: "http://localhosts/images"
});
And, similar to my previous post, I just include the environment I need in the page, and modify my widget to use medryx.environment.pathToPhotos as its pathToPhotos attribute.
This actually works fairly well. Especially for constants (and indeed I still use this model for handling constants). But lets think about this model when I need a service – say a data store.
dojo.provide("medryx.environment.Static");
dojo.mixin(medryx.environment, {
MyStore: new dojox.data.JsonRestStore({/* omitted */});
});
Now, to reference my store, I simply use medryx.environment.MyStore. This too, works fairly well. Except for a few problems:
- It mandates that you initialize your services at load time. There may be many services that you will never use on a given page, or that you certainly do not need to initialize up front, and this could slow down your boot of your application.
- This does not fail gracefully. For example, if you forget to create the MyStore in your environment, and then you attempt to use the store, you may or may not get an error, and it may or may not be useful. But either way, things won’t work until you figure out that you forgot to create the MyStore service.
- If the service was dependent on some other outside process being completed – such as the DOM having to be loaded before it would work, or something else – then this would work in a local install (where you can guarantee the order in which your scripts are loaded and executed), but could fail (in very mysterious “sometimes it works, sometimes it doesn’t” ways) in a cross-domain load when that guarantee is not present.
So, now we are getting to my previously suggested “service container”. By using a simple call to a method to “get” the service I need, I can lazily load the services if required, I can guarantee when the initialization will occur, and I don’t incur much overhead. And even as I write, this, I can think of at least one improvement, since the “service container” is accessed quite frequently, lets make it more terse:
dojo.provide("medryx.environment.Static");
(function() {
medryx.$ = medryx.service = function(serviceName) {
switch(serviceName) {
case "medryx.environment.pathToPhotos":
return "http://blog.medryx.org/images";
case "medryx.environment.MyStore":
return getInitializedMyStore();
default:
throw new Error("Could not find service: " + serviceName);
}
}
var myStore;
function getInitializedMyStore() {
if (!myStore) {
myStore = new dojox.data.JsonRestStore();
}
return myStore;
}
})();
And the calling class simply calls either medryx.service(“name.of.Service”) or even medryx.$(“name.of.Service”) and is guaranteed to get the initialized service, lazily initialized if needed, and with a nice, clear “Could not find service: name.of.Service” if you forget to define your service before you use it.
I hope that makes the rationale behind the service container easier to understand. Now, my next installment will describe how we can better decide which “environment” script to include, and make a build system that will be easy to maintain.