Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Bridging the module gap between Node.js and browsers

DZone's Guide to

Bridging the module gap between Node.js and browsers

· Web Dev Zone
Free Resource

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

This post examines four solutions for writing a module that works on both browsers and Node.js.

Synchronous versus asynchronous modules

Node.js – synchronous modules. Node.js loads its modules synchronously. The following code is a module that uses the function require() to import two modules module1 and module2. It exports the function bar.
    var module1 = require("./module1");
    module1.foo();
    var module2 = require("./module2");
    export.bar = function() {
        module2.baz();
    }
When module1 is imported, JavaScript execution stops, a file is loaded from disk and evaluated and execution continues.

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, you cannot load the imports anywhere in a module, like in the Node.js code above; you need to load all of them first and then execute the module in one go. The Asynchronous Module Definition (AMD) standard has been developed for this purpose. 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 it is necessary to make the asynchronous approach work. For development, having many little modules is great because it provides structure. For deployment, you want to have as few files as possible, due to performance considerations. 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:

  • Use boilerplate to ensure compatibility.
  • Transform “pure” modules on the fly or via compilation.

Compatibility via boilerplate

By inserting boilerplate code, you can detect what platform the module is executed on and adapt it as necessary. Given a function call such as define(...) above, there are three main patterns for adapting the call so that it works on both platforms. In all cases, the assumption is that on browsers, a script has already been loaded that set up a global function define(). The patterns can be distinguished by how they provide a value for define():
  • Via a variable.
        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.
  • Via a function parameter.
        (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.
  • Via a method definition.
        ({ 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).
All boilerplate below uses the method definition approach. Apart from putting redundant code into each module, a great disadvantage of boilerplate code is that it might prevent tools from working that combine modules into single files, for browsers.

Boilerplate to browser-enable a Node.js module.

    ({ define: typeof define === "function"
        ? define
        : function(F) { F(require,exports,module) } }).
    define(function(require, exports, module) {
        // Node.js code goes here
    });
Approach: Wrap the Node.js code. On Node.js, the boilerplate can use Node’s require, exports and module quite easily. On browsers, you rely on an advanced AMD feature where define() receives arguments suitable for Node.js code. It goes beyond the core syntax introduced above, but every complete AMD implementation (such as RequireJS [1]) supports it. Such an AMD implementation will parse the module code to extract the names of all modules that are required inside of it, it will preload those modules and then hand in a require() function that simply returns the modules that are already in RAM. The parsing is possible thanks to a non-standard feature of most JavaScript engines: If you invoke the toString() method of a function, you get its source code.
  • Disadvantages: boilerplate, source code must be parsed in browsers.
  • Advantage: works with script tags.

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: The second argument of define() is called the module body. On Node.js, use require() to compute the arguments for the body, call it and assign the result to module.exports. On browsers, use an AMD compatible script loader such as RequireJS [1].
  • Disadvantage: boilerplate.
  • Advantage: no parsing on browsers, works with script tags.

Pure Node.js or AMD modules

Node.js modules on browsers. On Node.js, you can use the modules directly. On browsers, you need to distinguish between development and deployment. During development, you can take a simpler, but slower approach and load modules via the lobrow tool [2]. For deployment, you can compile several modules into a single file via a tool such as browserify [3] or webmake [4].
  • Disadvantage: lobrow needs to parse the code and is limited by its use of XMLHttpRequest (compared to script tags).
  • Advantage: no boilerplate.
AMDs on Node.js. The tool node-amd-loader [5] supports AMDs natively under Node.js. However, you must import it to activate it. Obviously, it is a hassle if you have to perform that import each time you use Node’s REPL as an interactive JavaScript command line. Thankfully, there is a trick [6] that lets you avoid that – by automatically executing code when you start the REPL. On browsers, you use an AMD-compatible script loader.
  • Disadvantage: need for adapter on Node.js.
  • Advantage: can use script tags, no boilerplate.

Structuring modules

I mainly use two ways to structure my modules. Many other ways exist, some of them a mixture of what is shown below.

Separate exports.

  • Node.js
        function foo() { } // public
        function bar() {  // private
            foo(); // call public function
        }
        
        // Exports are separate:
        exports.foo = foo;
    
  • AMDs
        function foo() { } // public
        function bar() { // private
            foo(); // call public function
        }
    
        // Exports are separate:
        return {
            foo: foo
        };
    
Disadvantage: you have to mention an identifier a total of three times.

Inline exports.

  • Node.js
        var e = exports;
    
        e.foo = function () { }; // public
        function bar() { // private
            e.foo(); // call public function
        }
    
  • AMDs
        var e = {};
    
        e.foo = function () { }; // public
        function bar() { // private
            e.foo(); // call public function
        }
    
        return e;
    
Disadvantages: You always have to prefix exported identifiers with “e.”, minimally less efficient than directly accessing the exported values.

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.

References

  1. RequireJS, a JavaScript file and module loader
  2. Load Node.js modules in browsers via lobrow
  3. Browserify – make node-style require() work in the browser
  4. modules-webmake - Bundle CommonJS modules for web browser
  5. node-amd-loader, an AMD loader for Node.js
  6. Execute code each time the Node.js REPL starts

 

From http://www.2ality.com/2011/11/module-gap.html

Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.

Topics:

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}