I'm using a basic loader to load some js on demand in my pages. On one or two machines (I don't have access to those machines) I'm getting errors like "Uncaught SyntaxError: Invalid or unexpected token in xxx", or "Uncaught SyntaxError: Unexpected token ILLEGAL".
Of course the source files are ok and this is working on thousands of clients. For sure this must be some antivirus that it is modifying my js files, but I want to know what's happening to see if I can prevent or at least notify the client about this.
My problem is that I'm loading the scripts using XMLHttpRequest, and onload I'm attaching the text into the header, like
request.onload = function () {
var txt = request.responseText + "\n//# sourceURL=" + file + "\n";
try {
var script = domDocument.createElement("script"),
head = domDocument.head || domDocument.getElementsByTagName("head")[0];
script.defer = true;
script.text = txt;
head.appendChild(script);
} catch (e) {
domWindow.onerror("Error loading js.", file, 0, 0, e, txt);
}
request.onload = function () { };
};
Where "file" points to the file on the server. The thing is, that the catch never executes as the browser seems to happily execute the above code and THEN parse/throws the actual error.
I would like to know if I can, in some way, catch the error inside my loading script so I can report back what the browser really received instead of my js.
try catch alone can not catch syntax errors, which occur on parsing time. As far as I know, the only way to catch a syntax error is to use eval.
try{
var txt = request.responseText + "\n//# sourceURL=" + file + "\n";
eval(txt);
var script = domDocument.createElement("script"),
head = domDocument.head || domDocument.getElementsByTagName("head")[0];
script.defer = true;
script.text = txt;
head.appendChild(script);
}catch(er){
console.log(er);
}
I don't recommend this approach. It's better to just load the script file via its url then listening to the error events on both the script tag and window object;
window.onerror = function(msg, url, lineNo, columnNo, error){
console.log(msg, url, lineNo, columnNo, error);
}
var script = domDocument.createElement("script"),
head = domDocument.head || domDocument.getElementsByTagName("head")[0];
script.defer = true;
script.onerror = function(er){
console.log(er);
};
script.onload = function(){
console.log('script loaded');
};
script.src = '/path/to/script.js';
head.appendChild(script);
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);
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
}
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);
}
I'm trying to build a javascript bookmarklet for a special URL shortening service we've built at http://esv.to for shortening scripture references (i.e. "Matthew 5" becomes "http://esv.to/Mt5". The bookmarklet is supposed to do a GET request to http://api.esv.to/Matthew+5, which returns a text/plain response of http://esv.to/Mt5.
The code for the bookmarklet itself looks like this (expanded for readability):
var body = document.getElementsByTagName('body')[0], script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'http://esv.to/media/js/bookmarklet.js';
body.appendChild(script);
void(0);
The code from http://esv.to/media/js/bookmarklet.js looks like this:
(function() {
function shorten(ref, callback) {
var url = "http://esv.to/api/" + escape(ref);
var req = new XMLHttpRequest();
req.onreadystatechange = function shortenIt() {
if ( this.readyState == 4 && this.status == 200 ) {
callback(req.responseText);
};
};
req.open( "GET", url );
req.send();
};
function doBookmarklet() {
var ref = prompt("Enter a scripture reference or keyword search to link to:", "")
shorten(ref, function (short) {
prompt("Here is your shortened ESV URL:", short);
});
};
doBookmarklet();
})();
When called from http://esv.to itself, the bookmarklet works correctly. But when used on another page, it does not. The strange thing is, when I watch the request from Firebug, the response is 200 OK, the browser downloads 17 bytes (the length of the returned string), but the response body is empty! No error is thrown, just an empty responseText on the XmlHttpRequest object.
Now, according to Ajax call from Bookmarklet, GET shouldn't violate the same origin policy. Is this a bug? Is there a workaround?
Cross-site XMLHttpRequests can only be done in browsers that implement the W3C Cross-Origin Resource Sharing spec and if the server returns the appropriate access control headers (see MDC article), e.g.:
Access-Control-Allow-Origin: *
But this is not implemented by all browsers. The only sure-fire way to do cross-site requests is to use JSONP, for (untested) example:
(function() {
function shorten(ref, callback){
var callbackFuncName = 'esvapiJSONPCallback' + (new Date()).valueOf();
var script = document.createElement('script');
script.type = "text/javascript";
script.src = "http://esv.to/api/" + escape(ref) + "?callback=" + callbackFuncName;
window[callbackFuncName] = function(shorturl){
script.parentNode.removeChild(script);
window.callbackFuncName = null;
delete window[callbackFuncName];
callback(shorturl);
};
document.getElementsByTagName("head")[0].appendChild(script);
}
var ref = prompt("Enter a scripture reference or keyword search to link to:", "");
shorten(ref, function(shorturl) {
prompt("Here is your shortened ESV URL:", shorturl);
});
})();
When the server sees the callback parameter it would then need to return text/javascript instead of text/plain, and the response body would need to be wrapped in an invocation of the provided callback, for example:
<?php
#... after $shorturl is set ...
if(isset($_GET['callback'])){
header('Content-Type: text/javascript');
$callback = preg_replace('/\W+/', '', $_GET['callback']); #sanitize
print $callback . "(" . json_encode($shorturl) . ");";
}
else {
header("Content-Type: text/plain");
print $shorturl;
}
?>