Skip to content

Commit

Permalink
Add YUI, $script.js, toast, and little-loader
Browse files Browse the repository at this point in the history
Recommend little-loader. Make clear that this is a reference
implementation.
  • Loading branch information
exogen committed Dec 19, 2015
1 parent 91290c5 commit 457e858
Show file tree
Hide file tree
Showing 10 changed files with 12,203 additions and 103 deletions.
94 changes: 60 additions & 34 deletions README.md
@@ -1,19 +1,16 @@
Hey people! You’re welcome to use this code, but I recommend checking out the
even-better-tested, error-handling, well-supported version at
[walmartlabs/little-loader](https://github.com/walmartlabs/little-loader). Enjoy!

[![Build Status][trav_img]][trav_site]

Your script loader probably doesn’t have the callback behavior you want.
We test some competing loaders for atomic `onload` behavior in our build matrix.

Using a popular library?

[![HeadJS Status][headjs_img]][trav_site]
[![jQuery Status][jquery_img]][trav_site]
[![LABjs Status][labjs_img]][trav_site]
[![RequireJS Status][requirejs_img]][trav_site]
[![$script.js Status][scriptjs_img]][trav_site]
[![yepnope Status][yepnope_img]][trav_site]
[![YUI Status][yui_img]][trav_site]

Or perhaps one of these lesser-known packages?

Expand All @@ -23,49 +20,65 @@ Or perhaps one of these lesser-known packages?
[![loads-js Status][loads-js_img]][trav_site]
[![script-load Status][script-load_img]][trav_site]
[![scriptload Status][scriptload_img]][trav_site]
[![toast Status][toast_img]][trav_site]

Sorry.

Introducing…

# script-atomic-onload

```bash
npm install script-atomic-onload
```
A build matrix of every script loader ever made.

This project tests script loaders for atomic `onload` support, which is the
only correct behavior. It also contains a reference implementation of correct
behavior, which has been adopted in the production-ready
[little-loader][little-loader] module.

[![little-loader Status][little-loader_img]][trav_site]

:trophy: **[little-loader][little-loader] is the only correct script loader ever made.**


## An asynchronous script loader with atomic/synchronous `onload` behavior everywhere
### The Only Correct Behavior

Yes, calling `onload` *immediately* (aka synchronously or atomically) after a
`<script>` has executed is the correct and officially defined behavior. So
what’s the problem? **Internet Explorer.** Below version 10, getting this
behavior requires you jump through some hoops, and *many* script loaders get it
wrong or just don’t try. Even [jQuery’s `getScript`](https://api.jquery.com/jquery.getscript/)
does not make this guarantee, documenting that “The callback is fired once the
script has been loaded but not necessarily executed.”
behavior requires you jump through some hoops. Some script loaders just don’t
try; for example, [jQuery’s `getScript`][getScript] does not make this
guarantee, documenting that “The callback is fired once the script has been
loaded but not necessarily executed.” Those that do try often try *very hard*
and end up being far too clever and still incorrect.

If you haven’t designed for it by bundling all your code or using a system
like AMD, having other code run in between your script and its `onload`
callback can be potentially disastrous. For instance, let’s say you make a
widget people can load on their site, and it relies on jQuery. You want to load
jQuery from one of the many CDNs that publish it. But since your widget might
be used on sites that already use jQuery, you need to use `jQuery.noConflict` to
keep yours isolated. The problem comes when you load your version of jQuery in
IE, and before its `onload` callback fires, other code on the site can see it
and, mistaking it for a different instance of jQuery, start attaching plugins
and such to it. Eventually your `noConflict` gets called, but it’s too late –
the plugins are attached to the wrong jQuery instance.

**There may be other script loading libraries that already do this, but I’ve
never found one.** If I did, I’d add it to the CI build matrix and you’d see
its results above. This particular implementation may not be widely adopted,
but it has been battle-tested on many high-traffic, script-laden sites in
production. Just because you’ve never had an issue with your script loader,
doesn’t mean it’s correct! One particular issue that this loader resolved was
only ever seen on one site, and only sometimes (when certain race conditions
were met).

## Usage
jQuery from a CDN. But since your widget might be used on sites that already
use jQuery, you need to use `jQuery.noConflict` to keep yours isolated. When
you load your version of jQuery in IE, it’s possible other code on the page can
see it before your `onload` callback fires. Any code can then modify your
instance of jQuery, adding plugins and such (most likely mistaking it for a
different instance of jQuery). Eventually your `noConflict` gets called, but
it’s too late – the plugins are attached to the wrong jQuery instance. This is
not a problem with jQuery, but with the script loader.

This particular implementation may not be widely adopted, but it has been
battle-tested on many high-traffic, script-laden sites in production. Just
because you’ve never had an issue with your script loader, doesn’t mean it’s
correct! One particular issue that this loader resolved was only ever seen on
one site, and only sometimes (when certain race conditions were triggered).

## Reference Implementation

### Install

```sh
npm install script-atomic-onload
```

### Usage

```javascript
loadScript(src[, callback, thisValue])
Expand All @@ -80,7 +93,7 @@ Arguments:
cross-domain scripts in older versions of IE anyway.)
* `thisValue`: The `this` value that your `callback` will receive.

## Examples
### Examples

```javascript
var loadScript = require('script-atomic-onload');
Expand All @@ -98,7 +111,7 @@ Maybe! Have a look at the results from our build matrix:

Library | Browser Status
------: | --------------
:trophy: **script-atomic-onload** | ![script-atomic-onload Browser Status][script-atomic-onload_browsers_img]
:trophy: **little-loader** | ![little-loader Browser Status][little-loader_browsers_img]
HeadJS | ![HeadJS Browser Status][headjs_browsers_img]
jQuery | ![jQuery Browser Status][jquery_browsers_img]
LABjs | ![LABjs Browser Status][labjs_browsers_img]
Expand All @@ -109,31 +122,44 @@ kist-loader | ![kist-loader Browser Status][kist-loader_browsers_img]
load-script | ![load-script Browser Status][load-script_browsers_img]
loads-js | ![loads-js Browser Status][loads-js_browsers_img]
script-load | ![script-load Browser Status][script-load_browsers_img]
$script.js | ![$script.js Browser Status][scriptjs_browsers_img]
scriptload | ![scriptload Browser Status][scriptload_browsers_img]
YUI | ![YUI Browser Status][yui_browsers_img]

[little-loader]: https://github.com/walmartlabs/little-loader

[trav_img]: https://img.shields.io/travis/exogen/script-atomic-onload/master.svg
[getscript_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=getscript&label=getscript
[headjs_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=headjs&label=HeadJS
[jquery_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=jquery&label=jQuery
[kist-loader_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=kist-loader&label=kist-loader
[labjs_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=labjs&label=LABjs
[little-loader_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=little-loader&label=little-loader
[load-script_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=load-script&label=load-script
[loads-js_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=loads-js&label=loads-js
[requirejs_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=requirejs&label=RequireJS
[scriptjs_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=scriptjs&label=$script.js
[scriptload_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=scriptload&label=scriptload
[script-load_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=script-load&label=script-load
[toast_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=toast&label=toast
[yepnope_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=yepnope&label=yepnope
[trav_site]: https://travis-ci.org/exogen/script-atomic-onload
[yui_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload?branch=master&env=TEST_LOADER=yui&label=YUI

[script-atomic-onload_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=script-atomic-onload
[headjs_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=headjs
[jquery_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=jquery
[labjs_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=labjs
[little-loader_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=little-loader
[requirejs_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=requirejs
[yepnope_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=yepnope
[getscript_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=getscript
[kist-loader_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=kist-loader
[load-script_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=load-script
[loads-js_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=loads-js
[script-load_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=script-load
[scriptjs_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=scriptjs
[scriptload_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=scriptload
[yui_browsers_img]: http://badges.herokuapp.com/travis/exogen/script-atomic-onload/sauce/script-atomic-onload?name=yui

[trav_site]: https://travis-ci.org/exogen/script-atomic-onload
[getScript]: https://api.jquery.com/jquery.getscript
27 changes: 17 additions & 10 deletions index.js
@@ -1,21 +1,28 @@
/**
* NOTE: This is a reference implementation. It's very simple and doesn't
* support `onerror` (another hard problem), but it's known to have the correct
* (atomic) `onload` behavior, and will never change. If you're looking for a
* correct library based on this code, I recommend little-loader:
*
* https://github.com/walmartlabs/little-loader
*
* Script loading is difficult thanks to IE. We need callbacks to fire
* immediately following the script's execution, with no other scripts
* running in between. If other scripts on the page are able to run
* between our script and its callback, bad things can happen, such as
* `jQuery.noConflict` not being called in time, resulting in plugins
* latching onto our version of jQuery, etc.
* immediately following the script's execution, with no other scripts running
* in between. If other scripts on the page are able to run between our script
* and its callback, bad things can happen, such as `jQuery.noConflict` not
* being called in time, resulting in plugins latching onto our version of
* jQuery, etc.
*
* For IE<10 we use a relatively well-documented 'preloading' strategy,
* which ensures that the script is ready to execute *before* appending
* it to the DOM. That way when it is finally appended, it is
* executed immediately.
* For IE<10 we use a relatively well-documented 'preloading' strategy, which
* ensures that the script is ready to execute *before* appending it to the
* DOM. That way when it is finally appended, it is executed immediately.
*
* References:
* 1. http://www.html5rocks.com/en/tutorials/speed/script-loading/
* 2. http://blog.getify.com/ie11-please-bring-real-script-preloading-back/
* 3. https://github.com/jrburke/requirejs/issues/526
* 4. https://connect.microsoft.com/IE/feedback/details/729164/ie10-dynamic-script-element-fires-loaded-readystate-prematurely
* 4. https://connect.microsoft.com/IE/feedback/details/729164/
* ie10-dynamic-script-element-fires-loaded-readystate-prematurely
*/
var _pendingScripts = {},
_scriptCounter = 0;
Expand Down
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -41,11 +41,14 @@
"karma-sourcemap-loader": "^0.3.6",
"karma-webpack": "^1.7.0",
"kist-loader": "^0.4.7",
"little-loader": "^0.1.0",
"load-script": "^1.0.0",
"loads-js": "^1.0.0",
"mocha": "^2.3.3",
"phantomjs": "^1.9.18",
"pyrsmk-toast": "^1.2.7",
"script-load": "^0.1.0",
"scriptjs": "^2.5.8",
"scriptload": "^0.2.0",
"webpack": "^1.12.8"
}
Expand Down
1 change: 1 addition & 0 deletions test/loaders/little-loader.js
@@ -0,0 +1 @@
module.exports = require('little-loader');
1 change: 1 addition & 0 deletions test/loaders/scriptjs.js
@@ -0,0 +1 @@
module.exports = require('scriptjs');
1 change: 1 addition & 0 deletions test/loaders/toast.js
@@ -0,0 +1 @@
module.exports = require('pyrsmk-toast');
24 changes: 24 additions & 0 deletions test/loaders/yui.js
@@ -0,0 +1,24 @@
var YUI = require('../vendor/yui');
var sandbox = YUI(); // YUI instances are called sandboxes.
var Y;

function loadScript(src, callback) {
var options = { async: true }; // Is false by default.
var tx = Y.Get.js(src, options, callback);
// This is the most important bit. Each call is a transaction, and
// transactions are processed serially. That means you can't actually load
// scripts in parallel if you use Get multiple times. Calling `execute` will
// ensure that we fetch right away, in parallel with any other transactions.
tx.execute();
}

module.exports = function(src, callback) {
if (Y) {
loadScript(src, callback);
} else {
sandbox.use('get', function(instance) {
Y = instance; // Save Y instance so we don't have to do this every time.
loadScript(src, callback);
});
}
};
121 changes: 62 additions & 59 deletions test/spec/loadScript.spec.js
@@ -1,66 +1,69 @@
var expect = require("expect.js"); // Can't use Chai due to IE8.
var loadScript = require("../loader");

describe("loadScript", function() {
it("runs the callback after execution", function(done) {
// Sometimes IE (via Sauce Labs) loads these scripts VERY slowly.
this.timeout(300000);
// Could be done with an async library or Promises, but this works fine
// for now.
var count = 0;
function checkDone() {
if (++count === 10) {
done();
}
function jQueryTest(done) {
// Sometimes IE (via Sauce Labs) loads these scripts VERY slowly.
this.timeout(300000);
// Could be done with an async library or Promises, but this works fine
// for now.
var count = 0;
function checkDone() {
if (++count === 10) {
done();
}
function assertIsolatedLoad(version) {
// Need the try/catch because we're in an async callback and any assertion
// errors thrown won't be caught by Mocha; it'll time out instead.
try {
// jQuery must exist.
expect(window.jQuery).to.be.a('function');
// Remove the global.
var jQuery = window.jQuery.noConflict(true);
// Shouldn't be an instance we've previously loaded and marked.
expect(jQuery.FOO).to.be(undefined);
// Should be the expected version.
expect(jQuery.fn.jquery).to.equal(version);
// Mark the instance with a flag.
jQuery.FOO = true;
checkDone();
} catch(err) {
done(err);
}
}
function assertIsolatedLoad(version) {
// Need the try/catch because we're in an async callback and any assertion
// errors thrown won't be caught by Mocha; it'll time out instead.
try {
// jQuery must exist.
expect(window.jQuery).to.be.a('function');
// Remove the global.
var jQuery = window.jQuery.noConflict(true);
// Shouldn't be an instance we've previously loaded and marked.
expect(jQuery.FOO).to.be(undefined);
// Should be the expected version.
expect(jQuery.fn.jquery).to.equal(version);
// Mark the instance with a flag.
jQuery.FOO = true;
checkDone();
} catch(err) {
done(err);
}
loadScript("http://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js", function() {
assertIsolatedLoad("1.11.3");
});
loadScript("http://code.jquery.com/jquery-1.11.3.min.js", function() {
assertIsolatedLoad("1.11.3");
});
loadScript("http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js", function() {
assertIsolatedLoad("1.11.2");
});
loadScript("http://code.jquery.com/jquery-1.11.2.min.js", function() {
assertIsolatedLoad("1.11.2");
});
loadScript("http://code.jquery.com/jquery-1.7.2.min.js", function() {
assertIsolatedLoad("1.7.2");
});
loadScript("http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js", function() {
assertIsolatedLoad("1.11.1");
});
loadScript("http://code.jquery.com/jquery-1.11.1.min.js", function() {
assertIsolatedLoad("1.11.1");
});
loadScript("http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js", function() {
assertIsolatedLoad("1.7.2");
});
loadScript("http://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js", function() {
assertIsolatedLoad("1.11.3");
});
loadScript("http://code.jquery.com/jquery-1.11.3.min.js", function() {
assertIsolatedLoad("1.11.3");
});
}
loadScript("http://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js", function() {
assertIsolatedLoad("1.11.3");
});
loadScript("http://code.jquery.com/jquery-1.11.3.min.js", function() {
assertIsolatedLoad("1.11.3");
});
loadScript("http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js", function() {
assertIsolatedLoad("1.11.2");
});
loadScript("http://code.jquery.com/jquery-1.11.2.min.js", function() {
assertIsolatedLoad("1.11.2");
});
loadScript("http://code.jquery.com/jquery-1.7.2.min.js", function() {
assertIsolatedLoad("1.7.2");
});
loadScript("http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js", function() {
assertIsolatedLoad("1.11.1");
});
loadScript("http://code.jquery.com/jquery-1.11.1.min.js", function() {
assertIsolatedLoad("1.11.1");
});
loadScript("http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js", function() {
assertIsolatedLoad("1.7.2");
});
loadScript("http://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js", function() {
assertIsolatedLoad("1.11.3");
});
loadScript("http://code.jquery.com/jquery-1.11.3.min.js", function() {
assertIsolatedLoad("1.11.3");
});
}

describe("loadScript", function() {
it("atomically fires the callback after execution", jQueryTest);
it("runs the exact same test again for good measure", jQueryTest);
});

0 comments on commit 457e858

Please sign in to comment.