JavaScript Modules
JavaScript modules allow you to break up your code into separate files, making it more maintainable, reusable, and organized. This tutorial covers different module systems in JavaScript and how to use them effectively.
Introduction to Modules
Modules are a way to organize code into separate files with their own scope, allowing you to:
- Split code into smaller, more manageable pieces
- Encapsulate code and hide implementation details
- Reuse code across different parts of an application
- Manage dependencies between different parts of your code
- Avoid polluting the global namespace
Module Systems in JavaScript
JavaScript has evolved several module systems over time:
Module System | Environment | Syntax | Loading |
---|---|---|---|
ES Modules (ESM) | Modern browsers, Node.js 12+ | import /export |
Static, asynchronous |
CommonJS | Node.js | require() /module.exports |
Dynamic, synchronous |
AMD (Asynchronous Module Definition) | Browsers (with RequireJS) | define() /require() |
Dynamic, asynchronous |
UMD (Universal Module Definition) | Both browsers and Node.js | Combination of patterns | Varies |
Note: ES Modules (ESM) is the official standard for JavaScript modules and is recommended for new projects. Other module systems are still widely used in existing codebases.
ES Modules (ESM)
ES Modules were introduced in ES6 (ES2015) and are now supported in all modern browsers and Node.js.
Basic Syntax
// Exporting from a module (math.js)
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// Default export
export default class Calculator {
add(a, b) {
return a + b;
}
}
// Importing in another module (app.js)
import Calculator, { PI, add, multiply } from './math.js';
console.log(PI); // 3.14159
console.log(add(2, 3)); // 5
const calc = new Calculator();
console.log(calc.add(4, 5)); // 9
Named Exports and Imports
// Named exports (utils.js)
export function formatDate(date) {
return date.toISOString().split('T')[0];
}
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// Named imports (app.js)
import { formatDate, capitalize } from './utils.js';
console.log(formatDate(new Date())); // "2023-05-15"
console.log(capitalize('hello')); // "Hello"
// Renaming imports
import { formatDate as format, capitalize as cap } from './utils.js';
console.log(format(new Date()));
console.log(cap('hello'));
Default Exports and Imports
// Default export (user.js)
export default class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getInfo() {
return `${this.name} (${this.email})`;
}
}
// Default import (app.js)
import User from './user.js';
const user = new User('John', 'john@example.com');
console.log(user.getInfo()); // "John (john@example.com)"
Importing All Exports as a Namespace
// Import all exports as a namespace
import * as mathUtils from './math.js';
console.log(mathUtils.PI); // 3.14159
console.log(mathUtils.add(2, 3)); // 5
console.log(mathUtils.multiply(4, 5)); // 20
// The default export is available as 'default'
const Calculator = mathUtils.default;
const calc = new Calculator();
Re-exporting
// Re-exporting from another module (index.js)
export { formatDate, capitalize } from './utils.js';
export { default as User } from './user.js';
// Now other modules can import from index.js
import { formatDate, capitalize, User } from './index.js';
Dynamic Imports
// Dynamic import (loads the module on demand)
async function loadModule() {
try {
const module = await import('./dynamic-module.js');
module.doSomething();
} catch (error) {
console.error('Error loading module:', error);
}
}
// Or with then/catch
import('./dynamic-module.js')
.then(module => {
module.doSomething();
})
.catch(error => {
console.error('Error loading module:', error);
});
Tip: Dynamic imports are useful for code splitting and lazy loading, improving initial load performance by only loading modules when needed.
Using ES Modules in the Browser
<!-- Add type="module" to use ES modules in the browser -->
<script type="module" src="app.js"></script>
<!-- You can also include inline module scripts -->
<script type="module">
import { formatDate } from './utils.js';
console.log(formatDate(new Date()));
</script>
Note: When using ES modules in the browser, you need to serve your files from a web server due to CORS restrictions. Opening HTML files directly with file://
URLs won't work.
CommonJS Modules
CommonJS is the module system used in Node.js. It uses require()
and module.exports
for importing and exporting.
// Exporting in CommonJS (math.js)
const PI = 3.14159;
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
class Calculator {
add(a, b) {
return a + b;
}
}
// Exporting multiple items
module.exports = {
PI,
add,
multiply,
Calculator
};
// Or export individual items
module.exports.PI = PI;
module.exports.add = add;
// Or export a single item
module.exports = Calculator;
// Importing in CommonJS (app.js)
const math = require('./math.js');
console.log(math.PI); // 3.14159
console.log(math.add(2, 3)); // 5
// Destructuring import
const { PI, add, multiply } = require('./math.js');
console.log(PI); // 3.14159
console.log(add(2, 3)); // 5
// If the module exports a single item
const Calculator = require('./calculator.js');
const calc = new Calculator();
Module Bundlers
Module bundlers are tools that process your JavaScript modules and dependencies, combining them into one or more optimized bundles for the browser.
Popular Module Bundlers
- Webpack: The most widely used bundler, with extensive plugin ecosystem
- Rollup: Focused on ES modules, produces smaller bundles with tree-shaking
- Parcel: Zero-configuration bundler with built-in support for many file types
- esbuild: Extremely fast bundler written in Go
- Vite: Modern build tool that leverages ES modules for development
Basic Webpack Configuration
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
};
Project Structure with Modules
- src/
- index.js
- utils/
- math.js
- format.js
- index.js
- components/
- Button.js
- Modal.js
- index.js
- webpack.config.js
- package.json
- dist/
- bundle.js
- index.html
Barrel Files (index.js)
A common pattern is to use "barrel" files (usually named index.js) to re-export multiple modules from a directory:
// src/utils/index.js
export { add, subtract, multiply, divide } from './math.js';
export { formatDate, formatCurrency } from './format.js';
// Now you can import from the directory
import { add, formatDate } from './utils';
Best Practices for JavaScript Modules
- Single Responsibility: Each module should have a single, well-defined purpose.
- Explicit Exports: Be explicit about what you're exporting from a module.
- Avoid Side Effects: Modules should ideally be pure, without side effects when imported.
- Use Named Exports: Prefer named exports over default exports for better refactoring and auto-imports.
- Organize by Feature: Group related modules together by feature rather than by type.
- Keep Modules Small: Aim for small, focused modules rather than large, monolithic ones.
- Use Barrel Files: Use index.js files to simplify imports from directories.
- Avoid Circular Dependencies: Circular dependencies can cause issues and should be avoided.
Warning: Circular dependencies (when module A imports from module B, and module B imports from module A) can lead to unexpected behavior and should be avoided. Restructure your code to break the cycle if you encounter this issue.
Next Steps
Now that you understand JavaScript modules, you can explore:
- Advanced module bundling techniques like code splitting and lazy loading
- Module federation for micro-frontends
- TypeScript modules and namespaces
- Package management with npm or yarn
- Creating and publishing your own npm packages