7

I've been tracking down a bug for days... then I realized the bug was me. :/

I had been using webRequest.onComplete, filtered for scripts. My error was that I made the incorrect association between the scripts being loaded and being executed. The get loaded in a different order than they get executed, and thus the timing of the events is not in the order I need them in. I need to inject between certain scripts so I need an event right after a file has been executed and before the next one.

The only solution I can think of at the moment is to alter the JS being loaded before it gets executed. But it makes my stomach turn. And the bfcache would wreak even more havoc, so not a great solution either.

I would use the HTML5 spec's afterscriptexecute, but that is not implemented in Chrome. Is there another API, perhaps an extension API that I can use?

sroussey
  • 408
  • 4
  • 6
  • By the way, can't use the HTML5 load event, it is a simple event, and therefore does not bubble. – sroussey Aug 28 '13 at 17:00
  • You can see when various events are executed with this unpacked extension: https://github.com/simov/event-logger. It's not a direct solution to your problem but it might help you figure out which event you should listen for. Also you can click on the objects in the console to see what data you have at each step. – simo Sep 01 '13 at 10:35

3 Answers3

8

Note: This method no longer works as of Chrome 36. There are no direct alternatives.

Note: The answer below only applies to external scripts, i.e. those loaded with <script src>.

In Chrome (and Safari), the "beforeload" event is triggered right before a resource is loaded. This event allows one to block the resource, so that the script is never fetched. In this event, you can determine whether the loaded resource is a script, and check whether you want to perform some action

This event can be used to emulate beforescriptexecute / afterscriptexecute:

document.addEventListener('beforeload', function(event) {
    var target = event.target;
    if (target.nodeName.toUpperCase() !== 'SCRIPT') return;
    var dispatchEvent = function(name, bubbles, cancelable) {
        var evt = new CustomEvent(name, {
            bubbles: bubbles,
            cancelable: cancelable
        });
        target.dispatchEvent(evt);
        if (evt.defaultPrevented) {
            event.preventDefault();
        }
    };
    var onload = function() {
        cleanup();
        dispatchEvent('afterscriptexecute', true, false);
    };
    var cleanup = function() {
        target.removeEventListener('load', onload, true);
        target.removeEventListener('error', cleanup, true);
    }
    target.addEventListener('error', cleanup, true);
    target.addEventListener('load', onload, true);

    dispatchEvent('beforescriptexecute', true, true);
}, true);

The dispatch times are not 100% identical to the original ones, but it is sufficient for most cases. This is the time line for the (non-emulated) events:

beforeload             Before the network request is started
beforescriptexecute    Before a script executes
afterscriptexecute     After a script executes
onload                 After the script has executed

Here's an easy way to see that the events are working as expected:

window.addEventListener('afterscriptexecute', function() {
    alert(window.x);
});
document.head.appendChild(document.createElement('script')).src = 'data:,x=1';
document.head.appendChild(document.createElement('script')).src = 'data:,x=2';

The demo can be seen live at http://jsfiddle.net/sDaZt/

Rob W
  • 341,306
  • 83
  • 791
  • 678
  • I didn't know beforeload was a bubbling event (load is not, which is an incredible pain). I think this will work! Ingenious, thanks! – sroussey Sep 05 '13 at 22:06
  • @sroussey FYI, `beforeload` might be removed from a future Chrome version (see http://crbug.com/333318). – Rob W Jan 13 '14 at 13:55
0

I'm not familiar with Chrome Extensions (only browser javascript), but I think that you will unfortunately have to edit your loaded JS so that is calls a function of your choice when it is executed, if you want to do this nicely. This it what Google does for asynchronously loading its Maps Javascript file:

function loadScript() {
  var script = document.createElement("script");
  script.type = "text/javascript";
  script.src = "http://maps.googleapis.com/maps/api/js?sensor=false&callback=executed";
  document.body.appendChild(script);
}

function executed() {
  /* Google maps has finished loading, do awesome things ! */
}

If you really don't want to edit your loaded JS files, you could have a setInterval (or a recursive function with setTimeout) checking regularly if some functions or variables are initialized.

Romain Paulus
  • 2,306
  • 1
  • 20
  • 20
  • I can't use either technique. I don't know the files that are being loaded, so I don't know what to look for. And a timeout won't work, as it does not guarantee that by code will run between the JS run as a result of two script tags. – sroussey Aug 21 '13 at 17:43
0

Have you tried script loading using Modernizr.js?

I had a similar issue, where the timing of script loading was causing conflict. I used Modernizr.js, which includes the library yepnope.js by default. Below is an example of some scripts I loaded conditionally. You can include a test clause, or simply load them in the order you prefer, with the guarantee that they will load and execute in the order you wish due to the callback.

Here is an example with a conditional clause:

Modernizr.load({
    test: false, //Or whatever else you'd like. Can be conditional, or not so conditional
    yep: {
        'script1': 'MyJavascriptLibrary1.js'
    },
    nope: {
        'script2': 'MyJavascriptLibrary2.js',
        'script3': 'MyJavascriptLibrary3.js'
    },
    callback: {
        'script1': function (url, result, key) {
            console.log('MyJavascriptLibrary1.js loaded'); //will not load in this example
        },
        'script2': function (url, result, key) {
            console.log('MyJavascriptLibrary2.js loaded first');
        },
        'script3': function (url, result, key) {
            console.log('MyJavascriptLibrary3.js loaded second');
        }
    }
});

If triggering false, MyJavascriptLibrary2.js and MyJavascriptLibrary3.js will load in the appropriate order, no matter what elements influence how they would behave normally (file size, connection speed, etc.). In these callbacks, you may fire additional javascript as well, in the order you wish to do so. Example:

'script2': function (url, result, key) {

            alert('anything in here will fire before MyJavascriptLibrary3.js executes');
        },

Note this can be done without Modernizr.load({...

but using simply yepnope({...

For more documentation, check out the yepnope.js API

scniro
  • 16,844
  • 8
  • 62
  • 106
  • I am working on an extension, and thus don't get to author the page, otherwise a whole set of tools would be available to me. Like this one. :) – sroussey Sep 05 '13 at 18:47