Short Bytes: Many node modules have to support Cross Environment Javascript namely in Node.js and browser. UMD was a good wrapper around checking the environment and then writing our module’s logic, but it has its caveats. Namely, we’d have to write too much if the else logic in one single function or file node-browser-resolve npm package helps us write a module for multiple environments right from the package.json file.
If you are thinking of authoring an npm module, the chances are that you might stumble across ways to make your module work cross environment, for example with Node, browser. And if you are a maintainer of legacy projects, require js’s AMD pattern modules. The very common way to author a module to ensure cross environment compatibility is UMD or Universal Module Definition.(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['b'], factory); } else if (typeof module === 'object' && module.exports) { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(require('b')); } else { // Browser globals (root is window) // It is assumed that b is a browser style dependency attached to the root as well root.returnExports = factory(root.b); } }(this, function (b) { //use b in some fashion. // Just return a value to define the module export. // This example returns an object, but the module // can return a function as the exported value. return {}; }));
This beautiful pattern was inspired by @kriskowal when used for the Q promise library.
It passes this context and a function having a dependency b (which might be optional) in an IIFE, which are accessed inside the function as root and factory function respectively.
The if and else conditional statements check for the environment in which the module is being used. If define is defined and is a function, that means something like RequireJS, is being used to manage dependencies asynchronously. If module is an object and module.exports is not null or undefined, then that means our module is being used in a node environment. If it’s none of the above two (for example the module is being used in the browser, where the this context is the window object), then we attach the factory function of our module (where the module’s logic resides) directly to the root (in this case window global browser object).
But wait, I do not use RequireJS anymore because I’ve ES2015 imports or Node’s CommonJS module like require very handy with me, along with the power of build tools now that I can use with ease. There’s an easy way, where you can leverage npm and Node together to seamlessly tell your module in which environment it is being used, and how to behave accordingly. Cross Environment behavior made easy.
Let’s take an example of a simple javascript utility package, which takes a string as an input and outputs base-64-encoded version.
For the browser, this is easy, since we can just use the btao inbuilt function:
module.exports = function (string) { return btoa(string); };
In Node though, there is btao function. So, we’ll have to create a Buffer instead, and then call buffer.toString() on it:
module.exports = function (string) { return Buffer.from(string, 'binary').toString('base64'); };
Now, we need some way (like the UMD pattern but less verbose, since we can) to detect whether we’re running in the browser or in Node, so that we could be sure to use the right version in the right environment. Both Browserify and Webpack bundling tools define a process.browser field which returns true, whereas in Node it’s false. So we can simply do
if (process.browser) { module.exports = function (string) { return btoa(string); }; } else { module.exports = function (string) { return Buffer.from(string, 'binary').toString('base64'); }; }
Now, we just name our file index.js then type npm publish through the root of our module through our terminal, and we should be done.
but there’s a big performance glitch
Since our index.js file now contains references to the Node’s built-in process and buffer modules, both Browserify and Webpack will automatically include polyfills for those entire modules in our bundle (the final file after bundling). Thus, we have an enormous amount of unnecessary code sitting in our bundle.
With this simple looking 9 line code of our module, Browserify and Webpack will create a bundle weighing 24.7KB minified (7.6KB min+gz). That’s a lot of bytes for something that, in the browser, only needs to be expressed with btao inbuilt browser function.
ENTER: THE BROWSER FIELD IN PACKAGE.JSON
With node-browser-resolve npm package, it lets us include a new field by the key name browser in our package.json file.
Using this technique, we can add the following to our package.json
{ /* ... */ "browser": { "./index.js": "./browser.js" } }
And then separate the functions into two different files, index.js and browser.js
// index.js module.exports = function (string) { return Buffer.from(string, 'binary').toString('base64'); }; // browser.js module.exports = function (string) { return btoa(string); };
After this fix, both of the bundlers produce significantly less sized bundles.
Browserify : 511 bytes (315 min + gzipped)
Webpack : 550 bytes (297 bytes + gzipped)
Now when we publish our module, using it in Node environment will get the Node version of our code, and anyone using it for the browser using bundlers like browserify and webpack will get the browser version.
There you go, the simplest way to keep browser and node code separate for those cross environment compatibility for the same functionality!
Got something to add? Drop your thoughts and feedback.
Also Read:Â Open Collective, A New Chapter of Open Source Funding