Domanda

Here's the scenario. I am doing a $.getScript()function call to get a script in my javascript file. The script that I'm downloading from the $.getScript() tries to download some other scripts that it's dependent on. In my script I'm using done() to check if the script loaded completely or not. And if it did, then I try calling the function that's not on the script that I just loaded form $.getScript but the script that was loaded in it.

It's getting confusing so let me demonstrate with some code:-

//my script.js
$.getScript("http://myexternaljs.com/first.js").done(function(){
    doSecond(); //<- this resides in second.js which is being called in first.js below
}

//first.js
(function(){
    $.getScript("http://firstexternal.com/second.js");
}());

//second.js
function doSecond(){
    return console.log("hello world");
}

The problem here is second.js takes a little time to download so I keep getting doSecond() is undefined error on my call to doSecond() on done().

I could use a timeout and check if second.js loaded or not but is there a better way to do this?

I'm open to any AMD loaders or Promises answers as well.

È stato utile?

Soluzione

You can also use $.ajaxSuccess:

$(document).ajaxComplete(function(ev, jqXhr, options) {
  // You could be as specific as you need to here.
  if ( options.url.match('second.js') ) {
    doSecond();
  }
});

Alternatively, you could do this inside ajaxComplete:

$(document).ajaxComplete(function(ev, jqXhr, options) {
  // You could simplify to 
  // doSecond && doSecond()
  // if you can trust that it will always be a function
  if ( doSecond && $.isFunction(doSecond) ) {
    doSecond();
  }
});

Altri suggerimenti

The facts:

  • You have first.js and within this script is an include for second.js
  • You need to make a call to doSecond() which is defined in second.js
  • You need to ensure doSecond() is available before you call it
  • You can't directly change first.js or second.js but you can have someone else change it

Possible solutions, ordered by best to worst

1) Request that second.js be removed from first.js. Call them separately so that you can nest them:

$.getScript("first.js").done(function(){
  $.getScript("second.js").done(function(){
    doSecond(); 
  });
});

This is the best solution. There are alternatives to this that basically do he same thing in principle (e.g. other people's answers here). If first.js was including second.js synchronously or otherwise forcing load before continuing (e.g. option #3 below), you wouldn't be running up against this problem to begin with. Therefore first.js already must be structured to deal with second.js be *a*sync loaded, so there shouldn't be an issue with them removing it from the file and you calling it yourself.

But you mentioned that the location of second.js is defined in first.js so this somehow isn't feasible to you (why not? can they put the path/to/script in a variable for you to access?)

2) Request that second.js be wrapped in a .done or equivalent loaded callback that pops a callback function that you can define.

// inside first.js
$.getScript("second.js").done(function(){
  if (typeof 'secondLoaded'=='function') 
    secondLoaded();      
});


// on-page or elsewhere, you define the callback
function secondLoaded() {
  doSecond();
}

This is just a generic and easy "callback" example. There are a million ways to implement this principle, depending on what all is actually in these scripts and how much effort people are willing to make to restructure things.

3) Request that second.js script include be changed to be included via document.write

document.write(unescape("%3Cscript src='second.js' type='text/javascript'%3E%3C/script%3E"));

This will force js to resolve document.write before js can move on, so second.js should be loaded by the time you want to use doSecond(). But this is considered bad practice because until document.write is resolved, nothing else can happen. So if second.js is taking forever to load or eventually times out.. that makes for bad UX. So you should avoid this option unless you have no other choice because of "red tape" reasons.

4) use setTimeout to try and wait for it to load.

function secondLoaded() {
  if (!secondLoaded.attempts) secondLoaded.attempts = 0;
  if (secondLoaded.attempts < 5) {
    if (typeof 'doSecond'=='function') {
      doSecond();
    } else {
      secondLoaded.attempts++;
      window.setTimeout('secondLoaded()',100);
    }
  }
}
secondLoaded();

I list this worse than #3 but really it's kind of a tossup.. In this situation you basically either have to pick between deciding a cutoff time to just not execute doSecond() (in this example, I try 5 times at 100ms intervals), or code it to just keep checking forever and ever (remove the .attempts logic or else swap it up w/ setInterval and removeInterval logic).

You could modify how $.getScript works.

$.fn.getScript = (function() {
  var originalLoad = $.fn.getScript;
  return function() {
    originalLoad.apply($, arguments);
    $(document).trigger('load_script')
  };
})();

This will fire an event every time a script is loaded.

So you can wait for these events to fire and check if your method exists.

$(document).one('second_loaded', function() {
  doSecond();
}).on('load_script', function() {
  doSecond && document.trigger('second_loaded');
});

Note that one rather than on. It makes the event fire once.

Have you considered using jQuery.when:

$.when($.getScript("http://myexternaljs.com/first.js"),$.getScript("http://firstexternal.com/second.js"))
.done(function(){
  doSecond();
}

If I were you, I'd facade $.getScript and perform some combination of the above tricks. After reading through the comments it seems that there is a possibility of loading the same script twice.

If you use requirejs this problem is solved for you because it only loads each script once. The answer here is to hang on to the requests made.

Loader:

var requiredScripts = {};

function importScript(url) {
   if (!requiredScripts[url]) {
       requiredScripts[url] = $.getScript(url);
   }

   return requiredScripts[url];
}

Usage:

// should cause 2 requests
$.when(
    importScript('first.js'),
    importScript('second.js')
).done(function() {
    // should cause no requests
    $.when(importScript('first.js')).done(function() {});
    $.when(importScript('second.js')).done(function() {});
});

Real world example here using MomentJS and UnderscoreJS: http://jsfiddle.net/n3Mt5/

Of course requirejs would handle this for you and with better syntax.

Requirejs

define(function(require) {
    var first = require('first'),
        second = require('second');
});
$(window).one("second", function(e, t) {
  if ( $(this).get(0).hasOwnProperty(e.type) && (typeof second === "function") ) { 
   second(); console.log(e.type, e.timeStamp - t); 
   $(this).off("second") 
  };
   return !second
});


$.getScript("first.js")
.done(function( data, textStatus, jqxhr ) {
  if ( textStatus === "success" ) {
    first();
    $.getScript("second.js") 
    .done(function( script, textStatus, jqxhr, callbacks ) { 
      var callbacks = $.Callbacks("once");
      callbacks.add($(window).trigger("second", [ $.now() ]));         
      return ( textStatus === "success" && !!second 
             ? callbacks.fire()
             : $(":root").animate({top:"0"}, 1000, function() { callbacks.fire() })
             )
    });
  }; 
})
// `first.js` : `function first() { console.log("first complete") }`
// `second.js` : `function second() { console.log("second complete") }`
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top