Waiting on JS class load from dynamic script loading - javascript

I have a JS script which depends on jQuery.
I want to check for jQuery, and if it is not loaded/available add it myself, wait for it to load, and then define my script class.
The code I currently use:
// load jQuery if not loaded yet
if (typeof (jQuery) == 'undefined') {
var fileref = document.createElement('script');
fileref.setAttribute("type", "text/javascript");
fileref.setAttribute("src", 'http://code.jquery.com/jquery-1.4.4.min.js');
document.getElementsByTagName('body')[0].appendChild(fileref);
(ready = function() {
if ( typeof (jQuery) == 'undefined' || !jQuery) {
return setTimeout( ready, 1 );
} else {
// jQuery loaded and ready
jQuery.noConflict();
}
})();
}
// … class definition follows
var MView = function() …
Now, with FireFox 4 (I think it did work before, or execution was just too slow), it will continue the scripts execution even when I still want to wait on jQuery. The recursive setTimeout is non-blocking.
How can I fix this? Make setTimeout blocking? Use another approach? Is there a better way? A way at all?
The class should be global scope, so it can be used on the page that includes this script file.

I would recommend 2 things.
Use 'if (!jQuery)' since undefined is considered falsey
Use the script tag's onload event
if (!window.jQuery) {
var fileref = document.createElement('script');
fileref.setAttribute("type", "text/javascript");
fileref.setAttribute("src", 'http://code.jquery.com/jquery-1.4.4.min.js');
fileref.onload = function() {
// Callback code here
};
document.getElementsByTagName('body')[0].appendChild(fileref);
}

Related

How to call a function from injected script?

This is code from my contentScript.js:
function loadScript(script_url)
{
var head= document.getElementsByTagName('head')[0];
var script= document.createElement('script');
script.type= 'text/javascript';
script.src= chrome.extension.getURL('mySuperScript.js');
head.appendChild(script);
someFunctionFromMySuperScript(request.widgetFrame);// ReferenceError: someFunctionFromMySuperScript is not defined
}
but i got an error when calling a function from injected script:
ReferenceError: someFunctionFromMySuperScript is not defined
Is there is a way to call this function without modifying mySuperScript.js?
Your code suffers from multiple problems:
As you've noticed, the functions and variables from the injected script (mySuperScript.js) are not directly visible to the content script (contentScript.js). That is because the two scripts run in different execution environments.
Inserting a <script> element with a script referenced through a src attribute does not immediately cause the script to execute. Therefore, even if the scripts were to run in the same environment, then you can still not access it.
To solve the issue, first consider whether it is really necessary to run mySuperScript.js in the page. If you don't to access any JavaScript objects from the page itself, then you don't need to inject a script. You should try to minimize the amount of code that runs in the page itself to avoid conflicts.
If you don't have to run the code in the page, then run mySuperScript.js before contentScript.js, and then any functions and variables are immediately available (as usual, via the manifest or by programmatic injection).
If for some reason the script really needs to be loaded dynamically, then you could declare it in web_accessible_resources and use fetch or XMLHttpRequest to load the script, and then eval to run it in your content script's context.
For example:
function loadScript(scriptUrl, callback) {
var scriptUrl = chrome.runtime.getURL(scriptUrl);
fetch(scriptUrl).then(function(response) {
return response.text();
}).then(function(responseText) {
// Optional: Set sourceURL so that the debugger can correctly
// map the source code back to the original script URL.
responseText += '\n//# sourceURL=' + scriptUrl;
// eval is normally frowned upon, but we are executing static
// extension scripts, so that is safe.
window.eval(responseText);
callback();
});
}
// Usage:
loadScript('mySuperScript.js', function() {
someFunctionFromMySuperScript();
});
If you really have to call a function in the page from the script (i.e. mySuperScript.js must absolutely run in the context of the page), then you could inject another script (via any of the techniques from Building a Chrome Extension - Inject code in a page using a Content script) and then pass the message back to the content script (e.g. using custom events).
For example:
var script = document.createElement('script');
script.src = chrome.runtime.getURL('mySuperScript.js');
// We have to use .onload to wait until the script has loaded and executed.
script.onload = function() {
this.remove(); // Clean-up previous script tag
var s = document.createElement('script');
s.addEventListener('my-event-todo-rename', function(e) {
// TODO: Do something with e.detail
// (= result of someFunctionFromMySuperScript() in page)
console.log('Potentially untrusted result: ', e.detail);
// ^ Untrusted because anything in the page can spoof the event.
});
s.textContent = `(function() {
var currentScript = document.currentScript;
var result = someFunctionFromMySuperScript();
currentScript.dispatchEvent(new CustomEvent('my-event-todo-rename', {
detail: result,
}));
})()`;
// Inject to run above script in the page.
(document.head || document.documentElement).appendChild(s);
// Because we use .textContent, the script is synchronously executed.
// So now we can safely remove the script (to clean up).
s.remove();
};
(document.head || document.documentElement).appendChild(script);
(in the above example I'm using template literals, which are supported in Chrome 41+)
As long as the someFunctionFromMySuperScript function is global you can call it, however you need to wait for the code actually be loaded.
function loadScript(script_url)
{
var head= document.getElementsByTagName('head')[0];
var script= document.createElement('script');
script.type= 'text/javascript';
script.src= chrome.extension.getURL('mySuperScript.js');
script.onload = function () {
someFunctionFromMySuperScript(request.widgetFrame);
}
head.appendChild(script);
}
You can also use jQuery's getScript method.
This doesn't work, because your content script and the injected script live in different contexts: what you inject into the page is in the page context instead.
If you just want to load code dynamically into the content script context, you can't do it from the content script - you need to ask a background page to do executeScript on your behalf.
// Content script
chrome.runtime.sendMessage({injectScript: "mySuperScript.js"}, function(response) {
// You can use someFunctionFromMySuperScript here
});
// Background script
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message.injectScript) {
chrome.tabs.executeScript(
sender.tab.id,
{
frameId: sender.frameId},
file: message.injectScript
},
function() { sendResponse(true); }
);
return true; // Since sendResponse is called asynchronously
}
});
If you need to inject code in the page context, then your method is correct but you can't call it directly. Use other methods to communicate with it, such as custom DOM events.

Injecting Javascript in a page from a Chrome extension : concurrency issue?

I am taking part in the development of a Chrome extension.
At some point, we need to run some static non-generated code which has to run in the context of the page and not of the extension.
For simple scripts, there is no problem, using either $.getScript(chrome.extension.getURL(....)) either script = document.createElement('script'); ... document.body.appendChild(script);
For more complex scripts, we'd need sometimes to incluque jquery itself, or some other script definition (because of dependencies).
In this latter case, though, despite the fact that Javascript is supposedly single threaded, it seems that JQuery is not parsed entirely when the dependend script are run, leading to the following
Uncaught ReferenceError: $ is not defined
Am I wrong when assuming that JScript is single-threaded?
What is the correct way to inject scripts in a page when there are dependencies between those scripts? (e.g. script X uses a function defined in script Y)
You could use the onload event for the script....
function addScript(scriptURL, onload) {
var script = document.createElement('script');
script.setAttribute("type", "application/javascript");
script.setAttribute("src", scriptURL);
if (onload) script.onload = onload;
document.documentElement.appendChild(script);
}
function addSecondScript(){
addScript(chrome.extension.getURL("second.js"));
}
addScript(chrome.extension.getURL("jquery-1.7.1.min.js"), addSecondScript);
Appending a script element with src attribute in the DOM doesn't mean that the script is actually loaded and ready to be used. You will need to monitor when the script has actually loaded, after which you can start using jQuery as normal.
You could do something like this:
var script = doc.createElement("script");
script.src = url;
/* if you are just targeting Chrome, the below isn't necessary
script.onload = (function(script, func){
var intervalFunc;
if (script.onload === undefined) {
// IE lack of support for script onload
if( script.onreadystatechange !== undefined ) {
intervalFunc = function() {
if (script.readyState !== "loaded" && script.readyState !== "complete") {
window.setTimeout( intervalFunc, 250 );
} else {
// it is loaded
func();
}
};
window.setTimeout( intervalFunc, 250 );
}
} else {
return func;
}
})(script, function(){
// your onload event, whatever jQuery etc.
});
*/
script.onload = onreadyFunction; // your onload event, whatever jQuery etc.
document.body.appendChild( script );

$(document).ready() when jQuery is loaded as a dynamically appended <script>

For reasons that are beyond my control I have to load jQuery via a dynamically appended <script> tag, and only do this upon some arbitrary event, not at page load.
My problem is in detecting the moment when jquery is ready, for the code below doesn't work:
(function(){
var s=document.createElement('script');
s.setAttribute('type','text/javascript');
s.setAttribute('src','http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js');
document.getElementsByTagName('head')[0].appendChild(s);
})()
$(document).ready(function() {
// something that uses jquery and currently doesn't work
});
How do I detect the moment when jquery is ready to be used in this particular configuration?
Thanks in advance
Use the onload and onreadystatechange event handlers:
var scr = document.createElement('script'),
head = document.getElementsByTagName('head')[0];
scr.onload = scr.onreadystatechange = function(){
if( scr.readyState ){
if(scr.readyState === 'complete' || scr.readyState === 'loaded'){
scr.onreadystatechange = null;
myReadyFunc();
}
}
else{
myReadyFunc();
}
};
head.insertBefore(scr, head.firstChild);
function myReadyFunc() {
$(document).ready(function() {
// something that uses jquery and currently doesn't work
});
}
An old-school answer :)
You can create a recursive polling function that check to see if the $ object exists eg:
function poller(){
if($.length != 0){
//do what you want
}else{
setTimeout(poller, 100);
}
}
And right after you load the jQuery script run the poller function.
You can handle the <script> element's onreadystatechange event, and, if this.readyState is complete or loaded, run your function.
For Firefox, handle the onload event.
You can expose this in a wrapper function which takes a function as a parameter, and calls it if jQuery has been loaded, or puts it in an array (to call in the load handler) if it's not loaded.

Load jQuery in a js, then execute a script that depends on it

I have a unique issue -
I am designing a web application that creates widgets that a user can then embed in their own page (blog posts, mostly). I want them to just have to embed one line, so I just had that line be an include statement, to pull a Javascript off my server.
The problem is, I am building the widget code using jQuery, and I need to load the jQuery plugin, since I obviously don't know whether or not my users will have it available. I thought 'this should be pretty simple'....
function includeJavaScript(jsFile) {
var script = document.createElement('script');
script.src = jsFile;
script.type = 'text/javascript';
document.getElementsByTagName('head')[0].appendChild(script);
}
includeJavaScript('http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js');
jQuery();
So, I am appending the jQuery file to the head, then afterwards, trying to run a jQuery function. Trouble is, this doesn't work! Everytime I run it, I get the error that variable jQuery is not defined. I have tried a few things. I tried putting the jQuery functions in an onLoad trigger, so that the whole page (including, presumably, the jQuery file) would load before it called my script. I tried putting the jQuery function in a seperate file, and loading it after loading the jQuery lib file. But I get the idea I'm missing something simple - I'm new to jQuery, so if I'm missing something obvious, I apologize...
EDIT
OK,I tried the suggestion offered by digitalFresh, as follows (using Safari 5, if that helps), but I still get the same error?
function test() {
jQuery()
}
var script = document.createElement("script");
script.type = "text/javascript";
script.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js';
script.onload = test(); //execute
document.body.appendChild(script);
EDIT
OK, I FINALLY got it to work, in an offhand suggestion from Brendan, by putting the call ITSELF in an 'onload' handler, like so:
function addLoadEvent(func) {
var oldonload = window.onload;
if (typeof window.onload != 'function') {
window.onload = func;
} else {
window.onload = function() {
if (oldonload) {
oldonload();
}
func();
}
}
}
addLoadEvent( function() {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js';
document.body.appendChild(script);
jQuery();
});
At this point, as you can see, I don't even have to put it in an 'onload' - it just works. Though I have to admit, I still don't understand WHY it works, which bothers me...
The solution you end up using works but slow to start, and not 100% fail proof. If someone rewrites window.onload your code will not run. Also window.onload happens when all the content on the page is loaded (including images) which is not exactly what you want. You don't want your script to wait that long.
Fortunately <script> elements have their own onload (ready) event, which can be used to couple other scripts with them.
function include(file, callback) {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = file;
script.onload = script.onreadystatechange = function() {
// execute dependent code
if (callback) callback();
// prevent memory leak in IE
head.removeChild(script);
script.onload = null;
};
head.appendChild(script);
}
include('http://ajax.googleapis.com/.../jquery.min.js', myFunction);
In this case the function will be called exactly when jquery is available. Your code is fail proof and starts quickly.
The script is not loaded or executed when you call the jQuery function. Thats why you get the error that jQuery is not defined.
I answered this problem on Loading Scripts Dynamically. The script is simple for all browsers except IE. Just add an onload event listener
You should first check to make sure they do not already use jquery so something like:
if (jQuery) {
// jQuery is loaded
} else {
// jQuery is not loaded
}
Secondly, you should make sure you use jQuery in no conflict mode and do not use the $ operator as it may be claimed by another script library.
Then to embed, declare this function:
function load_script (url)
{
var xmlhttp;
try {
// Mozilla / Safari / IE7
xmlhttp = new XMLHttpRequest();
} catch (e) {
//Other IE
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
}
xmlhttp.open('GET', url, false); x.send('');
eval(xmlhttp.responseText);
var s = xmlhttp.responseText.split(/\n/);
var r = /^function\s*([a-z_]+)/i;
for (var i = 0; i < s.length; i++)
{
var m = r.exec(s[i]);
if (m != null)
window[m[1]] = eval(m[1]);
}
}
Then call it:
load_script('http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js');
Hope this helps.
Use LABjs. Embedding <script> tags work because the browser loads and executes whatever script in it or referenced by it upon seeing any of them, but that also means the browser will block until it is done with the script.
If you're using jQuery, you can use getScript(). Facebook has a good example on how to employ this. This is a more manageable solution to the original one I posted below.
Alternatively:
Building off of galambalazs's answer you can wrap his include() function in an event like the jQuery $(document).ready() to create deferred scripts with a dependency line without having to rely on an extra JS plugin. I also added a callbackOnError, which is handy if you're fetching from Google CDN.
/* galambalazs's include() */
function include(file, callback, callbackOnError) {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = file;
script.onload = script.onreadystatechange = function() {
// execute dependent code
if (callback) callback();
// prevent memory leak in IE
head.removeChild(script);
script.onload = null;
script.onerror = null;
};
script.onerror = function() {
if(callbackOnError) callbackOnError();
// prevent memory leak in IE
head.removeChild(script);
script.onload = null;
script.onerror = null;
};
head.appendChild(script);
}
/* If you need a deferred inline script, this will work */
function init() {
// Insert Code
}
/* Example with callback */
$(document).ready(function() {
include('something.js', init);
});
/* Maybe I want something on window.load() */
$(window).load(function() {
// Without a callback
include('whatever.js');
});
/* Or use an event to load and initialize script when it's needed */
$(".nearAButton").hover(include('button.js',function() {initButtonJS()}));
function initButtonJS() {
$(".nearAButton").off("mouseenter mouseleave");
}
/* Attempt to load Google Code, on fail load local */
include(
'//ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js',
function () {
// Inline Callback
},
function () {
include('/local/jquery-ui-1.10.3.custom.min.js', function () {
// Inline Callback
});
}
);

Execute javascript after external javascript document has loaded

I want to include a remote js file and then invoke a function once that has finished executing. I thought I could do something like this:
var sc = document.createElement('script');
sc.setAttribute('type', 'text/javascript');
sc.setAttribute('src', src);
sc.innerHTML = "alert('testing');"
parentNode.appendChild(sc);
Turns out, the alert('testing') gets wiped out be whatever is in the file. Is there anyway to do this?
This function will load library from scriptPath and execute passed handler function once script is loaded:
loadExternalScript : function(scriptPath, handler) {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = scriptPath;
script.charset = 'utf-8';
script.onload = handler;
head.appendChild(script);
}
First thing is, forget about using src and inner contents on the same script tag. It doesn't work in any general way, although John Resig gave it some thought in this blog post.
Second thing is, decide whether you want to load the script synchronously or asynchronously. If the script is large or long-running, you'll either want to do it asynchronously, or do it synchronously at the bottom of the page so as not to block rendering.
Your approach (dynamically appending script tags) will load and run it asynchronously, which means the code that should run after it's finished needs to go in a callback that fires when the script is finished. Setting this up isn't very straightforward, so I'd suggest either using jQuery and its ajax.getScript function, or just copy the getScript functionality right out of the jQuery source (lines 3473-3505 of jQuery 1.3.2).
If you want to avoid all of that, just load it synchronously. This is done by using document.write. Your provided example would look like:
document.write("<scr" + "ipt src='" + src + "' type='text/javascript'></script>");
// The script is guaranteed to have executed at this point
alert('testing');
Be sure to keep "script" split up like that, I'm not sure why but it's a quirk of JavaScript.
Have you tried just creating a second script element containing the code you want to run and adding that after the you've added the one that needs downloading?
Adding another
<script></script>
section after the first one should work. AFAIK you can't mix external and inline JS in one tag.
However I'm not sure whether putting code into "innerHTML" will work as expected. I'm interested to see whether it does.
You might be able to use the sc load event to figure out when that script has loaded then do some action.
example http://iamnoah.blogspot.com/2008/01/ie-script-load-event.html
I created this script for myself yesterday. It uses jQuery to load JavaScript files via AJAX and adds them in a script tag to the header, and then calls a callback function I pass it.
Has been working fine for me.
/**
* Fetches and executes JavaScript files from the server.
* #param files A list of files to load, or a single filename as a string.
* #param callback The function to call when the process is done. Passes a boolean success value as the only parameter.
* #param thisObject The calling object for the callback.
*/
window.include = function(files, callback, thisObject) {
var current_location = null;
var recursive = false;
if(!(thisObject instanceof Object)) {
thisObject = window;
}
if(files instanceof Array || files instanceof Object) {
if(files.length > 0) {
current_location = files.shift();
recursive = true;
}
else {
callback.apply(thisObject, [true]);
return;
}
}
else if(typeof files == 'string') {
current_location = files;
}
else {
callback.apply(thisObject, [false]);
return;
}
if((current_location instanceof String || typeof current_location == 'string') && current_location != '')
{
$.ajax({
type : 'GET',
url : current_location,
timeout : 5000,
success : function(data) {
var scriptTag = $(document.createElement('script'));
scriptTag.attr('type', 'text/javascript');
scriptTag.html(data);
$('head').append(scriptTag);
if(recursive) {
window.adlib.include(files, callback, thisObject);
}
else {
callback.apply(thisObject, [true]);
}
},
error : function() {
callback.apply(thisObject, [false]);
}
});
}
}

Categories