Câu 1Nếu nạp default export Cart bằng import { Cart } from "./cart.js" thì điều gì xảy ra?
Module — import và export
export công khai giá trị trong một file, import nạp giá trị đó vào file khác. Bài viết gồm named export và default export, type="module" trên trình duyệt, ESM và CommonJS trong Node.js.
Nếu cứ viết thêm code của màn hình đặt hàng vào một file duy nhất, phần tính thuế, giỏ hàng và thanh toán sẽ lẫn vào nhau trong hàng trăm dòng, và tìm một hàm cũng mất thời gian. Dù có chia thành nhiều file rồi đặt nhiều thẻ <script> (thẻ dùng để nạp file JavaScript) trong HTML, phía dùng hàm vẫn không biết hàm đó nằm ở file nào.
Bài này nói về export và import, cách truyền giá trị giữa các file.
Không dùng được import trong console
Console bài tập chạy code của bạn bằng cách đặt nó vào bên trong một hàm. import và export chỉ viết được ở cấp ngoài cùng của file, nên ở đó chúng gây lỗi SyntaxError. Code trong bài này là ví dụ chạy trên Node.js hoặc trình duyệt với các file riêng, và kết quả được ghi trong comment ở cuối dòng.
Dùng chung hàm giữa các file — named export
Giả sử cả cart-page.js (màn hình giỏ hàng) lẫn checkout.js (màn hình thanh toán) đều cần cùng một phép tính giá có thuế. Nếu chép hàm vào cả hai file, khi thuế suất thay đổi bạn phải sửa hai chỗ, và quên sửa một chỗ là hai màn hình hiện hai số tiền khác nhau.
Biến và hàm khai báo trong một module (file JavaScript truyền giá trị bằng import và export; cách nạp module được nói ở phần sau) không nhìn thấy được từ file khác. Thêm export trước khai báo để công khai nó; cách này gọi là named export. File sử dụng thì viết đúng tên đó trong ngoặc nhọn: import { withTax } from "./price.js";.
// ---- price.js ----
const TAX_RATE = 0.1; // Không có export: chỉ dùng bên trong price.js
export function withTax(price) { // Có export: file khác import được
return Math.floor(price * (1 + TAX_RATE));
}
export function formatYen(price) {
return `${price} yên`;
}
// ---- cart-page.js ----
import { withTax, formatYen } from "./price.js"; // Liệt kê các tên sẽ dùng trong ngoặc nhọn
console.log(formatYen(withTax(4800))); // 5280 yên
// Nếu import một tên không được export
// import { TAX_RATE } from "./price.js";
// SyntaxError: The requested module './price.js' does not provide an export named 'TAX_RATE'
Chỉ withTax bên trong price.js đọc TAX_RATE, nên cart-page.js nhận được giá có thuế mà không cần biết thuế suất. Khi thuế suất thay đổi, bạn chỉ sửa một dòng TAX_RATE trong price.js, và mọi file import nó đều nhận thay đổi.
Chỉ công khai một class — default export
Nếu cart.js chứa class giỏ hàng, file đó chỉ cần công khai một class Cart. Bạn sẽ hay gặp import Cart from "./cart.js"; trong code của người khác, nhưng nếu thêm ngoặc nhọn theo cách viết ở phần trước thì sẽ gặp SyntaxError và không nạp được.
Default export (giá trị công khai mà mỗi file chỉ có tối đa một, và file import không cần viết đúng tên) được viết là export default class Cart { ... }. File import viết không có ngoặc nhọn, và ở vị trí của Cart có thể đặt bất kỳ tên nào mình muốn.
// ---- cart.js ----
import { withTax } from "./price.js";
export const MAX_ITEMS = 20; // Named export có thể nằm cùng file
export default class Cart { // Mỗi file có tối đa một default export
#prices = [];
add(price) { this.#prices.push(price); }
get total() { return withTax(this.#prices.reduce((sum, price) => sum + price, 0)); }
}
// ---- checkout.js ----
import Cart, { MAX_ITEMS } from "./cart.js"; // Default ở ngoài ngoặc nhọn, named ở trong
import ShoppingCart from "./cart.js"; // Default có thể nhận bằng tên khác
const cart = new ShoppingCart();
cart.add(1200); cart.add(3600);
console.log(cart.total, MAX_ITEMS); // 5280 20
console.log(Cart === ShoppingCart); // true (cùng một class)
// Có ngoặc nhọn thì sẽ tìm named export tên Cart
// import { Cart } from "./cart.js";
// SyntaxError: The requested module './cart.js' does not provide an export named 'Cart'
'Cart' ở cuối thông báo lỗi là tên của named export đã được tìm nhưng không thấy. Thông báo không nhắc gì tới việc có một default Cart, nên hãy kiểm tra xem mình có dùng ngoặc nhọn hay không. Bảng dưới đây tóm tắt các cách viết import dùng trong bài.
| Cách viết import | Nhận được gì | Tên ở phía import |
|---|---|---|
| import { withTax } from … | Named export | Trùng tên với export |
| import Cart from … | Default export | Đặt tên tùy ý |
| import Cart, { MAX_ITEMS } from … | Default và named | Cart và MAX_ITEMS (default trước) |
Để trình duyệt lần theo các import — type="module"
Tiếp theo, HTML của trang thanh toán cần nạp checkout.js. Chỉ với <script src="./checkout.js"></script>, Chrome báo SyntaxError: Cannot use import statement outside a module ở dòng import đầu tiên, và không dòng nào trong file được chạy.
Với type="module" (thiết lập của <script> để chạy file được nạp dưới dạng module), trình duyệt lần theo các import trong file đầu vào và tải cả các file còn lại. Mạng lưới import này được gọi là dependency graph (quan hệ file nào import file nào).
// ---- index.html: chỉ ghi file đầu vào checkout.js ----
// <script type="module" src="./checkout.js"></script>
// ---- price.js ----
console.log("Đang chạy price.js"); // Chỉ in một lần dù được import từ 2 chỗ
const TAX_RATE = 0.1;
export function withTax(price) { return Math.floor(price * (1 + TAX_RATE)); }
// ---- cart.js ----
import { withTax } from "./price.js";
console.log("Đang chạy cart.js");
export const cartTotal = withTax(4800);
// ---- checkout.js ----
import { cartTotal } from "./cart.js";
import { withTax } from "./price.js"; // Nạp cùng price.js với cart.js
console.log(`Cần thanh toán: ${cartTotal + withTax(500)} yên`);
// Thứ tự in ra console: Đang chạy price.js → Đang chạy cart.js → Cần thanh toán: 5830 yên
Module chạy các file mà nó import trước khi chạy chính nó. File đầu vào checkout.js phải đợi cart.js và price.js mà nó import chạy xong, nên nó chạy cuối cùng. Thứ tự được quyết định bởi các import, nên bạn không cần bận tâm thứ tự đặt file trong HTML.
Không chạy được khi mở HTML qua file://
Nếu nhấp đúp vào file HTML và mở bằng file://, Chrome coi cả các file trong cùng thư mục là đến từ một origin (nguồn tải file) khác, và CORS (cơ chế hạn chế việc nạp từ origin khác) sẽ chặn việc nạp type="module". Hãy khởi động một web server, chẳng hạn bằng extension Live Server của VS Code, rồi mở trang từ đó.
Phân biệt định dạng trong Node.js — ESM và CommonJS
Trong code viết cho Node.js, bạn sẽ gặp những file dùng const { reserve } = require("./stock"); thay vì import. Nếu bắt chước và thêm require vào một file đang dùng import, bạn sẽ gặp lỗi ReferenceError khi chạy.
Bên cạnh ESM (viết tắt của ES Modules, định dạng chuẩn dùng import và export), còn có CommonJS (định dạng riêng của Node.js, nạp bằng require và công khai bằng module.exports). Node.js quyết định file dùng định dạng nào dựa vào đuôi file và trường "type" trong package.json (file cấu hình của project).
// ---- shop/package.json (file ghi cấu hình của project Node.js) ----
// { "type": "module" }
// ---- shop/stock.cjs: đuôi là .cjs nên được nạp như CommonJS ----
function reserve(count) { return `Đã giữ: ${count}`; }
module.exports = { reserve: reserve, LIMIT: 3 }; // Gom các giá trị công khai vào một object
// ---- shop/report.cjs: file CommonJS nạp bằng require ----
const { reserve, LIMIT } = require("./stock.cjs");
console.log(reserve(2), LIMIT); // Đã giữ: 2 3
// ---- shop/checkout.js: có "type": "module" nên được nạp như ESM ----
import stock from "./stock.cjs"; // Không có ngoặc nhọn: nhận giá trị của module.exports
console.log(stock.reserve(1)); // Đã giữ: 1
const { LIMIT } = require("./stock.cjs");
// ReferenceError: require is not defined in ES module scope, you can use import instead
- File có đuôi
.jsđược nạp như ESM
- Nạp được bằng
import stock from "./stock.cjs" - Viết
requirethì báo lỗiReferenceError
stock.cjs— công khai bằngmodule.exportsreport.cjs— nạp được bằngrequire
import stock không có ngoặc nhọn, nên nó nhận giá trị theo cùng cách với default export: biến nhận được object module.exports. Trong một file, đừng trộn import với require, hãy chỉ dùng một trong hai. Bảng dưới đây là cách viết của hai định dạng và quy tắc quyết định định dạng của file.
| Mục | ESM | CommonJS |
|---|---|---|
| Nạp | import { reserve } from "./stock.js" | const { reserve } = require("./stock.js") |
| Công khai | export function reserve() { ... } | module.exports = { reserve: reserve } |
| Đuôi file | Không bỏ được trên Node.js và trình duyệt ("./stock" không tìm thấy) | Bỏ được ("./stock" tìm stock.js) |
| File theo định dạng này | .mjs, và .js dưới "type": "module" | .cjs, và .js dưới "commonjs" hoặc khi không có "type" (không có "type" mà .js dùng import thì được đọc lại như ESM) |
| Trình duyệt | Nạp bằng type="module" | Không nạp trực tiếp được |
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2price.js được import từ 2 chỗ. console.log ở đầu file in ra mấy lần?
Câu 3Gọi require trong checkout.js (thuộc phạm vi "type": "module") thì điều gì xảy ra?