Modules ES6 trong JavaScript: Tổ Chức Code Hiện Đại

Modules là gì?

ES6 Modules là cơ chế chuẩn để chia code JavaScript thành các files riêng biệt, có thể import/export giữa chúng. Modules giúp code dễ maintain, tái sử dụng và tránh pollution global scope.

Trước khi có Modules

Vấn đề với Global Scope

<!-- index.html -->
<script src="utils.js"></script>
<script src="app.js"></script>

<!-- utils.js -->
<script>
var myVar = 'hello'; // Global variable - nguy hiểm!

function myFunction() {
    // ...
}
</script>

<!-- app.js -->
<script>
// Có thể truy cập myVar và myFunction
// Nhưng dễ bị conflict nếu có tên trùng!
console.log(myVar);
</script>

Solutions cũ

IIFE (Immediately Invoked Function Expression):

// utils.js
var Utils = (function() {
    var privateVar = 'private';
    
    return {
        publicMethod: function() {
            return privateVar;
        }
    };
})();

CommonJS (Node.js):

// utils.js
module.exports = {
    myFunction: function() {}
};

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

ES6 Modules

Export

Named Exports

// math.js
export const PI = 3.14159;

export function add(a, b) {
    return a + b;
}

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

// Hoặc export tất cả cùng lúc
const PI = 3.14159;
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }

export { PI, add, multiply };

Default Export

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

// calculator.js
export default class Calculator {
    add(a, b) {
        return a + b;
    }
}

// Hoặc
class Calculator {
    // ...
}
export default Calculator;

// Hoặc với function
export default function calculate(a, b) {
    return a + b;
}

Mix Default và Named

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

export const ADMIN_ROLE = 'admin';
export const USER_ROLE = 'user';

Import

Import Named Exports

// app.js
import { add, multiply, PI } from './math.js';

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

Import với Alias

import { add as sum, multiply as mult } 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));
console.log(Math.PI);

Import Default Export

// Tên import tùy ý
import Calculator from './calculator.js';
import Calc from './calculator.js'; // Cũng OK

const calc = new Calculator();

Import Mix

import User, { ADMIN_ROLE, USER_ROLE } from './user.js';

const admin = new User('John');
console.log(ADMIN_ROLE);

Import chỉ để thực thi

import './analytics.js'; // Chỉ chạy code, không import gì

Dynamic Import

Import trong runtime (async):

// Static import (compile-time)
import { add } from './math.js';

// Dynamic import (runtime)
button.addEventListener('click', async () => {
    const { add } = await import('./math.js');
    console.log(add(2, 3));
});

Use cases:

  • Code splitting
  • Lazy loading
  • Conditional loading
// Conditional import
if (userPrefersDarkMode) {
    const theme = await import('./dark-theme.js');
    theme.apply();
} else {
    const theme = await import('./light-theme.js');
    theme.apply();
}

// Lazy loading
async function loadFeature() {
    const module = await import('./heavy-feature.js');
    module.initialize();
}

Sử dụng Modules trong Browser

HTML

<!-- Cần type="module" -->
<script type="module" src="app.js"></script>

<!-- Inline module -->
<script type="module">
    import { add } from './math.js';
    console.log(add(2, 3));
</script>

Lưu ý:

  • Module scripts mặc định defer
  • Module scripts có strict mode
  • Module scope riêng biệt

Module vs Script

<!-- Module Script -->
<script type="module">
    import { add } from './math.js';
    
    // Strict mode tự động
    // Top-level 'this' là undefined
    // Có thể import/export
</script>

<!-- Regular Script -->
<script>
    // Không strict mode
    // Top-level 'this' là window
    // Không thể import/export
</script>

Module Patterns

1. Single Responsibility

// user-service.js
export class UserService {
    getUser(id) {
        // ...
    }
}

// user-repository.js
export class UserRepository {
    findById(id) {
        // ...
    }
}

// Mỗi file một responsibility

2. Facade Pattern

// api/index.js - Export tất cả APIs
export { UserAPI } from './user-api.js';
export { ProductAPI } from './product-api.js';
export { OrderAPI } from './order-api.js';

// app.js - Import tập trung
import { UserAPI, ProductAPI, OrderAPI } from './api/index.js';

3. Barrel Exports

// components/index.js
export { Button } from './Button.js';
export { Input } from './Input.js';
export { Modal } from './Modal.js';

// app.js
import { Button, Input, Modal } from './components/index.js';

4. Constants Module

// constants.js
export const API_URL = 'https://api.example.com';
export const TIMEOUT = 5000;
export const MAX_RETRIES = 3;

export const STATUS = {
    PENDING: 'pending',
    SUCCESS: 'success',
    ERROR: 'error'
};

5. Utilities Module

// utils.js
export function formatDate(date) {
    // ...
}

export function debounce(func, delay) {
    // ...
}

export function throttle(func, limit) {
    // ...
}

Circular Dependencies

Vấn đề

// a.js
import { b } from './b.js';
export const a = 'A';
console.log(b); // undefined!

// b.js
import { a } from './a.js';
export const b = 'B';
console.log(a); // undefined!

Giải pháp

1. Refactor để tách dependencies:

// shared.js
export const shared = 'shared';

// a.js
import { shared } from './shared.js';
export const a = 'A';

// b.js
import { shared } from './shared.js';
export const b = 'B';

2. Dùng function thay vì value:

// a.js
import { getB } from './b.js';
export const a = 'A';
export function getA() { return a; }

// b.js
import { getA } from './a.js';
export const b = 'B';
export function getB() { return b; }

// Gọi function để lấy giá trị
console.log(getA()); // Works!

Tree Shaking

Module system cho phép bundlers loại bỏ unused code:

// math.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
export function divide(a, b) { return a / b; }

// app.js
import { add } from './math.js';
// Chỉ add được bundle, 3 functions kia bị loại bỏ!

Best practices cho tree shaking:

  • Dùng named exports thay vì default
  • Tránh side effects trong modules
  • Sử dụng pure functions

CommonJS vs ES Modules

CommonJS (Node.js)

// Export
module.exports = { add, multiply };
// hoặc
exports.add = add;

// Import
const math = require('./math');
const { add } = require('./math');

ES Modules

// Export
export { add, multiply };

// Import
import { add, multiply } from './math.js';

Differences

Feature CommonJS ES Modules
Syntax require()/module.exports import/export
Loading Synchronous Asynchronous
When Runtime Parse-time
Tree Shaking
Browser
Node.js ✅ (default) ✅ (.mjs hoặc type: "module")

ES Modules trong Node.js

Cách 1: File extension .mjs

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

// app.mjs
import { add } from './math.mjs';

Cách 2: package.json

{
    "type": "module",
    "name": "my-app"
}
// Bây giờ .js files là ES modules
// math.js
export function add(a, b) {
    return a + b;
}

// app.js
import { add } from './math.js';

Import CommonJS trong ES Module

// CommonJS module
// lib.js
module.exports = { hello: 'world' };

// ES Module
// app.mjs
import lib from './lib.js'; // Default import
console.log(lib.hello);

Ví dụ thực tế

1. Service Architecture

// services/api.js
class API {
    async get(url) {
        const response = await fetch(url);
        return response.json();
    }
}
export default new API();

// services/user-service.js
import api from './api.js';

export class UserService {
    async getUser(id) {
        return api.get(`/users/${id}`);
    }
}

// app.js
import { UserService } from './services/user-service.js';

const userService = new UserService();
const user = await userService.getUser(1);

2. Config Management

// config/development.js
export default {
    apiUrl: 'http://localhost:3000',
    debug: true
};

// config/production.js
export default {
    apiUrl: 'https://api.example.com',
    debug: false
};

// config/index.js
const env = process.env.NODE_ENV || 'development';
const config = await import(`./${env}.js`);
export default config.default;

// app.js
import config from './config/index.js';
console.log(config.apiUrl);

3. Plugin System

// plugins/analytics.js
export function initialize() {
    console.log('Analytics initialized');
}

// plugins/auth.js
export function initialize() {
    console.log('Auth initialized');
}

// app.js
const plugins = ['analytics', 'auth'];

for (const plugin of plugins) {
    const module = await import(`./plugins/${plugin}.js`);
    module.initialize();
}

Best Practices

  1. Một file, một concern
// ✅ GOOD: Mỗi class một file
// Button.js
export class Button {}

// Input.js
export class Input {}
  1. Named exports cho utilities
// ✅ GOOD
export function formatDate() {}
export function parseDate() {}

// ❌ BAD
export default {
    formatDate() {},
    parseDate() {}
};
  1. Default export cho main component
// ✅ Component chính
export default class UserProfile {}

// Named exports cho helpers
export function formatUserName() {}
  1. Đặt tên file theo export
// UserService.js
export class UserService {}

// constants.js
export const API_URL = '...';
  1. Tránh circular dependencies
// ✅ Tách shared code ra file riêng

Kết luận

ES6 Modules mang lại:

  • Code organization tốt hơn
  • Encapsulation và reusability
  • Tree shaking cho bundle nhỏ hơn
  • Static analysis (type checking, linting)
  • Native browser support

Modules là nền tảng của JavaScript hiện đại, giúp xây dựng ứng dụng scalable và maintainable!