dynamically loading external scripts in Safari

I was just given a loan of a MacOSX box in order to debug some parts of our CMS.

After a bit of digging, I found that the biggest bug was that you cannot dynamically add script tags to the document in Safari and expect them to run. This includes external scripts.

For example, here is my usual loadExternalScript() function:

function loadExternalScript(url){
	for(i in loadedScripts)if(loadedScripts[i]==url)return 0;
	loadedScripts.push(url);
	var el=newEl('script');
	el.type="text/javascript";
	if(sajax_is_loaded&&/\\.php/.test(url))url+=(/\\?/.test(url)?'&':'?')+'sajax_is_loaded';
	el.src=url;
	getEls('head')[0].appendChild(el);
	return 1;
}

This works in everything except Safari.

One fairly obvious solution to this is to grab the contents of the requested file with XMLHttpRequest and eval() it.

Sample code to demonstrate this:

function loadExternalScript(url){
	for(i in loadedScripts)if(loadedScripts[i]==url)return 0;
	loadedScripts.push(url);
	if(sajax_is_loaded&&/\\.php/.test(url))url+=(/\\?/.test(url)?'&':'?')+'sajax_is_loaded';
	var x=new XMLHttpRequest();
	x.open('GET',url,true);
	x.onreadystatechange=function(){
		if(x.readyState!=4)return;
		eval(x.responseText);
	}
	x.send(null);
	return 1;
}

Anyone spot the subtle bug above?

The bug is that the resultant code will be run only within the scope of the anonymous function created by the XMLHttpRequest callback.

A lengthy solution to this would be to parse the code and add “window.” in front of all functions and variables, but that’s ugly.

A more pretty solution is to run the resulting code with setTimeout() instead, which allows it to be written directly to the global scope.

function loadExternalScript(url){
	for(i in loadedScripts)if(loadedScripts[i]==url)return 0;
	loadedScripts.push(url);
	if(sajax_is_loaded&&/\\.php/.test(url))url+=(/\\?/.test(url)?'&':'?')+'sajax_is_loaded';
	var x=new XMLHttpRequest();
	x.open('GET',url,true);
	x.onreadystatechange=function(){
		if(x.readyState!=4)return;
		var t=x.responseText.replace(/\\\\/g,'\\\\\\\\').replace(/"/g,'\\\\"').replace(/\\n/g,"\\\\n");
		setTimeout('eval("'+t+'");',0);
	}
	x.send(null);
	return 1;
}

Of course, the original DOM insertion is the preferred method, but the above function should be used for Safari.

12 Comments.

  1. I am a bit puzzled. I use dynamic script insertion in Safari and it works fine. I’ve only tested as far back as Safari 1.2.

    Here is a test case:

    http://mg.to/test/scripttag/test.html

    The test creates and inserts two dynamic script tags. One uses an external .js file, the other inline script. Each script displays an alert box.

  2. Hmm… I just noticed that the inline script in that test case fails in IE6/Windows, although the external script works. Both work in Safari and Firefox.

  3. for the first comment; I can’t remember exactly what version it was that I wrote it for, but it was one of the earlier OSX versions. maybe the Safari version you have now has fixed that bug.

    for the second; the function was written specifically for Safari, so it’s not surprising that some other browsers might complain πŸ˜‰

    the way I usually do my coding is to write a function specifically for Firefox, then include an external script depending on the browser type (js-safari.js, for example), which then “overrides” some of the functions with browser-specific versions.

    this allows me to reduce the amount of browser-sniffing needed, and therefore helps to make the code a bit faster.

  4. Shaun McCormick

    fyi: Doesn’t seem to work in Safari 2.0.3, when trying to externally load .js files that use the prototype library to become Classes using Class.create();. Safari doesn’t recognize them, and can’t find them.

  5. you don’t need the eval statement; setTimeout(t,0) will work just fine.

    frankly, i find executing the script directly to be preferable to appending a child node to the document altogether. its simpler, it works in all the browsers, and when you view generated source in firefox you dont have to wade through all the library code that would otherwise display.

    personal preference. πŸ™‚

  6. Hmm — I am noticing that when using setTimeout in IE, the context is *not* the global scope. πŸ™ Seems like everyone else gets it right though. So I guess we’re still going to be writing script nodes to the DOM for IE 5+6 if we need to dynamically load libraries like Prototype. πŸ™

  7. I spoke too soon!!!

    In IE– use window.execScript(your_script_here) and the global context is used! So…

    if(window.execScript) window.execScript(script);
    else window.setTimeout(script,0);

    Can I get an amen?

  8. Brendan, I’ll give you a non-denominational congratulation πŸ˜‰ I didn’t know about that one.

  9. The “original DOM insertion is the preferred method, but the above function should be used for Safari”.

    Actually, xmlHTTP won’t work cross domain (including a script from elsewhere). That means this only solves the problem if your includes are in the same domain.

    I’m still looking for a good solution.

  10. Anthony - trackback on November 1, 2006 at 1:38 am
  11. Emily - trackback on November 21, 2006 at 7:24 am
  12. Thanks Michael – always interested in trying out new code. I’ll have a look at that site in the morning.

Trackbacks and Pingbacks:

  • Anthony - Trackback on 2006/11/01/ 01:38
  • Emily - Trackback on 2006/11/21/ 07:24
%d bloggers like this: