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.