Chào anh em Viblo! 👋
Nếu anh em đã từng làm việc với Node.js ở kỷ nguyên CommonJS, hoặc đang cấu hình các file script, file webpack, vite trong các dự án hiện đại, chắc chắn anh em đã gõ câu lệnh này hàng vạn lần:
const fs = require('node:fs');
Chúng ta đều gọi miệng require() là một cái "hàm" dùng để nạp thư viện. Thế nhưng, nếu anh em dùng TypeScript và lỡ tay Ctrl + Click vào chữ require, anh em sẽ được dẫn tới một dòng khai báo toàn cục (global declaration) nằm sâu trong gói @types/node trông như thế này:
declare var require: NodeRequire;
Khựng lại một giây nào! Tại sao một cái "hàm" lại được khai báo bằng từ khóa var? Và cái kiểu dữ liệu NodeRequire kia chứa đựng những ma thuật gì bên trong? Hóa ra, require trong Node.js không đơn thuần là một cái hàm toàn cục global function như parseInt() hay setTimeout(), mà nó là một đối tượng cực kỳ quyền lực bọc trong một callable interface.
Hôm nay, hãy cùng mình mổ xẻ "thân phận thật sự" của var require: NodeRequire; để xem các kỹ sư Core Node.js đã thiết kế hệ thống module kinh điển này tinh tế như thế nào nhé!
1. Bóc Tách Bản Chất: require Là Một Hàm Hay Một Object?
Câu trả lời chính xác nhất là: Cả hai! Trong JavaScript, Hàm bản chất cũng là một Đối tượng (Function is an Object). Khai báo interface NodeRequire trong TypeScript đã tận dụng triệt để tính chất này để tạo ra một cấu trúc vừa có thể gọi trực tiếp giống như hàm, vừa có thể truy cập vào các thuộc tính/phương thức đính kèm trên nó.
Cấu trúc thu gọn của interface NodeRequire trong core sẽ trông như thế này:
interface NodeRequire {
// 1. Cấu trúc Callable (Giúp require có thể gọi như một hàm)
(id: string): any;
// 2. Các thuộc tính và phương thức đính kèm
resolve: RequireResolve;
cache: Dict<Module>;
extensions: RequireExtensions;
main: Module | undefined;
}
2. Tại Sao Lại Khai Báo Bằng var Mà Không Phải const?
Có hai lý do cốt lõi khiến TypeScript buộc phải dùng declare var require ở tầng global:
- Tương thích ngược với kiến trúc Global Scope cũ: Từ khóa
varkhi khai báo ở tầng global sẽ tự động đính kèm vào đối tượng toàn cục (globaltrong Node.js hoặcwindowtrong Browser). Nếu dùnglethoặcconst, biến đó sẽ bị giới hạn trong block scope của file định nghĩa và không phủ sóng toàn hệ thống được. - Cho phép "Hóa Thân" / Mocking khi Test: Vì khai báo bằng
var, trong các môi trường Testing (như Jest, Mocha), các kỹ sư có thể tạm thời ghi đè hoặc bọc (spy/stub) lên hàmrequiregốc để kiểm tra xem một module có được nạp đúng cách hay không. Nếu khai báo bằngconst, việc bọc hàm này là hoàn toàn bất khả thi.
3. Những "Vũ Khí Bí Mật" Nằm Trong NodeRequire (Kinh Nghiệm Thực Chiến)
Hàm require(id) thì ai cũng biết rồi, giờ chúng ta sẽ soi vào đống thuộc tính đính kèm – nơi phân biệt giữa một Dev biết xài và một Senior làm chủ công cụ.
Vũ khí 1: require.resolve(id) — Tìm đường nhưng không nạp
Hàm require('lodash') sẽ bốc nguyên toàn bộ code của thư viện Lodash nạp vào RAM. Nhưng nếu bạn chỉ muốn biết: "Cái file đó nằm chính xác ở thư mục nào trong ổ cứng?" mà không muốn tốn RAM nạp nó, hãy dùng .resolve().
try {
const absolutePath = require.resolve('my-secret-config.json');
console.log('File tồn tại tại:', absolutePath);
// 👉 Kết quả: /Users/project/src/my-secret-config.json
} catch (err) {
console.log('File không tồn tại trong hệ thống!');
}
- Ứng dụng thực chiến: Thường dùng để check xem một plugin, một file config có tồn tại trong project hay không trước khi kích hoạt một logic nào đó, hoặc dùng để truyền đường dẫn tuyệt đối vào các tool công cụ khác.
Vũ khí 2: require.cache — Cơn ác mộng và vị cứu tinh "Hot Reload"
Mặc định, Node.js cực kỳ thông minh: Khi bạn require('./config') lần đầu, nó sẽ đọc file từ ổ cứng và lưu kết quả vào một bộ nhớ đệm gọi là require.cache. Từ lần thứ hai trở đi, nó chỉ việc bốc từ RAM ra, tốc độ nhanh chớp nhoáng.
Vết sẹo thực chiến: Bạn làm một tool CMS, admin vừa thay đổi cấu hình hệ thống trong file config.json. Bạn muốn Node.js đọc lại file đó để cập nhật data mới mà không muốn phải restart lại toàn bộ Server. Nếu bạn cứ cắm đầu gọi lại require('./config.json'), dữ liệu nhận về vẫn là dữ liệu cũ vì Node.js đang lấy từ cache ra!
Bốc thuốc: Để ép Node.js đọc lại file mới từ ổ cứng, bạn phải chủ động xóa sạch cache của file đó trong require.cache:
const configPath = require.resolve('./config.json');
// Xóa bộ nhớ đệm của file cụ thể này
delete require.cache[configPath];
// Lần này Node.js buộc phải mò xuống ổ cứng đọc lại file mới tinh!
const freshConfig = require('./config.json');
Vũ khí 3: require.main — Xác định "Ai là cảnh sát trưởng?"
Thuộc tính này chứa thông tin của Module đóng vai trò là Điểm khởi chạy đầu tiên (Entry Point) của ứng dụng (Ví dụ file node app.js thì app.js chính là require.main).
- Ứng dụng thực chiến: Giúp bạn viết một file vừa có thể làm thư viện cho file khác require, vừa có thể chạy độc lập như một tool CLI.
if (require.main === module) {
console.log('File này đang được chạy trực tiếp bằng lệnh `node file.js`');
// Chạy các script setup tự động tại đây...
} else {
console.log('File này đang được một file khác require vào làm thư viện');
}
4. Góc Nhìn Thời Đại: Require vs Import
Mặc dù hệ sinh thái Node.js đang dịch chuyển mạnh mẽ sang ES Modules (ESM) với cú pháp import/export, nhưng require và NodeRequire vẫn giữ một vị trí tối thượng:
- ESM chạy bất đồng bộ (Asynchronous), còn
requirechạy đồng bộ (Synchronous). Trong các script cấu hình hệ thống, việc nạp file tuần tự bằngrequirevẫn mang lại sự tiện lợi, mạch lạc và dễ kiểm soát lỗi hơn rất nhiều. - Hiểu sâu về
require.cachehayrequire.resolvegiúp anh em tự tin viết các công cụ build tool chuyên sâu, các plugin hệ thống hoặc tối ưu hóa hiệu năng nạp module cho các server có dung lượng RAM hạn chế.
Đúc Kết Lại
Dòng định nghĩa declare var require: NodeRequire; ngắn ngủi trong file core thực chất là một thiết kế mẫu mực thể hiện sự linh hoạt tuyệt đối của ngôn ngữ JavaScript. Bản chất của require là một hàm lai đối tượng (Callable Object), tích hợp sẵn các công cụ quản lý cache, định vị đường dẫn và định danh entry point cực kỳ mạnh mẽ bên dưới lớp cánh gà.
Hy vọng bài viết này giúp anh em "thông suốt" thêm một ngóc ngách thú vị của Node.js. Chúc anh em ứng dụng thành công và nâng trình viết script lên một tầm cao mới! Happy Coding!