12/24/2014

An Angular-ized Require-Based Script Loader

The co-author for this approach, who did a lot of the heavy lifting, is the very talented Nate Ferrero.

We recently found another speed bump as we carefully drove down our path to removing require.js in favor of Angular purity. We found hidden dependencies nestled into the code. Code development of any substantial kind that lives for any substantial amount of time with multiple developers quickly gains warts. As far as I am concerned, it's the natural result of lots of requirements and limited resources. In fact, the more successful a project gets the more likely it is to get warts. As demands increase, the need for results over "best practices" heightens. So I never hold it against any other developer when I find one. I have no doubt there are developers out there who have, or maybe are, shaking their heads as they extricate warts I may have added to a previous system.

We found certain scripts being required when very particular circumstances occurred. The code would be following a logical path, and then suddenly a little require statement appears hidden in an if or loop clause.

The easiest way to resolve this would be to add those files to our main minified js file. We didn't want to do that since we don't want to arbitrarily add every script to our core files that may at some point be needed. Most users would never need these files and we try to be as diligent against bloat as possible. We couldn't write replacements for them since they were mostly brilliant 3rd party libraries. Having said all that, there was a strong desire to not keep the cleverly buried require statements in. Our answer: Factories.

We built an Angular factory whose sole purpose was to take in a url, pass it to require, and run a supplied function once loaded. Armed with this factory, we could inject it into a new module that had an external script dependency. We were pretty pleased with the result. The code was loaded as needed via require, we weren't recreating the same code for every external file, and (most importantly) it was now listed in the dependency list for our code. There was no more hiding!

The process starts with a simple Angular factory in an isolated module that we can then inject whenever needed. Require is necessary on the page for this to work. The function binds the passed url to a require function call. In order to avoid simple data type issues, we slice the incoming n arguments into an array which require wants. I suspect this code may become more complicated over time and as other developers use it. For this sprint, and our immediate needs, this was sufficient.

angular.module('requireLoader', []).factory ('libReady', function () {
    return function (urls) {
      var urls = Array.prototype.slice.apply(arguments)
      return require.bind(null, urls);
    };
});

Now that we have a require function with arguments already to go, it's time to put it into the code. First, we inject our loader library into the module.

    angular.module('demo', ['scriptLoader'])

Then, we add our factory to load the code via require.

    angular.module('demo').factory('externalScript', function (libReady) {
        return libReady('/3rdparty/script');
    });

We have taken our one simple require statement and added a level of complexity right on top. The initial generic loader could be removed, and all it's code added to the specific loader that comes next, but that creates an opportunity for boilerplate and confusion.

With this approach, the loading process is centralized and easily accessible to all who need it. The complexity in this case is worth it. The use of external libraries are managed by centralized loader functions, so if versions or needs change, there is only one place to modify. Anyone having to maintain this code in the future will have a standardized and detailed way to follow the use of require.

The last part is to add the newly created loader into the main code, and wrap it around the code that needs to run after the resource is loaded.

    angular.module('demo').factory('mainFactory', function (externalScript){
        if (y.hasSomething && x.doesNotHaveSomething && z.occurred()){
            externalScript(function (){
              //This only runs when this specific situation occurs that requires the external script
            });
        }
    }

Sticking as close to the Angular native development path as possible is very important to us. Vanilla Angular means fewer integration points and easier testing. Making the two libraries work together as equals is not easy and could make for a brittle application. This approach makes Angular dominant which is easier for us and the browser.