fs.readFile(path, 'utf8', callback) đọc toàn bộ một tệp và truyền nội dung của nó cho callback dưới dạng chuỗi. fs.readFileSync(path, 'utf8') trả về chuỗi trực tiếp và chờ đến khi đọc xong, còn await readFile(path, 'utf8') từ fs/promises làm điều tương tự mà không chặn chương trình. Bỏ 'utf8' đi thì bạn nhận được một Buffer chứa byte thô thay vì văn bản.
readFile called được in ra trước: readFile chỉ bắt đầu việc đọc, còn callback chạy khi nội dung đã sẵn sàng. Xóa 'utf8', rồi chạy lại: callback nhận <Buffer 42 75 79 20 6d 69 6c 6b 0a 43 61 6c 6c 20 41 64 61>, tức các byte thô của tệp.
readFile, readFileSync và fs/promises
Cả ba đều đọc cùng một tệp. Chúng khác nhau ở cách bạn nhận kết quả và việc phần còn lại của chương trình có phải chờ hay không.
Dòng sync được in ra trước vì không có gì khác chạy cho đến khi nó trả về. Hai cách còn lại xong sau phần code chính, theo thứ tự mà các lần đọc hoàn tất.
readFileSync | readFile (callback) | readFile của fs/promises | |
|---|---|---|---|
| Trả về | Nội dung tệp | undefined | Promise chứa nội dung |
| Chặn chương trình | Có | Không | Không |
| Lỗi | Bị ném ra, dùng try/catch | Đối số đầu tiên của callback | Promise bị reject, try/catch với await |
| Dùng trong | Code khởi động và script | Code callback cũ | Code mới |
Dùng readFileSync khi đọc cấu hình lúc khởi động hoặc trong một script ngắn. Trên server, một lần đọc sync sẽ chặn mọi request khác trong lúc ổ đĩa làm việc, nên hãy dùng bản promise ở đó. Trang fs/promises trình bày phần còn lại của API này.
Xử lý tệp không tồn tại (ENOENT)
Khi tệp không tồn tại, lỗi có code: 'ENOENT' ("error, no entry"). Kiểm tra err.code để phân biệt tệp bị thiếu với các lỗi khác, như lỗi quyền truy cập.
Lần gọi đầu không tìm thấy tệp nên dùng giá trị dự phòng default settings; lần thứ hai đọc được dark mode. Ném lại các lỗi khác giúp những vấn đề thật vẫn hiện ra, thay vì lặng lẽ dùng giá trị mặc định.
fs.existsSync: vì sao nên đọc luôn
fs.existsSync(path) trả về true hoặc false, và bạn dễ muốn kiểm tra trước khi đọc:
if (fs.existsSync('data.txt')) {
const text = fs.readFileSync('data.txt', 'utf8');
}
Như vậy là hai bước trong khi một bước là đủ, và tệp có thể bị xóa giữa hai bước, nên lệnh đọc vẫn có thể ném lỗi. Hãy đọc luôn và xử lý ENOENT như ở trên. existsSync vẫn ổn khi bạn chỉ cần biết thứ gì đó có tồn tại hay không mà không mở nó, ví dụ để quyết định có tạo thư mục hay không.
Đọc tệp JSON
Đọc văn bản, rồi parse bằng JSON.parse. Tệp có lỗi cú pháp sẽ ném SyntaxError, nên hãy đặt nó trong cùng khối try.
Chương trình in ra 3001 và boolean: sau khi parse, các giá trị mang đúng kiểu JSON của chúng. require('./config.json') cũng dùng được, nhưng nó cache kết quả, nên lần require thứ hai vẫn trả về object cũ dù tệp đã thay đổi.
Đọc tệp từng dòng
Với tệp vừa bộ nhớ, hãy đọc toàn bộ rồi tách theo ký tự xuống dòng. Lọc bỏ chuỗi rỗng do ký tự xuống dòng cuối tệp để lại.
Phép tách cho ra 4 phần tử, phần tử cuối rỗng, nên vòng lặp in ra ba dòng có đánh số. Tệp viết trên Windows kết thúc dòng bằng \r\n; hãy tách theo /\r?\n/ để xử lý cả hai.
Một tệp log vài gigabyte không vừa bộ nhớ. Hãy đọc nó theo luồng (stream), để mỗi lúc chỉ giữ một dòng:
const fs = require('fs');
const readline = require('readline');
async function main() {
const rl = readline.createInterface({
input: fs.createReadStream('big.log'),
crlfDelay: Infinity,
});
for await (const line of rl) {
console.log(line);
}
}
main();
Lỗi thường gặp
Dùng kết quả bên ngoài callback. const text = fs.readFile(...) là undefined. Nội dung tệp chỉ có bên trong callback, hoặc sau await.
Quên encoding. Thiếu 'utf8' thì bạn nhận một Buffer, và data.split('\n') ném lỗi data.split is not a function. Hãy truyền encoding mỗi khi bạn muốn nhận văn bản.
Đường dẫn tương đối tính từ sai thư mục. 'data.txt' được tìm ở nơi bạn khởi chạy node. Dùng path.join(__dirname, 'data.txt') cho tệp đi kèm cạnh script.
Câu hỏi thường gặp
Vì sao fs.readFile trả về undefined?
fs.readFile không trả về nội dung tệp. Nó bắt đầu việc đọc và gọi callback của bạn sau đó với (err, data). Hãy dùng dữ liệu bên trong callback, gọi readFileSync, hoặc await bản promise.
Node tìm đường dẫn tương đối như 'data.txt' ở đâu?
Trong thư mục làm việc hiện tại, tức thư mục bạn chạy lệnh node, không phải thư mục chứa script. Để đọc tệp nằm cạnh script, hãy ghép đường dẫn bằng path.join(__dirname, 'data.txt').
fs.readFile có đọc được tệp từ URL không?
Không, nó chỉ đọc từ hệ thống tệp. Để lấy tệp qua HTTP, hãy dùng fetch rồi await res.text().
readFile xử lý được tệp lớn đến mức nào?
Nó nạp toàn bộ tệp vào bộ nhớ, nên phù hợp với tệp cấu hình, template và tệp dữ liệu nhỏ. Với tệp hàng trăm megabyte, hãy đọc từng phần bằng fs.createReadStream.