2012-04-24

Declaring module exports (Node.js, AMD)

Modules are mostly the same in Node.js and AMD [1]: A sequence of statements that assign internal values to variables and exported values to an object. This blog post shows several patterns for doing the latter. It also explains how ECMAScript.next handles exports.

Node.js versus AMD

The main difference between Node.js and AMD is how the object with the exports is passed to the system: In Node.js, you assign to a variable exports that is global to the module:
    exports.value1 = ...;
In an AMD, you return an object:

    define(function () {  // no imports
        return {
            value1: ...
        };
    });

Patterns for declaring exports

This section shows several patterns for declaring exports. Some of their pros and cons hinge on whether you think that having to refer differently to internal and exported values is a good thing or not. Here we assume that you will often change your mind as to what is exported and that you don’t like the added syntactic noise of referring to an exported value via an object. One can, however, also argue that it is desirable to see exported values marked everywhere. Accessing values in an object versus in local variables can make a difference in performance, but you would have to profile to be really sure. I’d expect it to rarely matter in practice.

Pattern: fill object literal

    function funcInternal(...) { ... }
    var e = {
        otherFuncExported: ...,
        funcExported: function (...) {
            funcInternal(...);
            e.otherFuncExported(...);
        }
    };
    module.exports = e;
    // AMD: return e;
Pro and cons:
  • Pro: Compact syntax.
  • Cons: You need to prefix “e.” to refer to exported values. Internal and exported values are separate; you cannot, e.g., put helper functions close to where they are used.

Pattern: set properties

    var e = exports;
    // AMD: var e = {};
    
    e.otherFuncExported = ...;
    
    function funcInternal(...) { ... }
    e.funcExported = function (...) {
        funcInternal(...);
        e.otherFuncExported(...);
    };
    
    // AMD: return e;
Pro and con:
  • Pro: You can freely mix internal and exported values.
  • Con: You need to prefix “e.” to refer to other exported values.

Pattern: refer to exports in an object literal

    function otherFuncExported() { ... }
    
    function funcInternal(...) { ... }
    function funcExported(...) {
        funcInternal(...);
        otherFuncExported(...);
    };

    // AMD: return {
    module.exports = {
        otherFuncExported: otherFuncExported,
        funcExported: funcExported
    }
Pro and con:
  • Pro: Can refer to exported and internal values in the same manner.
  • Con: Redundant mention of exported identifiers (three times, in two different locations!).

Pattern: set local variables and properties

    var e = exports;
    // AMD: var e = {};
    
    var otherFuncExported = e.otherFuncExported = function () { ... };
    
    function funcInternal(...) { ... }
    var funcExported = e.funcExported = function (...) {
        funcInternal(...);
        otherFuncExported(...);
    };

    // AMD: return e
Pro and con:
  • Pro: Can refer to exported and internal values in the same manner.
  • Con: Some redundancy, but not as bad is in pattern #3.

ECMAScript.next

For ECMAScript.next modules, there are two ways of declaring an export. Approach 1: Prefix a declaration with “export”.
    export function otherFuncExported() { ... }
    
    function funcInternal(...) { ... }
    export function funcExported(...) {
        funcInternal(...);
        otherFuncExported(...);
    };
Approach 2: Refer to exported values.
    function otherFuncExported() { ... }
    
    function funcInternal(...) { ... }
    function funcExported(...) {
        funcInternal(...);
        otherFuncExported(...);
    };

    export otherFuncExported, funcExported;
It would be nice if one could emulate the latter approach in current module systems, but there is no way to access the entries of an environment in JavaScript. That would allow one to do the following (using Underscore’s pick() function which clones an object, but only copies the properties whose names are mentioned):
    module.exports = _.pick(environmentToObject(),
        "otherFuncExported", "funcExported"
    );
ECMAScript.next also has a shortcut for object literals that would be useful for current modules:
    module.exports = { otherFuncExported, funcExported };
Which is syntactic sugar for:
    module.exports = {
        otherFuncExported: otherFuncExported,
        funcExported: funcExported
    };

Conclusion

I find it hard to commit to any single one of the shown patterns, as each of them has advantages and disadvantages. My current favorite is pattern #4. Do you use any of the patterns described here (which one?) or a different one? Let us know in the comments...

References

  1. Bridging the module gap between Node.js and browsers

No comments: