gulp. Call it the bright side of hipsterism, if you will.
It’s not a perfect world, though — ask any ten JS devs what their major points of pain are and all of them will name client-side modules and code packaging:
It’s one of the areas, like Streams, where the community has done a solid job of defining a de-facto standard API for something and sticking to it, and a rather schizophrenic job of documenting what problems the de-facto standard is solving and how it’s solving them.
It’s easy to find boilerplate and recipes for things like setting RequireJS up to make your client-side code modular and even maybe concatenate and minify it for you, but what actually is a module? How do the different systems compare? When should you use AMD or CommonJS or an ES6 polyfill? Just what exactly is Browserify actually doing? I’d like to explore confusing questions like this in this series.
It’s inevitably necessary in any nontrivial software project to split code into a tree of interdependent segments. And we need to be able to reuse those segments.
But say segment
foo relies on segment
bar in turn absolutely needs the segment
baz to get its job done. How do we manage that? Does
foo then need to directly ensure the presence of
baz (and anything
baz needs and so on up the whole chain)? That’s crazy-making and untenable, and it’s the reason that it’s not enough just to split your code into separate files and include the right script tags on each page.
Instead, I want to help you understand the context and the pieces involved in modules and packaging. The goal of this first of three articles is to give you a bigger picture of what modules—the building-block segments of our code—are at an abstract level so that we can start putting them together and making them work.
Let’s step back from any particular language, library or standard and think about modules in the abstract for a moment. What, exactly, is a module? In the strict computer science sense, a module is a way of associating a value (most usually a collection of named subroutines) with a name of some type — and that’s it.
To make this actually useful, however, one also needs a mechanism for specifying module dependencies, that is, a way to say “This piece of code needs the values provided by this list of module names in order to be understood”.
These two parts, taken together with some under-the-hood code responsible for connecting a module name to its associated value, is what we mean when we talk about module systems.
require statement to assign the value associated with that module name to a local variable. Like so:
There are a number of different, competing specifications for defining modules—module definition schemes. Examples are AMD, UMD, CommonJS, and
Fortunately, although there are numerous module schemes, the community has settled on a very simple formal definition of what a module itself actually is that is common across all schemes.
Read those examples carefully: you really can, in all of the major module definition schemes, create a module whose value is just the integer
As we’ll see to be a trend, this all actually isn’t quite true for ES6 modules — but isn’t quite false, either. More on this in a future post.
A factory function is a bit of code that gets called and returns the value of the module. Usually, this takes the form of a function you supply to the module system that will get called when the value of your module is needed.
That said, it may or may not look like you’re writing a function when you create your module. In
AMD, it does — this is the function passed to
define— while in
node.js, your file (module) is effectively wrapped in a function that returns the
This value, the one returned by the factory function, is the value that users of your module get when they request your module by name from the module system (e.g. in
node, this looks like
var foo = require('foo-module');).
Each time the module name associated with this module gets requested, the module system is expected to return whatever the value of the module should be by invoking this factory function. Generally this happens once per runtime environment (that is, once per web page load), and the module system caches the value for later use if more things request it.
Every module needs to have a string-valued name, e.g.
Watch out! This is not a filename, though it sometimes looks like one. It’s just an identifier to associate a value with.
Some module loading schemes do apply some level of structure to this — e.g.
AMD allows relative pseudo-file-paths (to be explained in a future post, don’t worry).
…and that’s it. All nontrivial browser-side module loading schemes share this definition of a module (but as usual, we’ll see that ES6 modules are the exception), which means the main difference is the syntax for how modules get defined and resolved. This is why the
UMD module definition scheme can work — also to be explained shortly.
Recall that a module is not, necessarily, associated with a file on a filesystem or web server. Many module systems do make this association in some way, but it’s not a part of the fundamental concept of a module and it’s important to understand this distinction so that you can understand that one of the important questions about any given module system is how it maps modules to files.
Code module handling is one of those simple-in-theory, fiddly-in-practice concepts, and nowhere is this more true than in the browser, but hopefully this post has given you a compass by which to navigate some of these subtleties.
Cloud Four team members Tyler Sticka, Erik Jung and Lyza Gardner contributed to this post.