fs/promises là phiên bản dựa trên promise của module fs trong Node. Các hàm của nó (readFile, writeFile, readdir, stat, mkdir, rm, rename và các hàm khác) trả về một promise thay vì nhận callback, nên bạn await chúng và bắt lỗi bằng try/catch. Import nó bằng require('fs/promises') hoặc import fs from 'fs/promises'.
Chương trình in ra [ 'write docs', 'ship it' ]: mỗi await chờ một thao tác xong rồi dòng tiếp theo mới chạy, nên phần ghi thêm luôn diễn ra sau lần ghi đầu tiên. Trong ES module, await dùng được ở cấp cao nhất; trong CommonJS, hãy bọc code trong một hàm async.
Những hàm bạn dùng nhiều nhất
Tên hàm giống hệt API callback, chỉ bỏ đi callback. Trang readFile và writeFile trình bày chi tiết hai hàm này.
| Hàm | Chức năng | Resolve thành |
|---|---|---|
readFile(path, 'utf8') | Đọc toàn bộ tệp | Nội dung tệp |
writeFile(path, data) | Tạo mới hoặc ghi đè tệp | undefined |
appendFile(path, data) | Ghi thêm vào cuối | undefined |
readdir(path) | Liệt kê một thư mục | Mảng các tên |
stat(path) | Kích thước, ngày tháng, là tệp hay thư mục | Một object Stats |
mkdir(path, { recursive: true }) | Tạo thư mục | Thư mục đầu tiên được tạo, hoặc undefined |
rename(from, to) | Di chuyển hoặc đổi tên | undefined |
rm(path, { recursive: true, force: true }) | Xóa tệp hoặc thư mục | undefined |
Liệt kê thư mục với readdir và stat
readdir chỉ trả về tên. Gọi stat cho từng tên để lấy kích thước hoặc phân biệt tệp với thư mục, hoặc truyền { withFileTypes: true } để nhận các mục đã biết sẵn kiểu của mình.
Vòng lặp in ra README.md 6 bytes, notes.txt 3 bytes và src folder. stat reject với ENOENT khi đường dẫn không tồn tại, nên nó là cách kiểm tra tồn tại nhanh khi bạn cũng cần thêm thông tin chi tiết.
Đổi tên, di chuyển và xóa
rename vừa đổi tên vừa di chuyển tệp (trong cùng một ổ đĩa). rm dùng để xóa; nó cần recursive: true với thư mục có nội dung bên trong, và force: true để không báo lỗi khi đường dẫn đã không còn.
Tệp được chuyển từ drafts sang published với tên mới, và sau rm, thư mục blog chỉ còn published. Bỏ force: true khỏi lệnh rm thứ hai thì nó reject với ENOENT.
Xử lý lỗi bằng try/catch
Một thao tác thất bại sẽ reject promise của nó. Với await, lần reject đó trở thành một ngoại lệ (exception) mà bạn bắt như mọi ngoại lệ khác. Thuộc tính code của lỗi cho biết chuyện gì đã xảy ra.
Lần gọi đầu không tìm thấy tệp nên trả về []; lần thứ hai in ra Ada. Các mã lỗi nên biết: ENOENT (không có tệp đó), EEXIST (đã tồn tại), EISDIR (gặp thư mục ở chỗ cần một tệp), ENOTEMPTY (thư mục vẫn còn nội dung) và EACCES (không có quyền).
Một promise bạn không await và cũng không .catch() sẽ thành unhandled rejection khi thất bại, và Node thoát kèm lỗi. Hãy await mọi lệnh gọi, hoặc trả promise về cho nơi gọi có await nó.
Đọc nhiều tệp cùng lúc với Promise.all
Một vòng lặp for có await đọc từng tệp nối tiếp nhau. Khi các lần đọc không phụ thuộc vào nhau, hãy bắt đầu tất cả rồi chờ một lần bằng Promise.all: ổ đĩa xử lý chúng cùng lúc.
Promise.all giữ nguyên thứ tự của mảng đầu vào, nên kết quả là [ 'alpha', 'beta', 'gamma' ] dù lần đọc nào xong trước, và độ dài sau khi nối là 14. Đổi 'b.txt' trong mảng thành 'x.txt' thì cả lệnh await reject với ENOENT.
Nếu bạn cần mọi kết quả ngay cả khi vài lần đọc thất bại, hãy dùng Promise.allSettled; trang promise trình bày cả hai.
Lỗi thường gặp
Nhầm lẫn hai module. require('fs').readFile cần callback; require('fs/promises').readFile trả về promise. Gọi bản callback mà không truyền callback sẽ ném lỗi The "cb" argument must be of type function.
Quên await. const text = fs.readFile(...) là một promise đang chờ, và text.split ném lỗi text.split is not a function.
Ghi song song vào một tệp. Promise.all với nhiều lần ghi vào cùng đường dẫn có thể để lại nội dung lẫn lộn. Hãy chạy chúng tuần tự.
Câu hỏi thường gặp
fs.promises và fs/promises khác nhau thế nào?
Không khác gì cả. require('fs').promises và require('fs/promises') là cùng một object. Đường dẫn fs/promises (hoặc node:fs/promises) là cách import thường dùng hiện nay.
fs/promises có existsSync không?
Không. API promise có access, hàm này reject khi đường dẫn không tồn tại, nhưng cách làm thường gặp là gọi luôn thao tác bạn cần rồi bắt ENOENT. existsSync từ module fs thường vẫn dùng được cho những lần kiểm tra nhanh.
fs/promises có chậm hơn API callback không?
Chậm hơn một chút vì chi phí của promise, nhưng không đáng kể so với thời gian truy cập ổ đĩa. Hãy chọn theo độ dễ đọc; API promise là thứ phần lớn code mới đang dùng.
Làm sao biến một hàm fs dạng callback thành promise?
Hiếm khi bạn cần làm vậy, vì fs/promises đã có đủ các hàm làm việc với tệp. Với các API callback khác, util.promisify(fn) trả về một phiên bản trả về promise.