JavaScript Include, Part 3

In this third and (as seems most likely at the moment) final article about developing a JavaScript ‘include’ facility, I’ll address the provision of ‘include-once’ functionality. If you’ve not read the previous two articles, or you wish to refresh your memory, my first JavaScript ‘include’ article discussed the basic need for an ‘include’ feature, and offered a reasonable first stab at an implementation. It also outlined two key shortcomings of this first attempt. The first shortcoming, the proper handling of relative paths to included files, is dealt with in my second JavaScript ‘include’ article; the second issue of how to deal with the same files being included more than once, is presented here.

Disclaimer

Please see my JavaScript disclaimer in the first JavaScript ‘include’ article. It still applies, and constructive comments are welcome on any gaffes or oversights.

What Is Include-Once?

In addition to an include facility, many languages offer the ability to be able to ‘include-once’. Where source files are variously dependent upon other files and upon each other, it’s easy to see how the same files could be included over and over. Being able to include-once would prevent such multiple inclusions of the same file by automatically only including each file the first time it’s included.

From the caller’s point of view, using include-once is syntactically very similar to using a regular include, e.g.:

includeOnce("usefulLib.js");
usefulFn();

In this example, the ‘usefulLib.js’ file (which presumably includes the implementation of the ‘usefulFn()’ function) would only be included if it hadn’t previously been included before (irrespective of whether the previous inclusion was performed using ‘include()’ or ‘includeOnce()’).

Even with ‘includeOnce()’ available, it would still be possible to force an include to take place. In the following code snippet, both ‘someCode1.js’ and ‘someCode2.js’ would be included twice:

include("someCode1.js");
include("someCode1.js");

includeOnce("someCode2.js");
include("someCode2.js"); // Only 'includeOnce()' checks for previous inclusion.

Although for most purposes (or so it seems to me), ‘includeOnce()’ would be more useful than ‘include()’, a regular non-exclusive inclusion may still have its place. For example, you may want to include a chunk of JavaScript that generates dynamic page content in situ (using ‘document.write()’, say). If you want this dynamic content to appear more than once in the page, then it may be necessary to force the multiple inclusion of such code snippets. Ideally, then, both ‘include()’ and ‘includeOnce()’ would be available.

Implementing Include-Once

In order for the ‘include’ facility to be able to establish whether a file has previously been included, we need some means to uniquely identify files. Clearly, filename alone isn’t sufficient because different files could have the same filenames (as long as they’re within different directories). Although it might be tricky to think of a circumstance where a developer would have such an arrangement, it is possible in principle, therefore we need to make sure our ‘include-once’ feature won’t be wrong-footed by it.

Perhaps what we need is to use full pathnames (or even full URLs) to identify files. In [part 2 of this article series], better handling of file pathnames was introduced, but relative paths alone won’t cut it. If, for example, two files in different directories both include the same third file, they could refer to it using relative paths that won’t necessarily match exactly. In other words, although we’d have the same file being included twice, a casual glance at the relative paths alone might lead us to believe that two different files are being included.

Whenever an ‘include()’ or an ‘includeOnce()’ is requested, then, we’ll need to do a fair bit of processing on the supplied pathname to establish the full URL of the included file.

There are two parts to this process. Firstly, we need to take a look at the include file path we’ve been given and prefix it with whatever is required to make it into a full, absolute URL. At one extreme, this could involve no modification (in the case where we were supplied with a full URL anyway); at the other, we could be adding everything but the filename. Secondly, we need to make sure that the full URL we have doesn’t contain any ‘.’ or ‘..’ (i.e., ‘this directory’ or ‘parent directory’) parts, because this too could make two references to the same file look different from each other.

Although the process of converting (what could be) a relative include path into a full, absolute URL isn’t the proverbial rocket science, it is still a bit fiddly, and, requires more lines of code than it feels it deserves. I’ve implemented the process via two functions: ‘getRealUrl()’ that takes a full URL (i.e., one that begins with ‘http://’ and includes a domain) and parses out the ‘.’ and ‘..’; and ‘getAlreadyIncludedKey()’ that makes sure the include path is a full URL before calling upon ‘getRealUrl()’ to make sure there are no ‘.’ and ‘..’ parts.

So, ‘getAlreadyIncludedKey()’ provides us with a unique reference to a file to be included, thus giving us a means to recognise whether we’ve been asked to include a supplied file before. As the function’s name suggests, we can then use this reference as a key into a JavaScript object, for which I’ll use a global variable called ‘_alreadyIncluded’.

Using a file’s full URL as a key, we can then make a note of every file that’s included (using either ‘include()’ or ‘includeOnce()’) and automatically refrain from including it as necessary if it has been included before (using ‘includeOnce()’).

The Final Include Code in Full

It’s taken quite an effort to produce as comprehensive an ‘include’ facility as a modern language ought to offer, but here’s the full implementation in all its glory (I toyed with the idea of only including in full those extra bits required for ‘include-once’ support, but thought it would be more convenient to work with if anyone was actually tempted to try using this new facility):

var _includePath = "";
var _alreadyIncluded = {};

// Essentially 'new XMLHttpRequest()' but safer.
function newXmlHttpRequestObject()
{
    try
    {
        if (window.XMLHttpRequest)
        {
            return new XMLHttpRequest();
        }
        // Ancient version of IE (5 or 6)?
        else if (window.ActiveXObject)
        {
            return new ActiveXObject("Microsoft.XMLHTTP");
        }

        throw new Error("XMLHttpRequest or equivalent not available");
    }
    catch (e)
    {
        throw e;
    }
}

// Synchronous file read. Should be avoided for remote URLs.
function getUrlContentsSynch(url)
{
    try
    {
        var xmlHttpReq = newXmlHttpRequestObject();
        xmlHttpReq.open("GET", url, false); // 'false': synchronous.
        xmlHttpReq.send(null);

        if (xmlHttpReq.status == 200)
        {
            return xmlHttpReq.responseText;
        }

        throw new Error("Failed to get URL contents");
    }
    catch (e)
    {
        throw e;
    }
}

function include(filePath)
{
    includeMain(filePath, false);
}

function includeOnce(filePath)
{
    includeMain(filePath, true);
}

function includeMain(filePath, once)
{
    // Keep a safe copy of the current include path.
    var includePathPrevious = _includePath;

    if (isAbsolutePath(filePath) == true)
    {
        var actualPath = filePath;
        // Absolute paths replace.
        _includePath = getAllButFilename(filePath);
    }
    else
    {
        var actualPath = _includePath + filePath;
        // Relative paths combine.
        _includePath += getAllButFilename(filePath);
    }

    var alreadyIncludedKey = getAlreadyIncludedKey(actualPath);

    if (once == false || _alreadyIncluded.alreadyIncludedKey == undefined)
    {
        var headElement = document.getElementsByTagName("head")[0];
        var newScriptElement = document.createElement("script");

        newScriptElement.type = "text/javascript";
        newScriptElement.text = getUrlContentsSynch(actualPath);
        headElement.appendChild(newScriptElement);
        _alreadyIncluded.alreadyIncludedKey = true;
    }

    // Restore the include path safe copy.
    _includePath = includePathPrevious;
}

function isAbsolutePath(filePath)
{
    if (filePath.substr(0, 1) == "/" || filePath.substr(0, 7) == "http://")
    {
        return true;
    }

    return false;
}

// Strips the filename from a path, yielding everything else.
function getAllButFilename(filePath)
{
    return filePath.substr(0, filePath.lastIndexOf("/") + 1);
}

// Yields a full, real URL to be used as a file's include key.
function getAlreadyIncludedKey(filePath)
{
    if (filePath.substr(0, 7) == "http://") // Full URL?
    {
        return getRealUrl(filePath);
    }

    if (filePath.substr(0, 1) == "/") // Absolute path?
    {
        return getRealUrl("http://" + document.domain + filePath);
    }

    // Otherwise, assume relative path.
    return getRealUrl("http://" + document.domain +
        getAllButFilename(document.location.pathname) + filePath);
}

// Takes a file path as a 'full' URL (including 'http://' and domain) and
// yields a 'real' version of it with no '.' and '..' parts.
function getRealUrl(fullUrl)
{
    var protocolAndDomain = fullUrl.substr(0, fullUrl.indexOf("/", 7));
    var urlPath = fullUrl.substr(protocolAndDomain.length + 1);
    var urlPathParts = urlPath.split("/");
    var realPath = "";
    var parentCount = 0;

    for (var i = urlPathParts.length - 2; i >= 0; i--)
    {
        if (urlPathParts[i] == ".")
        {
            continue;
        }

        if (urlPathParts[i] == "..")
        {
            parentCount++;
            continue;
        }

        if (parentCount > 0)
        {
            parentCount--;
            continue;
        }

        realPath = urlPathParts[i] + "/" + realPath;
    }

    return protocolAndDomain + "/" + realPath +
        urlPathParts[urlPathParts.length - 1];
}

Conclusion

My own feeling about the final implementation is that it seems like an awful lot of code for adding a ‘mere’ include feature to a language, even if it is one that seems to be fairly comprehensive. I could have made the code shorter, of course, but only at the expense of making it either flakier or less flexible and powerful (unless we’re talking about just removing all unnecessary whitespace). If you’re prepared to put up with the limitations of the code presented in the first article, for example, then clearly it could be shorter, but only the fuller version presented here offers anything like the equivalent feature available in other languages.

Only time will tell whether I make use of my own new facility (and, in any event, it will need further testing and/or use before I’ll be satisfied that it is fully reliable with no lingering bugs), but at the very least, I’ve found that the journey has been interesting, and there are a few useful little functions and code snippets that, quite apart from forming part of the final ‘include’ implementation, may be useful in their own right. I can imagine, for example, that the algorithm used by the ‘getRealUrl()’ function for parsing the ‘.’ and ‘..’ bits from pathnames may in itself turn out to be useful one of these days.

Anyway, as ever, I’m open to your constructive comments and thoughts. And thanks for reading.

3 thoughts on “JavaScript Include, Part 3”

  1. Great article nice method.

    I wanted to share my approach to includeOnce, which is different and is not file centric but library centric.

    My include once looks like this and is good for defining anything and making sure is unique:
    window.defineOnce = function defineOnce(variable,scope,definition){
    var ref = scope[variable];
    if(!isDefined(ref)){
    scope[variable] = definition();
    }
    else
    {
    console.log(‘already defined’)
    }
    }

    and then I have a LibObject defined like this:

    defineOnce(‘LibObject’,Atlx.GlobalClasses,function(){
    var LibObject = function LibObject(name,weight,load){
    ……………………………
    };
    LibObject.prototype = {
    constructor:LibObject,
    load:false,
    que:null,
    name:’lib’,
    weight:0,
    files:null,
    loaded:false,
    isFileLoaded:null,
    addFile:function,
    _onLoaded:function,
    runUnder:function(f){},
    onLoaded:function(){},
    loadNow:function(){ }
    };
    return LibObject;
    });

    I did this mostly because i had the need to be able to load dynamical various JS libs like jQuery or Ext.

    Using the LibObject for example i create the jQueryLib object and similar any other library.

    The beauty I see in it is that you can organize your scripts if you have a few you need to load and you can also load some other stuff like css in one bundle.

    if((isDefined(window.jQuery)) || (isDefined(window.loadjQuery) && window.loadjQuery)){

    defineOnce(‘jQueryLib’,Atlx.ExternalLibs,function(){
    jQueryLib = new Atlx.GlobalClasses.LibObject(‘jQueryLib’,0,false);
    jQueryLib.que = new Array();
    jQueryLib.isLoaded = function(){
    return isDefined(window.jQuery);
    }
    jQueryLib.runUnder = function(func){
    ……………………….
    };
    jQueryLib._onLoaded = function(){
    ……………………
    };
    Atlx.addExternal(jQueryLib);

    if(!isDefined(window.jQuery)){
    jQueryLib.load = true;
    jQueryLib.addFile({type:”js”,url:”……../jquery.js”});
    jQueryLib.addFile({type:”css”,url:”……js/fancybox/jquery.fancybox.css”});
    jQueryLib.addFile({type:”css”,url:”………/jquery.ui.custom.css”});
    jQueryLib.addFile({type:”js”,url:”………/jquery.ui.custom.js”});
    }
    else{
    jQueryLib._onLoaded();
    }
    return jQueryLib;
    });
    Atlx.ExternalLibs.jQueryLib.runUnder(function(){
    //definitions using jQuery
    //called as soon as jQuery is available

    });
    }

    Its far from perfect but it helps me a lot so maybe it helps someone else too.
    Below a more extended example using jQuery and Ext:

    loadjQuery = true;
    jumpURL = ”;
    debug = false;

    window.isDefined = function isDefined(variable){
    try {
    variable === 1;
    } catch (e) {
    return false;
    console.log(‘not declared’);
    }
    return (typeof variable != ‘undefined’ && variable != null);
    }

    window.defineOnce = function defineOnce(variable,scope,definition){
    var ref = scope[variable];
    if(!isDefined(ref)){
    scope[variable] = definition();
    }
    else
    {
    console.log(‘already defined’)
    }
    }

    defineOnce(‘Atlx’,window,function(){
    var Atlx = function Atlx(){
    this.GlobalClasses = new Object();
    this.Libs = new Array();
    this.Styles = new Array();
    this.ExternalLibs = new Array();
    window.runUnderjQuery = this.runUnderjQuery;
    var me = this;
    defineOnce(‘AtlLoader’,me, function(){
    var AtlLoader = function(){
    this.list = new Array();
    this.loading = false;
    };
    AtlLoader.prototype = {
    activeFile :null,
    name:’AtlLoader’,
    constructor : AtlLoader,
    loading:false,
    onComplete : null,
    addEvents : function addEvents(elem){
    var loader = this;
    if (elem.attachEvent){
    elem.attachEvent(“onreadystatechange”,function(){
    if (elem.readyState == “complete” || elem.readyState == “loaded”){
    loader.loaded(elem);
    }
    });
    }
    else if (elem.addEventListener){
    elem.addEventListener(“load”, function(){
    loader.loaded(elem);
    }, false);
    }
    },
    loadCSS : function loadCSS(obj){
    var script = document.createElement(“link”);
    script.type = “text/css”;
    script.rel=”stylesheet”;
    script.href = obj.url;
    this.addEvents(script);
    document.getElementsByTagName(“head”)[0].appendChild(script);
    },
    loadScript : function loadScript(obj){
    var script = document.createElement(“script”);
    script.type = “text/javascript”;
    script.src = obj.url;
    this.addEvents(script);
    document.getElementsByTagName(“head”)[0].appendChild(script);
    },
    loaded : function loaded(elm){
    if(this.activeFile)
    {
    this.activeFile.loaded = true;
    this.activeFile = null;
    }
    if(this.list.length >0){
    this.start();
    }
    else{
    this.loading = false;
    this.onComplete && this.onComplete();
    }
    },
    start : function start(){
    var scr = this.list.shift();
    if(isDefined(scr))
    {
    this.activeFile = scr;
    if(scr.type == “css”){
    this.loadCSS(scr);
    }
    else if(scr.type == “js”){
    this.loadScript(scr);
    }
    else{
    this.activeFile = null;
    scr.loaded = true;
    console.log(‘wierd type:’ + scr.type);
    this.loaded(null);
    }
    }
    else{
    this.loaded(null);
    }

    }
    };
    me.addLib(AtlLoader);
    return (window.AtlLoader = me.AtlLoader = new AtlLoader());
    });
    };
    Atlx.prototype = {
    constructor:Atlx,
    isDefined:isDefined,
    defineOnce:defineOnce,
    Libs:null,
    Styles:null,
    ExternalLibs:null,
    AtlLoader:null,
    Utils:null,
    GlobalClasses:null,
    addLib:function addLib(lib){
    this.Libs[lib.prototype.name] = lib;
    },
    addStyle:function addStyle(lib){
    this.Styles[lib.name] = lib;
    },
    addExternal:function addExternal(lib){
    this.ExternalLibs[lib.name] = lib;
    this.ExternalLibs[lib.weight] = lib;
    },
    loadExternal:function loadExternal(complete){
    var l = this.ExternalLibs.length,
    lib = null,ll,file,me=this;
    for(var i = 0; i 0)
    {
    func = this.que.shift();
    this.runUnder(func);
    }
    },
    runUnder:function(f){},
    onLoaded:function(){},
    loadNow:function(){
    if(!this.load){ return; }
    if(this.isLoaded()){
    return this._onLoaded();
    }
    var f,l = this.files.length,me=this;
    for(var i = 0; i 0)
    {
    fn(fld);
    }
    }
    });
    }
    if((isDefined(window.Ext)) || (isDefined(window.loadExtJs) && window.loadExtJs)){

    defineOnce(‘ExtJsLib’,Atlx.ExternalLibs,function(){
    ExtJsLib = new Atlx.GlobalClasses.LibObject(‘ExtJsLib’,1,false);
    ExtJsLib.isLoaded = function(){
    return isDefined(window.Ext);
    }
    ExtJsLib._onLoaded = function(){
    Ext.documentId = MOOTOOLS_DOCUMENT_ID_VALUE;
    document.id = Ext.documentId;
    window.Ext$ = Ext;
    console.log(‘Extjs Loaded Atlx.ExternalLibs.ExtJsLib …’);
    Atlx.GlobalClasses.LibObject.prototype._onLoaded.apply(this);
    };
    ExtJsLib.runUnder = function(func){
    if(this.isLoaded()){
    Ext.onReady(func);
    }
    else
    {
    this.que.push(func);
    console.log(‘ExtJS not loaded’);
    }
    };
    if(!isDefined(window.Ext)){
    MOOTOOLS_DOCUMENT_ID_VALUE = document.id;
    ExtJsLib.load = true;
    ExtJsLib.addFile({type:”css”,url:”………../ext-all.css”});
    if(debug)
    {
    ExtJsLib.addFile({type:”js”,url:”……………./ext-all-debug.js”});
    }
    else
    {
    ExtJsLib.addFile({type:”js”,url:”……………./ext-all.js”});
    }
    }
    else{
    ExtJsLib._onLoaded();
    }
    Atlx.addExternal(ExtJsLib);
    return ExtJsLib;
    });
    Atlx.ExternalLibs.ExtJsLib.runUnder(function(){
    //definitions using ExtJS
    //called as soon as ExtJS is available
    });
    }
    Atlx.init(function(){
    //code run after all libs/scripts loaded
    //main app code.

    Atlx.runUnder(‘jQueryLib’,function(){

    runIfField(‘#close_strategy_description’,function(field){
    field.on(‘click’,function(evt){
    evt.preventDefault();
    evt.stopPropagation();
    j$.fancybox.close();
    });

    });

    runIfField(“.reports-menu”,function(field){
    field.on(“change”,function(evt){
    var elm = j$(evt.target);
    var url = elm.val();
    if(url){
    Atlx.redirect(jumpURL+url);
    }
    });
    });

    });

    Atlx.runUnder(‘ExtJsLib’,function(){
    //code added to Ext.onReady list
    Ext.require([
    ‘Ext.chart.*’,
    ‘Ext.Window’,
    ‘Ext.fx.target.Sprite’,
    ‘Ext.layout.container.Fit’,
    ‘Ext.window.MessageBox’
    ]);

    runIfField(“.profile-table”,function(field){
    chartFieldId = ‘profile-table’;
    window.buildReport = function(){
    var grid = Ext.define(‘myGrid’, {});
    this.add(grid);
    }
    });

    runIfField(“.cbrm-chart”,function(field){
    chartFieldId = ‘cbrm-chart’;
    window.buildReport = function(){
    var grid = Ext.define(‘myGrid’, { });

    }
    });

    runIfField(“.extrep”,function(field){
    j$(‘.tab-box-inner’).css(‘height’,’100%’);
    Ext.QuickTips.init();
    Ext.application({
    name: ‘SomeExtApp’,
    launch: function() {
    Ext.create(‘Ext.container.Container’, {
    layout: {
    type: ‘vbox’,
    align: ‘stretch’
    },
    minHeight: 400,
    renderTo: chartFieldId,
    listeners:{
    boxready:function(){
    buildReport.call(this);
    }
    },
    items: []
    });
    }
    });
    });

    });
    });
    }

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.