I am loading a number of JavaScript files dynamically by appending to
the <head/> element. To ensure they're added in the correct order I
came up with the following code which uses a PeriodicalExecuter to
step through an array of script names as and when an anticipated
object in each script becomes available.
function loadScripts() {
var scripts = [
{ src: 'js/s1.js' , obj: 'Stu.s1.func' },
{ src: 'js/s2.js' , obj: 'Stu.s2.func' },
{ src: 'js/s3.js' , obj: 'Stu.s3.func' }
];
var stoptime = (new Date()).getTime() + 5000;
var i = 0;
var scriptadded = false;
new PeriodicalExecuter(function(pe) {
if (!scriptadded) {
addScript(scripts[i].src);
scriptadded = true;
}
if ((window.Stu) && (eval(scripts[i].obj))) {
// Found an object
if (i < scripts.length - 1) {
// Prepare to load next script
i++;
scriptadded = false;
} else {
// That was the last script
pe.stop();
callback_success();
}
} else if ((new Date()).getTime() > stoptime) {
pe.stop();
callback_outoftime();
}
}, 0.2);
}
File "s1.js" is defined as:
var Stu = window.Stu || {};
Stu.s1 = { func: function () { alert('s1'); } };
File "s2.js" is defined as:
var Stu = window.Stu || {};
Stu.s2 = { func: function () { alert('s2'); } };
File "s3.js" is defined as:
var Stu = window.Stu || {};
Stu.s3 = { func: function () { alert('s3'); } };
The function addScript() is not defined here but was inspired by [1].
The function callback_success() will do something interesting with my
dynamically-loaded functionality ;)
The problem is that it works in IE8, Safari 4 and Chrome 3 but not
Firefox 3.5.7. It breaks when eval'ing the second object, i.e.
"Stu.s2.func". I guess this is something to do with scope, but why
should it work the first time around?
I appreciate this might not be a Prototype issue per se but any advice
would be welcome. I'm currently considering making one long script to
avoid any of these shenanigans.
Thanks
Stuart
[1] http://proto-scripty.wikidot.com/prototype:how-to-load-scripts-dynamically.
> I guess this is something to do with scope, but why
> should it work the first time around?
On the first loop, `scriptadded` will be false and so you do call
`addScript` and it all works. On the second loop, `scriptadded` will
be true, and so you never call `addScript` and the second script isn't
loaded.
OT: Why the `eval`? You can check whether the symbol exists directly,
you don't need `eval` to do it. (Avoid `eval` whenver possible.) It
requires a very small bit of rejigging your scripts array, but it's
easily done...
HTH,
--
T.J. Crowder
Independent Software Consultant
tj / crowder software / com
www.crowdersoftware.com
> [1]http://proto-scripty.wikidot.com/prototype:how-to-load-scripts-dynami....
'scriptadded' does get reset to false just after the loop counter 'i'
is incremented, and in fact through Firebug I can see the <script/>
tags appearing in the HTML - so I know this aspect of the code is
working.
> OT: Why the `eval`?
I know its use is to be avoided but I couldn't see a way around it.
If, for example, my array looked like this...
var scripts = [
{ src: 'js/s1.js' , obj: Stu.s1.func },
{ src: 'js/s2.js' , obj: Stu.s2.func },
{ src: 'js/s3.js' , obj: Stu.s3.func }
];
...then 'scripts[i].obj' will be null because at declaration time none
of the Stu literals exist. If there is an easy workaround I'd
appreciate a pointer ;)
Thanks
Stuart
> 'scriptadded' does get reset to false just after the loop counter 'i'
> is incremented
Doh! Sorry I missed that.
>, and in fact through Firebug I can see the <script/>
> tags appearing in the HTML - so I know this aspect of the code is
> working.
I think your issue is with these lines in the scripts:
var Stu = window.Stu || {};
You're declaring that via `var`, but looking for it as a property of
`window`. There is some weirdness about global `var` statements and
window properties which I'd try to avoid by being explicit:
if (!window.Stu) {
window.Stu = {};
}
...or if you prefer the functional style:
window.Stu = window.Stu || {};
> > OT: Why the `eval`?
>
> I know its use is to be avoided but I couldn't see a way around it.
You can check the individual parts of the path. If you change
`scripts` to
var scripts = [
{ src: 'js/s1.js' , obj: ['Stu', 's1', 'func'] },
{ src: 'js/s2.js' , obj: ['Stu', 's2', 'func'] },
{ src: 'js/s3.js' , obj: ['Stu', 's3', 'func'] }
];
...you could use
function checkObjExists(context, path) {
var index;
for (index = 0; index < path.length; ++index) {
context = context[path[index]];
if (!context) {
// This part wasn't found
return false;
}
}
// Found
return true;
}
...like so:
if ((window.Stu) && (checkObjExists(window, scripts[i].obj))) {
If you prefer to keep your scripts string the way it is, just use
`scripts[i].obj.split('.')` to create the array on the fly.
Both modifications were required and the checkObjExists() function is
dead handy.
I really am very grateful because this is the best solution to my
dynamic-loading requirement. I wanted to avoid having to rely on
concatenating several scripts into one (although some kind of
optimisation will need to be done to prevent too many separate HTTP
requests), and I also wanted to avoid more dumb, less elegant
solutions. Without your help I wouldn't have got there.
Cheers
Stuart
-- T.J. :-)