When writing a javascript package the first thing (like most things) to consider is the end users of that package. For me, one of the first questions that came to mind was how to support different environments.
I’ve been working on a new project called scrollAnim that easily allows the transition of an element or elements based on the scroll position of the browser. I wanted developers to be able to test/use the package quickly e.g by simply including a script tag and I also wanted to allow the use of the package via an imported module as part of a build system. This post outlines the process I went through to understand how to provide that support.
First, we will discuss modules and their implementation in different environments such as node and AMD environments. We will then cover supporting these different environments at the same time. Finally, we will look at incorporating this into a build process that will allow us to use ES6 and beyond.
What are modules?
When developing programs it can be extremely beneficial to break our program into small parts known as modules. The quote below is taken from Eloquent Javascript’s chapter on Modules and sum them up perfectly:
A module is a piece of program that specifies which other pieces it relies on (its dependencies), and which functionality it provides for other modules to use (its interface). – eloquentjavascript.net
Why modular development?
Modules make life easier because they break down the project into smaller parts and allow development of each specific area of functionality on its own.
Of the many benefits, here are some of the biggest:
- Each module can be worked on independently of the larger application
- It allows us to reuse the package over and over without having to duplicate the code
- This reusability makes it easy to share code with other developers, making everyone’s life easier (hopefully)
Support for different environments individually
The following examples show how to support three different environments individually. The three environments will be:
- The browser
- node/node like environments
- AMD environments.
In each example, we will create a module that has one function test
that has a dependency, lodash. The test
function will simply log a message and the result of two numbers added together:
function test {
console.log('test function was called adding 3 and 7: ', _.add(3,7));
}
Use Directly in the browser
To make our script available in the browser we simply need to declare our functionality in the global scope.
index.js – definition
var test = function() {
console.log('test function was called adding 3 and 7: ', _.add(3,7));
}
index.html – usage
<body>
<script src="lodash.js"></script>
<script src="index.js"></script>
<script>
test();
this.test();
window.test();
</script>
</body>
Use in node/node like environments
To recreate the previous example in node we need to use special exports property of the module object, module.exports
. Currently (v8.11.1) there isn’t support for es6 import
export
statements, it is coming, but for now you will need to transile/build if you would like to use this syntax.
module.js – definition
const _ = require('./lodash');
module.exports = {
test: function() {
console.log('test function was called adding 3 and 7: ', _.add(3,7));
},
}
index.js – usage
const m = require('./module.js');
m.test();
Use in an AMD Environment such as require.js
AMD stands for Asynchronous Module Definition and is a js specification that allows the definition of modules and their dependencies, and loads them asynchronously if desired. The AMD specification is implemented by Dojo Toolkit, RequireJS, and ScriptManJS. More from Wikipedia
index.html – setup
<body>
<script data-main="scripts/main" src="scripts/require.js"></script>
</body>
example-module.js – definition
define(['lodash'], function(_) {
return {
test: function() {
console.log('test function was called adding 3 and 7: ', _.add(3,7));
},
}
});
main.js – usage
requirejs(['example-module'], function(mod) {
mod.test();
});
How to support everything at the same time?
To support all the environments above in one file we need to use the UMD (Universal Module Definition). The UMD works by creating an IIFE that accepts two arguments:
- The root/context we are currently in
- A function that returns an object containing our functionality (dependencies are passed into this function as an argument)
The function checks for the existence of the functions/properties we’ve used above to establish what environment is asking for the module and can be broken down as follows:
- Check if
define
is a function and if it holds theamd
property- Define as an AMD module
- Check if
module
is an object and that is holdsexports
as a property- Define as a commonJs module
- If the above checks are false we fallback to browser
- Make the modules functionality globally available
Because the UMD is invoked immediately this happens the moment the file is included/required/imported and the correct module is returned for the environment containing the functionality defined in function passed as a second argument.
UMD Example combining our three environments above
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['lodash'], factory);
} else if (typeof module === 'object' && module.exports) {
// In a real world situation lodash would come from npm or similar
// and ./ wouldn't be required
module.exports = factory(require('./lodash'));
} else {
// Browser globals (root is window)
root.exampleModule = factory(root._);
}
}(typeof self !== 'undefined' ? self : this, function (_) {
return {
test: function() {
console.log('From UMD: test function was called adding 3 and 7: ', _.add(3,7));
},
}
}));
There are different UMD variations available and can be found at this github repo
You can also find all the examples for this tutorial on github here
What about ES6??
If you would like to use ES6 features such as import
and export
you will have to transpile your code with something like webpack/babel. Luckily, webpack offers a tutorial aimed at library authors specifically showing you how to set up a project that supports different environments, you can find that tutorial here or if you’d just like to see an example project you can view that on github.
Summary
In this article we’ve looked at modules and why writing modular code is a good practice to follow . We also covered the different types of environments in the javascript ecosystem and writing modules for these environments. We then followed up by using the UMD pattern to provide support for all of these environments at the same time without duplication of code. Finally, we discussed using webpack to enable us to use ES6 code in our modules.
References and further reading
Eloquent Javascript Modules
Understanding module exports and exports in Node.js
Writing Modular JavaScript With AMD, CommonJS & ES Harmony
ECMAScript 6 modules: the final syntax
RequireJs tutorial
Reddit – Is AMD / requirejs dying?
Rollup on Es6 Modules
UMD Github repo
Browserify and the universal module definition
RequireJS History
The state of javascript modules
How to write and build libraries in 2018
Writing JS libraries less than 1tb in size