ES6 Modules, Build Tools and Browser App Delivery
By Ryan Florence, published 2013-07-15
TL;DR Write for the future, use the tools of today.
Server-side dependency management is heaven
- install and declare dependencies
- require in your code
- package up generalized code and publish it to one place (npm, gems, etc.)
Browser dependency management is not even close to solved, and terrible
- multiple module formats
- multiple package managers
- package up generalized code, publish all over the place, people still can’t use it easily
This is important
We’ve got to solve this. When somebody’s got an angular directive, an ember component, a web component, or any old useful javascript library they need a simple way to share it. Node has had great success because of how simple npm is to use.
To use vendor code in node its this simple.
$ npm install backbone
from the cli.var Backbone = require('backbone')
in your app.
I want this for the browser. But first:
Three distribution use-cases
- entire app delivered at once (general use case)
- partial builds loaded over time (still common)
- no build (totally valid, haters)
Application build strategies
- configure and concat
- load up entire directories
- add explicit load order config
- trace dependencies programmatically (r.js, browserify)
- this is how server code works, it just doesn’t package it up
- generally the whole idea behind a build, create an environment of modules for the browser
- this is how server code works, it just doesn’t package it up
App delivery graph
Given those application distribution use cases and build strategies, we have:
modules | app distribution | build strategy |
---|---|---|
globals | full | concat, none |
AMD | full, partial | concat, dep tree, none |
CJS | full | concat, dep tree |
CJS/globals can do partial app loading with $.getScript and friends, but not as part of the module definition.
In times past I argued for AMD since–as you can see–it meets every use case. AMD has gained a lot of traction but will never see complete adoption by library authors, package managers, and JavaScript environments.
Existing tools cannot handle each of these use cases. You have to subscribe to the format that your tools subscribe to, and so do the libs you depend on.
We need a module format that existing tools can consume
- Globals, not declarative, can’t find dependencies with tools, bad
- AMD, ugly, not everybody will adopt it
- CJS, doesn’t meet all the app distribution use cases
So no matter what you pick for your application you’re going to have to have to be hacking around 3rd party module systems that don’t match yours.
Unless people shipped UMD.
WAT IS UMD?!
A long time ago a few of us came up with the saddest looking JavaScript ever called Universal Module Definition.
As its name implies, the module format works in pretty much every module environment.
If Backbone, Ember, moment, angular, and everybody else wrote their code with UMD then you could throw all the existing front-end tools at them. For example, you could:
- Choose AMD, install scripts with bower and build with r.js
- Do #1 with no build at all
- Choose CommonJS, install scripts with npm and build with browserify
- Choose AMD, install scripts with npm, and build with r.js
- Choose no format and simply include script tags on the page
- Use component with anything that supports UMD.
I could do a table of the cartesian product between all the module formats, package managers, and build tools but you get the point. It supports all the existing tools and therefore supports all the application delivery use-cases.
But there is one problem. “umd” gets auto-corrected to “mud”, and its no lie. Here is the terrible ugly using backbone as an example:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define('backbone', ['jquery', 'underscore'], function (jQuery, _) {
return factory(jQuery, _);
});
} else if (typeof exports === 'object') {
// Node.js
module.exports = factory(require('jquery'), require('underscore'));
} else {
// Browser globals
root.Backbone = factory(root.jQuery, root._);
}
}(this, function (jQuery, _) {
var Backbone = {};
// Backbone code that depends on jQuery and _
return Backbone;
}));
You can see why this never caught on. It is terrible. Nobody should have to write that. Looking at it makes me want to pick up cursing and describe it more colorfully.
Two years ago I didn’t know what to do to get over that. But today, things are different.
Author in ES6, transpile to UMD, distribute everywhere
Enter ES6 modules. Library devs can author in ES6 modules, transpile to UMD and distribute everywhere (npm, bower, whatever).
- authoring looks as good or better than CJS with destructuring
- its the future, you will eventually use this format anyway
All that’s left is to add UMD support to the es6-module-transpiler. Which machty and I are working on right now.
What this requires: Library authors to be on board
This could be an uphill battle with some people, but:
- anybody who cares about browser modules will likely see the benefit
- eventually ES6 is going to land and people have a platform incentive to start authoring that way
- we can fork/shim their repos and then point package managers to the fork/shim (we already do this with bower all the time).
The future
Step 1: getting library authors to author in es6.
Step 2: getting package managers to support es6 and transpile for us.
Step 3: simply waiting for ES6 modules to land and doing nothing.
We can do this.
Follow the es6-module-transpiler fork to see when we get the UMD compiler finished, and I’ll follow up with some live examples.