Dynamically load a JavaScript file

Viewed 200229

How can you reliably and dynamically load a JavaScript file? This will can be used to implement a module or component that when 'initialized' the component will dynamically load all needed JavaScript library scripts on demand.

The client that uses the component isn't required to load all the library script files (and manually insert <script> tags into their web page) that implement this component - just the 'main' component script file.

How do mainstream JavaScript libraries accomplish this (Prototype, jQuery, etc)? Do these tools merge multiple JavaScript files into a single redistributable 'build' version of a script file? Or do they do any dynamic loading of ancillary 'library' scripts?

An addition to this question: is there a way to handle the event after a dynamically included JavaScript file is loaded? Prototype has document.observe for document-wide events. Example:

document.observe("dom:loaded", function() {
  // initially hide all containers for tab content
  $$('div.tabcontent').invoke('hide');
});

What are the available events for a script element?

30 Answers

You may create a script element dynamically, using Prototypes:

new Element("script", {src: "myBigCodeLibrary.js", type: "text/javascript"});

The problem here is that we do not know when the external script file is fully loaded.

We often want our dependant code on the very next line and like to write something like:

if (iNeedSomeMore) {
    Script.load("myBigCodeLibrary.js"); // includes code for myFancyMethod();
    myFancyMethod(); // cool, no need for callbacks!
}

There is a smart way to inject script dependencies without the need of callbacks. You simply have to pull the script via a synchronous AJAX request and eval the script on global level.

If you use Prototype the Script.load method looks like this:

var Script = {
    _loadedScripts: [],
    include: function(script) {
        // include script only once
        if (this._loadedScripts.include(script)) {
            return false;
        }
        // request file synchronous
        var code = new Ajax.Request(script, {
            asynchronous: false,
            method: "GET",
            evalJS: false,
            evalJSON: false
        }).transport.responseText;
        // eval code on global level
        if (Prototype.Browser.IE) {
            window.execScript(code);
        } else if (Prototype.Browser.WebKit) {
            $$("head").first().insert(Object.extend(
                new Element("script", {
                    type: "text/javascript"
                }), {
                    text: code
                }
            ));
        } else {
            window.eval(code);
        }
        // remember included script
        this._loadedScripts.push(script);
    }
};

I used a much less complicated version recently with jQuery:

<script src="scripts/jquery.js"></script>
<script>
  var js = ["scripts/jquery.dimensions.js", "scripts/shadedborder.js", "scripts/jqmodal.js", "scripts/main.js"];
  var $head = $("head");
  for (var i = 0; i < js.length; i++) {
    $head.append("<script src=\"" + js[i] + "\"></scr" + "ipt>");
  }
</script>

It worked great in every browser I tested it in: IE6/7, Firefox, Safari, Opera.

Update: jQuery-less version:

<script>
  var js = ["scripts/jquery.dimensions.js", "scripts/shadedborder.js", "scripts/jqmodal.js", "scripts/main.js"];
  for (var i = 0, l = js.length; i < l; i++) {
    document.getElementsByTagName("head")[0].innerHTML += ("<script src=\"" + js[i] + "\"></scr" + "ipt>");
  }
</script>

I did basically the same thing that you did Adam, but with a slight modification to make sure I was appending to the head element to get the job done. I simply created an include function (code below) to handle both script and CSS files.

This function also checks to make sure that the script or CSS file hasn't already been loaded dynamically. It does not check for hand coded values and there may have been a better way to do that, but it served the purpose.

function include( url, type ){
    // First make sure it hasn't been loaded by something else.
    if( Array.contains( includedFile, url ) )
        return;
     
    // Determine the MIME type.
    var jsExpr = new RegExp( "js$", "i" );
    var cssExpr = new RegExp( "css$", "i" );
    if( type == null )
        if( jsExpr.test( url ) )
            type = 'text/javascript';
        else if( cssExpr.test( url ) )
            type = 'text/css';
            
    // Create the appropriate element.
    var element = null;
    switch( type ){
        case 'text/javascript' :
            element = document.createElement( 'script' );
            element.type = type;
            element.src = url;
            break;
        case 'text/css' :
            element = document.createElement( 'link' );
            element.rel = 'stylesheet';
            element.type = type;
            element.href = url;
            break;
    }
    
    // Insert it to the <head> and the array to ensure it is not
    // loaded again.
    document.getElementsByTagName("head")[0].appendChild( element );
    Array.add( includedFile, url );
}

another awesome answer

$.getScript("my_lovely_script.js", function(){


   alert("Script loaded and executed.");
  // here you can use anything you defined in the loaded script

 });

https://stackoverflow.com/a/950146/671046

Here is some example code I've found... does anyone have a better way?

  function include(url)
  {
    var s = document.createElement("script");
    s.setAttribute("type", "text/javascript");
    s.setAttribute("src", url);
    var nodes = document.getElementsByTagName("*");
    var node = nodes[nodes.length -1].parentNode;
    node.appendChild(s);
  }

Dynamic module import landed in Firefox 67+.

(async () => {
   await import('./synth/BubbleSynth.js')
})()

With error handling:

(async () => {
    await import('./synth/BubbleSynth.js').catch((error) => console.log('Loading failed' + error))
})()

It also works for any kind of non-modules libraries, on this case the lib is available on the window.self object, the old way, but only on demand, which is nice.

Example using suncalc.js, the server must have CORS enabled to works this way!

(async () => {
 await import('https://cdnjs.cloudflare.com/ajax/libs/suncalc/1.8.0/suncalc.min.js')
 .then( () => {
   let times = SunCalc.getTimes(new Date(), 51.5,-0.1);
   console.log("Golden Hour today in London: " + times.goldenHour.getHours() + ':' + times.goldenHour.getMinutes() + ". Take your pics!")
  })
})()

https://caniuse.com/#feat=es6-module-dynamic-import

does anyone have a better way?

I think just adding the script to the body would be easier then adding it to the last node on the page. How about this:

function include(url) {
  var s = document.createElement("script");
  s.setAttribute("type", "text/javascript");
  s.setAttribute("src", url);
  document.body.appendChild(s);
}

i've used yet another solution i found on the net ... this one is under creativecommons and it checks if the source was included prior to calling the function ...

you can find the file here: include.js

/** include - including .js files from JS - bfults@gmail.com - 2005-02-09
 ** Code licensed under Creative Commons Attribution-ShareAlike License 
 ** http://creativecommons.org/licenses/by-sa/2.0/
 **/              
var hIncludes = null;
function include(sURI)
{   
  if (document.getElementsByTagName)
  {   
    if (!hIncludes)
    {
      hIncludes = {}; 
      var cScripts = document.getElementsByTagName("script");
      for (var i=0,len=cScripts.length; i < len; i++)
        if (cScripts[i].src) hIncludes[cScripts[i].src] = true;
    }
    if (!hIncludes[sURI])
    {
      var oNew = document.createElement("script");
      oNew.type = "text/javascript";
      oNew.src = sURI;
      hIncludes[sURI]=true;
      document.getElementsByTagName("head")[0].appendChild(oNew);
    }
  }   
} 

Just found out about a great feature in YUI 3 (at the time of writing available in preview release). You can easily insert dependencies to YUI libraries and to "external" modules (what you are looking for) without too much code: YUI Loader.

It also answers your second question regarding the function being called as soon as the external module is loaded.

Example:

YUI({
    modules: {
        'simple': {
            fullpath: "http://example.com/public/js/simple.js"
        },
        'complicated': {
            fullpath: "http://example.com/public/js/complicated.js"
            requires: ['simple']  // <-- dependency to 'simple' module
        }
    },
    timeout: 10000
}).use('complicated', function(Y, result) {
    // called as soon as 'complicated' is loaded
    if (!result.success) {
        // loading failed, or timeout
        handleError(result.msg);
    } else {
        // call a function that needs 'complicated'
        doSomethingComplicated(...);
    }
});

Worked perfectly for me and has the advantage of managing dependencies. Refer to the YUI documentation for an example with YUI 2 calendar.

There's a new proposed ECMA standard called dynamic import, recently incorporated into Chrome and Safari.

const moduleSpecifier = './dir/someModule.js';

import(moduleSpecifier)
   .then(someModule => someModule.foo()); // executes foo method in someModule

With Promises you can simplify it like this. Loader function:

  const loadCDN = src =>
    new Promise((resolve, reject) => {
      if (document.querySelector(`head > script[src="${src}"]`) !== null) return resolve()
      const script = document.createElement("script")
      script.src = src
      script.async = true
      document.head.appendChild(script)
      script.onload = resolve
      script.onerror = reject
    })

Usage (async/await):

await loadCDN("https://.../script.js")

Usage (Promise):

loadCDN("https://.../script.js").then(res => {}).catch(err => {})

NOTE: there was one similar solution but it doesn't check if the script is already loaded and loads the script each time. This one checks src property.

The technique we use at work is to request the javascript file using an AJAX request and then eval() the return. If you're using the prototype library, they support this functionality in their Ajax.Request call.

jquery resolved this for me with its .append() function - used this to load the complete jquery ui package

/*
 * FILENAME : project.library.js
 * USAGE    : loads any javascript library
 */
    var dirPath = "../js/";
    var library = ["functions.js","swfobject.js","jquery.jeditable.mini.js","jquery-ui-1.8.8.custom.min.js","ui/jquery.ui.core.min.js","ui/jquery.ui.widget.min.js","ui/jquery.ui.position.min.js","ui/jquery.ui.button.min.js","ui/jquery.ui.mouse.min.js","ui/jquery.ui.dialog.min.js","ui/jquery.effects.core.min.js","ui/jquery.effects.blind.min.js","ui/jquery.effects.fade.min.js","ui/jquery.effects.slide.min.js","ui/jquery.effects.transfer.min.js"];

    for(var script in library){
        $('head').append('<script type="text/javascript" src="' + dirPath + library[script] + '"></script>');
    }

To Use - in the head of your html/php/etc after you import jquery.js you would just include this one file like so to load in the entirety of your library appending it to the head...

<script type="text/javascript" src="project.library.js"></script>

Here a simple example for a function to load JS files. Relevant points:

  • you don't need jQuery, so you may use this initially to load also the jQuery.js file
  • it is async with callback
  • it ensures it loads only once, as it keeps an enclosure with the record of loaded urls, thus avoiding usage of network
  • contrary to jQuery $.ajax or $.getScript you can use nonces, solving thus issues with CSP unsafe-inline. Just use the property script.nonce
var getScriptOnce = function() {

    var scriptArray = []; //array of urls (closure)

    //function to defer loading of script
    return function (url, callback){
        //the array doesn't have such url
        if (scriptArray.indexOf(url) === -1){

            var script=document.createElement('script');
            script.src=url;
            var head=document.getElementsByTagName('head')[0],
                done=false;

            script.onload=script.onreadystatechange = function(){
                if ( !done && (!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete') ) {
                    done=true;
                    if (typeof callback === 'function') {
                        callback();
                    }
                    script.onload = script.onreadystatechange = null;
                    head.removeChild(script);

                    scriptArray.push(url);
                }
            };

            head.appendChild(script);
        }
    };
}();

Now you use it simply by

getScriptOnce("url_of_your_JS_file.js");

An absurd one-liner, for those who think that loading a js library shouldn't take more than one line of code :P

await new Promise((resolve, reject) => {let js = document.createElement("script"); js.src="mylibrary.js"; js.onload=resolve; js.onerror=reject; document.body.appendChild(js)});

Obviously if the script you want to import is a module, you can use the import(...) function.

For those of you, who love one-liners:

import('./myscript.js');

Chances are you might get an error, like:

Access to script at 'http://..../myscript.js' from origin 'http://127.0.0.1' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

In which case, you can fallback to:

fetch('myscript.js').then(r => r.text()).then(t => new Function(t)());

In as much as I love how handy the JQuery approach is, the JavaScript approach isn't that complicated but just require little tweaking to what you already use... Here is how I load JS dynamically(Only when needed), and wait for them to load before executing the script that depends on them.

JavaScript Approach

//Create a script element that will load
let dynamicScript = document.createElement('script');

//Set source to the script we need to load
dynamicScript.src = 'linkToNeededJsFile.js';

//Set onload to callback function that depends on this script or do inline as shown below
dynamicScript.onload = () => {

    //Code that depends on the loaded script should be here

};

//append the created script element to body element
document.body.append(dynamicScript);

There are other ways approach one could accomplish this with JS but, I prefer this as it's require the basic JS knowledge every dev can relate.

Not part of the answer but here is the JQuery version I prefer with projects that already include JQuery:

$.getScript('linkToNeededJsFile.js', () => {

    //Code that depends on the loaded script should be here

});

More on the JQuery option here

all the major javascript libraries like jscript, prototype, YUI have support for loading script files. For example, in YUI, after loading the core you can do the following to load the calendar control

var loader = new YAHOO.util.YUILoader({

    require: ['calendar'], // what components?

    base: '../../build/',//where do they live?

    //filter: "DEBUG",  //use debug versions (or apply some
                        //some other filter?

    //loadOptional: true, //load all optional dependencies?

    //onSuccess is the function that YUI Loader
    //should call when all components are successfully loaded.
    onSuccess: function() {
        //Once the YUI Calendar Control and dependencies are on
        //the page, we'll verify that our target container is 
        //available in the DOM and then instantiate a default
        //calendar into it:
        YAHOO.util.Event.onAvailable("calendar_container", function() {
            var myCal = new YAHOO.widget.Calendar("mycal_id", "calendar_container");
            myCal.render();
        })
     },

    // should a failure occur, the onFailure function will be executed
    onFailure: function(o) {
        alert("error: " + YAHOO.lang.dump(o));
    }

 });

// Calculate the dependency and insert the required scripts and css resources
// into the document
loader.insert();

I have tweaked some of the above post with working example. Here we can give css and js in same array also.

$(document).ready(function(){

if (Array.prototype.contains === undefined) {
Array.prototype.contains = function (obj) {
    var i = this.length;
    while (i--) { if (this[i] === obj) return true; }
    return false;
};
};

/* define object that will wrap our logic */
var jsScriptCssLoader = {

jsExpr : new RegExp( "js$", "i" ),
cssExpr : new RegExp( "css$", "i" ),
loadedFiles: [],

loadFile: function (cssJsFileArray) {
    var self = this;
    // remove duplicates with in array
    cssJsFileArray.filter((item,index)=>cssJsFileArray.indexOf(item)==index)
    var loadedFileArray = this.loadedFiles;
    $.each(cssJsFileArray, function( index, url ) {
            // if multiple arrays are loaded the check the uniqueness
            if (loadedFileArray.contains(url)) return;
            if( self.jsExpr.test( url ) ){
                $.get(url, function(data) {
                    self.addScript(data);
                });

            }else if( self.cssExpr.test( url ) ){
                $.get(url, function(data) {
                    self.addCss(data);
                });
            }

            self.loadedFiles.push(url);
    });

    // don't load twice accross different arrays

},
addScript: function (code) {
    var oNew = document.createElement("script");
    oNew.type = "text/javascript";
    oNew.textContent = code;
    document.getElementsByTagName("head")[0].appendChild(oNew);
},
addCss: function (code) {
    var oNew = document.createElement("style");
    oNew.textContent = code;
    document.getElementsByTagName("head")[0].appendChild(oNew);
}

};


//jsScriptCssLoader.loadFile(["css/1.css","css/2.css","css/3.css"]);
jsScriptCssLoader.loadFile(["js/common/1.js","js/2.js","js/common/file/fileReader.js"]);
});

This function uses memorization. And could be called many times with no conflicts of loading and running the same script twice. Also it's not resolving sooner than the script is actually loaded (like in @radulle answer).

const loadScript = function (src) {
    let cache = [];
    return function (src) {
        return cache[src] || (cache[src] = new Promise((resolve, reject) => {
            let s = document.createElement('script');
            s.defer = true;
            s.src = src;
            s.onload = resolve;
            s.onerror = reject;
            document.head.append(s);
        }));
    }
}();

Please notice the parentheses () after the function expression.

Parallel loading of scripts:

Promise.all([
    loadScript('/script1.js'),
    loadScript('/script2.js'),
    // ...
]).then(() => {
    // do something
})

You can use the same method for dynamic loading stylesheets.

Related