Node.js 文件系统
读写、目录遍历、stat、watch、以及同步/异步/流三种形态怎么选。
Key Points生产代码用 `node:fs/promises`;只有启动时读配置这种一次性场景才用同步 API。
三种形态
| Form | Example | When |
|---|---|---|
| 同步 | fs.readFileSync | 启动时读配置、CLI 脚本 |
| 回调 | fs.readFile(path, cb) | 老代码 |
| Promise | fs.readFile(path) | 业务代码默认 |
| Stream | fs.createReadStream | 大文件 |
读写文件
JavaScript
const fs = require('node:fs/promises');
await fs.writeFile('out.txt', '第一行\n', 'utf8');
await fs.appendFile('out.txt', '第二行\n', 'utf8');
const text = await fs.readFile('out.txt', 'utf8');
console.log(text);
const buf = await fs.readFile('out.txt'); // 不传编码得到 Buffer
console.log(buf.length);Output
第一行
第二行
18「第一行\n」是 4 个 UTF-8 字节,「第二行\n」是 10 个(三个汉字各 3 字节 + 换行)。所以 Buffer 长度是 18,而字符串 length 只有 8。
目录
JavaScript
const fs = require('node:fs/promises');
const path = require('node:path');
// 建目录(递归,已存在不报错)
await fs.mkdir('logs/2026/10', { recursive: true });
// 列目录
const entries = await fs.readdir('.', { withFileTypes: true });
for (const e of entries) {
console.log(e.isDirectory() ? '[D]' : '[F]', e.name);
}
// 递归遍历
async function walk(dir, out = []) {
for (const e of await fs.readdir(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name);
if (e.isDirectory()) await walk(full, out);
else out.push(full);
}
return out;
}stat 与判断存在
JavaScript
const fs = require('node:fs/promises');
try {
const st = await fs.stat('package.json');
console.log('大小', st.size, '字节');
console.log('修改时间', st.mtime.toISOString());
console.log('是文件', st.isFile());
} catch (e) {
if (e.code === 'ENOENT') console.log('文件不存在');
else throw e;
}Do not use
fs.existsSync for check-then-act — the file can vanish in between (TOCTOU race). Instead **act directly and catch ENOENT**.监听变化
JavaScript
const fs = require('node:fs');
fs.watch('src', { recursive: true }, (event, filename) => {
console.log(event, filename); // change index.js
});fs.watch behaves differently across platforms (macOS supports recursive, older Linux does not). Use chokidar in production, and remember watcher.close() or the process will not exit.
路径
JavaScript
const path = require('node:path');
console.log(path.join('a', 'b', '..', 'c')); // a/c
console.log(path.resolve('a', 'b')); // 绝对路径
console.log(path.basename('/x/y/z.txt')); // z.txt
console.log(path.extname('/x/y/z.txt')); // .txt
console.log(path.parse('/x/y/z.txt'));Output
a/c
C:\Users\Administrator\demo\a\b
z.txt
.txt
{ root: '/', dir: '/x/y', base: 'z.txt', ext: '.txt', name: 'z' }三种形态
| 形态 | 例子 | 何时用 |
|---|---|---|
| 同步 | fs.readFileSync | 启动时读配置、CLI 脚本 |
| 回调 | fs.readFile(path, cb) | 老代码 |
| Promise | fs.readFile(path) | 业务代码默认 |
| Stream | fs.createReadStream | 大文件 |
读写文件
JavaScript
const fs = require('node:fs/promises');
await fs.writeFile('out.txt', '第一行\n', 'utf8');
await fs.appendFile('out.txt', '第二行\n', 'utf8');
const text = await fs.readFile('out.txt', 'utf8');
console.log(text);
const buf = await fs.readFile('out.txt'); // 不传编码得到 Buffer
console.log(buf.length);Output
第一行
第二行
18"第一行\n" is 4 UTF-8 bytes and "第二行\n" is 10 (3 bytes per CJK char plus newline), so the Buffer is 18 bytes while the string .length is 8.
目录
JavaScript
const fs = require('node:fs/promises');
const path = require('node:path');
// 建目录(递归,已存在不报错)
await fs.mkdir('logs/2026/10', { recursive: true });
// 列目录
const entries = await fs.readdir('.', { withFileTypes: true });
for (const e of entries) {
console.log(e.isDirectory() ? '[D]' : '[F]', e.name);
}
// 递归遍历
async function walk(dir, out = []) {
for (const e of await fs.readdir(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name);
if (e.isDirectory()) await walk(full, out);
else out.push(full);
}
return out;
}stat 与判断存在
JavaScript
const fs = require('node:fs/promises');
try {
const st = await fs.stat('package.json');
console.log('大小', st.size, '字节');
console.log('修改时间', st.mtime.toISOString());
console.log('是文件', st.isFile());
} catch (e) {
if (e.code === 'ENOENT') console.log('文件不存在');
else throw e;
}别用
fs.existsSync 做「先判断再操作」——两步之间文件可能被删掉(TOCTOU 竞态)。正确做法是**直接操作并捕获 ENOENT**。监听变化
JavaScript
const fs = require('node:fs');
fs.watch('src', { recursive: true }, (event, filename) => {
console.log(event, filename); // change index.js
});fs.watch 跨平台行为不一致(macOS 支持递归,Linux 早期不支持),生产环境用 chokidar。另外记得 watcher.close(),否则进程不退出。
路径
JavaScript
const path = require('node:path');
console.log(path.join('a', 'b', '..', 'c')); // a/c
console.log(path.resolve('a', 'b')); // 绝对路径
console.log(path.basename('/x/y/z.txt')); // z.txt
console.log(path.extname('/x/y/z.txt')); // .txt
console.log(path.parse('/x/y/z.txt'));Output
a/c
C:\Users\Administrator\demo\a\b
z.txt
.txt
{ root: '/', dir: '/x/y', base: 'z.txt', ext: '.txt', name: 'z' }