I'm trying to load JS scripts dynamically, but using jQuery is not an option.
I checked jQuery source to see how getScript was implemented so that I could use that approach to load scripts using native JS. However, getScript only calls jQuery.get()
and I haven't been able to find where the get method is implemented.
So my question is,
What's a reliable way to implement my own getScript method using native JavaScript?
Thanks!
Here's a jQuery getScript alternative with callback functionality:
function getScript(source, callback) {
var script = document.createElement('script');
var prior = document.getElementsByTagName('script')[0];
script.async = 1;
script.onload = script.onreadystatechange = function( _, isAbort ) {
if(isAbort || !script.readyState || /loaded|complete/.test(script.readyState) ) {
script.onload = script.onreadystatechange = null;
script = undefined;
if(!isAbort && callback) setTimeout(callback, 0);
}
};
script.src = source;
prior.parentNode.insertBefore(script, prior);
}
You can fetch scripts like this:
(function(document, tag) {
var scriptTag = document.createElement(tag), // create a script tag
firstScriptTag = document.getElementsByTagName(tag)[0]; // find the first script tag in the document
scriptTag.src = 'your-script.js'; // set the source of the script to your script
firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag); // append the script to the DOM
}(document, 'script'));
use this
var js_script = document.createElement('script');
js_script.type = "text/javascript";
js_script.src = "http://www.example.com/script.js";
js_script.async = true;
document.getElementsByTagName('head')[0].appendChild(js_script);
Firstly, Thanks for #Mahn's answer. I rewrote his solution in ES6 and promise, in case someone need it, I will just paste my code here:
const loadScript = (source, beforeEl, async = true, defer = true) => {
return new Promise((resolve, reject) => {
let script = document.createElement('script');
const prior = beforeEl || document.getElementsByTagName('script')[0];
script.async = async;
script.defer = defer;
function onloadHander(_, isAbort) {
if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
script.onload = null;
script.onreadystatechange = null;
script = undefined;
if (isAbort) { reject(); } else { resolve(); }
}
}
script.onload = onloadHander;
script.onreadystatechange = onloadHander;
script.src = source;
prior.parentNode.insertBefore(script, prior);
});
}
Usage:
const scriptUrl = 'https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoad&render=explicit';
loadScript(scriptUrl).then(() => {
console.log('script loaded');
}, () => {
console.log('fail to load script');
});
and code is eslinted.
This polishes up previous ES6 solutions and will work in all modern browsers
Load and Get Script as a Promise
const getScript = url => new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = url
script.async = true
script.onerror = reject
script.onload = script.onreadystatechange = function() {
const loadState = this.readyState
if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
script.onload = script.onreadystatechange = null
resolve()
}
document.head.appendChild(script)
})
Usage
getScript('https://dummyjs.com/js')
.then(() => {
console.log('Loaded', dummy.text())
})
.catch(() => {
console.error('Could not load script')
})
Also works for JSONP endpoints
const callbackName = `_${Date.now()}`
getScript('http://example.com/jsonp?callback=' + callbackName)
.then(() => {
const data = window[callbackName];
console.log('Loaded', data)
})
Also, please be careful with some of the AJAX solutions listed as they are bound to the CORS policy in modern browsers https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
There are some good solutions here but many are outdated. There is a good one by #Mahn but as stated in a comment it is not exactly a replacement for $.getScript() as the callback does not receive data. I had already written my own function for a replacement for $.get() and landed here when I need it to work for a script. I was able to use #Mahn's solution and modify it a bit along with my current $.get() replacement and come up with something that works well and is simple to implement.
function pullScript(url, callback){
pull(url, function loadReturn(data, status, xhr){
//If call returned with a good status
if(status == 200){
var script = document.createElement('script');
//Instead of setting .src set .innerHTML
script.innerHTML = data;
document.querySelector('head').appendChild(script);
}
if(typeof callback != 'undefined'){
//If callback was given skip an execution frame and run callback passing relevant arguments
setTimeout(function runCallback(){callback(data, status, xhr)}, 0);
}
});
}
function pull(url, callback, method = 'GET', async = true) {
//Make sure we have a good method to run
method = method.toUpperCase();
if(!(method === 'GET' || method === 'POST' || method === 'HEAD')){
throw new Error('method must either be GET, POST, or HEAD');
}
//Setup our request
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == XMLHttpRequest.DONE) { // XMLHttpRequest.DONE == 4
//Once the request has completed fire the callback with relevant arguments
//you should handle in your callback if it was successful or not
callback(xhr.responseText, xhr.status, xhr);
}
};
//Open and send request
xhr.open(method, url, async);
xhr.send();
}
Now we have a replacement for $.get() and $.getScript() that work just as simply:
pullScript(file1, function(data, status, xhr){
console.log(data);
console.log(status);
console.log(xhr);
});
pullScript(file2);
pull(file3, function loadReturn(data, status){
if(status == 200){
document.querySelector('#content').innerHTML = data;
}
}
Mozilla Developer Network provides an example that works asynchronously and does not use 'onreadystatechange' (from #ShaneX's answer) that is not really present in a HTMLScriptTag:
function loadError(oError) {
throw new URIError("The script " + oError.target.src + " didn't load correctly.");
}
function prefixScript(url, onloadFunction) {
var newScript = document.createElement("script");
newScript.onerror = loadError;
if (onloadFunction) { newScript.onload = onloadFunction; }
document.currentScript.parentNode.insertBefore(newScript, document.currentScript);
newScript.src = url;
}
Sample usage:
prefixScript("myScript1.js");
prefixScript("myScript2.js", function () { alert("The script \"myScript2.js\" has been correctly loaded."); });
But #Agamemnus' comment should be considered: The script might not be fully loaded when onloadFunction is called. A timer could be used setTimeout(func, 0) to let the event loop finalize the added script to the document. The event loop finally calls the function behind the timer and the script should be ready to use at this point.
However, maybe one should consider returning a Promise instead of providing two functions for exception & success handling, that would be the ES6 way. This would also render the need for a timer unnecessary, because Promises are handled by the event loop - becuase by the time the Promise is handled, the script was already finalized by the event loop.
Implementing Mozilla's method including Promises, the final code looks like this:
function loadScript(url)
{
return new Promise(function(resolve, reject)
{
let newScript = document.createElement("script");
newScript.onerror = reject;
newScript.onload = resolve;
document.currentScript.parentNode.insertBefore(newScript, document.currentScript);
newScript.src = url;
});
}
loadScript("test.js").then(() => { FunctionFromExportedScript(); }).catch(() => { console.log("rejected!"); });
window.addEventListener('DOMContentLoaded',
function() {
var head = document.getElementsByTagName('HEAD')[0];
var script = document.createElement('script');
script.src = "/Content/index.js";
head.appendChild(script);
});
Here's a version that preserves the accept and x-requested-with headers, like jquery getScript:
function pullScript(url, callback){
pull(url, function loadReturn(data, status, xhr){
if(status === 200){
var script = document.createElement('script');
script.innerHTML = data; // Instead of setting .src set .innerHTML
document.querySelector('head').appendChild(script);
}
if (typeof callback != 'undefined'){
// If callback was given skip an execution frame and run callback passing relevant arguments
setTimeout(function runCallback(){callback(data, status, xhr)}, 0);
}
});
}
function pull(url, callback) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
callback(xhr.responseText, xhr.status, xhr);
}
};
xhr.open('GET', url, true);
xhr.setRequestHeader('accept', '*/*;q=0.5, text/javascript, application/javascript, application/ecmascript, application/x-ecmascript');
xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest');
xhr.send();
}
pullScript(URL);
Related
Is it possible to catch <script type="text/javascript" src=".."> 404 with JavaScript so that I could correct src?
window.onerror doesn't seem to catch these.
Important note! This script is dynamically added into body by 3rd-party library, so I have no control over it.
It would be so nice if I could detect the script insertion or src setting and update it with correct src like I could do with image but I think it's not possible with script element?
var NativeImage = window.Image;
class MyImage {
constructor (w, h) {
var nativeImage = new NativeImage(w, h);
var handler = {
set: function (obj, prop, value) {
if (prop === 'src') {
return nativeImage[prop] = '/correct/image/path.jpg';
}
return nativeImage[prop] = value;
},
get: function (target, prop) {
return target[prop];
}
};
return new Proxy(nativeImage, handler);
}
}
window.Image = MyImage;
You can detect the script's insertion with MutationObserver and add an error listener to the tag:
// Your code (run this before the external script)
new MutationObserver((mutations, observer) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
// Add additional checks here if needed
// to identify if the script is the one added by the library
if (node.nodeType === 1 && node.matches('script')) {
node.addEventListener('error', () => {
console.log('Script could not be loaded');
});
// Remove the observer, since its purpose is fulfilled
observer.disconnect();
return;
}
}
}
})
// Watch for elements that are added as children to document.body:
.observe(document.body, { childList: true });
<script>
// External script:
window.addEventListener('DOMContentLoaded', () => {
const script = document.createElement('script');
script.src = 'doesntexist';
document.body.appendChild(script);
});
</script>
You can check the HTTP Status of your script src by sending a XMLHttpRequest:
HTTP Status Code from URL in Javascript
function getStatus(url) {
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState === 4){
return request.status;
};
request.open("GET", url, true);
request.send();
}
I'm working on a web project where using jQuery (or other libraries/dependencies) is not an option, so I'm trying to replicate the code jQuery uses to make AJAX requests. The project previously used jQuery, so I've structured my replacement to the $.ajax() method to have the same behavior, however I cannot get mine to make cross-domain requests. When trying to load in scripts for example, I get this error in the console.
XMLHttpRequest cannot load <URL>. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin <URL> is therefore not allowed access.
I'm familiar with CORS and the whole cross-origin-security policy and what that entails, but what I'm confused about is how jQuery can seem to circumvent that, while my code cannot. I'm thinking there must be something special jQuery is doing?
This is how my $.ajax() replacement function is currently scripted.
function ajax (options) {
var request = new XMLHttpRequest();
if ("withCredentials" in request)
request.withCredentials = false;
else if (typeof XDomainRequest !== "undefined")
var request = new XDomainRequest();
options.type = options.type || options.method || "GET";
request.open(options.type.toUpperCase(), options.url, true);
for (var i in options.headers) {
request.setRequestHeader(i, options.headers[i]);
}
if (typeof options.beforeSend == "function")
var go = options.beforeSend(request, options);
if (go == false)
return false;
request.onreadystatechange = function () {
if (this.readyState === XMLHttpRequest.DONE) {
var resp = this.responseText;
try {
var body = JSON.parse(resp);
this.responseJSON = body;
}
catch (err) {
var body = resp;
}
if (this.status < 300 && typeof options.success == "function")
options.success(body, this.status, this);
else if (typeof options.error == "function")
options.error(this, this.status, body);
if (typeof options.complete == "function")
options.complete(this, this.status);
}
};
if (typeof options.data == "object" && options.data !== null)
options.data = JSON.stringify(options.data);
if (options.type.toUpperCase() != "GET")
request.send(typeof options.data !== "undefined" ? options.data : null);
else
request.send();
return request;
}
Can someone point out if I'm missing something obvious? Do I need to manually also do the OPTIONS pre-flight or something?
I've found a fix for this. The issue seemed to stem from trying to load in cross-domain scripts specifically. jQuery uses $.getScript() to do that, which actually just adds a script to the page instead of making a cross-domain HTTP request.
I used this code as a replacement for that function.
function getScript (url, callback) {
var loadScript = function (url, callback) {
var scrpt = document.createElement("script");
scrpt.setAttribute("type", "text/javascript");
scrpt.src = url;
scrpt.onload = function () {
scrpt.parentNode.removeChild(scrpt);
console.log("Script is ready, firing callback!");
if (callback)
callback();
};
document.body.appendChild(scrpt);
}
var isReady = setInterval(function () {
console.log(document.body);
if (document.body) {
clearInterval(isReady);
loadScript(url, callback);
}
}, 25);
}
I want to send request parameters to other domain
I already know that Cross Scripting needs JsonP and I have used JsonP with Jquery ajax
but i do not figure out how to do Cross Scripting as using XMLHttpRequest
following code my basic XMLHttpRequest code.
i guess i need to chage xhr.setRequestHeader() and i have to add parsing code
please give me any idea
var xhr;
function createXMLHttpRequest(){
if(window.AtiveXObject){
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}else{
xhr = new XMLHttpRequest();
}
var url = "http://www.helloword.com";
}
function openRequest(){
createXMLHttpRequest();
xhr.onreadystatechange = getdata;
xhr.open("POST",url,true);
xhr.setRequestHeader("Content-Type",'application/x-www-form-urlencoded');
xhr.send(data);
}
function getdata(){
if(xhr.readyState==4){
if(xhr.status==200){
var txt = xhr.responseText;
alert(txt);
}
}
}
JSONP does not use XMLHttpRequests.
The reason JSONP is used is to overcome cross-origin restrictions of XHRs.
Instead, the data is retrieved via a script.
function jsonp(url, callback) {
var callbackName = 'jsonp_callback_' + Math.round(100000 * Math.random());
window[callbackName] = function(data) {
delete window[callbackName];
document.body.removeChild(script);
callback(data);
};
var script = document.createElement('script');
script.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'callback=' + callbackName;
document.body.appendChild(script);
}
jsonp('http://www.helloword.com', function(data) {
alert(data);
});
In interest of simplicity, this does not include error handling if the request fails. Use script.onerror if you need that.
I know you already got the answer for but if anyone else out here wanted an example of one using promises here's one.
function jsonp(url) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script')
const name = "_jsonp_" + Math.round(100000 * Math.random());
//url formatting
if (url.match(/\?/)) url += "&callback="+name
else url += "?callback="+name
script.src = url;
window[name] = function(data) {
resolve(data);
document.body.removeChild(script);
delete window[name];
}
document.body.appendChild(script);
});
}
var data = jsonp("https://www.google.com");
data.then((res) => {
console.log(res);
});
For google api I was forced to add callback and also v=1.0 parameter to url. Without v=1.0 parameter I get CORB error (for my version and also other answers code - however jQuery $.ajax with dataType: "jsonp" add this parameter - so I add it too and start working )
Cross-Origin Read Blocking (CORB) blocked cross-origin response https://ajax.googleapis.com/ajax/services/feed/load?callback=jsonp1555427800677 with MIME type text/javascript. See https://www.chromestatus.com/feature/5629709824032768 for more details.
Below is promise version of my solution
function jsonp(url) {
return new Promise(function(resolve, reject) {
var s = document.createElement('script');
var f="jsonp"+(+new Date()), b=document.body;
window[f] = d=>{ delete window[f]; b.removeChild(s); resolve(d); };
s.src=`${url}${url.includes('?')?'&':'?'}callback=${f}&v=1.0`;
b.appendChild(s);
})
}
async function send() {
let r = await jsonp("http://ajax.googleapis.com/ajax/services/feed/load");
console.log(r);
}
<button onclick="send()">Send JSONP</button>
function JsonpHttpRequest(url, callback) {
var e = document.createElement('script');
e.src = url;
document.body.appendChild(script); // fyi remove this element later /assign temp class ..then .remove it later
//insetead of this you may also create function with callback value and use it instead
window[callback] = (data) => {
console.log(data); // heres you data
}
}
// heres how to use
function HowTouse(params) {
JsonpHttpRequest("http://localhost:50702/api/values/Getm?num=19&callback=www", "www")
}
You can not able to do Cross Scripting using XMLHttpRequest.If you want to cross domain with out Jquery, you must create a new script node and set the src attribute of it.
http://jsfiddle.net/JamesKyle/HQDu6/
I've created a short function based on Mathias Bynens Optimization of the Google Analytics asynchronous script that goes as following:
function async(src) {
var d = document, t = 'script',
o = d.createElement(t),
s = d.getElementsByTagName(t)[0];
o.src = '//' + src;
s.parentNode.insertBefore(o, s);
}
This works great and I've already started using it for several different scripts
// Crazy Egg
async('dnn506yrbagrg.cloudfront.net/pages/scripts/XXXXX/XXXXX.js?' + Math.floor(new Date().getTime() / 3600000));
// User Voice
var uvOptions = {};
async('widget.uservoice.com/XXXXX.js');
// Google Analytics
var _gaq = [['_setAccount', 'UA-XXXXX-XX'], ['_setDomainName', 'coachup.com'], ['_trackPageview']];
async('google-analytics.com/ga.js');
// Stripe
async('js.stripe.com/v1');
The problem comes when I encounter a script that needs to be called after it's loaded:
// Snap Engage
async('snapabug.appspot.com/snapabug.js');
SnapABug.init('XXXXX-XXXXX-XXXXX-XXXXX-XXXXX');
So I figured I'd turn this into a callback function that would be used as so:
async('snapabug.appspot.com/snapabug.js', function() {
SnapABug.init('XXXXX-XXXXX-XXXXX-XXXXX-XXXXX');
});
I did not expect that this would be difficult for me to do but it has turned out that way.
My question is what is the most efficient way to add a callback without overcomplicating the code.
See the jsfiddle: http://jsfiddle.net/JamesKyle/HQDu6/
Thanks RASG for https://stackoverflow.com/a/3211647/982924
Async function with callback:
function async(u, c) {
var d = document, t = 'script',
o = d.createElement(t),
s = d.getElementsByTagName(t)[0];
o.src = '//' + u;
if (c) { o.addEventListener('load', function (e) { c(null, e); }, false); }
s.parentNode.insertBefore(o, s);
}
Usage:
async('snapabug.appspot.com/snapabug.js', function() {
SnapABug.init('XXXXX-XXXXX-XXXXX-XXXXX-XXXXX');
});
jsFiddle
A more recent snippet:
async function loadAsync(src) {
const script = document.createElement('script');
script.src = src;
return new Promise((resolve, reject) => {
script.onreadystatechange = function () {
if (script.readyState === 'loaded' || script.readyState === 'complete') {
script.onreadystatechange = null;
resolve(true);
}
};
document.getElementsByTagName('head')[0].appendChild(script);
});
}
utilisation
loadAsync(`https://....js`).then(_ => {
// ... script loaded here
})
James Kyle's answer doesn't take IE9 into account. Here is a modified version of the code I found in the link proposed in the comments. Modify the var baseUrl so it can find the script accordingly.
//for requiring a script loaded asynchronously.
function loadAsync(src, callback, relative){
var baseUrl = "/resources/script/";
var script = document.createElement('script');
if(relative === true){
script.src = baseUrl + src;
}else{
script.src = src;
}
if(callback !== null){
if (script.readyState) { // IE, incl. IE9
script.onreadystatechange = function() {
if (script.readyState === "loaded" || script.readyState === "complete") {
script.onreadystatechange = null;
callback();
}
};
} else {
script.onload = function() { // Other browsers
callback();
};
}
}
document.getElementsByTagName('head')[0].appendChild(script);
}
utilisation:
loadAsync('https://www.gstatic.com/charts/loader.js' , function(){
chart.loadCharts();
});
// OR relative path
loadAsync('fastclick.js', null, true);
The other answers works well, but aren't super readable or require Promises. Here is my two cents:
function loadScript(src, callback) {
var script = document.createElement('script');
script.setAttribute('src', src);
script.addEventListener('load', callback);
document.head.appendChild(script);
},
I am trying to load an external javascript file from within javascript but I cannot seem to get it to work. Am I doing something wrong?
sample file of my work
function loadJs() {
var fileref=document.createElement('script')
fileref.setAttribute("type","text/javascript")
fileref.setAttribute("src", "https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js")
document.body.appendChild(fileref); }
Perhaps you are trying to access the jQuery API before it is fully loaded. You can add a callback parameter to the loadJs function like this:
function loadJs(src, callback) {
var s = document.createElement('script');
document.getElementsByTagName('head')[0].appendChild(s);
s.onload = function() {
//callback if existent.
if (typeof callback == "function") callback();
callback = null;
}
s.onreadystatechange = function() {
if (s.readyState == 4 || s.readyState == "complete") {
if (typeof callback == "function") callback();
callback = null; // Wipe callback, to prevent multiple calls.
}
}
s.src = src;
}
loadJs('https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js', function() {
$('body').append('<p>It works!</p>');
});
Tested in chrome, FF, ie8. Fiddle: http://jsfiddle.net/Umwbx/2/
Use code similar to this:
function loadJs() {
var s = document.createElement('script');
var c = document.getElementsByTagName('script')[0];
s.type = 'text/javascript';
s.src = 'https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js';
c.parentNode.insertBefore(s, c);
}