Skip to content

Requirejs 2.0 draft

jrburke edited this page May 22, 2012 · 14 revisions

RequireJS 2.0 draft

This is a draft plan for RequireJS 2.0. Subject to change, particularly the naming of things, but this is a solid first pass, and there is an experimental implementation that can be tried out.

If you want to give feedback on the general direction or this draft, leave a comment in the RequireJS 2.0 draft issue.

Why

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

At the same time, there are 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 is not changing. If anything, I am hoping that the AMD loader implementers are starting to agree on higher level APIs, like common config.

These changes are 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 have changed, semantic versioning dictates that these changes warrant a 2.0 version.

The loader will not increase in size, but hopefully shrink a bit.

Primary Changes

These are the primary changes:

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 preferrable 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, I want to remove support for the order plugin and I want to follow the lead of Tim Branyen and Dave Geddes, of use and wrap respectively, and integrate that kind of dependency tree specification directly in requirejs.

This will be done by a new shim configuration. 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']
    }
});

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.

I have finally seen the light, and want to add this for RequireJS. In addition, Rawld Gill, the person behind the dojo AMD loader, has had a way to "undefine" a module. I want to add support for that too.

Errbacks with 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({
    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.

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. We are still discussing the final API for this, but for now I prototyped a load.error() call that would allow this:

//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() will not be usable for plugin resources. Loader plugins may have enough internal state that may make it difficult to reset properly.

I do not believe this should be a problem though, as a loader plugin should be able to read a config to know what to do if it does see 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.

Right now this is not possible with the context-backed multiversion support in requirejs today.

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

We have been talking on the amd-implement list about how to make this a more generic configuration option, something that is not just for packages but for any module ID. So we are still sorting out the details, but I want to support this feature in some way.

Right now I have prototyped the following in the dev2.0 branch (note, this one in particular is subject to change):

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

In the common config amd-implement thread, we also talked about a general method for passing configuration data to modules. It looks like so:

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;
});

Delayed module evaluation

Right now, 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 has been pointed out that this execution order is not desirable with modules since it does not allow for some modules to activate until later. This 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.

Better parser for optimizer

Right now the optimizer uses UglifyJS to parse modules to find the require/define calls and build up the dependency tree to inject into a build layer.

However, UglifyJS right now only understands today's common JS, and does not support some syntax features that may be found in JavaScript 1.8. I want to use a parser that does.

Previously I did an experiment with narcissus, but I did not finish it out. I am open to other suggestions. I have not made any progress on this yet in the dev2.0 branches.

Removed Items

The things that have been removed from the dev2.0 branch:

  • The order plugin is gone. Use the natively supported shim config instead.
  • The priority config will be 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. If this ends up being an issue in practice, I may provide an adapter script that would plugin into the core loader to do this.
  • 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.

Try it out

I have almost all of the above working now in a dev2.0 branch for requirejs and the r.js optimizer. All the existing unit tests pass, except for some of the error ones on IE (since it triggers success callbacks on scripts that load 404s).

That said, I'm sure there are issues. And recall that this is all subject to change. This is the first broadcast of the 2.0 draft, and feedback may indicate changes are needed.

That said, if you feel like living on the edge of the future:

Report issues

If you are trying out the dev2.0 versions of the files, file bugs with the appropriate repository, and prefix the issue name with dev2.0:.

Summary

Your feedback is welcome. Should I cut more out of the loader? Something else missing?

Doc Changes

May 22, 2012

  • Added map star config.

May 4, 2012

  • Rename "legacy" to "shim". Better name suggested by @rpflorence, community feels it fits better, and matches the wikipedia definition.