Tìm hiểu Module trong JavaScript

Javascript căn bản | by Học Javascript

Trong quá trình phát triển ứng dụng bằng JavaScript, mã nguồn có thể trở nên phức tạp và khó quản lý khi dự án mở rộng. Để giải quyết vấn đề này, Module đã được giới thiệu như một cách hiệu quả để tổ chức, tái sử dụng và bảo trì mã nguồn tốt hơn.

Trước đây, JavaScript không có hệ thống module tích hợp, khiến các lập trình viên phải sử dụng các phương pháp như IIFE (Immediately Invoked Function Expression) hoặc thư viện bên ngoài như RequireJS, CommonJS để quản lý mã nguồn. Tuy nhiên, kể từ ES6 (ECMAScript 2015), JavaScript đã chính thức hỗ trợ ES6 Modules, giúp lập trình viên dễ dàng chia nhỏ chương trình thành các phần nhỏ hơn, độc lập hơn, nhưng vẫn có thể kết nối với nhau khi cần thiết.

Bài viết này sẽ giúp bạn hiểu rõ về Module trong JavaScript, cách khai báo và sử dụng chúng, sự khác biệt giữa ES6 Modules và CommonJS, cũng như các ứng dụng thực tế và cách tổ chức Module hiệu quả trong các dự án lớn.

Module trong JavaScript

Module trong JavaScript là một cách để chia nhỏ chương trình thành các tệp độc lập (modules), mỗi module chứa một phần cụ thể của chương trình và có thể dễ dàng xuất (export) hoặc nhập (import) vào các tệp khác khi cần thiết. Điều này giúp mã nguồn trở nên dễ quản lý hơn, tăng khả năng tái sử dụng và bảo trì.

Lịch sử ra đời và lý do cần có Module

Trước khi ES6 Modules ra đời, JavaScript không có hệ thống module tích hợp. Các lập trình viên phải sử dụng các phương pháp như:

  • IIFE (Immediately Invoked Function Expression) để đóng gói mã nguồn.
  • CommonJS (sử dụng require() và module.exports), chủ yếu dùng trong Node.js.
  • AMD (Asynchronous Module Definition) và RequireJS để quản lý module trong trình duyệt.

Những phương pháp này có nhiều hạn chế, đặc biệt là trong các ứng dụng lớn, dẫn đến xung đột biến toàn cục (Global Scope Pollution) và khó bảo trì mã nguồn. Để giải quyết vấn đề này, ES6 Modules ra đời, giúp JavaScript có một hệ thống module chính thức, chuẩn hóa cách tổ chức mã nguồn.

Lợi ích khi sử dụng Module

Việc sử dụng Module trong JavaScript mang lại nhiều lợi ích quan trọng, bao gồm:

  • Tổ chức mã nguồn tốt hơn: Chia nhỏ chương trình thành các phần độc lập, giúp code dễ đọc, dễ hiểu và dễ bảo trì.
  • Tăng tính tái sử dụng code: Các Module có thể được sử dụng lại ở nhiều nơi mà không cần viết lại code.
  • Giảm xung đột biến toàn cục (Global Scope Pollution): Module giúp giới hạn phạm vi biến trong từng tệp, tránh làm ảnh hưởng đến các phần khác của chương trình.

Nhờ có Module, JavaScript trở nên mạnh mẽ hơn, hỗ trợ lập trình theo mô hình hướng đối tượng (OOP) và giúp phát triển ứng dụng quy mô lớn một cách dễ dàng!

Cách sử dụng Module trong JavaScript

Khai báo và xuất Module (Export)

Trong JavaScript, một module có thể xuất (export) dữ liệu, hàm hoặc class để các tệp khác có thể sử dụng. Có hai cách xuất module trong ES6 Modules:

Named Export (Xuất có tên)

  • Cho phép xuất nhiều phần tử trong cùng một module.
  • Khi import, cần sử dụng đúng tên đã export.

Ví dụ:
math.js (Module xuất dữ liệu)

export const PI = 3.14159;
export function add(a, b) {
    return a + b;
}
export function subtract(a, b) {
    return a - b;
}
main.js (Module nhập dữ liệu)
import { PI, add, subtract } from './math.js';

console.log(PI); // 3.14159
console.log(add(5, 3)); // 8
console.log(subtract(10, 4)); // 6

Default Export (Xuất mặc định)

  • Chỉ có thể có một default export trong một module.
  • Khi import, có thể đặt bất kỳ tên nào.

message.js (Module xuất dữ liệu mặc định)

export default function greet(name) {
    return `Hello, ${name}!`;
}

main.js (Module nhập dữ liệu mặc định)

import greet from './message.js';

console.log(greet('Alice')); // Hello, Alice!

Sự khác biệt giữa Named Export và Default Export

Thuộc tính Named Export Default Export
Số lượng exports Không giới hạn Chỉ có một
Cách import Phải dùng đúng tên Có thể đặt tên tùy ý
Đóng gói lại Có thể import tất cả vào một object Không thể

Import Module (Nhập Module)

Module có thể được nhập (import) vào một tệp khác để sử dụng.

Import Named Export

import { add, subtract } from './math.js';
Import Default Export
import greet from './message.js';

Import tất cả từ một module

Có thể import toàn bộ module vào một object bằng * as

import * as MathUtils from './math.js';

console.log(MathUtils.PI);
console.log(MathUtils.add(2, 3));

Sử dụng Module trong trình duyệt

Cách sử dụng <script type="module">

Trình duyệt hỗ trợ module thông qua type="module".

index.html

<script type="module">
    import { add } from './math.js';
    console.log(add(10, 5)); // 15
</script>

Hạn chế khi sử dụng Module trực tiếp trên trình duyệt

  • Cần chạy trên HTTP/HTTPS: Trình duyệt không hỗ trợ module nếu mở file .html trực tiếp bằng file://.
  • Không thể sử dụng biến toàn cục giữa các module.

Cách khắc phục:

  • Sử dụng Live Server hoặc chạy trên localhost bằng Node.js hoặc Python.

Sử dụng Module trong Node.js (CommonJS vs ES6 Modules)

Node.js hỗ trợ cả CommonJS (require)ES6 Modules (import/export).

CommonJS (require()module.exports)

math.js (CommonJS Module)

const PI = 3.14159;
function add(a, b) { return a + b; }
module.exports = { PI, add };

main.js

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

ES6 Modules trong Node.js

  • Để sử dụng import/export trong Node.js, cần thêm "type": "module" trong package.json.

package.json

{
  "type": "module"
}
math.js (ES6 Module)
export const PI = 3.14159;
export function add(a, b) { return a + b; }
main.js
import { PI, add } from './math.js';
console.log(PI);
console.log(add(3, 7));

Sự khác biệt giữa CommonJS và ES6 Modules

Thuộc tính CommonJS (require) ES6 Modules (import/export)
Hỗ trợ trong trình duyệt Không hỗ trợ Hỗ trợ
Hỗ trợ trong Node.js Mặc định Cần bật "type": "module"
Import đồng bộ hay bất đồng bộ Đồng bộ Bất đồng bộ
Có hỗ trợ dynamic import không? Không

Dynamic Import (import())

import() là một cách để nạp module động (chỉ khi cần thiết), giúp tối ưu hiệu suất.

main.js

Lợi ích của Dynamic Import

Tối ưu hiệu suất: Chỉ tải module khi cần thiết (Lazy Loading).
Giúp cải thiện thời gian tải trang trong các ứng dụng web lớn.

Ví dụ thực tế:

  • Chỉ tải một module khi người dùng nhấn nút.
  • Nạp các thư viện lớn như Chart.js, Three.js khi cần thiết.

index.html

<button id="load">Tải Module</button>
<script type="module">
    document.getElementById('load').addEventListener('click', async () => {
        const { add } = await import('./math.js');
        console.log(add(2, 3)); // Chỉ tải khi nhấn nút
    });
</script>

Các lưu ý quan trọng khi sử dụng Module trong JavaScript

Cách tổ chức Module trong dự án lớn

Khi dự án mở rộng, việc tổ chức module một cách hợp lý giúp dễ quản lý, bảo trì và mở rộng. Dưới đây là một số cách phổ biến:

Quản lý Module theo thư mục

Phân chia module thành các thư mục riêng biệt dựa theo chức năng giúp mã nguồn rõ ràng hơn. Ví dụ:

/project
  /src
    /components    → Chứa các thành phần UI (React/Vue components)
    /utils         → Chứa các hàm tiện ích (utility functions)
    /services      → Chứa các API services
    /constants     → Chứa các hằng số dùng chung
    /config        → Chứa file cấu hình dự án
    index.js       → File khởi động chính

Ví dụ:
utils/math.js

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

rvices/api.js

export function fetchUser() {
    return fetch('https://api.example.com/user').then(res => res.json());
}

Sử dụng index.js để gom nhóm Module

Trong mỗi thư mục, có thể tạo một index.js để gom nhóm các module, giúp import dễ dàng hơn.

utils/index.js

export { add, subtract } from './math.js';
export { formatDate } from './date.js';

main.js

import { add, formatDate } from './utils';

console.log(add(2, 3));
console.log(formatDate(new Date()));

Xử lý lỗi khi sử dụng Module

Lỗi CORS khi import Module trong trình duyệt

Khi import module bằng <script type="module">, nếu chạy từ file://, trình duyệt sẽ chặn do CORS policy.

Lỗi thường gặp:

Access to script at 'file:///path/to/module.js' from origin 'null' has been blocked by CORS policy

Cách khắc phục:

  • Chạy bằng Live Server (VS Code).
  • Dùng một local server như Node.js hoặc Python:
    • Node.js: npx http-server
    • Python: python -m http.server 8000

Lỗi khi sử dụng require() trong ES6 Modules

Nếu dự án đang dùng "type": "module" trong package.json, nhưng lại sử dụng require(), sẽ bị lỗi:

Lỗi thường gặp:

SyntaxError: Cannot use import statement outside a module

Cách khắc phục:

  • Dùng import thay vì require().
  • Nếu cần dùng require(), có thể chuyển về CommonJS bằng cách xóa "type": "module" trong package.json.

Cách khắc phục lỗi phổ biến khác

Lỗi Nguyên nhân Cách khắc phục
Uncaught SyntaxError: Cannot use import statement outside a module Không khai báo type="module" trong <script> Thêm type="module" vào <script>
Uncaught ReferenceError: exports is not defined Dùng export trong CommonJS Chuyển sang module.exports
SyntaxError: Unexpected token 'export' Chạy ES6 module trong môi trường không hỗ trợ Chạy bằng Babel/Webpack hoặc dùng Node.js >= 14

Performance Optimization khi sử dụng Module

Cách giảm tải và tối ưu Module với Webpack, Rollup, Parcel

Các công cụ như Webpack, Rollup, Parcel giúp đóng gói và tối ưu hóa module.

  • Webpack: Phổ biến nhất cho React, Vue, Angular.
  • Rollup: Nhẹ hơn Webpack, tốt cho thư viện JS.
  • Parcel: Không cần cấu hình, nhanh hơn Webpack.

webpack.config.js (ví dụ Webpack)

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  }
};

Tree Shaking – Loại bỏ code không cần thiết

Tree Shaking giúp loại bỏ các module không được sử dụng, giảm dung lượng file.

Ví dụ trước khi Tree Shaking:
math.js

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

main.js

import { add } from './math.js'; // Chỉ dùng add()
console.log(add(5, 3));

Nếu Tree Shaking hoạt động, subtract() sẽ bị loại bỏ khi đóng gói bằng Webpack/Rollup.

Ứng dụng thực tế của Module trong JavaScript

Ứng dụng Module trong phát triển ứng dụng web

Trong các dự án JavaScript hiện đại, việc sử dụng module giúp tách biệt các phần logic, tái sử dụng codegiảm sự phụ thuộc giữa các thành phần.

Ví dụ: Tách biệt module trong ứng dụng web

Giả sử chúng ta có một ứng dụng web cần hiển thị danh sách người dùng.

api.js (Module xử lý API)

export async function fetchUsers() {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    return response.json();
}

render.js (Module xử lý giao diện)

export function renderUserList(users) {
    const container = document.getElementById('user-list');
    users.forEach(user => {
        const div = document.createElement('div');
        div.textContent = user.name;
        container.appendChild(div);
    });
}

app.js (Module chính)

import { fetchUsers } from './api.js';
import { renderUserList } from './render.js';

async function init() {
    const users = await fetchUsers();
    renderUserList(users);
}

init();

index.html

<!DOCTYPE html>
<html lang="vi">
<head>
    <title>Danh sách người dùng</title>
</head>
<body>
    <div id="user-list"></div>
    <script type="module" src="app.js"></script>
</body>
</html>
  • Tách biệt API, UI và logic chính giúp dễ bảo trì.
  • Có thể tái sử dụng fetchUsers() trong các module khác.

Sử dụng Module trong React, Vue, Angular

Các framework hiện đại như React, Vue, Angular đều dựa vào Module System để quản lý code hiệu quả.

Sử dụng Module trong React.js

Trong React, chúng ta tách biệt các component thành các module riêng biệt.

components/Button.js

export default function Button({ label, onClick }) {
    return <button onClick={onClick}>{label}</button>;
}

App.js

import Button from './components/Button';

function App() {
    return (
        <div>
            <h1>Chào mừng bạn!</h1>
            <Button label="Nhấn vào đây" onClick={() => alert('Hello!')} />
        </div>
    );
}

export default App;
  • Mỗi component là một module riêng, giúp dễ dàng tái sử dụng.
  • Không bị ảnh hưởng bởi biến toàn cục, giúp tránh lỗi xung đột.

Sử dụng Module trong Vue.js

Vue.js sử dụng module để quản lý components, Vuex store, router.

components/Card.vue

<template>
  <div class="card">
    <h3>{{ title }}</h3>
    <p>{{ content }}</p>
  </div>
</template>

<script>
export default {
  props: ['title', 'content']
};
</script>

<style>
.card { border: 1px solid #ddd; padding: 10px; }
</style>
App.vue
<template>
  <div>
    <Card title="Tiêu đề" content="Nội dung thẻ" />
  </div>
</template>

<script>
import Card from './components/Card.vue';

export default {
  components: { Card }
};
</script>
  • Tách biệt component giúp dễ bảo trì.
  • Có thể tái sử dụng <Card> trong nhiều nơi.

Sử dụng Module trong Angular

Trong Angular, mỗi tính năng có thể được tổ chức thành Modules riêng biệt.

user.module.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserComponent } from './user.component';

@NgModule({
  declarations: [UserComponent],
  imports: [CommonModule],
  exports: [UserComponent]
})
export class UserModule { }

app.module.ts

import { UserModule } from './user/user.module';

@NgModule({
  imports: [UserModule],
})
export class AppModule { }
  • Angular sử dụng Lazy Loading Module, giúp tăng tốc tải trang.

Sử dụng Module để thiết kế API và Backend với Node.js

Node.js hỗ trợ CommonJS (require())ES6 Module (import/export) để xây dựng backend.

Xây dựng API đơn giản với Express và Module

server.js

import express from 'express';
import userRoutes from './routes/userRoutes.js';

const app = express();
app.use('/users', userRoutes);

app.listen(3000, () => console.log('Server chạy trên cổng 3000'));

routes/userRoutes.js

import express from 'express';
import { getUsers } from '../controllers/userController.js';

const router = express.Router();
router.get('/', getUsers);

export default router;

controllers/userController.js

export function getUsers(req, res) {
    res.json([{ id: 1, name: 'Nguyễn Văn A' }, { id: 2, name: 'Trần Thị B' }]);
}
  • Tách biệt routes, controllers, giúp code dễ bảo trì.
  • Tái sử dụng module, không cần lặp lại logic.

Kết bài

Module trong JavaScript đóng vai trò quan trọng trong việc tổ chức mã nguồn, giúp tái sử dụng code, giảm sự phụ thuộc giữa các thành phần và tối ưu hiệu suất. Với sự hỗ trợ của ES6 Modules và CommonJS, JavaScript có thể dễ dàng xây dựng các ứng dụng web, frontend (React, Vue, Angular) và backend (Node.js) một cách chuyên nghiệp.

Việc sử dụng module không chỉ giúp viết code rõ ràng hơn, mà còn giúp các lập trình viên bảo trì và mở rộng ứng dụng dễ dàng. Hơn nữa, với Tree Shaking, Lazy Loading, module còn giúp cải thiện tốc độ tải trang và tối ưu tài nguyên.

Bài viết liên quan