JavaScript modules allow developers to organize code into separate, reusable files. As applications grow in complexity, a system for importing and exporting features between these files becomes essential. While Node.js has long supported modularity, modern browsers now offer native support for module features, often more efficiently than client-side libraries or bundlers alone. This guide explores how to effectively use JavaScript modules, with a specific focus on simplifying import paths and managing dependencies using import maps in a browser environment.
The Basics of Exporting and Importing Modules
To use module features, you must first export them from a module file. The export statement can be placed in front of individual items like functions, `var`, `let`, `const`, or classes. These must be top-level items within the module. Alternatively, a single `export` statement at the end of the file can list all features to be exported in curly braces, for example: `export { name, draw };`
Once features are exported, they can be imported into another script using the `import` statement. The simplest form is `import { feature1, feature2 } from "./path/to/module.js";`. The module specifier, such as `./path/to/module.js`, tells the JavaScript environment where to find the module file. In browsers, this is typically a path relative to the current file, making URLs portable. Imported values are read-only live bindings; they update if the exporting module re-assigns them, but the importing module cannot re-assign the imported variable itself, though it can modify properties of imported object values.
Simplifying Module Specifiers with Import Maps
By default, browser module specifiers are absolute or relative URLs. However, import maps allow developers to define custom module specifiers, including bare module names, which are common in environments like Node.js. An import map is defined within a `<script>` element with `type="importmap"` in your HTML, containing a JSON object.
The `imports` key within the import map defines a mapping from a module specifier (a property name) to its corresponding URL (the value). For instance, an import map like this:
<script type="importmap"> { "imports": { "shapes": "./shapes/square.js", "square": "./modules/shapes/square.js" } } </script>
allows you to import modules using bare names or simplified paths, such as `import { name as squareNameOne } from "shapes";` or `import { name as squareNameTwo } from "square";`. This significantly improves the ergonomics of import code, making it more concise and easier to manage.
Advanced Import Map Features
Import maps offer several advanced capabilities. You can use trailing forward slashes on both the module specifier key and its value to remap entire path prefixes, facilitating package-like imports. For example, `{ "lodash/": "/node_modules/lodash-es/" }` allows importing individual modules from a package like `import fp from "lodash/fp.js";`.
Another powerful feature is scoped modules, managed by the `scopes` key in the import map. This allows for version management, where different versions of the same module can be imported based on the path of the script performing the import. This mimics package manager behavior, allowing complex applications to use multiple versions of a dependency without conflicts. For instance, if a script from `/node_modules/dependency/` imports `cool-module`, a specific version defined within that scope can be used, while other scripts might use a different default version.
Import maps can also improve caching by decoupling module names from their hashed filenames. Instead of depending on `application-fg7744e1b.js`, you can map a logical name like `main_script` to the current hashed filename. When the file changes and its hash updates, only the import map needs modification, not every script that depends on it.
Furthermore, a unified module architecture, enabled by import attributes (e.g., `with { type: "json" }`), allows importing non-JavaScript resources like JSON or CSS directly as modules. Browsers validate the declared module type, preventing accidental execution of non-JavaScript code.
Applying Modules to HTML and Browser Compatibility
To apply a JavaScript module to an HTML page, you must include `type="module"` in the `<script>` element: `<script type="module" src="main.js"></script>`. Modules are automatically interpreted in strict mode. It's crucial to note that `import` and `export` statements can only be used inside scripts declared as modules; attempting to use them in regular scripts will throw a `SyntaxError`. You can check for import map support using `HTMLScriptElement.supports?.("importmap")`.
When testing locally with `file://` URLs, you may encounter CORS errors due to security requirements for JavaScript modules. Running a local web server is necessary to avoid these issues. While bundlers like webpack still serve important roles in optimizations like minification and tree-shaking, native browser module support, enhanced by import maps, provides an efficient and ergonomic way to manage JavaScript code in modern web development.