ES Modules trong JavaScript: import, export và tổ chức mã nguồn

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Khi dự án lớn dần, nhét tất cả vào một file JavaScript là con đường nhanh nhất đến sự hỗn loạn. ES Modules là cách chuẩn của ngôn ngữ để chia mã thành nhiều file nhỏ, mỗi file có phạm vi riêng và chỉ chia sẻ những gì cần thiết qua export và import.

Export: chia sẻ ra ngoài

Có hai kiểu export. Kiểu có tên (named export), một file có thể có nhiều:

// tien-ich.js
export const THUE = 0.1;

export function dinhDangTien(so) {
  return so.toLocaleString('vi-VN') + ' đ';
}

export function tinhThue(gia) {
  return gia * THUE;
}

Và kiểu mặc định (default export), mỗi file chỉ có một:

// gio-hang.js
export default class GioHang {
  constructor() { this.items = []; }
  them(sp) { this.items.push(sp); }
}

Import: lấy về để dùng

// main.js
import GioHang from './gio-hang.js';
import { dinhDangTien, tinhThue } from './tien-ich.js';
import { THUE as THUE_VAT } from './tien-ich.js';   // đổi tên
import * as tienIch from './tien-ich.js';            // lấy tất cả

const gh = new GioHang();
console.log(dinhDangTien(150000));
console.log(tienIch.THUE);

Named import đặt trong ngoặc nhọn và phải khớp tên, default import thì tự đặt tên. Đường dẫn tương đối cần bắt đầu bằng ./ hoặc ../, và trong trình duyệt thuần thì phải ghi đủ phần mở rộng .js.

Dùng module trong trình duyệt

Để trình duyệt hiểu bạn đang dùng module, đặt type='module' cho thẻ script:

<script type='module' src='main.js'></script>

Những điều cần biết về script module:

  • Tự động chạy ở strict mode và tự có hành vi như defer, không chặn việc hiển thị trang.
  • Biến khai báo ở cấp cao nhất chỉ nằm trong module, không rò rỉ ra phạm vi toàn cục.
  • Mỗi module chỉ được tải và chạy một lần, dù được import ở nhiều nơi.
  • Khi mở file HTML trực tiếp từ đĩa bằng giao thức file://, trình duyệt thường chặn module vì chính sách bảo mật. Hãy chạy một máy chủ cục bộ, như tiện ích Live Server của VS Code.

Named hay default?

Đây là chủ đề tranh luận quen thuộc. Một số lý do nhiều nhóm ưu tiên named export:

  • Tên bắt buộc nhất quán trong toàn dự án, dễ tìm kiếm.
  • Trình soạn thảo tự động gợi ý import chính xác hơn.
  • Dễ xuất nhiều thứ từ một file.

Default export vẫn hữu ích cho file chỉ có một thành phần chính, như một component. Quan trọng nhất là thống nhất quy ước trong nhóm.

Re-export và file index

Bạn có thể gom nhiều module vào một điểm vào duy nhất:

// components/index.js
export { Nut } from './nut.js';
export { Modal } from './modal.js';
export * from './the.js';

Nhờ vậy nơi khác chỉ cần import { Nut, Modal } from './components/index.js'. Đừng lạm dụng: file index quá lớn có thể làm công cụ đóng gói khó loại bỏ mã không dùng.

Dynamic import: tải khi cần

nutBieuDo.addEventListener('click', async function () {
  const { veBieuDo } = await import('./bieu-do.js');
  veBieuDo();
});

Hàm import() trả về promise và chỉ tải module khi cần. Đây là nền tảng của kỹ thuật tách mã (code splitting), giúp trang tải ban đầu nhẹ hơn. Các công cụ như Vite hay webpack dùng chính cú pháp này để tạo các gói nhỏ riêng.

Module và CommonJS

Trong Node.js, bạn sẽ gặp cả hai hệ thống: CommonJS với require và module.exports, và ES Modules với import và export. Có thể dùng ES Modules trong Node bằng cách đặt "type": "module" trong package.json hoặc dùng đuôi file .mjs. Dự án mới nên ưu tiên ES Modules vì đó là chuẩn của ngôn ngữ.

Gợi ý cấu trúc thư mục

Với một dự án nhỏ, cấu trúc đơn giản sau đủ dùng và dễ mở rộng:

src/
  main.js          // điểm vào, khởi tạo ứng dụng
  api.js           // các hàm gọi API
  state.js         // dữ liệu và trạng thái chung
  ui/
    danh-sach.js   // vẽ danh sách
    modal.js       // hộp thoại
  utils/
    dinh-dang.js   // hàm tiện ích thuần

Nguyên tắc là các module tiện ích không phụ thuộc vào giao diện, còn module giao diện được phép import module tiện ích chứ không làm ngược lại. Tránh import vòng tròn, tức A import B còn B lại import A, vì nó dẫn tới giá trị chưa sẵn sàng và lỗi khó hiểu. Nếu gặp tình huống này, hãy tách phần dùng chung ra một module thứ ba.

Kết luận

Hãy chia mã theo trách nhiệm: mỗi module một việc, chỉ export những gì cần, đặt tên nhất quán, và dùng dynamic import khi muốn tải theo nhu cầu. Đó là nền tảng để các dự án với Vite, React hay Vue có cấu trúc sạch sẽ.

Để thực hành tổ chức dự án nhiều file trong những bài tập có hướng dẫn, bạn có thể xem các khóa học của codebem.com.

author_photo
Nguyễn Đình Cường

0 comment