Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Challenge-Me: JavaScript Including Engine - Part II

5.00/5 (23 votes)
10 Jul 2008CPOL4 min read 1   256  
How to include JavaScript files in other JavaScript files.

Foreword

This is the second part of a three part article and for better understanding, I recommend reading the first part before reading this part.

Part II

From the Previous Part

The Challenge

To create an engine that will implement JavaScript’s missing functionality for including JavaScript files in other JavaScript files.

The Solution

In the first part, I presented a solution that can be easily deduced and implemented by everyone who has some knowledge about dynamic HTML and the way an external JavaScript file is loaded and executed. But it has a serious drawback: all files should contain only function declarations and object declarations (but only if they're not related to objects from other files).

Therefore, in this part, we’ll see a totally different approach.

Solution 2

The Logic

Let’s rethink the solution.

Ideally, only the part with the $include function of the file should be executed – we will name this part header, and then, when the time is right, the rest – body. But as I stated above, it cannot be done. So if we talk about header and body, then we have a logical split of the file, and where there is a logical split, can’t there be a physical one? A header file (with a jsh extension) and a source file (the normal js file)? Something like in C?

Let’s examine the implications of this approach.

We first load all the headers, and from them, we can determine the order in which the source files must be loaded. Perfect. Just, what we need. From the previous implementation, we’ll keep the code that lets us specify a path relative to the IncludingEngine.js file and not to the current HTML document.

I did the implementation and another problem appeared: the JavaScript code embedded in the HTML document gets executed before finishing the execution of all external files added dynamically to the document’s head.

I did some research – reading and testing. While there are some people who argue that external JavaScript files are loaded in separate threads, and because of that, you can never be sure of the loading order and the execution order, I cannot agree with the last part.

From my tests, it seems indeed that the browsers (especially Firefox, not 100% sure about Internet Explorer) loads the scripts in different threads; but if a script ends loading before a previously defined script, its execution is delayed until the previous script ends its loading and execution, and after that is executed; this way, all scripts are executed in the right order.

Unfortunately, this order of execution is messed up if you dynamically insert or remove script objects. Now we’re in a situation of not knowing the order of execution, but we can avoid this by loading a script only after the previous one was loaded and executed.

However, the embedded JavaScript code will be executed long before the execution of external scripts (most of the time), but the other way around is a possibility too.

So our code from the HTML document can use the code from the external script only after these are all executed.

So we must have an event like window.onload to tell us when the external files have finished their execution. Since there is the possibility for external scripts to be executed first, an event to handle both situations would be even better. And more intuitive would be a function that will be executed when “everything is ready”. And for obvious reasons, we will call this function Main.

The Implementation

JavaScript
var IncludingEngine = {};

IncludingEngine.FindPath = function()
{
    var scripts = document.getElementsByTagName("script");
    var foundNo = 0;
    
    for (var i=0; i<scripts.length; i++)
    {
        if (scripts[i].src.match(this.FileName))
        { 
            this.Path = scripts[i].src.replace(this.FileName, "");
            foundNo ++;
        }
    }
    
    if (foundNo == 0)
        throw new Error("The name of this file isn't " + 
                   this.FileName + "!\r\nPlease change it back!");
    if (foundNo > 1)
        throw new Error("There are " + foundNo + " files with the name " + 
                   this.FileName + "!\r\nThere can be only one!");
}    

IncludingEngine.Init = function ()
{

    this.FileName = "IncludingEngine.js";
    this.Path = "";//the root path of the engine; 
                   //all files included by this engine will be relative to this path
    this.FilesLoaded = new Array(); //will store the files already loaded 
                    //in order not to load (execute) a file many times

    //status files 
    //0 - just identified
    //1 - loading
    //2 - loaded
    this.HeaderFilesStatus = new Array();//hashtable with the status of the header files
    this.SourceFileStatus = new Array();//hashtable with the status of the source files
    this.SourceFiles = new Array();//array with the source files 
               //(used to create the head tag with them in the reverse order)
    
    this.FindPath();
}

//Loads the header files (.jsh)
IncludingEngine.HeaderScriptLoad = function()
{
    if (this.readyState)
        if (this.readyState != "complete") 
            return;

    IncludingEngine.HeaderFilesStatus[this.src] = 2;
    var done = true;
    for(var el in IncludingEngine.HeaderFilesStatus)
        if (IncludingEngine.HeaderFilesStatus[el] != 2)
            done = false;
    if (done)
    {
     var head = document.getElementsByTagName("head")[0];

        for (var k = IncludingEngine.SourceFiles.length - 1; k >= 0; k--)
        {
            if (IncludingEngine.SourceFileStatus[IncludingEngine.SourceFiles[k]] == 0)
            {
                var script = document.createElement("script");
                script.src = IncludingEngine.SourceFiles[k];
                script.type = "text/javascript";
                script.onload = script.onreadystatechange = 
                                IncludingEngine.SourceScriptLoad;
                IncludingEngine.SourceFileStatus[IncludingEngine.SourceFiles[k]] = 1
                head.appendChild(script);
            }
        }
    }
     var head = document.getElementsByTagName("head")[0];
     head.removeChild(this);
}

//Loads the source files (.js)
IncludingEngine.SourceScriptLoad = function()
{
    if (this.readyState)
        if (this.readyState != "complete") 
            return;

    IncludingEngine.SourceFileStatus[this.src] = 2;
    var done = true;
    for(var el in IncludingEngine.SourceFileStatus)
        if (IncludingEngine.SourceFileStatus[el] != 2)
            done = false;
    if (done)
    {
        if (Main)
        {
            if (typeof Main == "function")
                Main();
        }
    }
}

function $include()
{
    var head = document.getElementsByTagName('head')[0];

    for (var i = 0; i < arguments.length; i++)
    {
        if (IncludingEngine.HeaderFilesStatus[arguments[i] + ".jsh"] == null)
        {
            var script = document.createElement("script");
            script.src = IncludingEngine.Path + arguments[i] + ".jsh";
            script.type = 'text/javascript';
            script.onload = script.onreadystatechange = IncludingEngine.HeaderScriptLoad;

            IncludingEngine.SourceFiles.push(IncludingEngine.Path + arguments[i] + ".js");
            IncludingEngine.SourceFileStatus[IncludingEngine.Path + arguments[i] + ".js"] = 0;
            IncludingEngine.HeaderFilesStatus[script.src] = 1;
            head.appendChild(script);
        }    
    }
}

IncludingEngine.Init();

I think that the implementation is pretty much self-explanatory. Init is mostly like in the first solution and FindPath is identical. HeaderScriptLoad is for loading the headers and generating the proper order to load the source scripts, and SourceScriptLoad is for loading the source files in the order generated earlier.

As you can observe, I didn't check for circular dependencies; this is implemented in the next part.

Notes

  • I’ll edit this post to add the link to the last part as soon as I post it.
  • The latest version of this implementation can be found at IncludingEngine.jsFramework.com.

Here are all the parts of the "JavaScript Including Engine" article series: Part I, Part II, Part III. Also, you may want to visit the IncludingEngine website for more information.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)