While <script> tags are appealingly simple, there are some situations that require a more sophisticated approach to script loading. Perhaps we want a certain script to load only for users who meet certain requirements, such as premium subscribers or gamers who’ve reached a certain level. Or we may want a certain feature, like a chat widget, to load only when the user clicks to activate it.
In this section, we’ll look at how scripts can load other scripts. After a brief look at low-level approaches, we’ll look at two popular libraries that make script loading a breeze: yepnope and Require.js.
At the browser API level, there are two (reasonable) ways to fetch a script from a server and run it.
Make an Ajax request and then eval the response.
Insert a <script> tag into the DOM.
The latter approach is nicer, since the browser takes care of the work of making an HTTP request for you. Plus, eval has practical problems: leaking scope, making a mess of debugging, and possibly degrading performance. So, to load a script called feature.js, we would insert a <script> tag with some code like this:
| var head = document.getElementsByTagName('head')[0]; |
| var script = document.createElement('script'); |
| script.src = '/js/feature.js'; |
| head.appendChild(script); |
But wait—how do we find out when the script has finished loading? We could, of course, add some code in the script itself to trigger an event, but adding that code to every script we load would be a chore (or, in the case of scripts on a third-party server, impossible). The HTML5 specification defines an onload attribute that we can bind a callback to.
| script.onload = function() { |
| // now we can call functions defined in script |
| }; |
However, onload isn’t supported in IE8 and older, which instead uses onreadystatechange. There are also some weird edge cases in certain browsers when inserting <script> tags. And I haven’t even gotten into error handling! To avoid all of these headaches, I highly recommend using a script-loading library.
yepnope[59] is a simple and lightweight library (just 1.7KB minified and gzipped) designed to serve the most common dynamic loading needs without frills. It can be used on its own or as part of the Modernizr feature detection library.
At its simplest, yepnope loads a script and gives you a callback for when the script has run.
| yepnope({ |
| load: 'oompaLoompas.js', |
| callback: function() { |
| console.log('Oompa-Loompas ready!'); |
| } |
| }); |
Not impressed yet? Let’s use yepnope to load multiple scripts in parallel and run them in the given order. For example, suppose we want to load Backbone.js, which depends on Underscore.js. All we have to do is provide the two script locations in an array as the load parameter.
| yepnope({ |
| load: ['underscore.js', 'backbone.js'], |
| complete: function() { |
| // Backbone logic goes here |
| } |
| }); |
Notice that we used complete instead of callback here. The difference is that callback is run for every resource in the load list, while complete runs only after everything has been loaded.
yepnope’s trademark feature is conditional loading. Given a test parameter, yepnope can load different resources based on whether that value is truthy. For instance, if you’re using Modernizr, you can determine (to some degree of accuracy) whether the user is on a touchscreen device and load different stylesheets and scripts accordingly.
| yepnope({ |
| test: Modernizr.touch, |
| yep: ['touchStyles.css', 'touchApplication.js'], |
| nope: ['mouseStyles.css', 'mouseApplication.js'], |
| complete: function() { |
| // either way, the application is now ready! |
| } |
| }); |
With a handful of lines of code, we’ve set the stage to give users a completely different experience based on their input device. Of course, we don’t need both a yep and a nope for every condition. One of the most common uses of yepnope is loading shims to fill in functionality that’s missing from older browsers.
| yepnope({ |
| test: window.json, |
| nope: ['json2.js'], |
| complete: function() { |
| // now we can JSON safely |
| } |
| }); |
Here’s a good markup structure for a page that uses yepnope:
| <html> |
| <head> |
| <!-- metadata and stylesheets go here --> |
| <script src="headScripts.js"></scripts> |
| <script src="deferredScripts.js" defer></script> |
| </head> |
| <body> |
| <!-- content goes here --> |
| </body> |
| </html> |
Look familiar? This is the same structure we had in the section on defer. The only difference is that yepnope.js has been concatenated into one of the script files (likely at the top of deferredScripts.js), and anything that we need conditionally (because the browser needs a shim) or want to load dynamically (in response to a user action) can be loaded separately. The result should be a smaller deferredScripts.js.
I love yepnope. For relatively simple applications that just want to grab a few shims or load a feature when a user clicks something, yepnope is pretty much perfect. For truly voluminous applications, though, something stronger is called for.
Require.js is the script loader of choice for developers who want to turn the chaos of script-heavy applications into something more orderly. It’s a powerful package capable of sorting out even the most complex dependency graphs automatically with AMD.
We’ll get to AMD in a moment, but first let’s look at a simple script-loading example with Require.js’s eponymous function.
| require(['moment'], function(moment) { |
| console.log(moment().format('dddd')); // day of the week |
| }); |
The require function takes an array of script names and loads all of those scripts in parallel. Unlike yepnope, Require.js doesn’t ensure that the target scripts run in order. Instead, it ensures that they run in an order such that each script’s dependencies are satisfied, provided that those scripts are specified via the Asynchronous Module Definition (AMD).
AMD is a specification[60] that aims to do for the browser what the CommonJS standard has accomplished for the server. (Node.js modules are based on the CommonJS standard.) It mandates a global function (provided by Require.js) called define, which takes three parameters: a name, a list of dependencies, and a callback for when those dependencies are loaded. For example, this is a valid AMD definition for an application that depends on jQuery:
| define('myApplication' ['jquery'], function($) { |
| $('<body>').append('<p>Hello, async world!</p>'); |
| }); |
Notice that the jQuery object, $, is passed to the callback. In fact, the callback will always receive an argument corresponding to each item in the dependency list. You might be wondering how define knew to capture the jQuery object. The answer is that jQuery’s own AMD definition[61] returns jQuery from its define callback, thereby declaring “this is my exported object.”
| define( "jquery", [], function () { return jQuery; } ); |
There’s a little more to AMD than that, but that’s the essence. Adding AMD definitions to every script in your application means you can call require and rest assured that your callback won’t be invoked until not only are your direct dependencies met but their dependencies and their dependencies’ dependencies are as well, all loading with maximum parallelism and running in an order consistent with the dependency graph.
Sounds great, right? But there’s a flip side: while AMD has gotten some traction in the JavaScript community, there are plenty of doubters. Jeremy Ashkenas, for instance, has declined to add the requisite boilerplate to his popular Underscore.js and Backbone.js libraries, awaiting an anticipated ECMAScript module standard. As a result, you can’t count on third-party modules to have their own AMD definitions. Choosing AMD can make your application more consistent, but it can also be a recipe for boilerplate code.
In these last few pages, we’ve seen how you can load a script at runtime via DOM manipulation, and we’ve looked at two libraries for simplifying that process: yepnope, a small, precise tool, and Require.js, a large and powerful one. Which you choose ultimately depends on what kind of application you’re developing and what kind of development team you are. The more “enterprise-y” the application and the bigger the front-end team, the more likely you are to benefit from the AMD-style modularization encouraged by Require.js.
3.145.51.233