Load ordering of dynamically added script tags
I would like to avoid using the onload callback to solve this issue since I noticed a performance degradation with my application.
If you want the files ordered ... you need to wait for onload on each file. No way around it.
Here is a utility function I wrote once:
/**
* Utility : Resolves when a script has been loaded
*/
function include(url: string) {
return new Promise<{ script: HTMLScriptElement }>((resolve, reject) => {
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = function() {
resolve({ script });
};
document.getElementsByTagName('head')[0].appendChild(script);
});
}
I can mention some alternatives to overcome that requirement:
Use a library to inject dependencies (AMD or CommonJS modules)
Just use modules. ES2015:import
/export
, or CommonJS:require()
.- Create the
script
tag programmatically and set the callbackonload
in order to react when the script has been loaded asynchronously. The attributeasync = true
is set by default. If you are allowed to modify the scripts to inject, then add a line at the end of scripts with anobject
orarray
that keeps track of the scripts already loaded.- You can fetch the scripts as text (
XMLHttpRequest
), then, build astring
with the scripts in the required order and, finally, execute the text-script viaeval()
- And the less recommended option but frequently used, set a
setInterval
to check if the script was already executed.
I recommend going for the first option. But for academic purposes, I'm going to illustrate the second option:
Create the
script
tag programmatically and set the callbackonload
in order to react when the script has been loaded asynchronously.
I want to recommend a reading about script loaders: Deep dive into the murky waters of script loading, half hour worth spending!
The following example is a small module to manage scripts injection, and this is the basic idea behind it:
let _scriptsToLoad = [
'path/to/script1.js',
'path/to/script2.js',
'path/to/script3.js'
];
function createScriptElement() {
// gets the first script in the list
let script = _scriptsToLoad.shift();
// all scripts were loaded
if (!script) return;
let js = document.createElement('script');
js.type = 'text/javascript';
js.src = script;
js.onload = onScriptLoaded;
let s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(js, s);
}
function onScriptLoaded(event) {
// loads the next script
createScriptElement();
};
In this plunker you can test the injection of scripts asynchronously in a specific order:
- https://plnkr.co/edit/b9O19f
The main idea was to create an API that allows you interact with the scripts to inject, by exposing the following methods:
addScript
: receive an URL or a list of URLs for each script to be loaded.load
: Run the task to load scripts in the specified order.reset
: Clear the array of scripts, or cancels the load of scripts.afterLoad
: Callback executed after every script has been loaded.onComplete
: Callback executed after all scripts have been loaded.
I like Fluent Interface or method chaining technique, so I built the module that way:
scriptsLoader
.reset()
.addScript("script1.js")
.addScript(["script2.js", "script3.js"])
.afterLoad((src) => console.warn("> loaded from jsuLoader:", src))
.onComplete(() => console.info("* ALL SCRIPTS LOADED *"))
.load();
In the code above, we load first the "script1.js"
file, and execute the afterLoad()
callback, next, do the same with "script2.js"
and "script3.js"
and after all scripts has been loaded, the onComplete()
callback is executed.