Menu

module.exports và exports trong Node.js qua ví dụ

module.exports là giá trị mà một tệp CommonJS trao cho nơi gọi require() trên nó. exports là lối tắt trỏ đến cùng object đó, cho đến khi bạn gán lại nó.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

module.exports là giá trị mà một tệp Node.js trao cho bất kỳ tệp nào khác nạp nó bằng require(). Ban đầu nó là một object rỗng: thêm thuộc tính vào nó để export nhiều thứ, hoặc gán cho nó một giá trị mới (một hàm, một lớp) để export một thứ duy nhất. exports là một biến viết tắt trỏ đến cùng object đó, nên exports.add = ... vẫn hoạt động, nhưng exports = ... cắt đứt liên kết và không export gì cả.

Chương trình in ra 5 20 3.14159, rồi [ 'add', 'multiply', 'PI' ]. Cả ba dòng trong math.js đều thêm thuộc tính vào cùng một object, dù đi qua module.exports hay exports, và require('./math') trả về chính object đó.

Export một hàm, một lớp hoặc một object

Khi module có một thứ chính, hãy gán nó trực tiếp cho module.exports. Nơi gọi khi đó nhận chính hàm đó, không phải một object bọc quanh nó:

Chương trình in ra Hello, Ada và Grace (user). Nếu bạn gặp TypeError: greet is not a function, nơi gọi đã dùng sai dạng: viết const { greet } = require(...) cho thuộc tính của object và const greet = require(...) cho giá trị được gán trực tiếp.

module.exports và exports: cái bẫy khi gán lại

Trước khi tệp của bạn chạy, Node.js đặt exports = module.exports. Cả hai tên trỏ đến một object, và require trả về module.exports. Gán giá trị mới cho exports chỉ thay đổi biến cục bộ:

broken.js export ra {}, vì exports giờ trỏ đến một object mới mà không ai trả về. working.js export ra { version: 2 }.

Bạn viếtCó được export?
module.exports.x = 1Có
exports.x = 1Có, cùng object
module.exports = { x: 1 }Có, thay thế object
exports = { x: 1 }Không, giá trị bị mất

Quy tắc đơn giản nhất: luôn viết module.exports. Cách này đúng trong mọi trường hợp, và code nói rõ nó làm gì.

require trả về cùng một object đã cache

Node.js chạy code của module một lần, ở lần đầu tiên nó được require, và cache module.exports. Mọi lần require sau đó với cùng tệp đều trả về chính object đó, nên trạng thái bên trong module được chia sẻ cho mọi tệp nạp nó:

counter.js is running chỉ xuất hiện một lần, b.next() trả về 3, và a === b là true, vì khóa của cache là đường dẫn tệp đã được phân giải, và ./counter với ./counter.js cùng trỏ tới một tệp. Đó là lý do một kết nối cơ sở dữ liệu hay một object cấu hình tạo trong một module được dùng chung cho cả app.

module.exports và ES module

ES module dùng export và import thay thế. Các dạng tương ứng với nhau như sau:

// CommonJS                              // ES modules
module.exports = greet;                  export default greet;
module.exports.add = add;                export { add };
const greet = require('./greet');        import greet from './greet.js';
const { add } = require('./math');       import { add } from './math.js';

Một ES module có thể import tệp CommonJS: module.exports của tệp đó trở thành default import. Từ Node.js 22.12, require() cũng nạp được ES module không có await ở cấp cao nhất. Để biết khi nào chọn cái nào, xem CommonJS và ES module.

Lỗi thường gặp

Trộn exports.x với module.exports = ... trong một tệp. Phép gán thay thế object, nên mọi exports.x viết trước đó đều mất. Hãy chọn một kiểu cho mỗi tệp.

Export một giá trị sẽ thay đổi sau này. module.exports = { count } sao chép con số tại thời điểm nạp; khi module cập nhật count, nơi gọi vẫn thấy giá trị cũ. Hãy export một hàm trả về giá trị đó, như counter.js ở trên.

Bỏ sót ./. require('math') tìm một package trong node_modules, không phải math.js nằm cạnh tệp của bạn. Tệp cục bộ luôn bắt đầu bằng ./ hoặc ../.

Câu hỏi thường gặp

Có thể dùng module.exports và export trong cùng một tệp không?

Không. Một tệp hoặc là CommonJS (require, module.exports), hoặc là ES module (import, export). Node.js quyết định dựa vào phần mở rộng (.cjs hoặc .mjs) hoặc "type" trong package.json.

Vì sao require trả về object rỗng?

Thường là do module đã gán exports = ... thay vì module.exports = ..., hoặc hai module require lẫn nhau. Khi require vòng tròn, một trong hai nhận module.exports của module kia trước khi tệp đó chạy xong.

Có cần phần mở rộng .js trong require không?

Không. require('./math') thử ./math, rồi ./math.js, ./math.json và ./math.node, sau đó là thư mục ./math có index.js. Với import trong ES module thì bắt buộc phải có phần mở rộng.

Làm sao nạp lại một module mà require đã cache?

Xóa mục của nó bằng delete require.cache[require.resolve('./math')] rồi require lại. Cách này chủ yếu hữu ích trong test và công cụ phát triển; code ứng dụng không nên dựa vào nó.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu