How CoccyxJS uses RequireJS to define its own dependencies but can also be used from the global name space

images-1RequireJS is a great asset when you want to write highly modularized applications, but when it comes to using RequireJS to write highly modularized JavaScript libraries that are meant to be shared with others, then there are challenges. It is a fact that for what ever reasons, not everyone is enamored with RequireJS; they just wont use your library if it means they also have to use RequireJS for their own applications. I think that it is telling that many of today’s most popular libraries do not directly use RequireJS internally to define their own dependencies and modules because they don’t want to discourage other developers from using their libraries by imposing RequireJS as a dependency.

The ultimate solution to this problem would be if there was a way to create a library that is itself built using RequireJS, which defines its own modules and dependencies using the RequireJS define method, but that can also be used in the old “global” fashion that most libraries still tend to adopt and users like. It is easy to draw a comparison to jQuery, which is both AMD compliant and capable of being used from the global name space, and say, “just do it the way jQuery does.” The problem with that is that although jQuery is AMD compliant, it actually doesn’t use RequireJS at all to define its own modules and inter module dependencies.

As you can guess, I struggled with this very issue myself while developing CoccyxJS v0.6, which will be released very soon. I wanted to use AMD modules to break up CoccyxJS into its logical components and have its components declaring their own dependencies upon other CoccyxJS components, such as define(‘views’, [‘application’, ‘helpers’], function(app, helpers){…}); for example, but I also didn’t want to discourage the adoption of my library just because I chose to implement it using RequireJS. After some thought it dawned on me that conceptually it should be possible to have my cake and eat it too, to use RequireJS in a way that would make CoccyxJS a compliant AMD module, usable by applications that are also using RequireJS, and usable by application that access it from the global name space as well.

The solution I employed is actually quite simple and I offer it up to anyone else who is considering developing a library they’d like to build using RequireJS, but who also don’t want to discourage its adoption.

First and foremost, CoccyxJS is built as one would build a library that is intended to be used from the global name space. The difference though is that it also defines its own inter dependencies between its modules using the RequireJS define method. In order to get this to work, so that it can be used both as an AMD module and from the global name space, you never use modular dependencies as arguments to define’s callback functions and you never define your modules by returning objects from those callback functions.

For example, in CoccyxJS, the views module is defined as define(‘views’, [‘application’, ‘helpers’], function(){…}). The views module declares its dependency upon both the application module and the helpers module, but it doesn’t use them as arguments in the callback function. By declaring its dependency on these 2 modules, the callback function won’t be called until they are loaded and resolved. Internally, anything that is exposed in these two modules is done so in the old fashioned global way, by tacking them onto the single global variable, Coccyx. By the time the callback function is called, everything that the views module needs from the 2 other modules has already been placed into the Coccyx name space. For example, the helpers module, in total, is shown below:

define('helpers', [], function(){
    'use strict';

    var Coccyx = window.Coccyx = window.Coccyx || {};

    Coccyx.helpers = {
        //Returns true if s1 contains s2, otherwise returns false.
        contains: function(s1, s2){
            var i, len;
            if(typeof s1 === 'string'){
                for(i = 0, len = s1.length; i < len; i++){
                    if(s1[i] === s2) {
                        return true;
                    }
                }
            }
            return false;
        },
        //Returns a deep copy object o.
        deepCopy: function(o){
            return JSON.parse(JSON.stringify(o));
        },
        //Pass one or more objects as the source objects whose properties are to be copied to the target object.
        extend: function(targetObj){
            var len = arguments.length - 1,
                property, i;
            for(i = 1; i <= len; i++){
                var src = arguments[i];
                for(property in src){
                    if(src.hasOwnProperty(property)){
                        targetObj[property] = src[property];
                    }
                }
            }
            return targetObj;
        },
        //For each matching property name, replaces target's value with source's value.
        replace: function(target, source){
            for(var prop in target){
                if(target.hasOwnProperty(prop) && source.hasOwnProperty(prop)){
                    target[prop] = source[prop];
                }
            }
            //0.6.0 Return target.
            return target;
        }
    };

});

Within the views module, when it needs to call helpers.extend(), it would do so by calling Coccyx.helpers.extend(), which is how it is defined within the helpers module.

I use Grunt to concatenate all the modules that make up the CoccyxJS library into a single file. The last file concatenated at the end, the amd.js file, looks like this:

define('coccyx', ['application', 'helpers', 'router', 'history', 'models', 'collections', 'views', 'eventer', 'ajax'], function () {
    'use strict';
    return window.Coccyx;
});

The amd.js file is ultimatley responsible for turning CoccyxJS into a compliant AMD module. It states its dependencies on all the sub modules and it returns window.Coccyx, which allows it to be used from the global name space.

Now, in order to use CoccyxJS from the global name space, you just need to add this little JavaScript shim before your script tag that loads CoccyxJS:

//A shim that mocks the RequireJS define() method so that Coccyx.js can be used without RequireJS.
//Calls the RequireJS define callback function, allowing Coccyx.js modules to "define" themselves.
//IMPORTANT - include this script before Coccyx.js.
(function(){
    'use strict';
    window.define =  function define(){
        (arguments[arguments.length - 1])();
    };
}());

And there you have it. By using this technique when creating your own libraries you will be able to satisfy developers who want to use RequireJS and those that don’t. Your library will gain all the benefits of using RequireJS to build itself from individual AMD modules and ultimately defining itself as a compliant AMD module, while also being able to be used from the global name space.

I really like RequireJS, but I don’t want to alienate potential adopters of my library who might otherwise adopt CoccyxJS for their own development efforts but for its RequireJS dependency. By using this technique, I feel like I am able to have my cake and eat it too, and able to pass that cake on to the users. “Let them eat cake,” that’s what I say 🙂

:wq

Advertisements

2 thoughts on “How CoccyxJS uses RequireJS to define its own dependencies but can also be used from the global name space

    1. Jeffrey Schwartz Post author

      spacenick (@spacenick) that wont work for libraries whose modules map themselves to paths on a single global name space, such as in Coccyx.helpers, Coccyx.ajax, Coccyx.eventer, Coccyx.model, etc; the pattern you point to requires each argument passed to the callback function to have to be placed in its own global name space, such as in window.helpers, window.ajax, window.eventer, window.model, etc. to match the AMD module names used as arguments in the callback functions. Of course, it would work if each factory wrapped a different code base particular for each environment that it was targeting, but maintaining a single code base for a library is difficult enough.

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s