This post explains how to write modules that are universal – they run on browsers and Node.js. A previous post showed a simple way of doing so, this post presents a more sophisticated solution that also handles modules importing other modules. Additionally, we use the unit test framework Jasmine to write tests for those modules that are equally universal.
var format = require("util").format;In a browser, when you ask for a module, it usually means that it has to be loaded over the internet. While that happens, your the browser goes on to do other things and calls your code back once the module code has arrived. That’s why browser module systems are asynchronous, you have to work with callbacks. AMD (Asynchronous Module Definition) [1] is a standard for specifying asynchronous modules. Universal modules support the following subset of its syntax – a global function define() is used to define a module:
define(dependencies?, factory);The optional parameter dependencies is an array of names of modules that the module imports. After the imported modules have been loaded, they are passed to the function factory as parameters. factory then returns the module contents, usually an object. Example of an AMD:
define([ "./other_module" ], function (other_module) { return { myfunction: function() { other_module.func(...); ... } }; } );In browsers, you support AMDs by loading a script that defines a global function define(). To support Node.js as well, universal modules add a prefix to an AMD that enables their use on that platform:
"use strict"; (typeof define === "function" ? {define:define} : require("um_node")._(require, exports)). define([ "./other_module" ], function (other_module) { return { myfunction: function() { other_module.func(...); ... } }; } );Explanations:
"use strict";Enable ECMAScript 5 strict mode [4] for the complete file. Strict mode helps with writing better code, because it eliminates a few quirks and performs more checks.
(typeof define === "function" ? {define:define} : require("um_node")._(require, exports)).If a global function define() already exists, use it, otherwise load a definition from the Node.js module um_node (“universal modules for Node.js”). A more detailed explanation of the above three lines is given in Sect. “Explaining the universal module pattern”, below.
define([ "./other_module" ], function (other_module) {Import a module called ./other_module. After it has been loaded it is handed to the function in the second line as the parameter other_module.
return { myfunction: function() { other_module.func(...); ... } };This is the actual module. Caveat: In a module method, you cannot use this to refer to sibling methods, because such a method might not be invoked as a method, but as a function:
var f = mymodule.myfunction; f();Then it can’t reach its siblings via this. A work-around is to assign the module object to a variable before returning it:
var module = { func1: function() { ... }, func2: function() { module.func1(); } }; return module;
Module names. The legal names of universal modules are a subset of Node’s. The file extension ".js" is always omitted. There are three kinds of names:
/home/joe/js/node_modules/bar.js /home/joe/node_modules/bar.js /home/node_modules/bar.js /node_modules/bar.jsThe browser AMD loader that comes with the universal_modules project lets you map global module names to paths (as searching the file system in the above manner makes less sense in a browser). That allows one to use a single name for a module that has a browser-specific and a Node-specific implementation. See below for details.
/home/joe/js/foo.js – path of current module ./bar → /home/joe/js/bar.js ./bar/baz → /home/joe/js/bar/baz.js ../bar → /home/joe/bar.js ../../bar → /home/bar.js
<script> if (!window.global) { window.global = window; } </script>It would be nice to achieve the above task without window, but there is no easy way to do so.
Installation. Download the universal modules project. Copy the directory node_modules/ to your project.
├── client.html ├── mymodule.js ├── node_modules/ │ ├── um_browser.js │ └── um_node.js └── server.jsThe directory node_modules/ contains the universal module support. mymodule.js is a universal module with the following content:
"use strict"; (typeof define === "function" ? {define:define} : require("um_node")._(require, exports)). define(function () { return { twice: function(value) { return value + value; } }; } );How can we use that module in a browser and on Node.js?
Browser. client.html uses that module from a browser:
<!doctype html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>My web page</title> 1: <script type="text/javascript" src="node_modules/um_browser.js"></script> 2: <script> 2: um_browser.globalNames.foo = "../path/to/foo.js"; 2: // alternative: um_browser.globalNames = function (moduleName) { ... }; 2: </script> </head> <body> 3: <script> 3: define([ "./mymodule" ], 3: function(mymodule) { 3: alert("Twice foo: "+mymodule.twice("foo")); 3: } 3: ); 3: </script> </body> </html>The core ingredients are:
var twice = require("./mymodule").twice; console.log("Twice foo: "+twice("foo"));Note that mymodule is used like any other Node.js module. Alternatively, you could have written server.js as a universal module.
server.js is run from a command line via
node server.js
define([ "./other_module" ], function (other_module) { return { ... }; } );To fulfill the previously mentioned requirements, we have to make sure that browsers and Node.js understand the above AMD. On browsers, we simply assume that a global function define() has been defined, e.g. via a library. On Node.js, the goal is that a universal module can be loaded as either an AMD (via other AMDs) or a normal Node module.
Using an AMD on Node.js. On Node.js, we need to connect the AMD to the native module system. Possible solutions:
var define = (typeof define === "function" ? define : require("um_node")._(require, exports)); define(...);In browsers, there already is a define() function. Therefore, we check whether it exists. If not, we create a Node.js version of define(). To do so, we use the module um_node (“universal modules for Node.js”, see below for details on its contents). Each Node.js module needs a custom definition of define(), because that function must use the module-specific require() function and exports object. For require(), it is obvious why it is module-specific: If you are in a module A and load another module B via a module-relative path, then require() must know A’s absolute path to compute B’s absolute path. Node.js provides modules with require() functions that have this knowledge. Note that newer versions of Node.js allow one to access both require and exports via the module-specific variable module, but we also want to support older versions (where there is only module.require).
Main problem with this approach: Hoisting [5] can prevent this from working – it means that the above code is usually interpreted as
var define; define = (typeof define === "function" ? define : require("um_node")._(require, exports)); define(...);Then typeof define will always be "undefined".
if (typeof define !== "function") { global.define = require("um_node")._(require, exports); } define(...);The main drawback of this approach is that using a global variable this way is especially inelegant in a module system like Node’s which is all about local state. Constantly giving it a new value is even worse.
(function (define) { define(...); }(typeof define === "function" ? define : require("um_node")._(require, exports));The above is almost the same as defining a local variable, above. But this time, we create a new scope for that variable to live in. This is a very clean solution. However, it is a bracket around the AMD. It would be nicer if we could do with just a prefix.
(typeof define === "function" ? {define:define} : require("um_node")._(require, exports)). define(...);Granted, this changes the nature of define() (from a function to a method), but it is still just a prefix. If the global function define() already exists, we create a temporary object with that function as a single method. Otherwise, we let module um_node create such an object, via its _() function.
function loadScript(url, callback) { var script = document.createElement("script") script.type = "text/javascript"; if (script.readyState) { // IE script.onreadystatechange = function () { if (script.readyState === "loaded" || script.readyState === "complete") { script.onreadystatechange = null; callback(); } }; } else { // Others script.onload = function(){ callback(); }; } script.src = url; document.getElementsByTagName("head")[0].appendChild(script); }
exports._ = function(clientRequire, clientExports) { return { define: function (importNames, moduleBody) { if (arguments.length === 1 && typeof importNames === "function") { moduleBody = importNames; importNames = []; } // Step 1 var imports = importNames.map(function (x) { return clientRequire(x) }); // Step 2 var data = moduleBody.apply(null, imports); // Step 3 if (data) { Object.keys(data).forEach(function(key) { clientExports[key] = data[key]; }); } } }; }The function that provides the support is called _.
The spec: universal_modules/demo/spec/strset.spec.js
"use strict"; (typeof define === "function" ? {define:define} : require("um_node")._(require, exports)). define([ "../strset" ], // the universal to module to be tested function (strset) { describe('strset', function () { it('creates sets via the constructor', function () { var sset = new strset.StringSet("a"); expect(sset.contains("a")).toBeTruthy(); expect(sset.contains("b")).toBeFalsy(); }); ... }); } );The above code tests the universal module universal_modules/demo/strset.js and can be run directly via jasmine-node:
$ jasmine-node strset.spec.js Started ... Finished in 0.001 seconds 1 test, 5 assertions, 0 failures
Running the spec in a browser: universal_modules/demo/spec/strset.runner.html
The pertinent lines are:
<!-- The code to be tested and the tests --> <script type="text/javascript" src="../../node_modules/um_browser.js"></script> <script type="text/javascript" src="strset.spec.js"></script>You only need to load two scripts: First, um_browser.js to ensure that universal modules work. Second, strset.spec.js to load the code to be tested and to run the tests.