Javascript includes – yet another way of RPC-ing

July 20th, 2005. Tagged: JavaScript

Javascript files can be included and executed on the fly -- either when loading the page or in run-time. This means that HTTP requests are made without the use of XMLHttpRequest or iframes. This post provides a trail of my experiments with the inclusion of external javascript files.

The problem

I was wondering how feasible it is to call Javascripts only when you need them. Basically not to have a load of <script src="js.js">s at the top of the page, but only those that are actually needed, when they are needed. Also I wanted to figure out an elegant solution whereby you call only one JS and leave it to that script to include more scripts if needed.

First reflex

My first instinct was to try something with document.write and it worked just OK. I created two JS files 1.js and 2.js, then wrote a small function to document.write a script tag and, finally, called this function randomly so that one of the script files is executed randomly.

1.js and 2.js are very simple, each of them contains just an alert().

The function to include a script looks like this:

function include(script_filename) {
    document.write('<' + 'script');
    document.write(' language="javascript"');
    document.write(' type="text/javascript"');
    document.write(' src="' + script_filename + '">');
    document.write('</' + 'script' + '>');
}

Then the random include looks like:

var which_script = Math.floor(Math.random() * 2) + 1 + '.js';

include(which_script);

Here's the test in action.

Now do it 10 times

I wanted to test if I can do the above several times per page and also if I can include and execute for a second time a script that was already executed. Well, it is possible, here's the test. It loops ten times, randomly including 1.js and 2.js

The DOM way

Well, document.write() is not a preferred method for altering an HTML page. Since the page is an XML tree to begin with, we should be using the DOM methods to include a javascript on the fly. But will it work? Yes, it turns out. Here's what I tried:

function include_dom(script_filename) {
    var html_doc = document.getElementsByTagName('head').item(0);
    var js = document.createElement('script');
    js.setAttribute('language', 'javascript');
    js.setAttribute('type', 'text/javascript');
    js.setAttribute('src', script_filename);
    html_doc.appendChild(js);
    return false;
}

This basically creates a new element - a <script> and appends it as a child to the <head> node. Then again the script comes up with a random number - 1 or 2 and calls the function include_dom() as the page loads:

var which_script = Math.floor(Math.random() * 2) + 1 + '.js';

include_dom(which_script);

This worked just beautifully (test 3). Note that in Firefox and IE you can get around without actually having a <head> node to add the script node to, but Opera is more strict and will give you an error. (Thumbs up, Opera! To begin with, the browsers tolerating such quirks got us into that lapse-of-standards trouble we're in today. I've read somewhere that (wildly guessing here), more than 50% of the browsers' source code deals with not properly nested tags, unclosed tags, and so on and so on programmer's mistakes made out of pure laziness)

DOM way after page load

The next test waits for the page to load completely before adding a script child to the HTML node. This is done by:

function afterload(){
    var which_script = Math.floor(Math.random() * 2) + 1 + '.js';
    include_dom(which_script);

}
	
window.onload = afterload;

Here's the test.

Javascript include on user demand

Now a bit more of a real life example - the javascript is included when the user performs an action (click). Both the DOM and the document.write methods are tested, only to prove that the DOM is better suited for such tasks. When you document.write after the page is loaded, you basically write to a new page and this is not what the user expects.

Here's the test #5.

Include once

Here I just wanted to try how a PHP-like include will work. In PHP there's include() and include_once(). include_once() means that if the file was already included, it should not be included again. This in my example is done by a simple array that is a list of all included files. Included using include_once(), that is. To accomplish this effect I also coded a Javascript equivalent of the PHP's in_array().

var included_files = new Array();

function include_once(script_filename) {
    if (!in_array(script_filename, included_files)) {
        included_files[included_files.length] = script_filename;
        include_dom(script_filename);
    }
}


function in_array(needle, haystack) {
    for (var i = 0; i < haystack.length; i++) {
        if (haystack[i] == needle) {
            return true;
        }
    }
    return false;

}

Here's test #6. In this test the .js files are included only once, on the first click. After that, nothing happens.

Conclusion

And that's it, this is the way (the DOM one) to include javascripts on the fly. Using this technique you can have only one <script src="..." ...> tag in your HTML source. You include one Javascript and it takes care of it's dependent scripts (libraries) and it also can include other scripts conditionally. Let's say (for the sake of the example, not that you should do it) you have a script that works in Mozillas and another one that works in IE. So you write one new script that checks the browser and this new script (using the DOM inclusion technique from above) checks for the browser and includes only the required of the two other scripts.

Wait a second...

Then I thought about this for a while and I realized that including new scripts on the fly is in essence another way of making a remote call, yes, the ever so popular remote scripting. In addition to the two techniques used for making remote calls - XMLHttpRequest (or AJAX) and using iframes, with this javascript DOM inclusion described above you can also make HTTP requests without reloading the page. Not that it's a viable alternative to XMLHttpRequest (no POST-ing, HEAD-ing or anything, just a GET), but still... something interesting.

As a proof-of-concept I did a PHP script that comes up with a random number between 1 and 100 and then outputs the result using a javascript alert(). My intent was to call this script as a javascript and do it several times par one page load. So I added some extra headers to

  1. Send a correct text/javascript header. Not that it's needed but still a good practice, FireFox is already picky when it comes to text/css for example;
  2. Prevent the php-javascript from being cached. OK, the cache prevention gets a little carried away (actually these headers are taken from php.net's manual), but ... you never know. The only needed header is:
    header("Cache-Control: no-store, no-cache, must-revalidate");
    It's needed by IE anyway. FireFox didn't cache the included script even with no cache-prevention headers and Opera cached even with all of them. For Opera I came up with a random junk as part of the GET request when a script name when it's called, just to make the script name unique.

So the PHP scripts is:

<?php
// javascript header
header('Content-type: text/javascript');
// Date in the past
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
// always modified
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
// HTTP/1.1
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);

echo 'alert(\'' . rand(1,100) . '\')';
?>

The JavaScript that calls this PHP script on the fly is using the same include_dom() function, only it adds this random key/value pair at the end of the script call url.

function include_rpc(script_filename) {
    script_filename += '?' + Math.random(0, 1000) + '=' + Math.random(0, 1000);
    var html_doc = document.getElementsByTagName('head').item(0);
    var js = document.createElement('script');
    js.setAttribute('language', 'javascript');
    js.setAttribute('type', 'text/javascript');
    js.setAttribute('src', script_filename);
    html_doc.appendChild(js);
    return false;

}

And so this proof-of-concept worked of FireFox (of course), IE (well, enything worth mentioning has to work on that browser) and Opera. There is a demo here. Every click on that list item that says "click" includes a php-generated Javascript on the fly which alerts a random number. So it behaves like a true RPC thingie - new files are HTTP-requested without the page being reloaded!

Like I said I don't see that as being a full-scale alternative to XMLHttpRequest, because it makes GET requests only and also it always returns javascript. What is being called using these javascript includes is a javascript, so any server-side script that needs to be called, has to return the results as javascript. Well, this can actually be handy sometimes. Using XMLHTTP you get XML which contains data you need and you have to access that data using DOM methods and technically translate the XML to javascript variable(s). In the JS include method above, the data returned is already a Javascript - it can be a JS function that does something on the document or it can be a JS variable (might as well be an XMLDoc JS variable). So because it doesn't necessarily return XML (although it can), this method has only AJA from AJAX -- the "asynchronous" and the "javascript" ;)

Resources

After I finished this posting I figured out I can search for similar articles on the web. Here are two that I found, created back in year 2002. It's again surprising how old the remote scripting techniques are (3 years is almost forever on the web), only that before Gmail most of the developers were ignoring them.

2006-10-23 update: In IE, I found a way to tell when the new script is done loading - described here.

2006-10-25 update: The cross-browser way to tell when a script is loaded is here.

Tell your friends about this post: Facebook, Twitter, Google+

Sorry, comments disabled and hidden due to excessive spam. Working on restoring the existing comments...

Meanwhile, hit me up on twitter @stoyanstefanov