Ever had to use statements like the following?
1 2 3 4 5 6
It’s a simple check to see whether a script is being executed in in a browser, and if not, assume that it’s a node.js process. I noticed that I need them more and more. I mostly develop libraries. As soon as I need functionality which is not standard available in both browser and node.js, like fetching urls, I have to create two different implementations and switch depending on the environment. It’s annoying and I don’t yet have a seamless solution for dealing with environment specific code.
Types of isomorphic code
- Environment agnostic modules.
process. Examples are lodash, async, moment.js, numeral.js, and math.js.
- Modules shimmed per environment. They offer a single API, and different implementations per environment. For example superagent, socket.io, debug, and pubnub.
Environment agnostic modules just work fine everywhere. Shimmed modules however require selecting the right implementation depending on the current environment. This is no problem when the module is consumed by an application. In that case it’s clear in which environment the application will run, like server side or client side. But when the module is consumed by another module, this choice can’t be made. For example a library for currency conversion (which makes network requests to a REST API), or a library using a proper debug library. The consuming module will have to distribute implementations for the different environments too. This results in a cascading effect, forcing all modules down the chain to create builds for various environments.
In the previous section we discussed isomorphic modules offering a single API and different implementations per environment. This is only possible when the underlying functionality is available on each of the targeted environments. Let’s for now focus on the browser and node.js. In both, networking is a core functionality: the browser offers AJAX technology, and node.js has HTTP (and other protocols) in it’s genes. Both environments offer similar functionality, but with totally different API’s.
The following table shows an (incomplete) list with similar functionality available on both browser and node.js:
|HTTP requests||XMLHttpRequest (AJAX)||http|
|Background processes||Web Worker||child_process|
|Rendering||DOM||React, jsdom, PhantomJS|
|Debugging||Web Console: Chrome, FireFox||console, Winston, Bunyan|
Notable here is the approach of browserify, which offers browser shims for many node.js specific modules like
console. In practice though, the size of these shims can be quite large, which can be a serious problem for browser applications.
How to distribute isomorphic libraries?
There are basically two ways for distributing isomorphic libraries:
- Distribute different implementations per environment. The library consumer will have to select the right implementation depending on the environment.
- Distribute a single, universal implementation, which automatically detects the environment at runtime and loads the needed environment specific code.
The problem with the first approach is that it does not solve the problem but passes it on to the library consumer. This is fine when consumed by an application, but not when consumed by an other library. This library in turn has to check the environment, load the correct implementation of the consumed library, and has to distribute multiple versions of itself too. This results in a cascading effect.
The problem with the second approach is that the isomorphic library will contain the code for all supported environments. For node.js modules this is no big deal, but for the browser it’s important to minimize the amount of bundled code. It’s simply not acceptable to send precious kB’s over the wire containing unused, node.js specific code.
The only scalable, long term solution here is the second one. The exposure of environment specific code should be minimized. Library consumers should not have to bother about which implementation to pick, nor should they be bothered with distributing their own library in an isomorphic way due to isomorphic dependencies. This means we have to tackle the problem of distributing bloated application code containing code for multiple environments. We need build tools which can strip away non-relevant, environment specific code, like removing node.js specific code when bundling a browser app.
Tooling and conventions
In order to facilitate universally usable isomorphic libraries, two things are needed:
- Tooling to detect the current environment at runtime.
- Tooling to strip node.js specific code from application bundles when redundant.
In order to be able to let an isomorphic library switch to the right implementation, the library needs a way to detect in which environment it is running. This does not need to be very complicated. There may exist already exist solutions for this without me knowing it. We need detection and handling of environments. It could look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
This module should of course be published as an UMD module. Suppose we have an http library with an implementation for the browser (
myModule.browser.js) and node.js (
myModule.node.js), the module could expose it’s main file,
1 2 3
Bundling for the browser
Suppose that modules are distributed as a single, universal implementation, containing multiple implementations for different environments. When bundling an application for use in the browser, the parts of the code not relevant for the browser (i.e. implementations for node.js) should be removed.
Tools like browserify, webpack, and uglifyjs are typically used for bundling and minification of browser apps. When bundling an application, it is possible to configure specific modules to be excluded from the bundle. This mechanism could be used to exclude environment specific modules without forcing the end-user to specify them one by one. When all environment specific code uses a suffix, it would be easy to filter away these files from an application bundle. Files could be named like:
1 2 3 4 5
To bundle an application with browserify, all you have to do is ignore all
Note that such a solution will only work if every isomorphic module uses the same environment suffixes. There is a convention needed to let this be successful.
Update 2018-01-10: Replaced a broken link to an article about the CommonJS module system.