How can I prevent caching when loading javascript with document.createElement('script')? - javascript

I am using a technique described by Aaron Smith on https://aaronsmith.online/easily-load-an-external-script-using-javascript/ to dynamically load Javascript code. It works fine when I first create a code file but if I change that file, the browser doesn't pick up the new version. I assume that means the code is being cached in the browser.
Here is my code, which is nearly identical to Aaron's example:
const loadScript = (src) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.on load = resolve;
script.onerror = reject;
script.src = src;
document.head.append(script);
});
};
How can I ensure that users always load the latest version of my code? I prefer not to change the name of the code file every time I update it. I am also not able to ask users to reconfigure their browsers to run my solution.

Simply assign a random querystring to make the browser think it's different:
const loadScript = (src) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.onload = resolve;
script.onerror = reject;
script.src = src + `?dummy=${encodeURIComponent(Math.random())}`;
document.head.append(script);
});
};
Every time you load the page you're loading a 'different' script.

Related

Javascript .onload and .onerror alternative way

I have this peace of JS code:
// url = some url that return 200 or 403/404 status code
function f1() {
let script = document.createElement('script')
script.src = url;
script.onload = () => console.log("onload triggered");
script.onerror = () => console.log("onerror triggered");
document.head.appendChild(script)
}
f1();
I need an alternative way to trigger load and error events with no .onload and .onerror strings.
Any ideas?

Include multiple javascript files in a JS file

So, if I have to include a Javascript file in a .js file, I use to below script. It works fine.
var script = document.createElement("SCRIPT");
script.src = 'https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js';
script.type = 'text/javascript';
script.onload = function() {
//Some code
};
document.getElementsByTagName("head")[0].appendChild(script);
What should I do If I need to include more than 1 files.
You can make a function and pass the js files you want to include like so:
function scriptLoader(path, callback)
{
var script = document.createElement('script');
script.type = "text/javascript";
script.async = true;
script.src = path;
script.onload = function(){
if(typeof(callback) == "function")
{
callback();
}
}
try
{
var scriptOne = document.getElementsByTagName('script')[0];
scriptOne.parentNode.insertBefore(script, scriptOne);
}
catch(e)
{
document.getElementsByTagName("head")[0].appendChild(script);
}
}
And call it like so:
scriptLoader('/path/to/file.js');
in the similar manner you can call as many JS file you like this:
scriptLoader('/path/to/file2.js');
scriptLoader('/path/to/file3.js');
and even with onload callback functions like so:
scriptLoader('/path/to/file6.js',function(){
alert('file6 loaded');
});
I would imagine you'd do the same as you've got there but just change the variable name from var script to something like var scriptA and change the code that follows to match like script.src = to scriptA.src =
This function will load one script or many, pass a single file or an array of many:
function include(src, cb) {
arr = (src instanceof Array) ? src : [{
'src': src,
'cb': cb
}];
arr.forEach(function(item) {
_include(item.src, item.cb);
})
function _include(src, cb) {
var script = document.createElement("SCRIPT");
script.src = src;
script.async = true;
script.type = 'text/javascript';
script.onload = function() {
if (cb) cb()
}
document.getElementsByTagName("head")[0].appendChild(script);
}
}
include("/js/file1.js");
include("/js/file1.js", function(){console.log("file1 loaded")});
include([{src:"/js/file1.js"},{src:"/js/file2.js"},{src:"/js/file3.js"}]);

Function return from XMLHttpRequest [duplicate]

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);

How to check if a dynamically created file exists on AWS S3 using JS

I have a server side script that dynamically creates a js file in an s3 bucket after a given url has 1 page view. The server side script runs every 1 minute.
On any given URL on the site, I have a script which tries to call this dynamically created file.
Such as:
var host = window.location.host;
var pathName = window.location.pathname;
var script = document.createElement('script');
script.async = true;
script.type = 'text/javascript';
script.src="https://s3.amazonaws.com/some_bucket/"+host+pathName+".js;
var node = document.getElementsByTagName('script')[0];
node.parentNode.insertBefore(script, node);
})();
The problem is that if a new URL is created on the site and the first visitor goes to that page then the cron job has had a chance to run, and therefore the js file does not exist in the bucket yet. This causes a 404 error but also causes an error in the on-page javascript above crashing the js library that contains it.
Is there a way to check the existence of the file on the fly and default to a different file if the dynamically created one has not been built yet?
For example (pseudo code):
try{
script.src="https://s3.amazonaws.com/some_bucket/"+host+pathName+".js;
}
catch(e){
404error = true;
}
finally{
if(404error == true){
script.src="https://s3.amazonaws.com/some_bucket/DEFAULT.js";
}
}
Updated code with calling a different file on error. This successfully calls the other file, however it also still causes a 404 error in Chrome.
var host = window.location.host;
var pathName = window.location.pathname;
var script = document.createElement('script');
script.async = true;
script.type = 'text/javascript';
script.onerror = function(){
var host = window.location.host;
var pathName = window.location.pathname;
var script = document.createElement('script');
script.async = true;
script.type = 'text/javascript';
script.src="https://s3.amazonaws.com/some_bucket/some_default_file.js";
var node = document.getElementsByTagName('script')[0];
node.parentNode.insertBefore(script, node);
}
script.src="https://s3.amazonaws.com/some_bucket/"+host+pathName+".js;
var node = document.getElementsByTagName('script')[0];
node.parentNode.insertBefore(script, node);
})();
Use script.onerror:
var pathName = window.location.pathname;
var script = document.createElement('script');
script.async = true;
script.type = 'text/javascript';
script.onerror = function() {
console.log('script failed to load');
};
script.src = "https://s3.amazonaws.com/some_bucket/" + host + pathName + ".js";
var node = document.getElementsByTagName('script')[0];
node.parentNode.insertBefore(script, node);

Checking if jQuery has been loaded in bookmarklet

I am using the following piece of code to make sure that jQuery has been loaded in page before I execute the main body of javascript in my bookmarklet. Is there any chance this code fails to load jQuery properly in any browser?
if (!window.jQuery) {
var script = document.createElement("script");
script.src = "//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js";
script.onload = function () {
foo();
};
document.head.appendChild(script);
} else {
foo();
}
function foo() {
// bookmarklet code goes here
}
Is there any chance this code fails to load jQuery properly in any browser?
Not unless the page in question was loaded from a local resource rather than via http / https. In that case, the protocol-relative URL will fail.
You'll want to reverse the order in which you set the src and onload properties, though; if the resource is in cache, the event can be fired before you hook up the handler (JavaScript in browsers is single-threaded other than Web Workers, but browsers are not single-threaded).
Also, there's no need to wrap an additional function around foo:
if (!window.jQuery) {
var script = document.createElement("script");
script.onload = foo;
script.src = "//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js";
document.head.appendChild(script);
} else {
foo();
}
function foo() {
// bookmarklet code goes here
}
If you want to handle the protocol issue so it works with file: URLs and such, that's easy enough to do:
if (!window.jQuery) {
var protocol = location.protocol;
if (protocol !== "http:" && protocol !== "https:") {
protocol = "http:";
}
var script = document.createElement("script");
script.onload = foo;
script.src = protocol + "//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js";
document.head.appendChild(script);
} else {
foo();
}
function foo() {
// bookmarklet code goes here
}

Categories