Medryx Observations

May 31, 2009

A Professional JavaScript Build System: Part II – Bootstrapping

Filed under: dojo, javascript — maulin @ 6:31 am

In my last post I described the parts that can make up a robust build system for a complex JavaScript library that would allow for simplifying development, promotion to QA, and deployment to live systems. I outlined in some detail the importance of abstracting any use of external systems (via XHR, stores, or any other resource access that can change from environment to environment), and using a service container to request those services as needed. In this post, I will describe how I bootstrap my JavaScript library (based on Dojo).

The goal of a bootstrapping script is to allow page developers (i.e. consumers of widgets) to have to know a minimal amount (if any) JavaScript to utilize the library. While I know that in most circumstance, the page author is also the widget author, from the standpoint of testing and separation of concerns (making pages robust as requirements change), it is also good practice to keep your pages as much about layout, content, and style as possible. Your JavaScript widgets are there to bring in the functionality and interactivity of the page.

In my ideal scenario, using my JavaScript library will require exactly one line of markup to include the library, bootstrap it, and parse the page rendering any widgets as needed. Widgets will be ridiculously simple to use, and will only expose attributes (or only require attributes) that have a real impact on their function or style. Configuration should be done elsewhere.So here is my ideal page:

<html>

<head>

    <script type="text/javascript" src="path/to/bootstrap.js"></script>

</head>

<body>

    <div dojoType="medryx.widgets.HeadshotGrid" columns="4" style="width:400px;></div>

</body>

</html>

Notice how clean this is from an HTML perspective. Even though I love Dojo’s idea of namespacing CSS with html and body class names, I think that in a standard page, the page author shouldn’t have set the body class. The page author shouldn’t have to think about if they want “xd” version of the toolkit or same domain (especially considering that during development they probably want local, and during deployment they will likely want xd). The page author shouldn’t have to do any dojo.require calls. They simply call the widgets they need, and the rest is magic. If they want to do some JavaScript, they can, if they want to override the body class they can. But in the simplest (and most common) case, they don’t need to do anything. And indeed, in the simplest case, no matter what environment the page is executed in, it should just work. No changes to the HTML required.

So how do we get to this end? Its actually easier than it may seem. It takes a little from the school of thought that favors convention to configuration. It does not bar configuration, but if you don’t do any configuration, and simply adhere to convention, things should just work.

Lets think about what we need our bootstrap to do:

  1. Load the right version of dojo
  2. Load the right version of the environment
  3. Require any necessary scripts
  4. Parse the page

Loading the right version of dojo

In all of my projects, I put my library on the same server as the one serving Dojo. This may or may not be the same as the HTML page that is including the libraries (so we still have to deal with XD issues), but it does simplify a few things. If I know the path to my bootstrap script, then I can derive my paths to everything else, regardless of where the bootstrap lives.

So the first thing the bootstrap does, is some introspection. What is the path to the bootstrap file, relative to the calling page?

This is actually a cinch to do with Dojo. But at the time of loading, we don’t yet have Dojo loaded. So it requires some old fashioned DOM scripting:

var script = document.getElementsByTagName("script");


    for (var i = 0; i < script.length; i++) {

        var src = script[i].getAttribute("src");

        if (src) {

            //if the src of this script element is "bootstrap.js"

            //then everything before the text bootstap.js

            //is the path that got us here

            var match = src.match(/(.*)\/bootstrap.js/);

            if (match && match.length) {

                djConfig.baseUrl = match[1] + "/dojo/dojo/";

            }

        }


    }

So in this snippet I am simply loading all the “script” tags in the already loaded DOM. Since this script is executing, then by definition, at the very least the script tag that called this script is loaded in the DOM. Once I find the script tag that loaded this script, grab the path to this script, and map the path to dojo relative to this script. Not too bad, but a little but of a funky concept if you don’t get your head around it.

So now we can create the script element to load dojo onto the page. But remember, the DOM is probably not fully loaded yet. So the safest thing to do will be to us a document.write to load dojo. But using a document.write to “inject” dojo into a page is not the normally expected use of dojo. And you don’t necessarily know that the moment after you perform your write statement that your script will have access to dojo.addOnLoad. As such, you must define a djConfig variable (for many reasons) before you do your document.write, and that djConfig should have an addOnLoad method defined. This is your first method that “knows” that it has access to Dojo. So the next bit of code looks something like:

djConfig = {

    isDebug: true,

    modulePaths: {

        dojo: "../dojo",

        dijit: "../dijit",

        dojox: "../dojox",

        pages: "../../pages",

        medryx: "../../medryx"

    },

    addOnLoad: BOOTSTRAP,

    useXDomain: false

}


//now include the dojo.js -- if cross domain, then use xd.js

var extension = djConfig.useXDomain ? ".xd.js" : ".js";


//load dojo!

document.write("<scr" + "ipt type='text/javascript' src='" + djConfig.baseUrl + "dojo" + extension + "'></scr" + "ipt>");


So djConfig declares the appropriate modulePaths (note that the dojo/dijit/dojox are included in case this is a cross-domain load). If defines the addOnLoad method. Then we put together our path to dojo, and decide which version of dojo to use based on the useXDomain flag in djConfig.

Loading the right environment

Dojo will call BOOTSTRAP as soon as it is loaded. Which is where we will start to load our application. The first thing our BOOTSTRAP method does is figure out which environment to load. The first place it looks is in a custom configuration in djConfig called “environment”. If that value is set to “page”, then it will look for a custom attribute on the script tag that loaded the bootstrap to see if any environment was described there. This is useful in development or testing, if for example, you want to temporarily force a page to use a particular environment. So the modified djConfig and bootstrap looks like this:

// Below this line are configurations that are usually swapped in and out at build time

//==============================================================================================================

///BEGIN CONFIG

djConfig = {

    isDebug: true,

    modulePaths: {

        dojo: "../dojo",

        dijit: "../dijit",

        dojox: "../dojox",

        medryx: "../../medryx",

        pages: "../../pages"

    },

    addOnLoad: BOOTSTRAP,

    optimize: false, //for loading the compiled js.

    medryxConfig: {

        environment: "page" // use 'page' to read the 'environment' attribute on the iforms <script> tag

    }

    //may need to put this in a common location


    // dojoBlankHtmlUrl: "blank.html", //required for xdomain

    // dojoCallbackUrl: "blank.html" //required for xdomain

    // useXDomain: false

};

///END CONFIG

//===================================================================================================================


medryx = {

    _initializers: [],


    addOnLoad: function(/* Function */f){

        // summary:

        //    this implementation is to be sure pages have access to addOnLoad immediately.

        //    a more formal/robust implementation is loaded later

        //    that handles the more complex use cases (like calling

        //    addOnLoad after page is loaded)

        iforms._initializers.push(f);

    },


    config: djConfig.medryxConfig,


    ready: function(){

        //    summary:

        //        event called back when form is ready to display

    }

}


function BOOTSTRAP() {

    var environment = medryx.config.environment;

    if (environment == "page") { //then this is a development environment

        environment = dojo.query("script[environment]").attr("environment")[0] || "Static"; //default is static

    }

}


(function(){ //scope protection

    //we don't have dojo yet, and we need the path to this file.

    var script = document.getElementsByTagName("script");


    for (var i = 0; i < script.length; i++) {

        var src = script[i].getAttribute("src");

        if (src) {

            var match = src.match(/(.*)\/bootstrap.js/);

            if (match && match.length) {

                djConfig.baseUrl = match[1] + "/dojo/dojo/";

            }

        }


    }


    if (!djConfig.baseUrl) {

        throw new Error("could not find path to JS. this file must be name bootstrap.js for everything to work");

    }

    //now include the dojo.js -- if cross domain, then use xd.js

    var extension = djConfig.useXDomain ? ".xd.js" : ".js";


    //load dojo!

    document.write("<scr" + "ipt type='text/javascript' src='" + djConfig.baseUrl + "dojo" + extension + "'></scr" + "ipt>");


})();

Okay, so its getting a little more complicated, but still not overwhelmingly so. This script did everything I described above, and added just a bit more. First, it declared the global namespace of “medryx” and it created a config, as well as its own addOnLoad method.  Don’t worry too much about that right now except to say, its used to be sure certain scripts are executed after all the dojo.addOnLoad methods are executed. Again in a local build the order is pretty reliable, so its not a big deal, but when using a cross-domain “compiled” script, getting the order of things right can be a bit tricky, so this helps solve that. More on that in another post in the future. I also check if djConfig.baseUrl gets set, if it doesn’t, we get a meaningful error message.

So now I know what environment I want. I’ve loaded dojo. Next step dojo.require() the environment. And once I have the environment, I am ready to load any scripts needed to run the page.

This is where convention over configuration comes back to play. You may have noticed my “pages” namespace. This is actually a special namespace. Each page has a script in the pages namespace. And in fact, you can figure out the pages.* script you need by looking at the calling html page’s pathname. Pick whatever convention you want, but just be consistent. Then you can deduce what js needs to be loaded next and you can just do it, without the page have to do any configuration. Of course, if you choose, you could create another “custom attribute” on the bootstrap script tag with the name of the page module to load, or you could simply require() it into the HTML page on the HTML page itself. But following the convention gives you the benefit of zero configuration. Less code, less configuration = fewer silly bugs.

So here is a snippet in my BOOTSTRAP method that first checks if any of the script tags has the special “module” attribute. If they don’t it tries to figure out the module attribute based on a path name. This version assumes all HTML pages will be under a folder called “pages”.

var module = dojo.query("script[module]").attr("module")[0];

    if (!module) {

        var page = window.location.pathname.match(/pages\/(.*)\..*/);

        if (!page) {

            console.warn("Pages that use this bootstrapper must be in a subdirectory of 'pages'");

            console.warn("Using mock module name: MOCK");

            page = "MOCK";

        } else {

            page = page[1].replace(/\//g, '.').toLowerCase();

        }


        module = "pages." + page;

    }

So its time do some dojo.require()’ing. But before we do, lets consider the case of the “optimized” flag I have in my djConfig. Note that this is NOT a standard djConfig attribute. I simply use this to decide if I want to include a script on the page which is my compiled library. This is the version of the library that takes my hundreds of files and folders and compresses them into a single js file. With that in mind:

if (djConfig.optimize) {

        //load the compiled js: a single large file that

        //encompasses the code needed for each environment

        var head = dojo.query("head")[0];


        var extension = djConfig.useXDomain ? ".xd.js" : ".js";


        dojo.create("script", {

            src: djConfig.baseUrl + "/medryx-" + environment + extension

        }, head, "last");


    }


    //this contains all of the most common scripts a page 

    //uses -- like form, layout, etc.. if optimized, then

    //this does nothing, since its already loaded


    dojo.require("medryx.environment." + environment);


    //in case the module forgets... or if there is no module page (and makes this "first")

    dojo.require("pages.common");

    try {

        dojo.require(module);

    } catch (e) {

        console.warn("Unable to load module for this page (Did you remember to create a script for this?): ", module);

    }

I’ll describe the optimize flag and the location/name of the “built” js file later (when I discuss my profile layers). You can see I do a simple dojo.require for the environment. Also note that you must build your compiled scripts as one-per-environment, since you can only have one environment included on a page (or the “last one loaded wins”). The module dojo.require is in a try/catch block. I chose this instead of other ways to load the module that don’t throw an error when not found because I wanted to create the appropriate development warning to remind people to create their “module” file in the pages namespace.

Loading the style sheets

One more step, and we are done with our bootstrap. I wanted to be sure that our appropriate CSS file is loaded on the page. This is the CSS that is needed for widgets to work at all. Its a silly error when everything seems broken, and its just because the CSS didn’t import and failed silently. I also add the class name to the body  (though I should probably check if none exists first).

//import the style to the page

dojo.create("link", {

    href: dojo.moduleUrl("medryx", "../../themes/tundra.css").toString(),

    type: "text/css",

    rel: "stylesheet"

}, dojo.query("head")[0], "first"); //use "first" so that any locally declared style/css takes precedence

//add the css namespace so that the calling page doesn't have to remember to

dojo.query("body").addClass("tundra");

Okay, so what have we done here? We now have a single script that gets dojo into your page, loads you environment, decides if you need a cross-domain build, and handles incorporating your widget stylesheets. Not too shabby. And now with the use of a single <script> tag, your HTML page just works.

But aside from the niceness of everything just working, how else does this help us? Did you notice my comments strings above and below the djConfig variable? While I will describe in detail in my next post how I replace this depending on the environment desired, suffice it to say for now, that my build system can swap out the djConfig section depending on the kind of build I would like done. So a simple call to:

build.bat environment=DynamicLocal

…builds the entire library, and makes all the pages use the pretty optimized JS if needed and use all the right resources. Voila.

Next post: how to modify the dojo build system to handle environment swaps (warning – some mild dojo hacking is required for now until I can get them to fix some issues in the build system…)

May 30, 2009

Prequel: Professional JavaScript Build System

Filed under: dojo, javascript — maulin @ 11:43 pm

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:

  1. 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.
  2. 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.
  3. 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.

A Professional JavaScript Build System

Filed under: dojo — maulin @ 4:05 pm

Its been a long time since my last post. That’s not to say I have run out of things to say. But the list of topics has gotten long enough, that I think its high time I start writing again.

In this post, I will discuss a build system for JavaScript development based on the Dojo build system. Having done plenty of server side development, “building” a project was not a foreign concept to me. And the Dojo build system is quite nice. It has a rich feature set, and does an amazing job of optimizing scripts. If you look at your set of scripts after they have been build and optimized, they resemble binary code more than they resemble any meaningful text. And as long as you adhere to the basic design principles encouraged by Dojo (namespacing, using dojo.moduleUrl when loading resources, using templatePath in widgets, etc), you get most of this for free.

But when we build server software, we frequently think of various “build environments” – dev, QA, and live, for example. That is not a concept that I have seen applied in the Dojo community in any of the many example projects that exist. In a previous post, I had described a “MockRestService” that allowed you to test all your JsonRestStore functionality without an app server. The MockRestService simply called static json files and “served them” to your store.

When I develop complicated applications, and when I try to apply Test Driven Development principles, I am constantly striving to simplify my test. I want to remove as many complicating factors as possible. Having an app server running, even if its just a simple PHP script, is just another layer of complexity that can be weeded out when developing.

As another example, I frequently have other xhr calls in my applications, hitting multiple internal and external resources. I would love to be able to hit one version of the service for local development and testing, another for QA, and yet another for live systems. To this end, I have developed my own work pattern, that I think makes my development much more productive. This breaks down into a few main concepts:

  1. Using a bootstrap JavaScript to load the library
  2. Abstracting any calls to resources that can change into a service layer
  3. Modifying the build system to allow easy swapping of resources.
  4. Creating multiple build environments to facilitate development – static resources (statically served files without the need of an app server), dynamic resources (using a local app server, and using a foreign app server if you will need to test cross-domain access), qa, and live.

In some ways, you can think of this whole system as a “dependency injection” framework for JavaScript. The bootstrap is your “container”, and controls which services get sent to your scripts. Your scripts only use “interfaces” to access resources – never making “low-level” calls to xhr, but instead letting a service layer handle that. A store is one example of a service – abstracting your calls to the actual resource. If you always use a data store, then you simply pass your script a different store depending on which resource you want to access.

So lets look at how to do this. I recently build an application that displays a set of people, their picture and their title. Clicking on their picture sends them a message. Not rocket science. This application contained three widgets:

  1. The individual “headshot” for each person
  2. The “grid” of headshots that uses a data store to access the information about each person, and creates headshot for each person and places them in a table (not a dojox.grid, just a simple table).
  3. The messaging system that presents a dialog to the user when they click on a headshot to send a message to that person.

(This application actually had quite a bit more complexity which warranted all this scaffold, but I’m trying to simplify.)

The headshot widget is given a JS object with a “image” path and a person’s name and uses them to create something like this:

[Need to insert image here]

But the path to the image is a relative path, and depending on our environment, that absolute path may be different. So this is the first “service” we are going to require. A very simple service that is given a relative path to an image and returns the absolute path.

dojo.provide("medryx.services.static.PhotoPathService");
 
medryx.services.static.PhotoPathService = function(relativePath) {
    return "http://blog.medryx.org/pics/" + relativePath;
}

So that is NOT a lot of code by any stretch. But if you were to  dojo.require(“medryx.services.PhotoPathService”) in your widget, you would be defeating the purpose. You would still have to actually change the service to get a different build environment. And remember, the goal is to have to update exactly one file to get a completely different environment behavior. So we need to build one more script – a service accessor. This is very similar to dojo.require (you could even argue that you could simply extend dojo.require – but I just now thought of that, so I haven’t tried it).

dojo.provide("medryx.environment.Static");
dojo.require("medryx.services.static.PhotoPathService");
 
medryx.service = function(service) {
    switch(service) {
        case "medryx.services.PhotoPathService":
            return medryx.services.static.PhotoPathService;
        default:
            throw new Error("Could not find the requested service: " + service);
    }
}

So this to, is not a complicated class. It just takes a string name of the desired interface, and returns an instance that “implements” it. So now if my bootstrapper changes its “require” from “medryx.environment.Static” to “medryx.environment.DynamicLocal” (for example), I could change what services I return.

<pre lang="javascript">dojo.provide("medryx.environment.DynamicLocal");
dojo.require("medryx.services.dynamic.PhotoPathService");
 
medryx.service = function(service) {
    switch(service) {
        case "medryx.services.PhotoPathService":
            return medryx.services.dynamic.PhotoPathService;
        default:
            throw new Error("Could not find the requested service: " + service);
    }
}

As long as the “environment” scripts define a “medrxy.service” method, then all my other scripts know that as long as an environment has been required, then they can call medryx.service() with the name of the service they need. So my headhsot widget does:

dojo.provide("medryx.widgets.Headshot");
dojo.require("dijit._Widget");
dojo.require("dijit._Templated");
 
dojo.declare("medryx.widgets.Headshot", [dijit._Widget, dijit._Templated], {
    templatePath: dojo.moduleUrl("medryx.widgets", "templates/Headshot.html"),
    person: null,
    postMixInProperties: function() {
      var pathService = medryx.service("medryx.services.PhotoPathService");
      this.imagePath = pathService(this.person.imagePath);
    }
});

So already, even without the bootstrapping and building described above we can change the behavior of a page with the change of one line:

<html>
 <head>
 <script type="text/javascript" src="/path/to/dojo.js"></script>
 <script type="text/javascript">
    dojo.require("medryx.environment.Static");
    dojo.require("medryx.widgets.Headshot");
</script> 
</head>
<body>
<div person="p" dojotype="medryx.widgets.Headshot"></div>
</body>
</html>

And this behaves differently than:

<html>
  <head>
  <script type="text/javascript" src="/path/to/dojo.js"></script>
  <script type="text/javascript">
    dojo.require("medryx.environment.DynamicLocal");
    dojo.require("medryx.widgets.Headshot");
  </script>
</head> 
<body>
<div person="p" dojotype="medryx.widgets.Headshot"></div>
</body>
</html>

So this is a very simple way to abstract one part of my application that has a dependency on what environment the application is running in. Clearly, this is a bit of overkill for this simple example. In fact, in this example, I still do use a service, but I have a generic “ConstantsService” that gets me information like which server I should hit, or which absolute path to use. But, for more complicated services – likely stores, or services that have many methods that use various XHR calls, this is the pattern I use.

So what about the bootstrapper? What does that look like? Because hard-coding the dojo.require to which environment I am using might be okay for a test page, but for a live page, I don’t want to have to write a script (or manually) change all those require statements to include the right environment. That is what I will write about in my next post.

Let me know if you have any comments or suggestions!

Powered by WordPress