Node.js 里操作文件,最常见的三个导入方式是:
import * as fs from 'node:fs/promises';
import fs from 'node:fs';
import fs from 'fs';
这几个不是一回事。平时写项目,别乱用,直接按场景选。
1. 先记结论
日常优先用:
import * as fs from 'node:fs/promises';
需要文件流、监听文件、同步 API 时,再用:
import { createReadStream, createWriteStream, watch } from 'node:fs';
老代码里看到这个也能用:
import fs from 'fs';
但新代码更推荐写:
import fs from 'node:fs';
原因很简单:node: 前缀明确表示这是 Node.js 内置模块。
2. 三种导入方式的区别
| 写法 | 主要用途 | 是否推荐 |
|---|---|---|
node:fs/promises |
Promise 版文件操作,配合 async/await |
日常首选 |
node:fs |
完整 fs 模块,包括回调、同步、流、监听等 | 特定场景使用 |
fs |
旧写法,通常等价于 node:fs |
能用,但新代码不优先 |
3. 推荐写法
普通文件操作:
import * as fs from 'node:fs/promises';
大文件流式处理:
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
路径处理一般配合:
import path from 'node:path';
常用组合:
import * as fs from 'node:fs/promises';
import path from 'node:path';
4. 常用 API 速查
4.1 读取文件:readFile
适合读取文本、JSON、小文件。
import * as fs from 'node:fs/promises';
const content = await fs.readFile('./a.txt', 'utf-8');
console.log(content);
读取 JSON:
const content = await fs.readFile('./user.json', 'utf-8');
const user = JSON.parse(content);
console.log(user);
注意:
const buffer = await fs.readFile('./logo.png');
不传编码时,返回的是 Buffer。
4.2 写入文件:writeFile
文件不存在会创建,文件存在会覆盖。
import * as fs from 'node:fs/promises';
await fs.writeFile('./a.txt', 'hello node', 'utf-8');
写入 JSON:
const user = {
name: 'Tom',
age: 18,
};
await fs.writeFile(
'./user.json',
JSON.stringify(user, null, 2),
'utf-8'
);
注意:writeFile 默认是覆盖,不是追加。
4.3 追加内容:appendFile
适合写日志。
import * as fs from 'node:fs/promises';
await fs.appendFile('./log.txt', '用户登录\n', 'utf-8');
也可以用 writeFile 的 flag:
await fs.writeFile('./log.txt', '用户登录\n', {
encoding: 'utf-8',
flag: 'a',
});
常见 flag:
w 写入,存在则覆盖
a 追加,不存在则创建
r 只读
4.4 创建目录:mkdir
创建单层目录:
await fs.mkdir('./uploads');
创建多层目录:
await fs.mkdir('./uploads/images/avatar', {
recursive: true,
});
真实项目里,一般都加:
recursive: true
否则父级目录不存在时会报错。
4.5 读取目录:readdir
读取目录下的文件名:
const files = await fs.readdir('./uploads');
console.log(files);
判断文件还是文件夹:
const items = await fs.readdir('./uploads', {
withFileTypes: true,
});
for (const item of items) {
if (item.isFile()) {
console.log('文件:', item.name);
}
if (item.isDirectory()) {
console.log('目录:', item.name);
}
}
这个比先 readdir 再一个个 stat 更方便。
4.6 获取文件状态:stat
判断文件类型、大小、修改时间。
const stat = await fs.stat('./a.txt');
console.log(stat.size); // 文件大小,单位 byte
console.log(stat.isFile()); // 是否是文件
console.log(stat.isDirectory()); // 是否是目录
console.log(stat.mtime); // 最后修改时间
常见封装:
async function getFileInfo(filePath) {
const stat = await fs.stat(filePath);
return {
size: stat.size,
isFile: stat.isFile(),
isDirectory: stat.isDirectory(),
modifiedAt: stat.mtime,
};
}
4.7 判断文件是否存在:access
try {
await fs.access('./a.txt');
console.log('文件存在');
} catch {
console.log('文件不存在');
}
判断是否可读写:
import { constants } from 'node:fs';
await fs.access('./a.txt', constants.R_OK | constants.W_OK);
但实际项目里,不要过度依赖"先判断,再操作"。
比如:
await fs.access('./a.txt');
const content = await fs.readFile('./a.txt', 'utf-8');
这不是最稳的写法,因为文件可能在 access 之后、readFile 之前被删除。
更直接的方式是:
try {
const content = await fs.readFile('./a.txt', 'utf-8');
console.log(content);
} catch (err) {
console.log('读取失败:', err.message);
}
4.8 删除文件:unlink
只删除文件,不删除目录。
await fs.unlink('./a.txt');
常见写法:
try {
await fs.unlink('./a.txt');
console.log('删除成功');
} catch (err) {
console.log('删除失败:', err.message);
}
4.9 删除文件或目录:rm
删除文件:
await fs.rm('./a.txt');
删除目录:
await fs.rm('./uploads', {
recursive: true,
force: true,
});
参数说明:
recursive: true 递归删除目录内容
force: true 文件不存在也不报错
现在删除目录优先用 rm,不用老的 rmdir。
4.10 重命名 / 移动:rename
重命名文件:
await fs.rename('./old.txt', './new.txt');
移动文件:
await fs.rename('./a.txt', './backup/a.txt');
注意:目标目录必须存在,否则会报错。
4.11 复制文件:copyFile
await fs.copyFile('./a.txt', './backup/a.txt');
默认情况下,如果目标文件存在,会被覆盖。
4.12 复制目录:cp
复制整个目录:
await fs.cp('./public', './dist/public', {
recursive: true,
});
适合复制静态资源、备份目录、构建脚本。
4.13 打开文件句柄:open
一般业务不常用,做底层文件操作时会遇到。
const file = await fs.open('./a.txt', 'r');
try {
const stat = await file.stat();
console.log(stat.size);
} finally {
await file.close();
}
重点:打开后要关闭。
5. 大文件处理:用 Stream
小文件可以用 readFile,大文件不要一把梭。
不要这样处理大文件:
const data = await fs.readFile('./big.mp4');
await fs.writeFile('./copy.mp4', data);
这样会把整个文件读进内存。
大文件用流:
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
await pipeline(
createReadStream('./big.mp4'),
createWriteStream('./copy.mp4')
);
常见场景:
大文件复制
文件上传
文件下载
视频处理
日志处理
6. 监听文件变化:watch
import { watch } from 'node:fs';
const watcher = watch('./src', (eventType, filename) => {
console.log(eventType, filename);
});
// 停止监听
// watcher.close();
适合开发工具、自动构建、热更新。
但不要把它当成强业务依赖,不同系统环境下表现可能有差异。
7. 同步 API:readFileSync / writeFileSync
同步 API 一般以 Sync 结尾。
import fs from 'node:fs';
const content = fs.readFileSync('./config.json', 'utf-8');
console.log(content);
写入:
fs.writeFileSync('./a.txt', 'hello', 'utf-8');
同步 API 会阻塞后续代码执行。
适合:
启动时读取配置
命令行脚本
构建脚本
学习测试
不适合:
接口请求里频繁使用
高并发服务
大量文件读写
接口里更推荐:
import * as fs from 'node:fs/promises';
app.get('/config', async (req, res) => {
const content = await fs.readFile('./config.json', 'utf-8');
res.send(content);
});
8. 常用 API 汇总表
| API | 来源 | 作用 | 常用程度 |
|---|---|---|---|
readFile |
node:fs/promises |
读取文件 | 高 |
writeFile |
node:fs/promises |
写入文件 | 高 |
appendFile |
node:fs/promises |
追加内容 | 高 |
mkdir |
node:fs/promises |
创建目录 | 高 |
readdir |
node:fs/promises |
读取目录 | 高 |
stat |
node:fs/promises |
获取文件状态 | 高 |
access |
node:fs/promises |
检查访问权限 | 中 |
unlink |
node:fs/promises |
删除文件 | 中 |
rm |
node:fs/promises |
删除文件或目录 | 高 |
rename |
node:fs/promises |
重命名 / 移动 | 高 |
copyFile |
node:fs/promises |
复制文件 | 中 |
cp |
node:fs/promises |
复制目录 | 中 |
open |
node:fs/promises |
打开文件句柄 | 低 |
createReadStream |
node:fs |
创建读取流 | 高 |
createWriteStream |
node:fs |
创建写入流 | 高 |
watch |
node:fs |
监听文件变化 | 中 |
readFileSync |
node:fs |
同步读取 | 中 |
writeFileSync |
node:fs |
同步写入 | 中 |
9. 真实项目常用封装
9.1 确保目录存在
import * as fs from 'node:fs/promises';
export async function ensureDir(dirPath) {
await fs.mkdir(dirPath, {
recursive: true,
});
}
9.2 写入文本文件
import * as fs from 'node:fs/promises';
import path from 'node:path';
export async function writeText(filePath, content) {
const dir = path.dirname(filePath);
await fs.mkdir(dir, {
recursive: true,
});
await fs.writeFile(filePath, content, 'utf-8');
}
使用:
await writeText('./logs/app.log', '启动成功\n');
9.3 读取 JSON
import * as fs from 'node:fs/promises';
export async function readJSON(filePath) {
const content = await fs.readFile(filePath, 'utf-8');
return JSON.parse(content);
}
使用:
const config = await readJSON('./config.json');
9.4 写入 JSON
import * as fs from 'node:fs/promises';
import path from 'node:path';
export async function writeJSON(filePath, data) {
const dir = path.dirname(filePath);
await fs.mkdir(dir, {
recursive: true,
});
await fs.writeFile(
filePath,
JSON.stringify(data, null, 2),
'utf-8'
);
}
使用:
await writeJSON('./data/user.json', {
name: 'Tom',
age: 18,
});
9.5 判断路径类型
import * as fs from 'node:fs/promises';
export async function getPathType(filePath) {
try {
const stat = await fs.stat(filePath);
if (stat.isFile()) return 'file';
if (stat.isDirectory()) return 'directory';
return 'other';
} catch {
return 'not_exists';
}
}
使用:
const type = await getPathType('./uploads');
console.log(type);
9.6 复制大文件
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
export async function copyLargeFile(source, target) {
await pipeline(
createReadStream(source),
createWriteStream(target)
);
}
10. 最后记这个规则
普通文件操作:
import * as fs from 'node:fs/promises';
大文件操作:
import { createReadStream, createWriteStream } from 'node:fs';
同步脚本:
import fs from 'node:fs';
旧写法:
import fs from 'fs';
一句话:
日常用 node:fs/promises;
需要流、监听、同步方法时用 node:fs;
fs 是旧写法,能看懂就行,新代码优先写 node:fs。