What is a module?
As our application grows bigger, we want to split it into multiple files, so called “modules”. A module may contain a class or a library of functions for a specific purpose.
A module is just a file. One script is one module. Modules can load each other and use special directives export
and import
to interchange functionality, call functions of one module from another one:
export
keyword labels variables and functions that should be accessible from outside the current module.import
allows the import of functionality from other modules.
For instance, if we have a file sayHi.js exporting a function:
// sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
Then another file may import and use it:
import { sayHi } from "./sayHi.js";
alert(sayHi);
sayHi("John"); // Hello, John!
The import
directive loads the module by path ./sayHi.js
relative to the current file, and assigns exported function sayHi
to the corresponding variable.
As modules support special keywords and features, we must tell the browser that a script should be treated as a module, by using the attribute <script type="module">
.
Note: A module code is evaluated only the first time when imported. If the same module is imported into multiple other modules, its code is executed only once, upon the first import. Then its exports are given to all further importers.
There’s a rule: top-level module code should be used for initialization, creation of module-specific internal data structures. If we need to make something callable multiple times – we should export it as a function, like we did with sayHi above.
Let's see an example:
// admin.js
export let admin = {
name: "John",
};
If this module is imported from multiple files, the module is only evaluated the first time, admin
object is created, and then passed to all further importers.
All importers get exactly the one and only admin object:
// 1.js
import { admin } from "./admin.js";
admin.name = "Pete";
// 2.js
import { admin } from "./admin.js";
alert(admin.name); // Pete
// Both 1.js and 2.js reference the same admin object
// Changes made in 1.js are visible in 2.js
Build Tools
In real-life, browser modules are rarely used in their “raw” form. Usually, we bundle them together with a special tool such as Webpack and deploy to the production server.
One of the benefits of using bundlers – they give more control over how modules are resolved, allowing bare modules and much more, like CSS/HTML modules.
Build tools do the following:
- Take a “main” module, the one intended to be put in
<script type="module">
in HTML. - Analyze its dependencies: imports and then imports of imports etc.
- Build a single file with all modules (or multiple files, that’s tunable), replacing native import calls with bundler functions, so that it works. “Special” module types like HTML/CSS modules are also supported.
- In the process, other transformations and optimizations may be applied:
- Unreachable code removed.
- Unused exports removed (“tree-shaking”).
- Development-specific statements like console and debugger removed.
- Modern, bleeding-edge JavaScript syntax may be transformed to older one with similar functionality using Babel.
- The resulting file is minified (spaces removed, variables replaced with shorter names, etc).
Export and Import
Export and import directives have several syntax variants. We saw a simple use, now let’s explore more examples.
Export before declarations
We can label any declaration as exported by placing export before it, be it a variable, function or a class. For instance, here all exports are valid:
// export an array
export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// export a constant
export const MODULES_BECAME_STANDARD_YEAR = 2015;
// export a class
export class User {
constructor(name) {
this.name = name;
}
}
Export apart from declarations
Also, we can put export separately. Here we first declare, and then export:
function sayHi(user) {
alert(`Hello, ${user}!`);
}
function sayBye(user) {
alert(`Bye, ${user}!`);
}
export {sayHi, sayBye}; // a list of exported variables
Import *
If there’s a lot to import, we can import everything as an object using import * as <obj>
, for instance:
import * as say from './say.js';
say.sayHi('John');
say.sayBye('John');
Import “as”
We can also use as to import under different names.
For instance, let’s import sayHi
into the local variable hi
for brevity, and import sayBye
as bye
:
import {sayHi as hi, sayBye as bye} from './say.js';
hi('John'); // Hello, John!
bye('John'); // Bye, John!
Note: Export "as" works the same way.
Export default
In practice, there are mainly two kinds of modules.
- Modules that contain a library, pack of functions, like
say.js
above. - Modules that declare a single entity, e.g. a module
user.js
exports onlyclass User
.
Mostly, the second approach is preferred, so that every “thing” resides in its own module. Naturally, that requires a lot of files, as everything wants its own module, but that’s not a problem at all. Actually, code navigation becomes easier if files are well-named and structured into folders.
Modules provide a special export default
(“the default export”) syntax to make the “one thing per module” way look better.
export default class User { // just add "default"
constructor(name) {
this.name = name;
}
}
There may be only one export default per file and then import it without curly braces:
import User from './user.js'; // not {User}, just User
new User('John');