Skip to content

Loading…

jQuery plugins not loading after jQuery fallback is used #82

Closed
Jakobud opened this Issue · 18 comments

5 participants

@Jakobud

I have the following:

yepnope([
{
  load: '//ajax.googleapisISOFFLINE.com/ajax/libs/jquery/1.7.1/jquery.min.js',
  complete: function()
  {
    //jQuery fallback
    if ( !window.jQuery )
    {
      console.log("Loading the jQuery fallback...");
      yepnope('http://cdnjs.cloudflare.com/ajax/libs/jquery/1.7.1/jquery.min.js');
    }
  }
},
{
  load: 'http://malsup.github.com/jquery.cycle.all.js',
  complete: function()
  {
    console.log("Done loading the jQuery plugins");
  }
}
]);

Console output:
GET http://ajax.googleapisisoffline.com/ajax/libs/jquery/1.7.1/jquery.min.js <-- 404
Loading the jQuery fallback... <-- alternate jQuery loaded
jquery.cycle.all.js:1062 - Uncaught ReferenceError: jQuery is not defined <-- plugin can't find jQuery
Done loading the jQuery plugins

So I force yepnope to load the alternate jQuery. Then when trying to load the jQuery plugin, it can't find jQuery. Why is this? I can reproduce this with any jQuery plugin (certain plugins report that $ is not defined instead of jQuery).

Also, after trying to load jQuery from the Google APIs CDN (which I'm pretending is offline) there is about a 5-8 second delay where nothing happens. Then all the sudden the jQuery plugin is fetched (but fails to load). Where is this 5-8 second delay coming from? The original jQuery request timing out or something?

Am I doing something wrong?

Here is a jsfiddle to reproduce the problem: http://jsfiddle.net/Vyb44/6/

@SlexAxton
Owner

Hi Jake,

I think you're running into two things.

The delay comes from the fact that in some browsers errors aren't passed on to the onload event of scripts. We'd love to get these errors, but we can't act on the ones that we don't get. We put the timeout in place for this reason.

The other problem is a bug in yepnope that we've fixed in v1.1 (not yet released) but is pretty easy to work around yourself. Just use a callback instead of a complete when you're relying on the fallback. I think that should solve it.

@Jakobud

Thanks Alex! I will try using the callback instead.

@SlexAxton SlexAxton closed this
@Jakobud

hey Alex, any word on when you guys are going to push out v1.1?

@SlexAxton
Owner
@Jakobud

Woot!

@Jakobud

Hey Alex,

Just wanted to revisit this issue again with the latest version. It looks like the above bug is fixed, but there is another issue that is semi-related.

To summarize: I'm not sure where the best place is to put jQuery-dependent code at that will ensure it runs even if the jQuery fallback script is used.

Take the following example where the CDN is offline and the fallback is used:

yepnope([{
    load: '//ajax.googleapisOFFLINE.com/ajax/libs/jquery/1.7.1/jquery.min.js',
    complete: function () {
        if (!window.jQuery) {
            yepnope('/js/jquery-1.7.1.min.js');
        }

        $("div.whatever").css("color","red");
    }
}]);

This code will fail, because the jQuery code used to change the css of div.whatever runs before the fallback is done loading.

Now, if you do the following code, it works fine:

yepnope([{
    load: '//ajax.googleapisOFFLINE.com/ajaxX/libs/jquery/1.7.1/jquery.min.js',
    complete: function () {
        if (!window.jQuery) {
            yepnope('/js/jquery-1.7.1.min.js');
        }
    }
},
{
    load: "/js/jquery.plugin.js",
    complete: function() {
        $("div.whatever").css("color","red");
    }
}]);

This works, because jquery.plugin.js doesn't try to load until after the fallback is loaded. This works great. But what about the case where you don't have any plugins to load and you simply just need to run some jQuery to manipulate the DOM or CSS or whatever?

Can't do this:

yepnope([{
    load: '//ajax.googleapisOFFLINE.com/ajaxX/libs/jquery/1.7.1/jquery.min.js',
    complete: function () {
        if (!window.jQuery) {
            yepnope('/js/jquery-1.7.1.min.js');
        }
    }
},
{
    load: "",
    complete: function() {
        $("div.whatever").css("color","red");
    }
}]);

or this:

yepnope([{
    load: '//ajax.googleapisOFFLINE.com/ajaxX/libs/jquery/1.7.1/jquery.min.js',
    complete: function () {
        if (!window.jQuery) {
            yepnope('/js/jquery-1.7.1.min.js');
        }
    }
},
{
    load: null,
    complete: function() {
        $("div.whatever").css("color","red");
    }
}]);

because the div.whatever code doesn't wait until the jQuery fallback is loaded to run, since there is no script to load and wait on.

Also can't do this:

yepnope([{
    load: '//ajax.googleapisOFFLINE.com/ajax/libs/jquery/1.7.1/jquery.min.js',
    complete: function () {
        if (!window.jQuery) {
            yepnope('/js/jquery-1.7.1.min.js');
        }

        $(document).ready(function(){
            $("div.whatever").css("color","red");
        });
    }
}]);

And you can't put it outside yepnope either:

yepnope([{
    load: '//ajax.googleapisOFFLINE.com/ajax/libs/jquery/1.7.1/jquery.min.js',
    complete: function () {
        if (!window.jQuery) {
            yepnope('/js/jquery-1.7.1.min.js');
        }
    }
}]);

$(document).ready(function(){    // $ is undefined
    $("div.whatever").css("color","red");
});

or simply:

yepnope([{
    load: '//ajax.googleapisOFFLINE.com/ajax/libs/jquery/1.7.1/jquery.min.js',
    complete: function () {
        if (!window.jQuery) {
            yepnope('/js/jquery-1.7.1.min.js');
        }
    }
}]);

$("div.whatever").css("color","red");   // $ still undefined

The only solution I can come up with is this:

yepnope([{
    load: '//ajax.googleapisOFFLINE.com/ajax/libs/jquery/1.7.1/jquery.min.js',
    complete: function () {
        if (!window.jQuery) {
            yepnope('/js/jquery-1.7.1.min.js');
        }
    }
},
'/js/another.script.js'
]);

In that example I just load up another file and have all my jQuery init/setup stuff in it. This works because it waits to load that other script until after all the other stuff runs, including the jQuery fallback. The only bad thing about this solution is that you have to load another file, which if is really small, like <4kb then it's more optimal to have it inline with the yepnope loading script...

First of all, again, perhaps I'm misunderstanding how to use yepnope properly. Is there an obvious place I should be placing jQuery stuff that will ensure it runs properly even after the fallback is used?

There are two possible solutions I can think of with regards to improving yepnope to fix this problem:

  1. The whole problem would be solved (I think) if the yepnope('/js/jquery-1.7.1.min.js') fallback had an option to load the script synchronously instead of asynchronously. This would make the rest of the complete function wait until the script was loaded before continuing.

  2. The following would work:

yepnope([{
    load: '//ajax.googleapisOFFLINE.com/ajaxX/libs/jquery/1.7.1/jquery.min.js',
    complete: function () {
        if (!window.jQuery) {
            yepnope('/js/jquery-1.7.1.min.js');
        }
    }
},
{
    load: "",
    complete: function() {
        $("div.whatever").css("color","red");
    }
}]);

if you still treated the empty script name as a script and putting in the loading queue anyways. I see in the yepnope code that if the script is empty then it just returns. If it continued like usual, added the empty script to the queue and then when the script came to the front of the queue, return at that point, then it would maybe work then as well, because the complete function wouldn't run until then, at which point all the previous scripts (including the fallback) would have run because they already went through the queue.

Anyways, let me know what you think and please let me know if I'm just doing everything wrong :-)

@SlexAxton
Owner

Awesomely descriptive problem. I think yepnope could potentially solve this one, but I'd have to be sold on a better api than an empty string.

I have two solutions for you, both of which I don't think are ideal:

// This solution just uses a single function to kick off things when everything is ready.
(function(){
  function jqInit(){
    $(function(){
      $("div.whatever").css("color", "red");
    });
  }

  yepnope([{
    load: '//ajax.googleapisOFFLINE.com/ajaxX/libs/jquery/1.7.1/jquery.min.js',
    complete: function () {
        if (!window.jQuery) {
            yepnope({ load: '/js/jquery-1.7.1.min.js', complete: jqInit });
        }
        else {
          jqInit();
        }
    }
  }]);
})();

The following way is a hack that uses the new 'non-reexcuting' script as a means to an end.

yepnope([{
    load: '//ajax.googleapisOFFLINE.com/ajaxX/libs/jquery/1.7.1/jquery.min.js',
    complete: function () {
        if (!window.jQuery) {
            yepnope('/js/jquery-1.7.1.min.js');
        }
    }
},
{
    load: "//ajax.googleapisOFFLINE.com/ajaxX/libs/jquery/1.7.1/jquery.min.js",
    complete: function() {
        $("div.whatever").css("color","red");
    }
}]);

I didn't test that second one, because it's a bit silly, but I think it might work. Feel free to suggest the right syntax for this. It'd have to look and feel better than the first example though.

@SlexAxton
Owner

You could also maybe use the callback and complete properties to your advantage.

yepnope({
    load: '//ajax.googleapisOFFLINE.com/ajaxX/libs/jquery/1.7.1/jquery.min.js',
    callback: function () {
        if (!window.jQuery) {
            yepnope('/js/jquery-1.7.1.min.js');
        }
    },
    complete: function () {
      $(function(){
        $("div.whatever").css("color","red");
      });
    }
});
@Jakobud

Yeah I was going to also suggest the solution you mentioned above about a separate function that is called in both of those places. Hadn't considered the non-reexecuting thing, although I do recall reading about it in the changelog. I'll mess with the callback function. Haven't used that before.

And I agree, a blank/empty script name isn't a good solution. I'll take a look at the yepnope code and see if any better ideas hit him. Honestly, it kind of goes against the spirit of yepnope and it's async loading style, but having the option to synchronously load a script if you want would be nice in situations like this. Not sure how hard that would be to pull off with the existing code base.

Thanks for the suggestions, Alex!

@Jakobud

Okay so some more experimentation:

The combination of complete/callback didn't seem to work either. I think I'm doing it right... should that have worked?

I also tried the following (that I really thought would work actually).

yepnope([{
    load: '//ajax.googleapisOFFLINE.com/ajax/libs/jquery/1.7.1/jquery.min.js',
    complete: function ()
    {
        if (!window.jQuery)
        {
                console.log("loading local file");

                var script=document.createElement('script');
                script.setAttribute("type","text/javascript");
                script.setAttribute("src", 'local.jquery.min.js');
                document.getElementsByTagName("head")[0].appendChild(script);

                console.log("done loading local file");
        }

        console.log("some jQuery stuff");

        $("div.whatever").css("color","red");

        console.log("done with jQuery stuff");
    }
}]);

It just manually appends the local script to the head. I thought this would work for sure... Not sure why. I was thinking that if I didn't have the async attribute in there, that the script would not continue until it was done loading. I guess I was wrong. It looks like it appends the script node (at which point the local jquery .js starts to load) and then the script continues, at which it fails since $ still isn't defined yet.

I also tried this:

yepnope([{
    load: '//ajax.googleapisOFFLINE.com/ajax/libs/jquery/1.7.1/jquery.min.js',
    complete: function ()
    {
        if (!window.jQuery)
        {
                console.log("loading local file");

                var script=document.createElement('script');
                script.setAttribute("type","text/javascript");
                script.setAttribute("src", 'local.jquery.min.js');
                document.getElementsByTagName("head")[0].appendChild(script);

                while ( window.jQuery == null )
                {
                    setTimeout(function() {console.log("still not loaded yet");},100);
                }

                console.log("done loading local file");
        }

        console.log("some jQuery stuff");

        $("div.whatever").css("color","red");

        console.log("done with jQuery stuff");
    }
}]);

Tried to get the script just to sit in limbo until jQuery was loaded but it seemed to go on forever. This probably doesn't work because setTimeout isn't really "pausing" the script.

Honestly at this point, I think that the best solution here is go the external function route as you suggested above. Define a function that "inits" all your jQuery stuff. Set it to run after the fallback is run and run it if no fallback is run.

I don't know if it's possible, very well might not be, but adding in an async parameter to yepnope (who's default value is true) would possibly solve the problem. Something like this:

yepnope([{
    load: '//ajax.googleapisOFFLINE.com/ajaxX/libs/jquery/1.7.1/jquery.min.js',
    complete: function () {
        if (!window.jQuery) {
            yepnope({ load: '/js/jquery-1.7.1.min.js', async: false });   // async default value is true
        }

        // Do jQuery-dependent stuff here...         
    }
  }]);

I think that would be the prettiest solution. But it might not be possible with JS, not sure... I don't know if you can "pause" the script until the script is loaded or not. I don't understand all the yepnope code, but perhaps doing something with the readyState stuff in there... test it to see if it's done loading or not before going to the next script/css in the stack... not sure.

@SlexAxton
Owner

Injecting a script with javascript will always be async, unless you use document.write, but even then it doesn't happen until the end of the current script, so you still wouldn't have a workable api.

I'm interested in making the callback complete situation work. I feel like it should work.

Just FYI, yepnope exposes yepnope.injectJs to do alot of the script creation and injection that you're rewriting in these attempts.

@Jakobud

Yeah I agree, the callback complete solution should work, at least in the way that I understand how yepnope is supposed to work.

Actually once I think about it, if the callback complete solution was working properly, that really is the proper solution for this entire problem.

@SlexAxton
Owner

Agreed. Will get that working.

@DonSanto

Sweet. I was having the exact same problem. Good to know a solution is on its way. I like the callback/complete solution when you don't have any other jQuery/otherLibrary dependent script to load before domload.

@SlexAxton
Owner

I think this might have actually worked before complete was solidified a bit better in 1.5. I think I know where the issue lies:

Here's a test case:

http://jsbin.com/equzis

Ideally, this would alert 1.7.0

cc-ing @ralphholzmann just so he's aware of this issue.

@Jakobud

Alex, regarding the delay that yepnope has in place for when a script comes back 404, can you point me toward more information about why that is there? Like which browsers do and don't report back errors when trying to load a script and how do you test this?

@ralphholzmann
Collaborator

It looks like the complete callback is not waiting for inner yepnope calls to complete before it fires. This is indeed a bug. I'll look into this right now.

@EnigmaSolved

It looks like the complete callback is not waiting for inner yepnope calls to complete before it fires. This is indeed a bug.

Did this ever get resolved/fixed? I think I am seeing this problem with yepnope 1.5.4 as well as Modernizr 2.5.3 (which I assume is using the same yepnope code). Here is an example:

yepnope([
    {
        load: 'http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js',
        callback: function(url, result, key){
            if (!window.jQuery){yepnope('jquery/1.7.2/jquery.min.js');}
        },
        complete: function(){mft.init();}
    },
    {
        test: mft.ldHS,
        yep: 'highslide/4.1.9/highslide3.js',
        complete: function(){mft.loadHighslide();}
    }
]);

mft.init() and mft.loadHighslide both depend on jQuery. What I am observing is that if mft.ldHS is set to False, then the complete that calls mft.loadHighslide is firing before the complete that calls mft.init and also before jQuery is ready (so I get a jQuery is undefined error). Either I'm misunderstanding how yepnope works (which is certainly possible), or things aren't working the way they are supposed to.

My expectation/interpretation is that the order should be as follows:

  • try to load jQuery from Google, and if that fails, then load local fallback;
  • once jQuery (either one) has loaded, the complete fires that calls mft.init;
  • then, test mft.ldHS; if True, load highslide3.js; either way, call the complete that fires mft.loadHighslide (yes, I know I'm calling this even if highslide3.js is not loaded).

I'm posting this in this thread/issue because it seems like exactly what @ralphholzmann was describing (ie, the complete not waiting). I am guessing that I can use callbacks instead of completes in the meantime, but I'd rather use completes if they can function the way (I think) they're supposed to.

Any help on this would be greatly appreciated! :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.