Skip to content

Migration to AMD Syntax

patrick-steele-idem edited this page Jan 17, 2013 · 34 revisions

Overview

RaptorJS has been updated on the master branch to migrate to the Asynchronous Module Definition (AMD) syntax. In addition, RaptorJS now provides its own AMD implementation that extends AMD (in backwards compatible ways) to provide support for classes and enums, and RaptorJS works seamlessly in multiple CommonJS-based server-side JavaScript environments (e.g. Node and Rhino) and in all web browsers.

Why the Change?

  • Existing AMD code can now be used with RaptorJS
  • Better alignment of RaptorJS with the rest of the JavaScript community
  • Less vendor lock-in (no use of "raptor" in code)
  • Support for relative module names (can be helpful and allows for less code)
  • No more "raptor" global
  • Smaller developer code to type and send over the wire ("define" is shorter than "raptor.define" and "require" is always a local variable)

Benefits of the RaptorJS AMD Implementation

RaptorJS now provides an AMD implementation that provides many improvements over other AMD implementations:

  • RaptorJS provides a very lightweight AMD implementation (currently about 1.69KB minified and gzipped)
  • AMD module loader works seamlessly on the server-side in Node and Rhino (allows both CommonJS and AMD modules to be loaded)
  • The asynchronous module loader (and the code associated with it) is optional (include it only if you need to download modules asynchronously)
  • Extends AMD with new, backwards compatible features:
  • Classes
  • Enums
  • Extensions/Mixins
  • Loggers
  • Asynchronous package loading based on package.json files
  • Comes bundled with a collection of new cross-runtime modules to make your life easier, including a web application resource optimizer, a templating module and a widget framework that supports rendering components on both the server and in the browser (see RaptorJS API Docs)
  • Etc.

Differences from the AMD Spec

The RaptorJS AMD implementation introduces some backwards compatible improvements to the AMD specification that are described below:

Module IDs

Using dots as the separators instead of forward slashes is supported and module IDs will normalized by converting all dots to forward slashes. However, it is recommended to use forward slashes to be AMD-compliant.

Factory Function Arguments

For RaptorJS, the last three arguments will always be [require, exports, module]. If an array of dependency IDs is provided then those dependencies will be passed in before the three built-in arguments. For example:

define(
    'some/namespace/my-module', 
    ['a', 'b', 'c'],
    function(a, b, c, require, exports, module) {
        //...
    });

Developer code can still explicitly reference "require", "exports" and "module" as shown in the following example code:

define(
    'some/namespace/my-module', 
    ['require', 'exports', 'module', 'a', 'b', 'c'],
    function(require, exports, module, a, b, c) {
        //...
    });

Synchronous and Asynchronous require

Like most AMD implementations, RaptorJS supports both a synchronous "require" function and an asynchronous "require" function:

  1. Synchronous: require(id)
  2. Asynchronous: require(ids, callback)

Asynchronous loading requires the that separate raptor/loader/require module be included as part of the initial page load.

Asynchronous Package Loading

RaptorJS will not provide any support for loader plugins. Instead, RaptorJS will allow any "package" of code to be downloaded asynchronously. For a given package to be downloaded asynchronously, the client must be provided with the list of required JavaScript or CSS URLs for each package/module upfront (which the RaptorJS Optimizer tool will do for you automatically). The RaptorJS Optimizer will generate the required metadata (including JavaScript and CSS urls) that is required to download packages asynchronously. With this approach, developers no longer have to worry about configuring the client with base paths and the server will always generate cacheable URLs that can optionally be delivered through a CDN. During development, URLs are generated such that each file resource has a unique URL that points to the original file on disk. In comparison, during a production build, file resources are concatenated together as part of a single resource/URL and resources are minified based on the optimizer's configuration.

Modules/packages can be downloaded using asynchronous version of the require function (i.e. require(ids, callback) method. For example:

require(['a', 'b', 'c'], function(a, b, c) {
    //Modules a, b and c are ready to be used
}); 

The "require" method can be used outside the context of a "define" call

RaptorJS allows both the synchronous "require" function to be used outside the context of a define. If the "require" method is used outside the context of a define then absolute module names must be used (since there is nothing for the required module name to be relative to). This frees developers from having to create a "define" block just to make use of another module.

Additional define methods

define.Class(...)

The define.Class method allows for classes with inheritance to easily be defined. The method signature is the following define.Class(id, ?superclassId, ?dependencies, factory)

Example:

define.Class(
    'some/namespace/MyClass'
    'some/namespace/MySuperClass',
    function(require, exports, module) {
        var MyClass = function() {
        }
 
        MyClass.prototype = {
        }
         
        return MyClass;
    });

define.Enum(...)

The define.Enum method allows for enumerations to easily be defined. The method signature is the following define.Enum(id, enumValues, ?factory)

Examples:

define.Enum(
    'some/namespace/Day',
    [
        "SUN",
        "MON",
        "TUE",
        "WED",
        "THU",
        "FRI",
        "SAT"
    ]
);

define.Enum(
    'some/namespace/Day',
    {
        SUN: [false, "Sunday"],
        MON: [true, "Monday"],
        TUE: [true, "Tuesday"],
        WED: [true, "Wednesday"],
        THU: [true, "Thursday"],
        FRI: [true, "Friday"],
        SAT: [false, "Saturday"]
    },
    function(require) {
        return {
            init: function(isWeekday, longName) {
                this._isWeekday = isWeekday;
                this._longName = longName;
            },
             
            getLongName: function() {
                return this._longName;
            },
             
            isWeekday: function() {
                return this._isWeekday;
            }
        }
    }
);

define.extend(...)

The define.extend method allows mixins to be lazily applied to another module when the module is first required. The define.extend method is used heavily to support cross-environment modules by creating environment specific mixins for modules.

Example:

/**
 * Extends the "raptor/files" module with Rhino/Java specific mixins so that the
 * same module can work in multiple server-side JavaScript environments.
 * @extension Rhino
 */
define.extend(
    'raptor/files',
    function(require) {
        var JavaFile = Packages.java.io.File;

        return {
            exists: function(path) {
                return new JavaFile(path).exists();
            }
        }
    }
);

Additional module methods

module.logger()

The module argument that is passed to a factory function supports a new logger method that can be used to obtain a reference to a logger for the module being defined. For Example:

define('some/namespace/my-module', function(require, exports, module) {
    var logger = module.logger();
    logger.debug("This is a debug message"); 
    //Console Output: DEBUG some/namespace/my-module This is a debug message
});

Additional require methods

require.find(id)

The require.find method can be used to get a reference to a module. Unlike the require function, require.find will return null if a module with the specified ID is not found.

var myModule = require.find('my-module');
if (myModule) {
    // Module found
}
else {
    // Module not found
}

require.exists(id)

The require.exists method can be used to check if a module exists with the specified module ID. The method will return true of the module exists, false otherwise.

if (require.exists('my-module') {
    // Module exists
}
else {
    // Module does not exist
}

Migrating Existing RaptorJS Code

Overview

  • raptor.define(id, function(raptor) { var a = raptor.require('a'); });
    raptor. define(id, function(raptor require) { var a = raptor. require('a'); });
  • raptor.defineClass(id, superclass, function(raptor) { ... } );
    raptor. define.Class(id, superclass, function(raptor require) { ... });
  • raptor.defineEnum(id, enumValues, function(raptor) { ... } );
    raptor.defineEnum define.Enum(id, enumValues, function(raptor require) { ... });
  • raptor.extend(target, function(raptor, target, overridden) { ... })
    raptor.extend define.extend(target, function(raptor require, target) { ... }))
  • raptor.require(['a', 'b', 'c'], function(a, b, c) { ... })
    raptor.require(['a', 'b', 'c'], function(a, b, c) { ... })

In addition, all AMD modules distributed with RaptorJS are now prefixed with "raptor". For example:

  • var pubsub = raptor.require('pubsub');
    ↳var pubsub = raptor.require('raptor/pubsub');

Migrating Module Definitions

OLD:

raptor.define('some.namespace.my-module', function(raptor) {  
    var anotherModule = raptor.require('some.namespace.another-module');  
});

NEW:

//Using require to reference dependencies:
define(
    'some/namespace/my-module', 
    function(require, exports, module) {
        var anotherModule = require('some/namespace/another-module');
        ...
    });
 
//Using an array of dependency names to reference dependencies:
define(
    'some/namespace/my-module', 
    ['some/namespace/another-module'], 
    function(anotherModule, require, exports, module) {
        ...
    });

Relative names are also supported (relative names are relative to the ID of the parent module being defined):

define(
    'some/namespace/my-module', 
    function(require, exports, module) {
        var anotherModule = require('./another-module');
        ...
 
    });
 
define(
    'some/namespace/my-module', 
    ['./another-module'], 
    function(anotherModule, require, exports, module) {
        ...
    });

NOTE: Unlike with AMD, dependencies are always loaded synchronously unless the callback style of the "require" function is used (see below).

Migrating Class Definition

OLD:

raptor.defineClass(
    'some.namespace.MyClass'
    'some.namespace.MySuperClass',
    function(raptor) {
        var MyClass = function() {
        }
 
        MyClass.prototype = {
        }
         
        return MyClass;
    });

NEW:

define.Class(
    'some/namespace/MyClass'
    'some/namespace/MySuperClass',
    function(require, exports, module) {
        var MyClass = function() {
        }
 
        MyClass.prototype = {
        }
         
        return MyClass;
    });

Migrating Enum Definitions

OLD:

raptor.defineEnum(
    'some.namespace.MyEnum'
    ['One', 'Two', 'Three']);

NEW:

define.Enum(
    'some/namespace/MyEnum'
    ['One', 'Two', 'Three']);

Loading Modules Asynchronously

OLD:

raptor.require(
    ['a', 'b', 'c'],
    function(a, b, c) {
        //Modules a, b and c are ready to be used
    });

NEW:

require(['a', 'b', 'c'], function(a, b, c) {
    //Modules a, b and c are ready to be used
}); 

Loggers

OLD:

raptor.define('some.namespace.my-module', function(raptor) {
    var logger = this.logger();
    logger.debug("This is a debug message"); 
    //Output: DEBUG some.namespace.my-module This is a debug message
});

NEW:

define('some/namespace/my-module', function(require, exports, module) {
    var logger = module.logger();
    logger.debug("This is a debug message"); 
    //Output: DEBUG some/namespace/my-module This is a debug message
});

Node Integration

Node does not natively provide the "define" function that is required by AMD. RaptorJS solves this problem by modifying the Node runtime to automatically add a "define" function to every module. This is done by modifying the Node wrapper code to introduce an additional "define" parameter that gets initialized with a "define" function provided by RaptorJS. The "define" function understands the context where it is being defined which allows it to create a "require" function that wraps the Node provided require function so that it can be used to load both AMD modules and CommonJS modules on the server.

In addition, RaptorJS modifies the Node runtime to allow the native "require" function to be used to load both CommonJS modules and top-level (i.e. not relative) AMD modules. This is done by hooking into Node's path resolver.

The related code can be found in the following file: https://github.com/raptorjs/raptorjs/blob/master/lib/raptor/raptor_node.js

The end result is a seamless integration of the AMD pattern into a Node server environment. The AMD functionality is enabled the first time the "raptor" module is required, but the "define" function will then only be available in modules that are subsequently loaded by Node. To manually create a "define" function the following code can be used:

var define = require('raptor').createDefine(module /* Node module object */);

Status

The code for RaptorJS has been migrated to use the new AMD syntax in the master branch: https://github.com/raptorjs/raptorjs

The NPM version of "raptor" has been updated to 2.0.0.