Cơ Chế Module Trong JavaScript: Từ CommonJS Đến Chuẩn ES6

Sự Tiến Hóa Của Hệ Sinh Thái JavaScript

Quá trình phát triển của JavaScript có thể được tóm tắt qua các giai đoạn chính: từ các công cụ hỗ trợ tương thích trình duyệt, đến các thành phần chức năng, sau đó là các framework tổ chức logic và cuối cùng là các ứng dụng nghiệp vụ hoàn chỉnh. Mặc dù ngôn ngữ này đã được trừu tượng hóa và phân lớp nhiều hơn để quản lý logic phức tạp, nhưng bản thân JavaScript lại thiếu hụt một tính năng cốt lõi từ gốc: Hệ thống module.

Dù các chuẩn HTML5 đã được thúc đẩy mạnh mẽ bởi W3C và các ông lớn công nghệ, bản thân ngôn ngữ JavaScript vẫn tồn tại những hạn chế nhất định:

  • Thiếu hệ thống module chuẩn hóa ban đầu.
  • Thư viện chuẩn nghèo nàn (ECMAScript chỉ định nghĩa một số lõi cơ bản).
  • Không có các interface chuẩn cho các tác vụ như thao tác cơ sở dữ liệu hay web server.
  • Hệ thống quản lý gói (package management) không đồng bộ.

Hệ Thống Module CommonJS

CommonJS ra đời nhằm khắc phục những thiếu sót trên, giúp JavaScript có đủ năng lực để xây dựng các ứng dụng quy mô lớn tương tự như Python, Ruby hay Java, thay vì chỉ dừng lại ở các script nhỏ lẻ.

1. Định nghĩa module

Trong CommonJS, mỗi file được xem là một module độc lập. Biến module đại diện cho chính file đó. Để xuất các thành phần ra ngoài, ta sử dụng module.exports.

// taxCalculator.js
const taxRate = 0.1;
const calculateTax = function (amount) {
    return amount * taxRate;
}
module.exports.rate = taxRate;
module.exports.calc = calculateTax;

2. Sử dụng module

Để sử dụng module đã định nghĩa, ta dùng hàm require.

const taxModule = require('./taxCalculator');

console.log(taxModule.rate);  // 0.1
console.log(taxModule.calc(1000)); // 100

Đặc điểm nổi bật của CommonJS:

  • Mọi code đều chạy trong scope của module, không làm ô nhiễm scope toàn cục.
  • Module chỉ được thực thi lần đầu tiên khi加载, kết quả sau đó được lưu vào bộ nhớ cache. Các lần gọi sau sẽ lấy trực tiếp từ cache. Để chạy lại, cần xóa cache thủ công.
  • Thứ tự加载 module tuân theo thứ tự xuất hiện trong code.

3. Cơ chế hoạt động trong Node.js

Khi Node.js tải một module, nó trải qua 3 bước: phân tích đường dẫn, định vị file và biên dịch thực thi. Module trong Node được chia làm 2 loại: Core modules (do Node cung cấp) và File modules (do người dùng viết).

Module Trên Chuẩn ES6

Thiết kế của ES6 Module hướng tới sự tĩnh hóa (static), cho phép xác định mối quan hệ phụ thuộc cũng như các biến đầu vào/ra ngay tại thời điểm biên dịch. Khác với CommonJS hay AMD chỉ xác định được những thứ này khi runtime.

Ví dụ, trong CommonJS, việc tải module thực chất là tải một对象 và truy cập thuộc tính của nó:

// CommonJS
let { stat, exists } = require('fs');
// Tương đương với việc load toàn bộ fs vào một object rồi mới lấy thuộc tính

Điều này gọi là "runtime loading", gây khó khăn cho việc tối ưu hóa tĩnh (static optimization) vì compiler không biết trước cấu trúc object.

ES6 Module không phải là object. Nó sử dụng lệnh export để chỉ định đầu ra và import để đầu vào.

1. Khai báo xuất (Export)

Cú pháp cơ bản để xuất các biến:

// config.js
export const DB_USER = 'admin';
export const DB_PASS = 'secret';
export const PORT = 5432;

Hoặc có thể gom nhóm lại ở cuối file:

// config.js
const DB_USER = 'admin';
const DB_PASS = 'secret';
const PORT = 5432;

export {DB_USER, DB_PASS, PORT};

Người dùng có thể đổi tên biến khi xuất bằng từ khóa as:

function init() { ... }
function destroy() { ... }

export {
  init as startSystem,
  destroy as stopSystem
};

Lưu ý quan trọng: export phải là interface tương ứng với biến bên trong. Không thể xuất trực tiếp một giá trị vô danh.

// Sai
export 1; 
let m = 1; export m;

// Đúng
export let m = 1;
let n = 1; export {n};

Các interface xuất ra có liên kết động (live binding) với giá trị bên trong module.

2. Nhập module (Import)

Giả sử ta có file system.js:

let status = 'active';
let run = function () {
    console.log('System running')
};
export {status, run}

Cách nhập vào file khác:

import {status, run} from "./system";

console.log(status);
run();

Có thể đặt bí danh khi nhập:

import {status as sysStatus, run} from "./system";
console.log(sysStatus);

3. Lệnh export default

Để tiện lợi cho người dùng không cần nhớ chính xác tên biến hàm, ES6 cung cấp export default. Về bản chất, nó xuất ra một biến tên là default.

// logger.js
export default function () {
  console.log('Log message');
}

Khi nhập, người dùng có thể đặt tên tùy ý:

import myLogger from './logger';
myLogger(); // 'Log message'

So sánh giữa xuất thường và xuất default:

// Nhóm 1: Default
export default function check() { ... }
import check from 'validator'; 

// Nhóm 2: Named
export function check() { ... }
import {check} from 'validator';

Cơ Chế Tải Module Trên Trình Duyệt

Để sử dụng ES6 Module trực tiếp trên browser, ta cần cấu hình thẻ <script> trong HTML.

Thông thường, script được tải như sau:

<script src="path/to/script.js"></script>

Để kích hoạt chế độ module, thuộc tính type phải được đặt là module:

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

Các script có type="module" sẽ được tải bất đồng bộ (async), không làm chặn quá trình render trang, tương đương với việc có thuộc tính defer.

<script type="module" src="./main.js"></script>
<!-- Tương đương với -->
<script type="module" src="./main.js" defer></script>

Module cũng có thể được viết trực tiếp (inline) trong HTML với cú pháp tương tự:

<script type="module">
  import helpers from "./utils.js";
  // code xử lý khác
</script>

Thẻ: JavaScript commonjs es6-modules nodejs module-system

Đăng vào ngày 24 tháng 6 lúc 08:51