ES6 modules are a new module format designed for all JavaScript environments. While Node.js has had a good module system for its whole existence, browser-side JavaScript has not. That left the browser-side community with either relying on the <script> tag, or using non-standardized solutions. For that matter, traditional Node.js modules were never standardized, outside of the CommonJS effort. Therefore, ES6 modules stand to be a big improvement for the entire JavaScript world, by getting everyone on the same page with a common module format and mechanisms.
The side effect is that the Node.js community needs to start looking at, learning about, and adopting the ES2015 module format.
ES6 modules are referred to by Node.js with the extension .mjs. When it came to implementing the new module format, the Node.js team determined that they could not support both CommonJS and ES6 modules with the .js extension. The .mjs extension was decided as the solution, and you may see tongue-in-cheek references to Michael Jackson Script for this file extension.
One interesting detail is that ES6 modules load asynchronously. This may not have an impact on Node.js programmers, except that this is part of the rationale behind requiring the new .mjs extension.
Create a file named simple2.mjs in the same directory as the simple.js example that we looked at earlier:
var count = 0;
export function next() { return ++count; }
function squared() { return Math.pow(count, 2); }
export function hello() {
return "Hello, world!";
}
export default function() { return count; }
export const meaning = 42;
export let nocount = -1;
export { squared };
ES6 items exported from a module are declared with the export keyword. This keyword can be put in front of any top-level declaration, such as variable, function, or class declarations:
export function next() { .. }
The effect of this is similar to the following:
module.exports.next = function() { .. }
The intent of both is essentially the same: to make a function, or other object, available to code outside the module. A statement such as export function next() is a named export, meaning the exported thing has a name, and that code outside the module uses that name to access the object. As we see here, named exports can be functions or objects, and they may also be class definitions.
Using export default can be done once per module, and is the default export from the module. The default export is what code outside the module accesses when using the module object itself, rather than when using one of the exports from the module.
You can also declare something, such as the squared function, and then export it later.
Now let's see how to use this ES2015 module. Create a simpledemo.mjs file with the following:
import * as simple2 from './simple2.mjs';
console.log(simple2.hello());
console.log(`${simple2.next()} ${simple2.squared()}`);
console.log(`${simple2.next()} ${simple2.squared()}`);
console.log(`${simple2.default()} ${simple2.squared()}`);
console.log(`${simple2.next()} ${simple2.squared()}`);
console.log(`${simple2.next()} ${simple2.squared()}`);
console.log(`${simple2.next()} ${simple2.squared()}`);
console.log(simple2.meaning);
The import statement does what it means: it imports objects exported from a module. This version of the import statement is most similar to a traditional Node.js require statement, meaning that it creates an object through which you access the objects exported from the module.
This is how the code executes:
$ node --experimental-modules simpledemo.mjs
(node:63937) ExperimentalWarning: The ESM module loader is experimental.
Hello, world!
1 1
2 4
2 4
3 9
4 16
5 25
42
As of Node.js 8.5, the new module format is available behind an option flag as shown here. You're also presented with this nice warning that it's an experimental feature. Accessing the default export is accomplished by accessing the field named default. Accessing an exported value, such as the meaning field, is done without parentheses because it is a value and not a function.
Now to see a different way to import objects from a module, create another file, named simpledemo2.mjs, containing the following:
import {
default as simple, hello, next
} from './simple2.mjs';
console.log(hello());
console.log(next());
console.log(next());
console.log(simple());
console.log(next());
console.log(next());
console.log(next());
In this case, each imported object is its own thing rather than being attached to another object. Instead of writing simple2.next(), you simply write next(). The as clause is a way to declare an alias, if nothing else so you can use the default export. We already used an as clause earlier, and it can be used in other instances where you wish to provide an alias for the value being exported or imported.
Node.js modules can be used from ES2015 .mjs code. Create a file named ls.mjs, containing the following:
import _fs from 'fs';
const fs = _fs.promises;
import util from 'util';
(async () => {
const files = await fs.readdir('.');
for (let fn of files) {
console.log(fn);
}
})().catch(err => { console.error(err); });You cannot, however, require an ES2015 module into regular Node.js code. The lookup algorithm for ES2015 modules is different, and as we mentioned earlier, ES2015 modules are loaded asynchronously.
Another wrinkle is handling the fs.promises submodule. We are using that submodule in the example, but how? This import statement does not work:
import { promises as fs } from 'fs';
This fails as so:
$ node --experimental-modules ls.mjs
(node:45186) ExperimentalWarning: The ESM module loader is experimental.
file:///Volumes/Extra/book-4th/chap03/ls.mjs:1
import { promises as fs } from 'fs';
^^^^^^^^
SyntaxError: The requested module 'fs' does not provide an export named 'promises'
at ModuleJob._instantiate (internal/modules/esm/module_job.js:89:21)
That leaves us with this construct:
import _fs from 'fs';
const fs = _fs.promises;
Executing the script gives the following:
$ node --experimental-modules ls.mjs
(node:65359) ExperimentalWarning: The ESM module loader is experimental.
(node:37671) ExperimentalWarning: The fs.promises API is experimental
ls.mjs
module1.js
module2.js
simple.js
simple2.mjs
simpledemo.mjs
simpledemo2.mjs
The last thing to note about ES2015 module code is that import and export statements must be top-level code. Even putting an export inside a simple block like this:
{
export const meaning = 42;
}
Results in an error:
$ node --experimental-modules badexport.mjs
(node:67984) ExperimentalWarning: The ESM module loader is experimental.
SyntaxError: Unexpected token export
at ModuleJob.loaders.set [as moduleProvider] (internal/loader/ModuleRequest.js:32:13)
at <anonymous>
While there are a few more details about ES2015 modules, these are their most important attributes.