Game lớn dần thì một file game.js dài 1000 dòng rất khó sửa. Module cho bạn chia code thành nhiều file nhỏ, mỗi file lo một việc: player.js lo nhân vật, ui.js lo giao diện. File nào cần gì thì import thứ đó từ file khác.
Vấn đề khi không có module
Cách cũ là gắn nhiều thẻ <script> liên tiếp. Mọi biến ở cấp ngoài cùng của mọi file dùng chung một chỗ. File player.js có let hp, file enemy.js cũng có let hp, trình duyệt báo SyntaxError: Identifier 'hp' has already been declared và file sau không chạy. Thứ tự thẻ script cũng phải đúng, không thì hàm gọi tới chưa tồn tại.
Module sửa cả hai: mỗi file có phạm vi riêng, và file tự nói nó cần gì từ file nào.
export: cho file khác dùng
Mặc định, mọi thứ trong module là riêng tư. Muốn chia sẻ thì ghi export phía trước.
// combat.js
export const MAX_HP = 100;
export function calcDamage(attack, defense) {
return Math.max(1, attack - defense);
}
function rollCrit() {
return Math.random() < 0.1;
}
MAX_HP và calcDamage dùng được ở file khác. rollCrit không có export nên chỉ dùng trong combat.js.
import: lấy thứ file khác chia sẻ
// game.js
import { MAX_HP, calcDamage } from "./combat.js";
let enemyHp = MAX_HP;
enemyHp -= calcDamage(20, 5);
console.log(enemyHp); // 85
Tên trong ngoặc nhọn phải trùng tên đã export. Đổi tên lúc import bằng as:
import { calcDamage as damageOf } from "./combat.js";
Lỗi hay gặp: viết
from "combat.js"hoặcfrom "./combat". Trình duyệt cần đường dẫn đầy đủ, bắt đầu bằng./hoặc../và có đuôi.js. Thiếu./sẽ báoTypeError: Failed to resolve module specifier "combat.js". Thiếu đuôi.jsthì trình duyệt tải không được file, Console báo lỗi 404.
export default
Mỗi file có thể có một export default, là thứ chính của file đó. Import nó thì không cần ngoặc nhọn, và tự đặt tên tuỳ ý.
// Enemy.js
export default class Enemy {
constructor(name, hp) {
this.name = name;
this.hp = hp;
}
}
// game.js
import Enemy from "./Enemy.js";
const slime = new Enemy("Slime", 30);
console.log(slime.name); // Slime
Nhiều người chọn chỉ dùng export có tên (named export) cho cả dự án. Tên cố định giúp tìm kiếm dễ hơn và editor tự gợi ý import chính xác hơn. Loạt bài này ưu tiên named export.
Chạy module trên trình duyệt
Thêm type="module" vào thẻ script. Chỉ cần gắn file đầu vào, các file khác được tải qua import.
<body>
<p>Máu: <span id="hp">100</span></p>
<script type="module" src="game.js"></script>
</body>
Thiếu type="module" thì dòng import đầu tiên báo SyntaxError: Cannot use import statement outside a module.
Script module có vài điểm khác script thường:
- Tự động chạy như có
defer: đợi trang dựng xong mới chạy, đặt trong<head>cũng được. - Dùng được
awaitở cấp ngoài cùng, không cần bọc trong hàmasync(xem trang Async await). - Luôn chạy ở strict mode, nên gán vào biến chưa khai báo sẽ báo lỗi thay vì âm thầm tạo biến.
- Cần chạy qua server. Mở file bằng
file://thì trình duyệt chặn tải module. Dùng Live Server của VS Code hoặcnpx serve.
Chạy module bằng Node
Node 20 hiểu import/export khi file có đuôi .mjs, hoặc khi package.json có dòng "type": "module":
{
"type": "module"
}
Sau đó node game.js chạy được với import như trên trình duyệt.
Một cấu trúc gợi ý cho mini game
index.html
js/
game.js điểm vào, gắn sự kiện
combat.js calcDamage, MAX_HP
ui.js updateHpText, showMessage
storage.js loadHighScores, saveScore
storage.js là chỗ hợp để đặt hai hàm lưu điểm ở trang JSON. ui.js gom các hàm sửa DOM.
Bài tập
Tạo ui.js export hàm setScore(value) ghi value vào #score. Trong game.js, import hàm đó và gọi setScore(42). Viết cả thẻ script trong HTML.
Xem đáp án
// js/ui.js
const scoreText = document.querySelector("#score");
export function setScore(value) {
scoreText.textContent = value;
}
// js/game.js
import { setScore } from "./ui.js";
setScore(42);
<p>Điểm: <span id="score">0</span></p>
<script type="module" src="js/game.js"></script>
Đường dẫn ./ui.js tính từ vị trí của game.js, không phải từ index.html.