JavaScript Module System
Learning Objectives
- Understand ES Modules (ESM) import/export syntax
- Identify differences between CommonJS and ESM
- Utilize dynamic import and code splitting
- Understand the role of module bundlers
1. The Need for Modules
1.1 What Are Modules?
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Module Benefits β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Before (global scope pollution): β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β <script src="lib1.js"></script> <!-- var helper = ... --> β β
β β <script src="lib2.js"></script> <!-- var helper = ... --> β β
β β <script src="app.js"></script> <!-- helper? Which one? -->β β
β β β β
β β Problems: name conflicts, dependency order, global pollutionβ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β After (module system): β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β // lib1.js β β
β β export const helper = () => { ... }; β β
β β β β
β β // app.js β β
β β import { helper } from './lib1.js'; β β
β β β β
β β Benefits: clear dependencies, encapsulation, reusability β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1.2 Types of Module Systems
// 1. CommonJS (Node.js default)
const fs = require('fs');
module.exports = { myFunction };
// 2. ES Modules (ECMAScript standard, browser + Node.js)
import fs from 'fs';
export const myFunction = () => {};
// 3. AMD (RequireJS, legacy)
define(['dependency'], function(dep) {
return { myFunction };
});
// 4. UMD (Universal, for compatibility)
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(['dep'], factory);
} else if (typeof module === 'object') {
module.exports = factory(require('dep'));
} else {
root.myModule = factory(root.dep);
}
}(this, function(dep) { ... }));
2. ES Modules (ESM) Basics
2.1 Export Syntax
// ==============================
// math.js - Named Exports
// ==============================
// Individual exports
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export class Calculator {
add(a, b) { return a + b; }
subtract(a, b) { return a - b; }
}
// Batch exports
const multiply = (a, b) => a * b;
const divide = (a, b) => a / b;
export { multiply, divide };
// Renamed exports
const internalName = () => 'internal';
export { internalName as publicName };
// ==============================
// utils.js - Default Export
// ==============================
// Default export (one per file)
export default function formatDate(date) {
return date.toISOString().split('T')[0];
}
// Or
function formatDate(date) {
return date.toISOString().split('T')[0];
}
export default formatDate;
// Or (anonymous function)
export default function(date) {
return date.toISOString().split('T')[0];
}
// ==============================
// Mixed Usage (recommended)
// ==============================
// api.js
export const API_URL = 'https://api.example.com';
export const fetchData = async (endpoint) => { /* ... */ };
// Also with default export
export default class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
}
2.2 Import Syntax
// ==============================
// Named Imports
// ==============================
// Individual imports
import { add, subtract } from './math.js';
console.log(add(1, 2)); // 3
// Renamed imports
import { add as sum } from './math.js';
console.log(sum(1, 2)); // 3
// Namespace import (entire module)
import * as math from './math.js';
console.log(math.add(1, 2)); // 3
console.log(math.PI); // 3.14159
// ==============================
// Default Imports
// ==============================
// Default import (name is flexible)
import formatDate from './utils.js';
import myFormatter from './utils.js'; // Same thing, different name
console.log(formatDate(new Date()));
// ==============================
// Mixed Imports
// ==============================
// Default + Named together
import ApiClient, { API_URL, fetchData } from './api.js';
// Or
import ApiClient from './api.js';
import { API_URL, fetchData } from './api.js';
// ==============================
// Side Effect Imports
// ==============================
// Execute code only (polyfills, styles, etc.)
import './polyfill.js';
import './styles.css'; // When using bundlers
// ==============================
// Re-exports
// ==============================
// index.js (barrel file)
export { add, subtract } from './math.js';
export { default as formatDate } from './utils.js';
export * from './helpers.js'; // All named exports
2.3 Using in HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ES Modules</title>
</head>
<body>
<!-- type="module" required -->
<script type="module">
import { add } from './math.js';
console.log(add(1, 2));
</script>
<!-- External module file -->
<script type="module" src="./app.js"></script>
<!-- Non-module fallback (legacy browsers) -->
<script nomodule src="./app-legacy.js"></script>
</body>
</html>
3. Dynamic Import
3.1 Basic Syntax
// Static import (top of file, parse time)
import { add } from './math.js';
// Dynamic import (runtime, returns Promise)
async function loadMath() {
const math = await import('./math.js');
console.log(math.add(1, 2)); // 3
}
// Or using then
import('./math.js').then(math => {
console.log(math.add(1, 2));
});
// Accessing default export
const module = await import('./utils.js');
const formatDate = module.default;
3.2 Conditional Loading
// Module loading based on user permissions
async function loadAdminPanel() {
if (user.isAdmin) {
const { AdminPanel } = await import('./admin.js');
return new AdminPanel();
}
return null;
}
// Loading after feature detection
async function loadPolyfill() {
if (!window.IntersectionObserver) {
await import('intersection-observer');
}
}
// Route-based loading
const routes = {
'/': () => import('./pages/Home.js'),
'/about': () => import('./pages/About.js'),
'/contact': () => import('./pages/Contact.js'),
};
async function loadPage(path) {
const loader = routes[path];
if (loader) {
const module = await loader();
return module.default;
}
}
3.3 Code Splitting
// Lazy loading in React
import React, { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
// Async components in Vue
const AsyncComponent = () => ({
component: import('./AsyncComponent.vue'),
loading: LoadingComponent,
error: ErrorComponent,
delay: 200,
timeout: 3000
});
// Pure JavaScript
class Router {
async loadRoute(path) {
const pageModule = await import(`./pages/${path}.js`);
const Page = pageModule.default;
this.render(new Page());
}
}
4. CommonJS vs ES Modules
4.1 Key Differences
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CommonJS vs ES Modules Comparison β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Feature CommonJS ES Modules β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β Syntax require/exports import/export β
β Load Time runtime (sync) parse time (static analysis)β
β Default Env Node.js browser + Node.js β
β Dynamic Load require() anywhere import() separate syntax β
β Tree Shaking difficult possible β
β Circular Refs partial support better support β
β File Ext .js (default) .mjs or type="module" β
β this value module.exports undefined β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
4.2 Code Comparison
// ==============================
// CommonJS
// ==============================
// utils.cjs
const helper = () => 'helper';
const PI = 3.14159;
module.exports = { helper, PI };
// Or
exports.helper = helper;
exports.PI = PI;
// app.cjs
const { helper, PI } = require('./utils.cjs');
const utils = require('./utils.cjs');
console.log(utils.PI);
// ==============================
// ES Modules
// ==============================
// utils.mjs
export const helper = () => 'helper';
export const PI = 3.14159;
// app.mjs
import { helper, PI } from './utils.mjs';
import * as utils from './utils.mjs';
console.log(utils.PI);
4.3 Using ESM in Node.js
// package.json
{
"name": "my-package",
"type": "module", // Use ESM for entire project
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
// Using .mjs extension (regardless of type)
// utils.mjs
export const hello = () => 'Hello';
// app.mjs
import { hello } from './utils.mjs';
// Importing ESM from CommonJS
// (Node.js 14+, without top-level await)
async function main() {
const { hello } = await import('./utils.mjs');
console.log(hello());
}
main();
5. Module Patterns
5.1 Barrel Files
// components/index.js (barrel file)
export { default as Button } from './Button.js';
export { default as Input } from './Input.js';
export { default as Modal } from './Modal.js';
export { Card, CardHeader, CardBody } from './Card.js';
// Consumer side
import { Button, Input, Modal, Card } from './components';
// Instead of
// import Button from './components/Button.js';
// import Input from './components/Input.js';
// ...
5.2 Factory Pattern
// logger.js
export function createLogger(prefix) {
return {
log: (msg) => console.log(`[${prefix}] ${msg}`),
error: (msg) => console.error(`[${prefix}] ERROR: ${msg}`),
warn: (msg) => console.warn(`[${prefix}] WARNING: ${msg}`),
};
}
// Usage
import { createLogger } from './logger.js';
const logger = createLogger('App');
logger.log('Started'); // [App] Started
5.3 Singleton Pattern
// config.js (module itself is a singleton)
let instance = null;
class Config {
constructor() {
if (instance) {
return instance;
}
this.settings = {};
instance = this;
}
set(key, value) {
this.settings[key] = value;
}
get(key) {
return this.settings[key];
}
}
export default new Config();
// Usage (always same instance)
import config from './config.js';
config.set('api_url', 'https://api.example.com');
5.4 Plugin Pattern
// core.js
class App {
constructor() {
this.plugins = [];
}
use(plugin) {
plugin.install(this);
this.plugins.push(plugin);
return this; // Chaining
}
}
export default new App();
// plugins/logger.js
export default {
install(app) {
app.log = (msg) => console.log(`[App] ${msg}`);
}
};
// plugins/analytics.js
export default {
install(app) {
app.track = (event) => console.log(`Track: ${event}`);
}
};
// main.js
import app from './core.js';
import logger from './plugins/logger.js';
import analytics from './plugins/analytics.js';
app.use(logger).use(analytics);
app.log('Hello'); // [App] Hello
app.track('pageview'); // Track: pageview
6. Bundlers and Modules
6.1 Role of Bundlers
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Bundler β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Input: Output: β
β βββββββββββ βββββββββββββββββββββββββββββββ β
β β app.js β β β β
β β ββ a.js β βββββββββββΆ β bundle.js (single file) β β
β β ββ b.js β Bundler β β β
β β ββ c.js β β + chunk1.js (code splitting)β β
β βββββββββββ β + chunk2.js β β
β βββββββββββββββββββββββββββββββ β
β β
β Key Features: β
β - Dependency analysis and resolution β
β - Code transformation (Babel, TypeScript) β
β - Code splitting β
β - Tree shaking (remove unused code) β
β - Minification β
β - Asset handling (CSS, images) β
β β
β Popular Bundlers: Vite, webpack, esbuild, Rollup, Parcel β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
6.2 Vite Example
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
// Manual chunk configuration
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'date-fns'],
},
},
},
},
});
// Dynamic imports automatically split chunks
const AdminPage = () => import('./pages/Admin.js');
6.3 Tree Shaking
// utils.js
export function usedFunction() {
return 'used';
}
export function unusedFunction() { // Removed from bundle
return 'unused';
}
// app.js
import { usedFunction } from './utils.js';
// unusedFunction is not imported
console.log(usedFunction());
// Bundle result (with tree shaking)
// unusedFunction code is not included
7. Real-World Project Structure
7.1 Recommended Structure
project/
βββ src/
β βββ index.js # Entry point
β βββ app.js # Main app logic
β β
β βββ components/ # UI components
β β βββ index.js # Barrel file
β β βββ Button.js
β β βββ Modal.js
β β βββ Card/
β β βββ index.js
β β βββ Card.js
β β βββ Card.css
β β
β βββ utils/ # Utility functions
β β βββ index.js
β β βββ format.js
β β βββ validation.js
β β
β βββ services/ # API calls
β β βββ index.js
β β βββ api.js
β β βββ auth.js
β β
β βββ store/ # State management
β β βββ index.js
β β βββ userStore.js
β β
β βββ constants/ # Constants
β βββ index.js
β
βββ public/
β βββ index.html
β
βββ package.json
βββ vite.config.js
7.2 Import Path Aliases
// vite.config.js
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@utils': path.resolve(__dirname, './src/utils'),
},
},
});
// Usage
import { Button } from '@components';
import { formatDate } from '@utils';
// Instead of
// import { Button } from '../../../components';
Summary
ESM Core Syntax
| Feature |
Syntax |
| Named Export |
export const foo = ... |
| Default Export |
export default ... |
| Named Import |
import { foo } from '...' |
| Default Import |
import foo from '...' |
| Namespace Import |
import * as mod from '...' |
| Dynamic Import |
await import('...') |
| Re-export |
export { foo } from '...' |
Best Practices
- Prefer Named Exports: Better IDE autocomplete, easier tree shaking
- Use Barrel Files: Clean import paths
- Avoid Circular Dependencies: Organize dependency direction
- Utilize Dynamic Imports: Lazy load large modules
Next Steps
References