JavaScript Modules: Hướng Dẫn Toàn Tập về Import & Export

Bạn đã từng đau đầu khi các biến trong file JavaScript này lại vô tình ghi đè lên biến ở file khác, gây ra những lỗi khó lường? Đó chính là lúc modules javascript trở thành người hùng thầm lặng, giúp bạn dọn dẹp mớ hỗn độn đó. Trong bài viết này, WiWeb sẽ cùng bạn tìm hiểu từ A-Z về JavaScript Modules, biến những khái niệm khô khan như import, export thành công cụ mạnh mẽ để viết code sạch sẽ, dễ bảo trì và chuyên nghiệp hơn rất nhiều.

Giới thiệu JavaScript Modules

Trước khi có modules, thế giới JavaScript giống như một căn bếp chung khổng lồ và bừa bộn. Mọi người đều vứt đồ đạc của mình ra một không gian chung. Bạn đặt một hũ đường (var sugar = ...) ra bàn, một người khác không biết lại đặt hũ muối của họ vào đúng chỗ đó. Kết quả? Món chè của bạn bỗng nhiên có vị mặn! Đó chính là bức tranh của code JavaScript truyền thống.

Giải quyết vấn đề của code JavaScript truyền thống

Vấn đề “hũ đường, hũ muối” trong lập trình được gọi là ô nhiễm không gian tên toàn cục (Global Scope Pollution). Khi bạn khai báo một biến hay một hàm trong một file <script>, nó sẽ tồn tại trong không gian toàn cục (window object trên trình duyệt). Nếu một file script khác cũng khai báo một biến có cùng tên, biến sau sẽ ghi đè lên biến trước. Đây là cơn ác mộng khi dự án lớn dần, với hàng chục, hàng trăm file JavaScript.

JavaScript Modules ra đời để giải quyết triệt để vấn đề này. Hãy hình dung mỗi module là một căn bếp riêng, sạch sẽ. Mọi thứ bạn làm trong module đó đều là của riêng bạn, không ai có thể vô tình đụng vào. Khi bạn muốn chia sẻ “món ăn” (hàm, biến) của mình cho căn bếp khác, bạn phải chủ động xuất khẩu (export) nó ra. Và người khác, nếu muốn dùng, phải chủ động nhập khẩu (import) nó vào. Không còn sự mơ hồ, không còn xung đột.

Lợi ích chính của việc sử dụng Modules

Khi bạn bắt đầu tư duy theo kiểu module, bạn sẽ nhận được ba lợi ích vàng:

  • Tổ chức (Organization): Thay vì một file script.js dài hàng ngàn dòng, bạn có thể chia nhỏ code thành các module logic. Ví dụ: auth.js xử lý đăng nhập, api.js quản lý các cuộc gọi API, utils.js chứa các hàm tiện ích. Mọi thứ trở nên ngăn nắp và dễ tìm kiếm.
  • Tái sử dụng (Reusability): Một hàm tiện ích trong utils.js có thể được import và sử dụng ở bất cứ đâu trong dự án. Thậm chí, bạn có thể mang module này sang một dự án hoàn toàn mới. Thật tuyệt vời, phải không?
  • Bảo trì (Maintainability): Khi có lỗi xảy ra, bạn biết chính xác cần phải vào “căn bếp” nào để sửa. Việc cập nhật hay nâng cấp một chức năng cũng trở nên đơn giản hơn rất nhiều vì nó được đóng gói gọn gàng trong một module, không ảnh hưởng đến phần còn lại của hệ thống.

Module và Component trong JavaScript khác nhau như thế nào?

Đây là một điểm nhiều người mới thường nhầm lẫn. Hãy phân biệt chúng một cách đơn giản:

  • Module là một khái niệm về tổ chức code. Nó là một file JavaScript có import hoặc export. Mục tiêu của nó là đóng gói và chia sẻ logic, biến, hàm.
  • Component (thường thấy trong các thư viện/framework như React, Vue, Angular) là một khái niệm về tổ chức giao diện người dùng (UI). Một component là một khối UI độc lập, có thể tái sử dụng (ví dụ: nút bấm, thanh điều hướng, card sản phẩm) và nó thường được xây dựng bên trong một hoặc nhiều module.

Nói cách khác, bạn dùng modules để xây dựng nên các components.

Giới thiệu JavaScript Modules: Module là gì và tại sao lại quan trọng?
Giới thiệu JavaScript Modules: Module là gì và tại sao lại quan trọng?

Cách tạo và sử dụng Module trong JavaScript (ES6 Modules)

Kể từ phiên bản ES6 (ECMAScript 2015), JavaScript đã có cú pháp module chính thức. Đây là tiêu chuẩn hiện đại và được hỗ trợ rộng rãi trên các trình duyệt. Hãy cùng xem cách chúng ta “xuất” và “nhập” các món đồ giữa những căn bếp riêng nhé.

Cú pháp export: Chia sẻ code từ một module

Từ khóa export được dùng để đánh dấu các biến hoặc hàm mà bạn muốn cho phép các module khác truy cập. Có hai cách chính để export: Named Exports (xuất theo tên) và Default Export (xuất mặc định).

Named Exports (Xuất theo tên): Xuất nhiều giá trị

Khi bạn muốn xuất nhiều “món đồ” từ một module, hãy dùng Named Exports. Bạn có thể đặt export ngay trước khai báo biến, hàm, hoặc class.

// file: utils.js

export const PI = 3.14159;

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

export class Rectangle {
  // ... class implementation
}

Hoặc bạn có thể khai báo tất cả trước rồi export một lần ở cuối file, cách này đôi khi dễ đọc hơn:

// file: utils.js

const PI = 3.14159;

function sum(a, b) {
  return a + b;
}

// ...

export { PI, sum };

Default Export (Xuất mặc định): Xuất một giá trị duy nhất

Mỗi module chỉ có thể có một Default Export. Đây thường là “nhân vật chính” của module đó. Ví dụ, trong một module về User, class User sẽ là default export.

// file: User.js

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

// Bạn cũng có thể export một hàm hoặc một giá trị
// export default function sayHello() { ... }

Cú pháp import: Sử dụng code từ các module khác

Để lấy những gì đã được export, chúng ta dùng từ khóa import. Cú pháp sẽ hơi khác một chút tùy thuộc vào việc bạn đang import một Named Export hay Default Export.

  • Import Named Exports: Bạn phải đặt tên các biến/hàm muốn import vào trong cặp dấu ngoặc nhọn {}.
    // file: main.js
    import { PI, sum } from './utils.js'; // Chú ý phần đuôi file .js là bắt buộc trên trình duyệt
    
    console.log(PI); // 3.14159
    console.log(sum(5, 10)); // 15
    
  • Import Default Export: Bạn có thể đặt bất kỳ tên nào bạn muốn cho giá trị được import mà không cần dấu {}.
    // file: app.js
    import MyUserClass from './User.js'; // 'MyUserClass' là tên bạn tự đặt
    
    const user = new MyUserClass('WiWeb');
    console.log(user.name); // WiWeb
    
  • Import cả hai: Bạn hoàn toàn có thể kết hợp cả hai trong cùng một câu lệnh.
    import mainFunction, { helper1, helper2 } from './my-module.js';
    

Ví dụ đơn giản về một Module trong JavaScript

Hãy kết nối tất cả lại với nhau. Giả sử chúng ta có cấu trúc file:

/project
  |- index.html
  |- main.js
  |- math.js

File math.js (Module cung cấp):

// math.js
export const add = (a, b) => a + b;
const subtract = (a, b) => a - b; // Biến này không được export, nó là 'private'

// Xuất mặc định một object chứa nhiều hàm
export default {
  version: '1.0'
};

File main.js (Module sử dụng):

// main.js
import mathInfo, { add } from './math.js';

console.log('Module version:', mathInfo.version); // Module version: 1.0
console.log('Kết quả phép cộng:', add(10, 20)); // Kết quả phép cộng: 30

// console.log(subtract(10, 5)); // Lỗi! subtract is not defined vì nó không được export.

Ví dụ này cho thấy rõ sức mạnh của modules: bạn chỉ chia sẻ những gì cần thiết (add, mathInfo) và giữ lại những chi tiết triển khai bên trong (subtract).

Cách tạo và sử dụng Module trong JavaScript (ES6 Modules)
Cách tạo và sử dụng Module trong JavaScript (ES6 Modules)

Thực thi JavaScript Modules trên Trình duyệt

Viết code module xong rồi, làm sao để chạy nó trên trình duyệt? Rất đơn giản, bạn chỉ cần một thay đổi nhỏ trong thẻ <script> của mình.

Giới thiệu thẻ script type=”module”

Để báo cho trình duyệt biết rằng file JavaScript bạn đang tải là một module (chứ không phải một script thông thường), bạn chỉ cần thêm thuộc tính type="module" vào thẻ <script>.

<!DOCTYPE html>
<html>
<head>
  <title>JS Modules Demo</title>
</head>
<body>
  <h1>Hãy xem console!</h1>

  <!-- Trình duyệt sẽ tải và xử lý main.js như một module -->
  <script type="module" src="./main.js"></script>
</body>
</html>

Chỉ với thay đổi nhỏ này, main.js giờ đây có thể sử dụng import để tải các module khác như math.js mà chúng ta đã thấy ở trên. Trình duyệt sẽ tự động phân tích các câu lệnh import và tải các file phụ thuộc một cách hiệu quả.

Các đặc điểm riêng của script module

Khi bạn sử dụng <script type="module">, file JavaScript của bạn sẽ hoạt động hơi khác một chút so với script thông thường:

  1. Luôn ở chế độ nghiêm ngặt (Strict Mode): Code bên trong module tự động chạy ở 'use strict'. Điều này giúp bạn tránh được một số lỗi phổ biến và viết code an toàn hơn mà không cần phải khai báo thủ công.
  2. Không rò rỉ biến ra toàn cục: Bất kỳ biến nào bạn khai báo ở cấp cao nhất trong một module đều thuộc về phạm vi (scope) của module đó. Nó sẽ không trở thành thuộc tính của window object. Đây chính là tính năng cốt lõi giúp chống lại “Global Scope Pollution”.
  3. Chỉ thực thi một lần: Ngay cả khi bạn chèn <script type="module" src="./app.js"> ở nhiều nơi trên trang, trình duyệt sẽ chỉ tải và thực thi file app.js đúng một lần duy nhất. Các lần gọi sau sẽ sử dụng lại module đã được xử lý.

Ví dụ thực tế: Kết nối các tệp JavaScript trên trang HTML

Hãy hoàn thiện ví dụ trước đó bằng cách tạo file index.html.

File index.html:

<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <title>Hướng dẫn JavaScript Modules</title>
</head>
<body>
  <p>Kết quả sẽ hiển thị trong Bảng điều khiển (Console) của trình duyệt.</p>

  <!-- Đây là điểm khởi đầu. Trình duyệt sẽ đọc file này. -->
  <!-- Khi thấy 'import', nó sẽ tự động tìm và tải 'math.js'. -->
  <script type="module" src="main.js"></script>
</body>
</html>

Bây giờ, khi bạn mở file index.html này bằng một server cục bộ (như Live Server trong VS Code), bạn sẽ thấy các dòng console.log từ main.js xuất hiện trong console của trình duyệt. Quá trình đã hoàn tất!

Thực thi JavaScript Modules trên Trình duyệt
Thực thi JavaScript Modules trên Trình duyệt

Các khái niệm Module nâng cao bạn cần biết

Khi đã nắm vững importexport cơ bản, bạn có thể nâng tầm kỹ năng của mình với một vài khái niệm nâng cao. Chúng giúp bạn tối ưu hóa hiệu suất và viết code linh hoạt hơn.

Dynamic Imports: Tải module theo điều kiện với import()

Thông thường, các câu lệnh import ở đầu file là tĩnh. Trình duyệt sẽ tải tất cả các module cần thiết ngay khi tải trang. Nhưng nếu có một module rất lớn và chỉ được sử dụng trong một trường hợp cụ thể (ví dụ, khi người dùng click vào một nút)?

Đó là lúc Dynamic Import tỏa sáng. Nó trông giống một lời gọi hàm: import('path/to/module.js'). Lời gọi này trả về một Promise, và Promise này sẽ resolve với module đã được tải.

// Ví dụ: Chỉ tải module xử lý biểu đồ khi người dùng nhấn nút

const chartButton = document.getElementById('show-chart-btn');

chartButton.addEventListener('click', async () => {
  try {
    // Module chỉ được tải về tại thời điểm này!
    const ChartModule = await import('./heavy-chart-library.js');

    // Sử dụng module sau khi đã tải xong
    const chart = new ChartModule.default();
    chart.draw();
  } catch (error) {
    console.error('Không thể tải module biểu đồ:', error);
  }
});

Cách này cực kỳ hữu ích cho việc tối ưu tốc độ tải trang (code splitting).

Top-level await: Sử dụng await bên ngoài hàm async trong module

Trước đây, bạn chỉ có thể dùng từ khóa await bên trong một hàm async. Tuy nhiên, với các module, bạn có thể sử dụng await ở cấp cao nhất (top-level). Điều này cho phép một module chờ một tác vụ bất đồng bộ hoàn thành trước khi nó tiếp tục thực thi và trước khi các module khác import nó có thể sử dụng.

// file: data-fetcher.js

// Chờ để lấy dữ liệu cấu hình từ server
const response = await fetch('https://api.example.com/config');
const configData = await response.json();

// Chỉ export dữ liệu sau khi đã lấy xong
export default configData;

// Bất kỳ module nào import 'data-fetcher.js' sẽ phải chờ cho đến khi
// fetch hoàn tất. Trình duyệt sẽ quản lý việc này một cách thông minh.

Namespace Imports: Gom tất cả export vào một object (import * as ...)

Nếu một module có rất nhiều Named Exports và bạn muốn import tất cả chúng một cách gọn gàng, bạn có thể dùng cú pháp import * as. Nó sẽ gom tất cả các export vào một object duy nhất.

File helpers.js:

export function func1() { /*...*/ }
export function func2() { /*...*/ }
export const SUPER_CONSTANT = 42;

File main.js:

// Import tất cả mọi thứ từ helpers.js vào một object tên là 'utils'
import * as utils from './helpers.js';

// Giờ đây bạn có thể truy cập chúng qua object utils
utils.func1();
utils.func2();
console.log(utils.SUPER_CONSTANT); // 42

Đây là một cách tuyệt vời để tránh làm lộn xộn không gian tên của module hiện tại, đặc biệt khi làm việc với các thư viện lớn.

Các khái niệm Module nâng cao bạn cần biết
Các khái niệm Module nâng cao bạn cần biết

Các hệ thống Module phổ biến trong JavaScript

ES Modules (ESM) mà chúng ta vừa tìm hiểu là tiêu chuẩn hiện đại, nhưng lịch sử của JavaScript đã chứng kiến nhiều hệ thống module khác nhau. Hiểu về chúng sẽ giúp bạn khi làm việc với các dự án cũ hơn hoặc trong môi trường Node.js.

ES Modules (ESM) – Tiêu chuẩn hiện đại cho trình duyệt và Node.js

Đây là hệ thống chính thức được tích hợp vào ngôn ngữ JavaScript. Nó sử dụng cú pháp import/export và là lựa chọn hàng đầu cho các ứng dụng web hiện đại. Các phiên bản Node.js mới cũng đã hỗ trợ đầy đủ ESM, biến nó trở thành tiêu chuẩn chung cho cả frontend và backend.

CommonJS (CJS) – Hệ thống module mặc định của Node.js

Trước khi ESM ra đời, Node.js đã tạo ra hệ thống module của riêng mình gọi là CommonJS. Nếu bạn đã từng làm việc với Node.js, chắc hẳn bạn đã quen thuộc với cú pháp require()module.exports.

// Xuất khẩu trong CommonJS (file: my-module.js)
const myFunction = () => { console.log('Hello!'); };
module.exports = { myFunction };

// Nhập khẩu trong CommonJS (file: main.js)
const { myFunction } = require('./my-module.js');
myFunction(); // Hello!

CommonJS được thiết kế để tải module một cách đồng bộ (synchronous), rất phù hợp với môi trường server nơi các file nằm trên ổ đĩa.

So sánh nhanh ES Modules và CommonJS

Đặc điểmES Modules (ESM)CommonJS (CJS)
Cú phápimport, exportrequire(), module.exports
Tải moduleBất đồng bộ (Asynchronous), phù hợp cho trình duyệtĐồng bộ (Synchronous), phù hợp cho server
Hỗ trợTiêu chuẩn JavaScript, trình duyệt, Node.js mớiMặc định trong các phiên bản Node.js cũ hơn
Strict ModeTự động bậtKhông tự động

Giới thiệu về các hệ thống cũ hơn (AMD, UMD)

Ngoài ESM và CJS, bạn có thể tình cờ gặp các hệ thống cũ hơn:

  • AMD (Asynchronous Module Definition): Được tạo ra cho trình duyệt trước cả ESM. Nó tập trung vào việc tải module bất đồng bộ, nổi tiếng với thư viện RequireJS.
  • UMD (Universal Module Definition): Một nỗ lực để tạo ra một hệ thống module có thể chạy ở mọi nơi (trình duyệt, server). Nó giống như một mẫu thiết kế cố gắng tương thích với cả AMD và CommonJS.

Hiện nay, bạn hiếm khi phải viết code bằng AMD hay UMD, nhưng biết về chúng giúp bạn hiểu rõ hơn về bối cảnh lịch sử và sự tiến hóa của JavaScript.

Các hệ thống Module phổ biến trong JavaScript
Các hệ thống Module phổ biến trong JavaScript

Lời kết

Vậy là chúng ta đã cùng nhau đi qua một hành trình khá toàn diện về modules javascript. Hy vọng rằng giờ đây bạn không chỉ hiểu importexport là gì, mà còn cảm nhận được tại sao chúng lại là một phần không thể thiếu của lập trình JavaScript hiện đại.

Tóm tắt những điểm chính về JavaScript Modules

  • Modules giúp chúng ta tổ chức code thành các file độc lập, tránh ô nhiễm không gian tên toàn cục.
  • Sử dụng export (named hoặc default) để chia sẻ code từ một module.
  • Sử dụng import để lấy code từ một module khác.
  • Trên trình duyệt, hãy dùng <script type="module"> để kích hoạt hệ thống module.
  • Các khái niệm nâng cao như Dynamic Import import() giúp tối ưu hóa hiệu suất ứng dụng của bạn.

Cách tốt nhất để thực sự hiểu một khái niệm là bắt tay vào làm. Hãy thử thách bản thân bằng một bài tập nhỏ: lấy một dự án cũ của bạn có một file script.js lớn, và thử tách nó ra thành các module nhỏ hơn theo chức năng (ví dụ: api.js, dom-utils.js, main.js). Bạn sẽ ngạc nhiên về sự rõ ràng và ngăn nắp mà nó mang lại.

Bạn thấy phần nào trong việc sử dụng modules là khó hiểu nhất? Hãy chia sẻ thắc mắc của bạn với WiWeb trong phần bình luận bên dưới nhé!

Nếu việc quản lý và xây dựng một cấu trúc website chuyên nghiệp, tối ưu làm bạn mất quá nhiều thời gian, WiWeb luôn sẵn sàng hỗ trợ bạn. Nhắn tin cho chúng tôi để biến ý tưởng của bạn thành một website hoàn chỉnh!

5/5 - (80 Đánh giá)
Bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *