Skip to content

Upgrading to RequireJS 2.0

jrburke edited this page Dec 16, 2016 · 17 revisions

Why

There has been a good deal real world usage of requirejs, and there were some features around configuration that have come up that would be nice to add.

At the same time, there were some configuration options and internal features that have not held their weight, and it would be good to remove them.

Support for the AMD APIs has not changed. If anything, I am hoping that the AMD loader implementers are starting to agree on higher level APIs, like common config.

The changes were more around the configuration and internal operation of requirejs. The hope is that most projects will not see any difference over requirejs 1.0.x, and it should just be a drop-in upgrade.

However, since some configuration options have been removed, and some things about how modules are loaded and executed were changed, semantic versioning dictates that these changes warrant a 2.0 version.

Primary Changes

These are the primary changes:

Delayed module evaluation

In RequireJS 1.0, any built file with a bunch of define() calls in them will have all the modules executed once a top-level require call is hit. This was done to match the behavior of non-modular JavaScript.

However, it was pointed out that this execution order is not desirable with modules since it does not allow for some modules to activate until later. That is useful for delaying the cost of some JS execution until needed, and it better matches the behavior when modules are not all combined into one file.

So RequireJS 2.0 will not execute the module's factory function (the function passed to define()), until there has been a require([]) call that has asked for it, or something that depends on it. The use of data-main on the require.js script tag counts as a require([]) call, so most best-practices use of require.js should not notice a difference.

However, if you are using the almond AMD shim to remove the dependency on require.js, almond does not know about data-main, and you may need to configure a require([]) call to your top-level, main script as part of the build. You can do this by specifying insertRequire in your build configuration, which will insert a require([]) call at the end of the built file.

If you are using almond, you should also upgrade to 0.1 since it also delays module evaluation in the same way that RequireJS 2.0 does and it supports the new config options like the module config.

shim config

The use and wrap plugins have shown that developers really want a way to configure dependencies for scripts that do not call define() to register a module. I originally thought a developer would not want to do this manual configuration, but it seems preferable to either waiting for the library developer to add AMD support or doing the modifications manually.

I also thought the order plugin would fill this gap, but what became apparent with projects that use Backbone, or anything with nested dependencies: order is insufficient and actually leads to idioms that are bad practice or will not even load.

It gets bad if order! is used to load a module that calls define(), and 'order' starts to be nonsense when there are nested dependencies.

So, the the order plugin has been removed and following the lead of Tim Branyen and Dave Geddes, of use and wrap respectively, requirejs 2.0 integrates that kind of dependency tree specification directly in requirejs.

It is configured via the shim configuration option. Example:

requirejs.config({
    shim: {
        'backbone': {
            //These script dependencies should be loaded before loading
            //backbone.js
            deps: ['underscore', 'jquery'],
            //Once loaded, use the global 'Backbone' as the
            //module value.
            exports: 'Backbone'
        },
        'foo': {
            deps: ['bar'],
            //A function can be used to generate the exported value.
            //"this" for the function will be the global object.
            //The dependencies will be passed in as function arguments.
            exports: function (bar) {
                //Using a function allows you to call noConflict for libraries
                //that support it. However, be aware that plugins for those
                //libraries may still want a global.
                return this.Foo.noConflict();
            }
        }
    }
});

For "modules" that are just jQuery or Backbone plugins that do not need to export any module value, the shim config can just be an array of dependencies:

requirejs.config({
    shim: {
        'jquery.colorize': ['jquery'],
        'jquery.scroll': ['jquery'],
        'backbone.layoutmanager': ['backbone']
    }
});

Note however if you want to get 404 load detection in IE so that you can use paths fallbacks or errbacks, then a string exports value should be given so the loader can check if the scripts actually loaded:

requirejs.config({
    shim: {
        'jquery.colorize': {
            deps: ['jquery'],
            exports: 'jQuery.fn.colorize'
        },
        'jquery.scroll': {
            deps: ['jquery'],
            exports: 'jQuery.fn.scroll'
        },
        'backbone.layoutmanager': {
            deps: ['backbone']
            exports: 'Backbone.LayoutManager'
        }
    }
});

Important caveat for "shim" config:

  • Only use other "shim" modules as dependencies for shimmed scripts, or AMD libraries that have no dependencies and call define() after they also create a global (like jQuery or lodash). Otherwise, if you use an AMD module as a dependency for a shim config module, after a build, that AMD module may not be evaluated until after the shimmed code in the build executes, and an error will occur. The ultimate fix is to upgrade all the shimmed code to have optional AMD define() calls.

Important optimizer notes for "shim" config:

  • You should use the mainConfigFile build option to specify the file where to find the shim config. Otherwise the optimizer will not know of the shim config. The other option is to duplicate the shim config in the build profile.
  • Do not mix CDN loading with shim config in a build. Example scenario: you load jQuery from the CDN but use the shim config to load something like the stock version of Backbone that depends on jQuery. When you do the build, be sure to inline jQuery in the built file and do not load it from the CDN. Otherwise, Backbone will be inlined in the built file and it will execute before the CDN-loaded jQuery will load. This is because the shim config just delays loading of the files until dependencies are loaded, but does not do any auto-wrapping of define. After a build, the dependencies are already inlined, the shim config cannot delay execution of the non-define()'d code until later. define()'d modules do work with CDN loaded code after a build because they properly wrap their source in define factory function that will not execute until dependencies are loaded. So the lesson: shim config is a stop-gap measure for for non-modular code, legacy code. define()'d modules are better.
  • If you are using uglifyjs to minify the code, do not set the uglify option toplevel to true, or if using the command line do not pass -mt. That option mangles the global names that shim uses to find exports.

Thanks to Tim and Dave for showing this was a desirable and workable feature by making something and proving out the point with real world experience.

require errbacks

A few people have asked for a way to specify an error handler for a require() call, to get called if there is an error or timeout loading a module that is part of the required set. John Hann, the person behind the curl AMD loader has also lobbied for this in the past.

RequireJS 2.0 supports this feature now. In addition, Rawld Gill, the person behind the dojo AMD loader, has had a way to "undefine" a module, and RequireJS 2.0 has a way to do that now too.

Errbacks, when used with requirejs.undef(), will allow you to detect if a module fails to load, undefine that module, reset the config to a another location, then try again.

A common use case for this is to use a CDN-hosted version of a library, but if that fails, switch to loading the file locally:

requirejs.config({
    enforceDefine: true,
    paths: {
        jquery: 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min'
    }
});

//Later
require(['jquery'], function ($) {
    //Do something with $ here
}, function (err) {
    //The errback, error callback
    //The error has a list of modules that failed
    var failedId = err.requireModules && err.requireModules[0],
    if (failedId === 'jquery') {
        //undef is function only on the global requirejs object.
        //Use it to clear internal knowledge of jQuery. Any modules
        //that were dependent on jQuery and in the middle of loading
        //will not be loaded yet, they will wait until a valid jQuery
        //does load.
        requirejs.undef(failedId);

        //Set the path to jQuery to local path
        requirejs.config({
            paths: {
                jquery: 'local/jquery'
            }
        });

        //Try again. Note that the above require callback
        //with the "Do something with $ here" comment will
        //be called if this new attempt to load jQuery succeeds.
        require(['jquery'], function () {});
    } else {
        //Some other error. Maybe show message to the user.
    }
});

With requirejs.undef(), if you later set up a different config and try to load the same module, the loader will still remember which modules needed that dependency and finish loading them when the newly configured module loads.

Note: errbacks only work with callback-style require calls, not define() calls. define() is only for declaring modules.

Catching load failures in IE

Internet Explorer has a set of problems that make it difficult to detect load failures for errbacks/paths fallbacks:

  • script.onerror does not work in IE 6-8. There is no way to know if loading a script generates a 404, worse, it triggers the onreadystatechange with a complete state even in a 404 case.
  • script.onerror does work in IE 9+, but it has a bug where it does not fire script.onload event handlers right after execution of script, so it cannot support the standard method of allowing anonymous AMD modules. So script.onreadystatechange is still used. However, onreadystatechange fires with a complete state before the script.onerror function fires.

So it is very difficult with IE to allow both anonymous AMD modules, which are a core benefit of AMD modules, and reliable detect errors.

However, if you are in a project that you know uses define() to declare all of its modules, or it uses the shim config to specify string exports for anything that does not use define(), then if you set the enforceDefine config value to true, the loader can confirm if a script load by checking for the define() call or the existence of the shim's exports global value.

So if you want to support Internet Explorer, catch load errors, and have modular code either through direct define() calls or shim config, always set enforceDefine to be true. See the next section for an example.

SPECIAL NOTE: If you do set enforceDefine: true, and you use data-main="" to load your main JS module, then that main JS module must call define() instead of require() to load the code it needs. The main JS module can still call require/requirejs to set config values, but for loading modules it should use define().

If you then also use almond to build your code without require.js, be sure to use the insertRequire build setting to insert a require call for the main module -- that serves the same purpose of the initial require() call that data-main does.

paths fallbacks

The above pattern for detecting a load failure, undef()ing a module, modifying paths and reloading is a common enough request that there is also a shorthand for it in RequireJS 2.0. Now the paths config allows array values:

requirejs.config({
    //To get timely, correct error triggers in IE, force a define/shim exports check.
    enforceDefine: true,
    paths: {
        jquery: [
            'http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min',
            //If the CDN location fails, load from this location
            'lib/jquery'
        ]
    }
});

//Later
require(['jquery'], function ($) {
});

This above code will try the CDN location, but if that fails, fall back to the local lib/jquery.js location.

Note: paths fallbacks only work for exact module ID matches. This is different from normal paths config which can apply to any part of a module ID prefix segment. Fallbacks are targeted more for unusual error recovery, not a generic path search path solution, since those are inefficient in the browser.

Loader plugin errors

Now that there are call-specific errbacks, John Hann said that people in curl's community also want a way for loader plugins to indicate when a plugin resource cannot be loaded or has an error. To allow that, requirejs 2.0 supports calling load.error():

//A loader plugin, actual one from one of the tests:
define({
    load: function (id, require, load, config) {
        if (id === 'broken') {
            var err = new Error('broken');
            err.plugMessage = id;
            //This is the new call to indicate an error with this resource.
            load.error(err);
        } else {
            //Success case.
            var value = fetchValueForId(id);
            load(value);
        }
    }
});

Notable caveat: requirejs.undef() is not be usable for plugin resources. Loader plugins may have enough internal state that may make it difficult to reset properly.

A loader plugin can use the module config to get instructions on what to do if it sees an error and what to do to try in a different way.

map config

Dojo's AMD loader has the concept of a "packageMap" config which allows you to specify, for a given package, what module ID to use in place of another module ID. For example, how to express "when 'bar' asks for module ID 'foo', actually use module ID 'foo1.2'.

This sort of capability is really important for larger projects which may have two sets of modules that need to use two different versions of 'foo', but they still need to cooperate with each other.

This is not possible with the context-backed multiversion support in requirejs 1.0.

In addition, the paths config is only for setting up root paths for module IDs, not for mapping one module ID to another one.

RequireJS 2.0 takes that packageMap idea and made it a more generic config option called map:

requirejs.config({
    map: {
        'some/newmodule': {
            'foo': 'foo1.2'
        },
        'some/oldmodule': {
            'foo': 'foo1.0'
        }
    }
});

If the modules are laid out on disk like this:

  • foo1.0.js
  • foo1.2.js
  • some/
    • newmodule.js
    • oldmodule.js

When 'some/newmodule' does require('foo') it will get the foo1.2.js file, and when 'some/oldmodule' does require('foo') it will get the foo1.0.js file.

This feature only works well for scripts that are real AMD modules that call define() and register as anonymous modules.

There is also support for a "*" map value which means "for all modules loaded, use this map config". If there is a more specific map config, that one will take precedence over the star config. Example:

requirejs.config({
    map: {
        '*': {
            'foo': 'foo1.2'
        },
        'some/oldmodule': {
            'foo': 'foo1.0'
        }
    }
});

Means that for any module except "some/oldmodule", when "foo" is wanted, use "foo1.2" instead. For "some/oldmodule" only, use "foo1.0" when it asks for "foo".

module config

There is a common need to pass configuration info to a module. That configuration info is usually known as part of the application, and there needs to be a way to pass that down to a module. In RequireJS, that is done with the config option for requirejs.config(). Modules can then read that info by asking for the special dependency "module" and calling module.config(). Example:

requirejs.config({
    config: {
        'bar': {
            size: 'large'
        },
        'baz': {
            color: 'blue'
        }
    }
});

//bar.js, which uses simplified CJS wrapping:
//http://requirejs.org/docs/whyamd.html#sugar
define(function (require, exports, module) {
    //Will be the value 'large'
    var size = module.config().size;
});

//baz.js which uses a dependency array,
//it asks for the special module ID, 'module':
//https://github.com/jrburke/requirejs/wiki/Differences-between-the-simplified-CommonJS-wrapper-and-standard-AMD-define#wiki-magic
define(['module'], function (module) {
    //Will be the value 'blue'
    var color = module.config().color;
});

Loader plugin changes

For 1.0, RequireJS had a few loader plugins in its repo and sometimes they would get an update with a 1.0.x release. However this was confusing and it made it harder for those plugins to live on their own. They are now broken out into their own repos as of the RequireJS 2.0 release:

All of the plugins now expect configuration to be passed to them via the module config instead of properties on the root of the config object. In particular, the i18n plugin now expects locale to be passed this way.

Specific changes over the 1.x versions of the plugins:

text plugin: It will now fire error handlers for 4xx or 5xx HTTP status codes.

domReady plugin: It no longer supports domReady.withResources() since the resourcesReady capability has been removed from the RequireJS 2.0 code.

Removed Items

Removed from RequireJS 2.0:

  • The order plugin is gone. Use the natively supported shim config instead.
  • The priority config has been removed. It was usually just used as a workaround for avoiding the order plugin, but the shim config should help with that. It also made it difficult to follow normal best practices for data-main app construction.
  • No more special hooks into jQuery to prevent its DOM ready callbacks from triggering until all modules have loaded.
  • domReady.withResources and the resourcesReady hook have been removed.
  • 'packagePaths' config has been removed. Just use 'packages' config. It does the same thing.
  • catchError.define config has been removed, now just pass a localized errback handler.

Report issues