One of the advantages of Node.js is that you can use the same programming language – JavaScript – on both server and client. When it comes to modularizing code that is portable between the two platforms, one is presented with a major challenge: they approach modularity differently. This post examines four solutions for writing cross-platform modules.
var module1 = require("./module1"); module1.foo(); var module2 = require("./module2"); export.bar = function() { module2.baz(); }The module uses the function require() to import the modules module1 and module2. It exports the function bar. While require() is doing its work of loading a file from disk and evaluating it, the code waits for the result.
Browsers – asynchronous modules. In browsers, things work differently. Loading can take a long time, so modules (scripts) are always loaded asynchronously: you give the order for loading a file plus a callback that is used to inform you when the file has been loaded. Hence, there is no (synchronous) waiting. That also means that you cannot load an import in the middle of your module, like on Node.js. The Asynchronous Module Definition (AMD) standard has been developed with asynchronicity in mind: the module becomes a callback that is invoked once all imports have loaded. The above Node.js module looks as follows as an AMD:
define([ "./module1", "./module2" ], function (module1, module2) { module1.foo(); return { // export bar: function () { module2.baz(); } }; } );Yes, there is a little more syntactic noise, but that is necessary to make things easy to parse. For development, having many little modules is great because it provides structure. For deployment, you want to have as few files as possible, because each download request costs time and bandwidth. Hence, the AMD tool RequireJS comes with an optimizer that lets you compile several AMDs into a single minified file. Such a file can be loaded via the simplified AMD module loader almond which is only 750 bytes when minified and gzipped.
Note that the above example demonstrates the core AMD syntax. The complete AMD standard has more features.
Crossing platforms. At their core, the two module formats are not dramatically different: specify a path to a file, assign the result of evaluating it to a variable. The following sections examine two approaches for using either module standard on both platforms:
var define; // does nothing if `define` has already been declared. if (typeof define === "undefined") { define = function (...) { ... } } define(...);This pattern relies on the peculiarities of var in a brittle manner. For example, it will cease to work if the code is wrapped in a function. Thus, it should be avoided.
(function (define) { define(...); }(typeof define === "function" ? define : function (...) { ... }));Approach: Wrap all of the code into an immediately-invoked function expression, check whether define() already exists and if not, provide a value for it.
({ define: typeof define === "function" ? define : function (...) { ... } }). define(...);Approach: turn define() into a method call by prepending an object with a suitable method inside. Advantage: shorter and only a prefix (easier to remove, easier to add via copy/paste).
Boilerplate to browser-enable a Node.js module.
({ define: typeof define === "function" ? define // browser : function(F) { F(require,exports,module) } }). // Node.js define(function (require, exports, module) { // Node.js module code goes here });Approach: Wrap the Node.js code in a function whose parameters provide the module API. That function is called the module body. On Node.js, the boilerplate can use require, exports and module directly. On browsers, you use advanced AMD features:
Boilerplate to Node.js-enable an AMD.
({ define: typeof define === "function" ? define : function(A,F) { module.exports = F.apply(null, A.map(require)) } }). define([ "./module1", "./module2" ], function (module1, module2) { return ... } );Approach: On Node.js, use require() to compute the arguments for the module body (the second argument of define()). The result of evaluating the body is assigned to module.exports. On browsers, an implementation of define() is provided by an AMD-compatible loader such as RequireJS [1].
Separate exports.
function foo() { } // public function bar() { // private foo(); // call public function } // Exports are separate: exports.foo = foo;
function foo() { } // public function bar() { // private foo(); // call public function } // Exports are separate: return { foo: foo };
Inline exports.
var e = exports; e.foo = function () { }; // public function bar() { // private e.foo(); // call public function }
var e = {}; e.foo = function () { }; // public function bar() { // private e.foo(); // call public function } return e;
Alternatively, one could put the exported values inside the object literal that is initially assigned to e:
var e = { foo: function () { } // public }; function bar() { // private e.foo(); // call public function }This avoids the redundant “e.” when defining an exported value, but then you can’t freely mix private and public identifiers.