Dynamically loading JavaScript synchronously
Asked Answered
S

18

70

I'm using the module pattern, one of the things I want to do is dynamically include an external JavaScript file, execute the file, and then use the functions/variables in the file in the return { } of my module.

I can't figure out how to do this easily. Are there any standard ways of performing a pseudo synchronous external script load?

function myModule() {
    var tag = document.createElement("script");
    tag.type = "text/javascript";
    tag.src = "http://some/script.js";
    document.getElementsByTagName('head')[0].appendChild(tag);

    //something should go here to ensure file is loaded before return is executed

    return {
        external: externalVariable 
    }
}
Svelte answered 21/5, 2010 at 4:10 Comment(7)
So you want to interrupt client operation completely while you load a file from a remote server?Mistrust
Not necessarily, I just want to know how to not return on myModule until a resource is available... or how to ensure some external variable is available before my module is executed.Svelte
RE: Pointy's comment about interrupting client operation while loading a file from a remote server, ... isn't this how things work already in certain browsers, when you have <script> tags one after the other in the head of the document?Paramaribo
@Mistrust Regarding your snarky comment: my own use case for this currently is client-side search from file:///. I don't want to load megabytes of JSON until I know that the user needs the 5-word phrase file, but once I know that it's needed I need to load it ASAP and need to wait until it is loaded before I can perform the search. So: yes, I want to interrupt client operation completely while the code necessary to complete the search results happens. :pCathodoluminescence
@Cathodoluminescence Why do you need to freeze up the UI during that time? (Note that I'm not 100% sure what exactly the "load" operation we're talking about here is, so maybe it doesn't freeze the UI.)Mistrust
@Mistrust Well, I don't need the UI to freeze, that's true. However, the use case is that the user is typing/has typed in a search term and I need to find the results, and I need to load additional JS files to do so. There is essentially no latency from the disk, and the browser is going to stutter for a second while the megabytes of JS are loaded anyhow, and the moment they are loaded I need to use them..so synchronous (if possible) would have been "good enough" for my case, and a hair simpler than the solution I came up with below.Cathodoluminescence
Possible duplicate of inject a script tag with remote src and wait for it to executeAnatto
T
58

There is only one way to synchronously load and execute a script resource, and that is using a synchronous XHR

This is an example of how to do this

// get some kind of XMLHttpRequest
var xhrObj = createXMLHTTPObject();
// open and send a synchronous request
xhrObj.open('GET', "script.js", false);
xhrObj.send('');
// add the returned content to a newly created script tag
var se = document.createElement('script');
se.type = "text/javascript";
se.text = xhrObj.responseText;
document.getElementsByTagName('head')[0].appendChild(se);

But you shouldn't in general use synchronous requests as this will block everything else. But that being said, there are of course scenarios where this is appropriate.

I would probably refactor the containing function into an asynchronous pattern though using an onload handler.

Twibill answered 21/5, 2010 at 7:2 Comment(11)
I ended up refactoring so that onload/onreadystate change would work. But this is the correct answer to my original question.Svelte
Note that this will not work for the (admittedly edge case) of trying to load JS synchronously from file:// on browsers like Chrome where XHR over file:// is prohibited.Cathodoluminescence
Looks like this fails cross-domain. Is there a way to do dynamic synch injection of 3rd party scripts? Tia.Alegar
@StevenFrancolla Nope, there's not. If the browser and server supports it, you can use CORS.Twibill
@SeanKinsey Thanks. I ended up mixing inline scripts with src imports, like doc.writeln("<script src='www.somewhere.com/script1.js'></script>"); doc.writeln("<script src='www.somewhere.com/script2.js'></script>"); doc.writeln("<script>myReliantBoilerplate();</script>");Alegar
Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help xhr.spec.whatwg.orgGavin
document.write is asyncronous in IERubirubia
@Rubirubia what makes you say that? Any script tags written to the document will be executed before the code continues evaluating, are you seeing different behavior?Twibill
createXMLHTTPObject is not definedSilverweed
@tomermes, it's up to you to decide on how to do the async load - this is just example code.Twibill
This does not wait for the script to be executed synchronously.Anatto
O
56

The accepted answer is NOT correct.

Loading a file synchronously is not the same as executing the file synchronously - which is what the OP requested.

The accepted answer loads the file sync, but does nothing more than append a script tag to the DOM. Just because appendChild() has returned does not in anyway guarantee that the script has finished executing and it's members are initialised for use.

The only (see caveat) way to achieve the OPs question is to sync load the script over XHR as stated, then read as text and pass into either eval() or a new Function() call and wait for that function to return. This is the only way to guarantee the script is loaded AND executed synchronously.

I make no comment as to whether this is a wise thing to do either from a UI or security perspective, but there are certainly use cases that justify a sync load & execute.

Caveat: Unless you're using web workers in which case just call loadScripts();

Obstinacy answered 12/2, 2014 at 14:36 Comment(0)
I
10

This is the code that I'm using for multiple file load in my app.

Utilities.require = function (file, callback) {
    callback = callback ||
    function () {};
    var filenode;
    var jsfile_extension = /(.js)$/i;
    var cssfile_extension = /(.css)$/i;

    if (jsfile_extension.test(file)) {
        filenode = document.createElement('script');
        filenode.src = file;
        // IE
        filenode.onreadystatechange = function () {
            if (filenode.readyState === 'loaded' || filenode.readyState === 'complete') {
                filenode.onreadystatechange = null;
                callback();
            }
        };
        // others
        filenode.onload = function () {
            callback();
        };
        document.head.appendChild(filenode);
    } else if (cssfile_extension.test(file)) {
        filenode = document.createElement('link');
        filenode.rel = 'stylesheet';
        filenode.type = 'text/css';
        filenode.href = file;
        document.head.appendChild(filenode);
        callback();
    } else {
        console.log("Unknown file type to load.")
    }
};

Utilities.requireFiles = function () {
    var index = 0;
    return function (files, callback) {
        index += 1;
        Utilities.require(files[index - 1], callBackCounter);

        function callBackCounter() {
            if (index === files.length) {
                index = 0;
                callback();
            } else {
                Utilities.requireFiles(files, callback);
            }
        };
    };
}();

And this utilities can be used by

Utilities.requireFiles(["url1", "url2",....], function(){
    //Call the init function in the loaded file.
    })
Indoaryan answered 19/6, 2012 at 10:42 Comment(4)
Does that work for nested items aswell? Say if you require more files inside the files you require?Bravin
This is async, not syncronues!Gale
the callback function is called upon the specified scripts are loaded. Isn't it good enough?Dorthadorthea
async is not sync. does not answer OP.Squalor
R
6

The most Node.js-like implementation I could come up with was able to load JS files synchonously, and use them as objects/modules

var scriptCache = [];
var paths = [];
function Import(path)
{
    var index = 0;
    if((index = paths.indexOf(path)) != -1) //If we already imported this module
    {
        return scriptCache [index];
    }

    var request, script, source;
    var fullPath = window.location.protocol + '//' + window.location.host + '/' + path;

    request = new XMLHttpRequest();
    request.open('GET', fullPath, false);
    request.send();

    source = request.responseText;

    var module = (function concealedEval() {
        eval(source);
        return exports;
    })();

    scriptCache.push(module);
    paths.push(path);

    return module;
}

An example source (addobjects.js):

function AddTwoObjects(a, b)
{
    return a + b;
}

this.exports = AddTwoObjects;

And use it like this:

var AddTwoObjects = Import('addobjects.js');
alert(AddTwoObjects(3, 4)); //7
//or even like this:
alert(Import('addobjects.js')(3, 4)); //7
Ripp answered 8/8, 2016 at 14:36 Comment(0)
O
4

the accepted answer is not correct:

the script.async = false; directive only means that html parsing will be paused during script execution. this does not guarantee in which order javascript code will run. see https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/loading-third-party-javascript/

the easiest and most elegant solution which was yet to be mentioned here is using promises, like so:

    function loadScript(url) {
      return new Promise((resolve, reject) => {
        var script = document.createElement('script')
        script.src = url
        script.onload = () => {
          resolve()
        }
        script.onerror = () => {
          reject('cannot load script '+ url)
        }
        document.body.appendChild(script)
      })
    }

and then when you want to execute scripts in order:

        loadScript('myfirstscript.js').then(() => {
          console.log('first script ran');
          loadScript('index.js').then(() => {
            console.log('second script ran');
          })
        })
Octahedron answered 20/9, 2018 at 8:53 Comment(0)
G
3

I had the following problem(s) with the existing answers to this question (and variations of this question on other stackoverflow threads):

  • None of the loaded code was debuggable
  • Many of the solutions required callbacks to know when loading was finished instead of truly blocking, meaning I would get execution errors from immediately calling loaded (ie loading) code.

Or, slightly more accurately:

  • None of the loaded code was debuggable (except from the HTML script tag block, if and only if the solution added a script elements to the dom, and never ever as individual viewable scripts.) => Given how many scripts I have to load (and debug), this was unacceptable.
  • Solutions using 'onreadystatechange' or 'onload' events failed to block, which was a big problem since the code originally loaded dynamic scripts synchronously using 'require([filename, 'dojo/domReady']);' and I was stripping out dojo.

My final solution, which loads the script before returning, AND has all scripts properly accessible in the debugger (for Chrome at least) is as follows:

WARNING: The following code should PROBABLY be used only in 'development' mode. (For 'release' mode I recommend prepackaging and minification WITHOUT dynamic script loading, or at least without eval).

//Code User TODO: you must create and set your own 'noEval' variable

require = function require(inFileName)
{
    var aRequest
        ,aScript
        ,aScriptSource
        ;

    //setup the full relative filename
    inFileName = 
        window.location.protocol + '//'
        + window.location.host + '/'
        + inFileName;

    //synchronously get the code
    aRequest = new XMLHttpRequest();
    aRequest.open('GET', inFileName, false);
    aRequest.send();

    //set the returned script text while adding special comment to auto include in debugger source listing:
    aScriptSource = aRequest.responseText + '\n////# sourceURL=' + inFileName + '\n';

    if(noEval)//<== **TODO: Provide + set condition variable yourself!!!!**
    {
        //create a dom element to hold the code
        aScript = document.createElement('script');
        aScript.type = 'text/javascript';

        //set the script tag text, including the debugger id at the end!!
        aScript.text = aScriptSource;

        //append the code to the dom
        document.getElementsByTagName('body')[0].appendChild(aScript);
    }
    else
    {
        eval(aScriptSource);
    }
};
Gilleod answered 15/2, 2014 at 8:34 Comment(0)
H
3
var xhrObj = new XMLHttpRequest();
xhrObj.open('GET', '/filename.js', false);
xhrObj.send(null);
eval(xhrObj.responseText);

If this is a cross-domain request, it will not work. In that case you have to upload the requested file to your server, or make a mirror php that outputs it, and require that php.

With jquery (works with cross-domain request too):

$.getScript('/filename.js',callbackFunction);

callbackFunction will be called synchronously.

For loading more scripts see this thread.

Hypogynous answered 16/7, 2014 at 10:7 Comment(0)
A
3

There actually is a way to load a list of scripts and execute them synchronously. You need to insert each script tag into the DOM, explicitly setting its async attribute to false:

script.async = false;

Scripts that have been injected into the DOM are executed asynchronously by default, so you have to set the async attribute to false manually to work around this.

Example

<script>
(function() {
  var scriptNames = [
    "https://code.jquery.com/jquery.min.js",
    "example.js"
  ];
  for (var i = 0; i < scriptNames.length; i++) {
    var script = document.createElement('script');
    script.src = scriptNames[i];
    script.async = false; // This is required for synchronous execution
    document.head.appendChild(script);
  }
  // jquery.min.js and example.js will be run in order and synchronously
})();
</script>

<!-- Gotcha: these two script tags may still be run before `jquery.min.js`
     and `example.js` -->
<script src="example2.js"></script>
<script>/* ... */<script>

References

Anatto answered 11/7, 2017 at 10:51 Comment(1)
Note - still executed asynchronously if called from within a new constructor. Otherwise - good find.Crabb
C
1

If you need to load an arbitrary number of scripts and only proceed when the last one is done, and you cannot use XHR (e.g. due to CORS limitations) you can do the following. It is not synchronous, but does allow a callback to occur exactly when the last file is done loading:

// Load <script> elements for all uris
// Invoke the whenDone callback function after the last URI has loaded
function loadScripts(uris,whenDone){
  if (!uris.length) whenDone && whenDone();
  else{
    for (var wait=[],i=uris.length;i--;){
      var tag  = document.createElement('script');
      tag.type = 'text/javascript';
      tag.src  = uris[i];
      if (whenDone){
        wait.push(tag)
        tag.onload = maybeDone; 
        tag.onreadystatechange = maybeDone; // For IE8-
      }
      document.body.appendChild(tag);
    }
  }
  function maybeDone(){
    if (this.readyState===undefined || this.readyState==='complete'){
      // Pull the tags out based on the actual element in case IE ever
      // intermingles the onload and onreadystatechange handlers for the same
      // script block before notifying for another one.
      for (var i=wait.length;i--;) if (wait[i]==this) wait.splice(i,1);
      if (!wait.length) whenDone();
    }
  }
}

Edit: Updated to work with IE7, IE8, and IE9 (in quirks mode). These IE versions do not fire an onload event, but do for onreadystatechange. IE9 in standards mode fires both (with onreadystatechange for all scripts firing before onload for any).

Based on this page there may be a small chance that old versions of IE will never send an onreadystatechange event with readyState=='complete'; if this is the case (I could not reproduce this problem) then the above script will fail and your callback will never be invoked.

Cathodoluminescence answered 20/6, 2012 at 16:12 Comment(4)
I think there may be some per-browser issues with getting the "load" event reliably (or at all), based on various grumblings I've seen from script loader authors.Mistrust
@Mistrust Ooh, that's good to know; thanks. I'll have to look into that more.Cathodoluminescence
I'm not very familiar with the issues; I've been looking through the source of LABjs and mostly it's just making me confused. (I picked that one because the author is an acquaintance.)Mistrust
@Mistrust Thanks to your note I found this page which describes the need to use onreadystatechange for IE. In testing, IE9 in standards mode does not require this, but older versions do. I've updated my answer with code that works in the current versions of all major browsers, and also IE7 and IE8.Cathodoluminescence
V
0

You can't and shouldn't perform server operations synchronously for obvious reasons. What you can do, though, is to have an event handler telling you when the script is loaded:

tag.onreadystatechange = function() { if (this.readyState == 'complete' || this.readyState == 'loaded') this.onload({ target: this }); };

tag.onload = function(load) {/*init code here*/}

onreadystatechange delegation is, from memory, a workaround for IE, which has patchy support for onload.

Voracity answered 21/5, 2010 at 4:20 Comment(1)
Saying 'You can't' is directly wrong as shown by my answer ;)Twibill
T
0

same as Sean's answer, but instead of creating a script tag, just evaluate it. this ensures that the code is actually ready to use.

Tish answered 21/5, 2012 at 19:23 Comment(1)
Please elaborate. eval() what? The text source of an external script?Kyanize
C
0

My strategy, classic example when load jQuery UI, i hope this can help you

( function( tools, libs ){
	
    // Iterator
    var require = function( scripts, onEnd ){
        
        onEnd = onEnd || function(){};
        
        if( !scripts || scripts.length < 1 )return onEnd();
        
        var src    = scripts.splice( 0, 1),
            script = document.createElement( "script" );
        
        script.setAttribute( "src", src );
        
        tools.addEvent( "load", script, function(){
            
            require( scripts, onEnd );
            
        } );
        
        document.getElementsByTagName( "head" )[ 0 ].appendChild( script );
        
    };
    
    // Install all scripts with a copy of scripts
    require( libs.slice(), function(){
    
        alert( "Enjoy :)" );
    
    } );
    
    // Timeout information
    var ti = setTimeout( function(){
        
        if( !window.jQuery || !window.jQuery.ui )alert( "Timeout !" );
        
        clearTimeout( ti );
        
    }, 5000 );

} )(

    { // Tools
    
        addEvent : function( evnt, elem, func ){
        
            try{

                if( elem.addEventListener ){

                    elem.addEventListener( evnt, func, false );

                }else if( elem.attachEvent ){

                     var r = elem.attachEvent( "on" + evnt, func );

                }

                return true;

            }catch( e ){

                return false;

            }		    

        }
    
    },
    [ // Scripts
    
        "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-alpha1/jquery.min.js",
        "https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"
        
    ]

);
Cabezon answered 26/8, 2015 at 11:1 Comment(0)
C
0

When using Angular you can take advantage of the fact that every Provider is instantiated before other services are instantiated. You can combine this fact with using xhr and the eval() as mentioned by @Neil. The code would be following:

app.provider('SomeScriptSyncLoader', function() {

    var resourceUrl =  'http://some/script.js';
    var dummy = {};

    this.$get = function() {

        var q = jQuery.ajax({
            type: 'GET', url: resourceUrl, cache: false, async: false
        });

        if (q.status === 200) {
            eval(q.responseText); // execute some script synchronously as inline script - eval forces sync processing
        }
        return dummy;
    };
});

To force the Provider to be inialized you need to inject it in at least one other directive/service. Preferably this would be the service which takes advantage of the code loaded by script.

app.directive('myDirective', ['SomeScriptSyncLoader', function(someScriptSyncLoader) {

return {
    restrict: 'E',
    link: function(scope, element, attrs) {
        // some ode
    },
    template: "this is my template"
   };
}]);
Christabella answered 26/1, 2016 at 12:36 Comment(0)
B
0

I know this is an old question, but maybe someone else read this and find it useful ! Just created a new components uses ES6 to load scripts dynamically in synchronous way. The Project details and source code are on GitHub https://github.com/amgadfahmi/scripty

Brakpan answered 20/10, 2016 at 14:49 Comment(0)
W
0

I may be late to answering this question.

My current solution is to recursively add <script> tags such that the addition of the subsequent script is in the callback of its predecessor. It assumes that each function contains one function and that function is the same as the file name (minus the extension). This probably isn't the best way to do things, but it works ok.

Code to consider

Code directory structure:

- directory
---- index.html
---- bundle.js
---- test_module/
-------- a.js
-------- b.js
-------- log_num.js
-------- many_parameters.js

index.html

<head>
  <script src="bundle.js"></script>
</head>

bundle.js

// Give JS arrays the .empty() function prototype
if (!Array.prototype.empty){
    Array.prototype.empty = function(){
        return this.length == 0;
    };
};

function bundle(module_object, list_of_files, directory="") {
  if (!list_of_files.empty()) {
    var current_file = list_of_files.pop()
    var [function_name, extension] = current_file.split(".")
    var new_script = document.createElement("script")
    document.head.appendChild(new_script)

    new_script.src = directory + current_file

    new_script.onload = function() {
      module_object[function_name] = eval(function_name)
      bundle(module_object, list_of_files, directory)
      /*
      nullify the function in the global namespace as - assumed -  last
      reference to this function garbage collection will remove it. Thus modules
      assembled by this function - bundle(obj, files, dir) - must be called
      FIRST, else one risks overwritting a funciton in the global namespace and
      then deleting it
      */
      eval(function_name + "= undefined")
    }
  }
}

var test_module = {}
bundle(test_module, ["a.js", "b.js", "log_num.js", "many_parameters.js"], "test_module/")

a.js

function a() {
  console.log("a")
}

b.js

function b() {
  console.log("b")
}

log_num.js

// it works with parameters too
function log_num(num) {
  console.log(num)
}

many_parameters.js

function many_parameters(a, b, c) {
  var calc = a - b * c
  console.log(calc)
}
Woolpack answered 9/5, 2017 at 18:32 Comment(0)
H
0

here is my code

var loaded_script = [];
function loadScript(urls, callback, sync) {
    var len = urls.length, count = 0;

    // check are all js loaded, then execute callback (if any)
    var check = function() {
        if (count == len) {
            callback && typeof callback=="function" && callback();
        }
    };

    for (var i = 0; i < len; i++) {
        var url = urls[i];

        // check if script not loaded (prevent load again)
        if (loaded_script.indexOf(url) == -1) {
            var script = document.createElement("script");
            script.type = "text/javascript";

            // set sync loading here (default is async)
            if (sync) {
                script.async = false;
            }

            // script onload event
            if (script.readyState) {    // IE
                script.onreadystatechange = function() {
                    if (script.readyState=="loaded" || script.readyState=="complete") {
                        script.onreadystatechange = null;
                        count++, check();
                    }
                };
            } else {    // Others
                script.onload = function() {
                    count++, check();
                };
            }

            // add script to head tag
            script.src = url;
            document.getElementsByTagName("head")[0].appendChild(script);

            // mark this script has loaded
            loaded_script.push(url);
        } else {
            count++, check();
        }
    }
}

I use this on pjax site.

loadScript(
    [
        "js/first.js",
        "js/second.js",
    ],
    function() {
        alert("Scripts loaded.");
    },
    true
);
Herder answered 20/2, 2019 at 4:17 Comment(0)
I
0

I've had a similar task a few days earlier, and here's how I did it.
This loader works both in file:// prefixes as well as in http:// and https://, and is cross-browser compatible.
It however, cannot load specific classes or functions as modules from scripts; it will load the whole script altogether and make it available to the DOM.

// Loads a script or an array of scripts (including stylesheets)
// in their respective index order, synchronously.
// By Sayanjyoti Das @https://stackoverflow.com/users/7189950/sayanjyoti-das
var Loader={
    queue: [], // Scripts queued to be loaded synchronously
    loadJsCss: function(src, onl) {
        var ext=src.toLowerCase().substring(src.length-3, src.length);
        if(ext=='.js') {
            var scrNode=el('script', null, null, null);
            scrNode.type='text/javascript';
            scrNode.onload=function() {onl();};
            scrNode.src=src;
            document.body.appendChild(scrNode);
        }else if(ext=='css') {
            var cssNode=el('link', null, null, null);
            cssNode.rel='stylesheet';
            cssNode.type='text/css';
            cssNode.href=src;
            document.head.appendChild(cssNode);
            onl();
        }
    },
    add: function(data) {
        var ltype=(typeof data.src).toLowerCase();

        // Load a single script
        if(ltype=='string') {
            data.src=data.src;
            Loader.queue.splice(0, 1, data, Loader.queue[0]);
            Loader.next();
        }
        // Load an array of scripts
        else if(ltype=='object') {
            for(var i=data.src.length-1; i>=0; i--) {
                Loader.queue.splice(0, 1, {
                    src: data.src[i],
                    onload: function() {
                        if(Loader.next()==false) {
                            data.onload();
                            return;
                        }
                        Loader.next();
                    }
                }, Loader.queue[0]);
            }
            Loader.next();
        }
    },
    next: function() {
        if(Loader.queue.length!=0 && Loader.queue[0]) {
            var scr=Loader.queue[0];

            // Remove the script from the queue
            if(Loader.queue.length>1)
                Loader.queue.splice(0, 2, Loader.queue[1]);
            else
                Loader.queue=[];

            // Load the script
            Loader.loadJsCss(scr.src, scr.onload);
        }else return false;
    }
};

The above function is very powerful and elegant; it allows you to load a single script or an array of script synchronously (i.e, next script not loaded until previous script loading finished). Moreover, a loaded script may load more scripts, which defers the queue in the parent script.

BTW, a script here means a JavaScript file or a CSS stylesheet.

Here's how to use it:-

// Load a single script
Loader.add({
    src: 'test.js',
    onload: function() {
        alert('yay!');
    }
});

// Load multiple scripts
Loader.add({
    src: ['test1.js', 'test2.js', 'mystyles.css', 'test3.js'],
    onload: function() {
        alert('all loaded!');
    }
});

Note that, the onload function in the Loader arguments is called when all of the scripts have loaded, not when one or a single script is loaded.

You can also load more scripts in the scripts you loaded, such as in test.js, test1.js, etc. By doing this, you will defer the load of the next parent script and the queue in the child script will be prioritized.

Hope it helps :-)

Irvin answered 4/4, 2019 at 4:59 Comment(0)
T
-1

I use jquery load method applied to div element. something like

<div id="js">
<!-- script will be inserted here --> 
</div>

...

$("#js").load("path", function() {  alert("callback!" });

You can load scripts several times and each time one script will completely replace the one loaded earlier

Tami answered 21/5, 2010 at 7:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.