Inject a script tag with remote src and wait for it to execute
JavascriptJavascript Problem Overview
How can I inject a <script src="https://remote.com/"></script>
element into my page, wait for it to execute, and then use functions that it defines?
FYI: In my case, the script will do some credit card processing in rare cases, so I don't want to include it always. I want to include it quickly when the user opens up a change-credit-card-options dialog, and then send it the new credit card options.
Edit for additional detail: I do not have access to the remote script.
Javascript Solutions
Solution 1 - Javascript
You could use Google Analytics or Facebook's method:
(function(d, script) {
script = d.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.onload = function(){
// remote script has loaded
};
script.src = 'http://www.google-analytics.com/ga.js';
d.getElementsByTagName('head')[0].appendChild(script);
}(document));
UPDATE:
Below is the new Facebook method; it relies on an existing script tag instead of <head>
:
(function(d, s, id){
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)){ return; }
js = d.createElement(s); js.id = id;
js.onload = function(){
// remote script has loaded
};
js.src = "//connect.facebook.net/en_US/sdk.js";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
- Replace
facebook-jssdk
with your unique script identifier to avoid it being appended more than once. - Replace the script's url with your own.
Solution 2 - Javascript
Same method using event listeners and ES2015 constructs:
function injectScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.addEventListener('load', resolve);
script.addEventListener('error', e => reject(e.error));
document.head.appendChild(script);
});
}
injectScript('https://example.com/script.js')
.then(() => {
console.log('Script loaded!');
}).catch(error => {
console.error(error);
});
Solution 3 - Javascript
This is one way to dynamically load and execute a list of scripts 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
-
There is a great article by Jake Archibald of Google about this called Deep dive into the murky waters of script loading.
Solution 4 - Javascript
import()
Dynamic Using dynamic import, you can now load modules and wait for them to excute, as simply as this:
import("http://example.com/module.js").then(function(module) {
alert("module ready");
});
If the module has already been loaded and executed, it won't get loaded and executed again, but the promise returned by import
will still resolve.
Note that the file is loaded as a module, not as just a script. Modules are executed in strict mode, and they are loaded in module scope, which means variables are not automatically made global the way they are in normally loaded scripts. Use the export
keyword in a module to share a variable with other modules or scripts.
References:
Solution 5 - Javascript
something like this should do the trick:
(function() {
// Create a new script node
var script = document.createElement("script");
script.type = "text/javascript";
script.onload = function() {
// Cleanup onload handler
script.onload = null;
// do stuff with the loaded script!
}
// Add the script to the DOM
(document.getElementsByTagName( "head" )[ 0 ]).appendChild( script );
// Set the `src` to begin transport
script.src = "https://remote.com/";
})();
hope that helps! cheers.
Solution 6 - Javascript
Create a loader
You can inject the script in an orderly manner in a loader.
Beware that the execution of the dynamically loaded scripts usually comes after statically loaded scripts (i.e.<script src="My_script.js"></script>
) (the order of injection in the DOM does not guarantee the opposite):
e.g., loader.js:
function appendScript(url){
let script = document.createElement("script");
script.src = url;
script.async = false //IMPORTANT
/*Node Insertion Point*/.appendChild(script);
}
appendScript("my_script1.js");
appendScript("my_script2.js");
my_script1.js
will effectively be executed before my_script2.js
, (helpful if dependencies of my_script2.js
are in my_script1.js
)
Note it's important to have script.async = false
because dynamically loaded scripts has async = true
by default, async
does not assure you the order of loading.
Solution 7 - Javascript
Here is my adapted version, based on the answer of Frank:
static async importScript(src, expressionToEvaluateAndReturn){
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.async = true;
script.src = src;
script.addEventListener('load', (event)=>{
if(expressionToEvaluateAndReturn){
try{
let result = eval(expressionToEvaluateAndReturn);
resolve(result);
} catch(error){
reject(error);
}
} else {
resolve();
}
});
script.addEventListener('error', () => reject('Error loading script "' + src + '"'));
script.addEventListener('abort', () => reject('Script loading aborted for "' + src + '"'));
document.head.appendChild(script);
});
}
Example usage:
let d3 = await importScript('/bower_components/d3/d3.min.js','d3')
.catch(error => {
console.log(error);
throw error;
});