Javascript includes not loading for later in the p

2019-04-05 04:21发布

We have a Rails application where we are including our application dependencies in the html head within application.js:

//= require jquery
//= require analytics
// other stuff...

Then on individual pages we have a script tag at the bottom of the page for analytics:

<script>
  analytics.track('on that awesome page');
</script>

This normally works fine, but very occasionally we see the error analytics is not defined, most recently on Chrome 43. Because everything should be loaded synchronously, this seems like it ought to work out of the box, but I changed the script to:

<script>
  $(document).ready(function () {
    analytics.track('on that awesome page');
  });
</script>

And now instead every once in a while we see $ is not defined instead. We don't see any other errors from the same IP, otherwise I would suspect something went wrong in application.js. Any other ideas why it might break? You can see an example page here.

The full application.js:

// Polyfills
//= require es5-shim/es5-shim
//= require es5-shim/es5-sham
//= require polyfills
//
// Third party plugins
//= require isMobile/isMobile
//= require jquery
//
//= require jquery.ui.autocomplete
//= require jquery.ui.dialog
//= require jquery.ui.draggable
//= require jquery.ui.droppable
//= require jquery.ui.effect-fade
//= require jquery.ui.effect-slide
//= require jquery.ui.resizable
//= require jquery.ui.tooltip
//
//= require jquery_ujs
//= require underscore
//= require backbone
//= require backbone-sortable-collection
//= require bootstrap
//= require load-image
//= require react
//= require react_ujs
//= require classnames
//= require routie
//= require mathjs
//= require moment
//= require stink-bomb
//= require analytics
//
// Our code
//= require_self
//= require extensions
//= require extend
//= require models
//= require collections
//= require constants
//= require templates
//= require mixins
//= require helpers
//= require singletons
//= require actions
//
//= require object
//= require components
//= require form_filler
//= require campaigns
//= require form_requests
//= require group_wizard
//= require step_adder

Chalk = {};
underscore = _;

_.templateSettings = {
  evaluate:    /\{\{(.+?)\}\}/g,
  interpolate: /\{\{=(.+?)\}\}/g,
  escape:      /\{\{-(.+?)\}\}/g
};

moment.locale('en', {
  calendar: {
    lastDay: '[Yesterday at] LT',
    sameDay: '[Today at] LT',
    nextDay: '[Tomorrow at] LT',
    lastWeek: 'dddd [at] LT',
    nextWeek: '[Next] dddd [at] LT',
    sameElse: 'L LT'
  }
});

Update:

We're still seeing this on production occasionally. We've also seen it in a case where we load a script before application.js and then reference it within:

javascript_include_tag 'mathjs'
javascript_include_tag 'application'

Every so often we see a math is not defined error. I'm wondering if an error happens during the loading of mathjs or other scripts preventing it from being loaded, but the fact that it happens on so many different libraries, and so infrequently, makes it seem less likely. We did put in some debug checks to see whether our application.js is fully loaded and it often doesn't seem to be, even if accessing something like Jquery later in the page.

One motivation in this was to avoid old browser notifications about scripts running too long, but we may just give up and pull it all into application.js to avoid the errors.

7条回答
在下西门庆
2楼-- · 2019-04-05 04:28

As others have described, you are loading scripts and then trying to execute code before your scripts have finished loading. Since this is an occasionally intermittent error around a relatively small amount of code located in your HTML, you can fix it with some simple polling:

(function runAnalytics () {
  if (typeof analytics === 'undefined') {
    return setTimeout(runAnalytics, 100);
  }
  analytics.track('on that awesome page');
}());

... the same can be used for other libraries, hopefully you see the pattern:

(function runJQuery () {
  if (typeof $ === 'undefined') {
    return setTimeout(runJQuery, 100);
  }
  $(document).ready(...);
}());

Edit: As pointed out in the comments, the load events would be better suited for this. The problem is that those events might have already fired by the time the script executes and you have to write fallback code for those scenarios. In favor of not writing 10+ lines of code to execute 1 line, I suggested a simple, non-invasive, quick and dirty, sure to work on every browser polling solution. But in order to not get any more downvotes, here's a more proper tedious and over complicated solution in case you have some beef with polling:

<script>
// you can reuse this function
function waitForScript (src, callback) {
  var scripts = document.getElementsByTagName('script');
  for(var i = 0, l = scripts.length; i < l; i++) {
    if (scripts[i].src.indexOf(src) > -1) {
      break;
    }
  }
  if (i < l) {
    scripts[i].onload = callback;
  }
}

function runAnalytics () {
  analytics.track('on that awesome page');
}

if (typeof analytics === 'undefined') {
  waitForScript('analytics.js', runAnalytics);
} else {
  runAnalytics();
}

function runJQuery () {
  $(document).ready(...);
}

if (typeof $ === 'undefined') {
  waitForScript('jquery.min.js', runJQuery);
} else {
  runJQuery();
}
</script>
查看更多
ゆ 、 Hurt°
3楼-- · 2019-04-05 04:36

It has a cool solution. Make js loading correctly for your application. Here, I am loading jQuery first then loading analytics.js . I hope this will solve your problem for html5 supported browsers. Code:

    var fileList =[
                    'your_file_path/jQuery.js',
                    'your_file_path/analytics.js'
                  ];

    fileList.forEach(function(src) {
    var script = document.createElement('script');
    script.src = src;
    script.async = false;
    document.head.appendChild(script);
 });

This code snippet will load jQuery first and then load analytics.js file. Hopefully, this will fix your issue.

查看更多
走好不送
4楼-- · 2019-04-05 04:38

Since scripts tend to load at random orders you may force your analytics script to load after everything is up and ready.

There are various approaches for this task.

HTML5 rocks has given a quite nice snippet for this kind of stuff. Moreover, depending on your needs you may use a module loader like require.js. Using loading promises and Require.js is pretty sweet too. Generally, you use a lot of JavaScript a module loader will help you with this mess.

I kept the ugliest and least efficient, still one solid approach for the end. Waiting for an external library to load may create huge bottlenecks and should be considered and also handled really carefully. Analytics fetching script is async and really hard to handle. On these cases I prefer

Loading external deferred scripts is somehow easy:

function loadExtScript(src, test, callback) {
  var s = document.createElement('script');
  s.src = src;
  document.body.appendChild(s);

  var callbackTimer = setInterval(function() {
    var call = false;
    try {
      call = test.call();
    } catch (e) {}

    if (call) {
      clearInterval(callbackTimer);
      callback.call();
    }
  }, 100);
}

Take a look at this example where I am loading jQuery dynamically with a promise function when the script is loaded: http://jsfiddle.net/4mtyu/487/

Here is a chaining demo loading Angular and jQuery in order :http://jsfiddle.net/4mtyu/488/

Considering the example above you may load your analytics as:

loadExtScript('https://code.jquery.com/jquery-2.1.4.js', function () {
    return (typeof jQuery == 'function');
}, jQueryLoaded);

function jQueryLoaded() {
    loadExtScript('https://analytics-lib.com/script.js', function () {
        return (typeof jQuery == 'function');
    }, analyticsLoadedToo);
}

function analyticsLoadedToo() {
    //cool we are up and running
    console.log("ready");
}
查看更多
Animai°情兽
5楼-- · 2019-04-05 04:45

This code will load each script URL put in libs in the exact order, waiting for one to fully load before adding the next one. It's not as optimised than letting the browser doing it, but it allow you to monitor the errors and force the order of the loading.

(function(){
    var libs = [
        "http://example.com/jquery.js",
        "http://example.com/tracker.js",
        "http://example.com/myscript.js"
    ];
    var handle_error = function() {
        console.log("unable to load:", this.src);
    };
    var load_next_lib = function() {
        if(libs.length) {
            var script = document.createElement("script");
            script.type = "text/javascript";
            script.src = libs.shift();
            script.addEventListener("load", load_next_lib);
            script.addEventListener("error", handle_error);
            document.body.appendChild(script);
        }
    };
    load_next_lib();
})();

But I would advise you to check every <script> tag of your website and see if they have a defer="" or async="" attribute. Most issues come from these because they tell the browser to execute the script later. They may also just be in the wrong order.

查看更多
做自己的国王
6楼-- · 2019-04-05 04:46

The basis of your problem probably lies in your assumption:

everything should be loaded synchronously

Everything is most decidedly not loaded synchronously. The HTTP 1.1 protocol supports piplining and due to chunked transfer encoding, your referenced objects may or may not complete loading before your main webpage has finished loading.

All of this happens asynchronously and you can't guarantee the order in which they are loaded. This is why browsers make multiple parallel connections to your web server when loading a single webpage. Because javascript and jQuery are event driven, they are by nature asynchronous which can become confusing if you don't understand that behavior well.

Compounding your problem is the fact that document onload JavaScript event (remember, jQuery just extends JavaScript) "is called when the DOM is ready which can be prior to images and other external content is loaded." And yes, this external content can include your link to the jquery.js script. If that loads after the DOM, then you will see the error "$ is not defined". Because the linked script has not yet loaded, the jquery selector is undefined. Likewise, with your other linked libraries.

Try using $(window).load() instead. This should work when all the referenced objects and the DOM has loaded.

查看更多
手持菜刀,她持情操
7楼-- · 2019-04-05 04:47

We're still seeing this error a few times a day and haven't reached any conclusive answer to what is causing it. My best guess is that either the earlier scripts time out downloading, or a user is quickly navigating between links, preventing the page from loading completely. They might also just be using the back button causing weird behavior in scripts on the page.

查看更多
登录 后发表回答