ECMAScript 6 modules: the future is now
Update 2014-09-07: Newer version of this blog post: “ECMAScript 6 modules: the final syntax”. Read it instead of this one.
This blog post first explains how modules work in ECMAScript 6, the next version of JavaScript. It then describes tools that allow you to already use them now.
Module systems for current JavaScript
JavaScript does not have built-in support for modules, but the community has created impressive work-arounds. The two most important (and unfortunately incompatible) standards are:
- CommonJS Modules (CJS): The dominant incarnation of this standard is Node.js modules (Node.js modules have a few features that go beyond CJS). Characteristics:
- Compact syntax
- Designed for synchronous loading
- Main use: server
- Asynchronous Module Definition (AMD): The most popular implementation of this standard is RequireJS. Characteristics:
- Slightly more complicated syntax, enabling AMD to work without eval() (or a compilation step).
- Designed for asynchronous loading
- Main use: browsers
- Slightly more complicated syntax, enabling AMD to work without eval() (or a compilation step).
The above is but a simplified explanation of the current state of affairs. If you want to read more in-depth material, take a look at “Writing Modular JavaScript With AMD, CommonJS & ES Harmony” by Addy Osmani.
ECMAScript 6 modules
The goal for ECMAScript 6 (ES6) modules was to create a format that both users of CJS and of AMD are happy with. To that end, their syntax is as compact as CJS. On the other hand, they are less dynamic than CJS (e.g., you can’t conditionally load a module with normal syntax). That has two main advantages:
- You get compile time errors if you try to import something that has not been exported.
- You can easily load ES6 modules asynchronously.
The ES6 module standard has two parts:
- Declarative syntax (for importing and exporting).
- Programmatic loader API: to configure how modules are loaded and to conditionally load modules.
ECMAScript 6 module syntax
ECMAScript 6 modules look very similar to Node.js modules. A module is simply a file with JavaScript code in it.
As an example, take the following project, whose files are stored in a directory calculator/.
calculator/
lib/
calc.js
main.js
Exporting
If there is something you want others to use, you export it, by prefixing the keyword export to a variable declaration (via var, let, const), a function declaration or a class declaration [1].
calculator/lib/calc.js contains the following text:
// calculator/lib/calc.js
let notExported = 'abc';
export function multiply(x) {
return x * MY_CONSTANT;
}
export const MY_CONSTANT = 7;
The above module exports the function multiply and the value MY_CONSTANT.
Note that this syntax is quite convenient. In Node.js modules (AMD is similar), you have two options. Option 1: be redundant.
// calculator/lib/calc.js
let notExported = 'abc';
function multiply(x) {
return x * MY_CONSTANT;
}
const MY_CONSTANT = 7;
module.exports = {
multiply: multiply,
MY_CONSTANT: MY_CONSTANT
};
Option 2: refer to exported values differently (and somewhat awkwardly).
// calculator/lib/calc.js
let notExported = 'abc';
exports.multiply = function (x) {
return x * exports.MY_CONSTANT;
};
exports.MY_CONSTANT = 7;
An alternative to inlined exports
If you don’t want to insert exports in your code, you have the option of exporting everything later, e.g. at the end:
let notExported = 'abc';
function multiply(x) {
return x * MY_CONSTANT;
}
const MY_CONSTANT = 7;
export { multiply, MY_CONSTANT };
You can also rename while exporting:
export { multiply as mult, MY_CONSTANT as SOME_CONSTANT };
Importing
main.js is another module and it imports multiply from calc.js:
// calculator/main.js
import { multiply } from 'lib/calc';
console.log(multiply(3));
main.js refers to calc.js via the module ID 'lib/calc' (a string). The default interpretation of the ID is as a path relative to the importing module. Note that you can import more than one value if you want to:
// calculator/main.js
import { multiply, MY_CONSTANT } from 'lib/calc';
Alternatively, you can import the module as an object and access the exports via properties:
// calculator/main.js
import 'lib/calc' as c;
console.log(c.multiply(3));
If you are unhappy with the name that an exporting module has chosen, you can rename locally:
// calculator/main.js
import { multiply as mult } from 'lib/calc';
console.log(mult(3));
Re-exporting
You can re-export some exports of another module:
export { encrypt as en } from 'lib/crypto';
You can also re-export everything:
export * from 'lib/crypto';
Default exports
Sometimes a module only exports a single value (for example, a large class). Then you can make that value the default export:
// myapp/models/Customer.js
export default class { // anonymous class
constructor(id, name) {
this.id = id;
this.name = name;
}
};
The syntax for importing a default export is similar to normal importing, but there are no braces (as a mnemonic, you are not importing something from inside the module, you are importing the module):
// myapp/myapp.js
import Customer from 'models/Customer';
let c = new Customer(0, 'Jane');
ECMAScript 6 module loader API
In addition to the declarative syntax for working with modules, there is also a programmatic API. It allows you to do two things: programmatically working with modules and scripts and configuring module loading.
Importing modules and loading scripts
You can programmatically import modules, with a syntax reminiscent of AMD modules:
System.import(
['module1', 'module2'],
function (module1, module2) { // success
...
},
function (err) { // failure
...
}
);
Among other things, this enables you to conditionally load modules.
System.load() works similarly to System.import(), but loads script files instead of importing modules.
Configuring module loading
The module loader API has various hooks for configuration. A few examples of what they allow you to do:
- Customize how module IDs are mapped to module files.
- Lint modules on import (e.g. via JSLint or JSHint).
- Automatically translate modules on import (they could contain CoffeeScript or TypeScript code).
- Use legacy modules (AMD, Node.js).
You’d have to implement these things yourself, but the hooks for them are there.
Using ECMAScript 6 modules today
The two most recent projects enabling you to use ECMAScript modules today are:
- ES6 Module Transpiler: write your modules using a subset of ECMAScript 6 (roughly: ECMAScript 5 + export + import), compile them to AMD or CommonJS modules. A blog post by Ryan Florence explains this approach in detail.
- ES6 Module Loader: polyfills the ECMAScript 6 module loader API on current browsers. To enter the world of modules, you use the API:
System.baseURL = '/lib';
System.import('js/test1', function (test1) {
test1.tester();
});
In actual modules, you use ECMAScript 5 + export + import. For example:
export function tester() {
console.log('hello!');
}
The project jspm loader builds on the ES6 Module Loader and enables you to load AMD modules and CJS modules in addition to ES6 modules.
Other possibilities:
- require-hm: a plugin for RequireJS allowing it to load ECMAScript 6 modules (only ECMAScript 5 plus importing and exporting is supported). A blog post by Caolan McMahon explains how it works. Warning: uses an older module syntax.
- Traceur (an ECMAScript 6 to ECMAScript 5 compiler): has partial support for modules and may eventually support them fully.
- TypeScript (roughly: ECMAScript 6 plus optional static typing): compiles modules in external files (which can use most of ECMAScript 6) to AMD or CommonJS.
Further reading
- The specification of ECMAScript 6 modules: Modules are not yet in the draft ECMAScript 6 specification. Until they are, consult the Harmony wiki for details.
- “ES6 Modules” by Yehuda Katz: a discussion of common use cases and interoperability with existing module systems.
Comments
Post a Comment