CommonJS vs ES Modules

Module hóa trong JavaScript - import/export, CommonJS vs ESModule

7 phút

Module hóa giúp chia code thành các file nhỏ, dễ quản lý và tái sử dụng. Hãy cùng tìm hiểu 2 hệ thống module phổ biến trong JavaScript và cách áp dụng vào project thực tế.

Tại sao cần Modules? 🤔

Trước khi có modules - Tất cả code chung 1 global scope:

<!-- ❌ Cách cũ - Global scope pollution -->
<script src="utils.js"></script>
<script src="user.js"></script>
<script src="app.js"></script>

<script>
// utils.js
function formatDate(date) { /* ... */ }

// user.js  
function formatDate(date) { /* ... */ } // ❌ Conflict!

// app.js
formatDate(); // Gọi function nào đây? 🤷
</script>

Với modules - Mỗi file có scope riêng:

// ✅ utils.js - Module riêng
export function formatDate(date) { /* ... */ }

// ✅ user.js - Module riêng
export function formatDate(date) { /* ... */ }

// ✅ app.js - Import rõ ràng
import { formatDate as formatDateUtil } from './utils.js';
import { formatDate as formatDateUser } from './user.js';

CommonJS (Node.js truyền thống) 📦

Hệ thống module của Node.js, dùng require()module.exports.

Export trong CommonJS

// utils.js
function add(a, b) {
    return a + b;
}

function subtract(a, b) {
    return a - b;
}

// Cách 1: Export từng cái
exports.add = add;
exports.subtract = subtract;

// Cách 2: Export một object
module.exports = {
    add,
    subtract
};

// Cách 3: Export default
module.exports = add;

Import trong CommonJS

// app.js
const utils = require('./utils');
console.log(utils.add(2, 3)); // 5

// Destructuring
const { add, subtract } = require('./utils');
console.log(subtract(5, 2)); // 3

// Import default
const add = require('./utils');
console.log(add(2, 3)); // 5

Đặc điểm CommonJS

  • Synchronous: Load module đồng bộ (phù hợp server)
  • Dynamic: Có thể require() trong if/else
  • Không tree-shaking: Bundle cả module dù không dùng hết
  • Không chạy được trên browser (cần bundler)
// Dynamic import
if (condition) {
    const utils = require('./utils'); // ✅ OK
}

ES Modules (ESM) - Modern JavaScript 🚀

Chuẩn module chính thức của JavaScript (ES6+), dùng importexport.

Named Export/Import

// math.js - Export nhiều thứ
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

export const PI = 3.14159;

// Hoặc export sau khi định nghĩa
function multiply(a, b) { return a * b; }
function divide(a, b) { return a / b; }

export { multiply, divide };
// app.js - Import
import { add, subtract, PI } from './math.js';

console.log(add(2, 3));    // 5
console.log(PI);           // 3.14159

// Import và đổi tên
import { add as sum } from './math.js';
console.log(sum(2, 3));    // 5

// Import tất cả
import * as Math from './math.js';
console.log(Math.add(2, 3));  // 5
console.log(Math.PI);         // 3.14159

Default Export/Import

Mỗi module chỉ có 1 default export:

// user.js - Default export
export default class User {
    constructor(name) {
        this.name = name;
    }
}

// Hoặc
class User { /* ... */ }
export default User;

// Hoặc inline
export default function() { /* ... */ }
// app.js - Import default (tên tùy chọn)
import User from './user.js';
import MyUser from './user.js'; // ✅ Tên khác cũng OK

const user = new User('Harry');

Mix Named và Default Export

// api.js
export default class API { /* ... */ }
export const BASE_URL = 'https://api.example.com';
export function request() { /* ... */ }
// app.js
import API, { BASE_URL, request } from './api.js';

Đặc điểm ES Modules

  • Static: Import phải ở top-level (tree-shaking tốt)
  • Asynchronous: Phù hợp browser
  • Tree-shaking: Chỉ bundle code được dùng
  • Strict mode: Tự động chạy trong strict mode
  • Live binding: Export là reference, không phải copy
// ❌ Dynamic import không được (top-level)
if (condition) {
    import { add } from './math.js'; // ❌ Error!
}

// ✅ Dùng dynamic import() function
if (condition) {
    const { add } = await import('./math.js'); // ✅ OK
}

CommonJS vs ES Modules 🔄

FeatureCommonJSES Modules
Syntaxrequire() / module.exportsimport / export
LoadSynchronousAsynchronous
Browser❌ (cần bundler)✅ Native support
Tree-shaking
Dynamic import✅ Anywhereimport() function
Default inNode.js (< v13)Browser, Node.js (≥ v13)
File extension.js.mjs hoặc "type": "module"

Ví dụ so sánh

CommonJS:

// math.js
module.exports = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b
};

// app.js
const math = require('./math');
console.log(math.add(2, 3));

ES Modules:

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// app.js
import { add } from './math.js';
console.log(add(2, 3));

Tree-shaking: Loại bỏ code không dùng 🌳

Tree-shaking chỉ hoạt động với ES Modules vì cấu trúc static.

// utils.js
export function usedFunction() { /* được dùng */ }
export function unusedFunction() { /* không dùng */ }

// app.js
import { usedFunction } from './utils.js';
usedFunction();

// ✅ Build output chỉ chứa usedFunction
// unusedFunction bị loại bỏ → Bundle nhỏ hơn

CommonJS không tree-shake được:

// utils.js
module.exports = {
    usedFunction() { /* được dùng */ },
    unusedFunction() { /* không dùng */ }
};

// app.js
const { usedFunction } = require('./utils');
usedFunction();

// ❌ Build output vẫn chứa cả 2 functions

Tổ chức Project với Modules 📁

Cấu trúc thư mục tốt

src/
├── components/
│   ├── Button.js
│   ├── Card.js
│   └── index.js          # Barrel export
├── utils/
│   ├── date.js
│   ├── string.js
│   └── index.js
├── services/
│   ├── api.js
│   └── auth.js
├── config/
│   └── constants.js
└── main.js               # Entry point

Barrel Export Pattern

Tập trung exports từ nhiều file:

// components/index.js - Barrel file
export { default as Button } from './Button.js';
export { default as Card } from './Card.js';
export * from './Modal.js';

// main.js - Import gọn
import { Button, Card } from './components';
// Thay vì:
// import Button from './components/Button.js';
// import Card from './components/Card.js';

Demo: Setup Project với Vite ⚡

Vite là build tool hiện đại, hỗ trợ ES Modules native.

1. Khởi tạo project

# Tạo project mới
npm create vite@latest my-project -- --template vanilla

cd my-project
npm install

2. Cấu trúc project

my-project/
├── index.html
├── package.json
├── vite.config.js
└── src/
    ├── main.js
    ├── utils/
    │   ├── math.js
    │   └── index.js
    └── components/
        └── greeting.js

3. Code ví dụ

// src/utils/math.js
export function add(a, b) {
    return a + b;
}

export function multiply(a, b) {
    return a * b;
}

// src/utils/index.js (barrel)
export * from './math.js';

// src/components/greeting.js
export default function greeting(name) {
    return `Hello, ${name}!`;
}

// src/main.js
import greeting from './components/greeting.js';
import { add, multiply } from './utils';

console.log(greeting('Harry'));
console.log('2 + 3 =', add(2, 3));
console.log('2 * 3 =', multiply(2, 3));

4. Chạy project

# Dev server với HMR
npm run dev

# Build production (tree-shaking tự động)
npm run build

# Preview build
npm run preview

5. Vite config (optional)

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
    build: {
        // Tùy chỉnh build
        minify: 'terser',
        sourcemap: true
    }
});

Demo: Setup với Webpack (Alternative) 📦

# Khởi tạo project
mkdir my-webpack-project && cd my-webpack-project
npm init -y

# Install webpack
npm install --save-dev webpack webpack-cli webpack-dev-server
// webpack.config.js
module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: __dirname + '/dist'
    },
    mode: 'production' // Tree-shaking enabled
};
// package.json
{
    "scripts": {
        "dev": "webpack serve",
        "build": "webpack"
    }
}

Dynamic Import: Load khi cần 🎯

// Lazy loading
async function loadUtils() {
    const { add, multiply } = await import('./utils/math.js');
    console.log(add(2, 3));
}

// Load khi click button
button.addEventListener('click', async () => {
    const module = await import('./heavy-module.js');
    module.default();
});

// Conditional import
if (user.isAdmin) {
    const adminPanel = await import('./admin-panel.js');
    adminPanel.init();
}

Best Practices 💡

  1. Ưu tiên ES Modules cho code mới (CommonJS chỉ dùng cho Node.js cũ)
  2. Named exports > Default exports (dễ refactor, tree-shake tốt hơn)
  3. Barrel exports cho nhiều components
  4. Dynamic import cho code splitting (giảm bundle size)
  5. 1 file = 1 responsibility (dễ test, maintain)
// ✅ Good - Named exports
export function add() { }
export function subtract() { }

// ❌ Avoid - Default export object
export default { add, subtract };

Kết luận

AspectCommonJSES Modules
Khi nào dùngNode.js legacyMọi project mới
PerformanceTốt (server)Tốt (browser)
Bundle sizeLớn hơnNhỏ hơn (tree-shaking)
Học hỏiDễ hơnPhức tạp hơn một chút

Khuyến nghị:

  • 🚀 Dùng ES Modules cho mọi project mới
  • 📦 Vite cho setup nhanh (khuyên dùng)
  • 🌳 Tree-shaking tự động giảm bundle size
  • 🎯 Dynamic import cho lazy loading

Module hóa đúng cách giúp code dễ maintain, test và scale! 🎉