Node.js 开发指南 / 第 10 章 · 文件系统
第 10 章 · 文件系统
10.1 fs 模块概览
Node.js 的 fs(File System)模块提供了与文件系统交互的 API,支持同步和异步两种风格。
const fs = require('fs');
const fsPromises = require('fs/promises');
const path = require('path');
API 风格对比
| 风格 | 示例 | 特点 |
|---|---|---|
| 同步(Sync) | fs.readFileSync() | 阻塞事件循环 |
| 回调 | fs.readFile(path, cb) | 非阻塞,错误优先回调 |
| Promise | fsPromises.readFile(path) | 非阻塞,async/await 友好 |
// 同步
try {
const data = fs.readFileSync('/etc/hosts', 'utf8');
console.log(data);
} catch (err) {
console.error(err);
}
// 回调
fs.readFile('/etc/hosts', 'utf8', (err, data) => {
if (err) return console.error(err);
console.log(data);
});
// Promise(推荐)
async function readFile() {
try {
const data = await fsPromises.readFile('/etc/hosts', 'utf8');
console.log(data);
} catch (err) {
console.error(err);
}
}
10.2 文件读取
const fs = require('fs/promises');
// 读取整个文件(小文件推荐)
const content = await fs.readFile('config.json', 'utf8');
const config = JSON.parse(content);
// 读取二进制文件
const buffer = await fs.readFile('image.png');
console.log(buffer.length); // 字节数
// 读取文件信息
const stat = await fs.stat('package.json');
console.log(stat.size); // 文件大小(字节)
console.log(stat.mtime); // 最后修改时间
console.log(stat.isFile()); // true
console.log(stat.isDirectory()); // false
console.log(stat.mode); // 文件权限
// 检查文件是否存在
async function fileExists(filePath) {
try {
await fs.access(filePath, fs.constants.F_OK);
return true;
} catch {
return false;
}
}
// 更好的方式(Node.js 14+)
const { access } = require('fs/promises');
async function fileExistsV2(filePath) {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
流式读取大文件
const { createReadStream } = require('fs');
const { createInterface } = require('readline');
// 逐行读取(内存友好)
async function processLargeFile(filePath) {
const rl = createInterface({
input: createReadStream(filePath, { encoding: 'utf8' }),
crlfDelay: Infinity,
});
let lineCount = 0;
for await (const line of rl) {
lineCount++;
// 处理每一行
}
return lineCount;
}
10.3 文件写入
const fs = require('fs/promises');
// 写入文件(覆盖)
await fs.writeFile('output.txt', 'Hello, World!', 'utf8');
// 追加写入
await fs.appendFile('log.txt', `${new Date().toISOString()} - 新日志\n`);
// 写入 JSON
const config = { host: 'localhost', port: 3000 };
await fs.writeFile('config.json', JSON.stringify(config, null, 2), 'utf8');
// 写入时自动创建目录
async function writeFileWithDirs(filePath, content) {
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, content, 'utf8');
}
// 以流的方式写入大文件
const { createWriteStream } = require('fs');
const writable = createWriteStream('output.txt');
for (let i = 0; i < 1000000; i++) {
writable.write(`行 ${i}\n`);
}
writable.end();
10.4 文件操作
const fs = require('fs/promises');
// 复制文件
await fs.copyFile('source.txt', 'dest.txt');
// 重命名/移动文件
await fs.rename('old-name.txt', 'new-name.txt');
await fs.rename('file.txt', 'archive/file.txt'); // 移动
// 删除文件
await fs.unlink('temp.txt');
// 创建硬链接
await fs.link('original.txt', 'hard-link.txt');
// 创建符号链接
await fs.symlink('/path/to/original', 'link-name');
// 修改文件权限
await fs.chmod('script.sh', 0o755);
// 修改文件时间
const now = new Date();
await fs.utimes('file.txt', now, now);
// 截断文件(保留前 N 字节)
await fs.truncate('file.txt', 100); // 保留前 100 字节
10.5 目录操作
const fs = require('fs/promises');
// 创建目录
await fs.mkdir('new-dir');
await fs.mkdir('path/to/deep/dir', { recursive: true }); // 递归创建
// 读取目录
const files = await fs.readdir('.');
console.log(files); // ['file1.txt', 'dir1', ...]
// 读取目录(带文件类型信息)
const entries = await fs.readdir('.', { withFileTypes: true });
for (const entry of entries) {
console.log(`${entry.name} - ${entry.isDirectory() ? '目录' : '文件'}`);
}
// 递归遍历目录
async function walkDir(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const results = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...(await walkDir(fullPath)));
} else {
results.push(fullPath);
}
}
return results;
}
const allFiles = await walkDir('src');
console.log(allFiles);
// 删除目录
await fs.rmdir('empty-dir'); // 只能删除空目录
await fs.rm('dir-with-files', { recursive: true }); // 递归删除
// 创建临时目录
const os = require('os');
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'myapp-'));
console.log(tmpDir); // /tmp/myapp-abc123
// 读取目录大小
async function dirSize(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
let size = 0;
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
size += await dirSize(fullPath);
} else {
const stat = await fs.stat(fullPath);
size += stat.size;
}
}
return size;
}
10.6 文件监听
const fs = require('fs');
// fs.watch(推荐,基于操作系统事件)
const watcher = fs.watch('src', { recursive: true }, (eventType, filename) => {
console.log(`事件: ${eventType}, 文件: ${filename}`);
});
// 关闭监听
watcher.close();
// fs.watchFile(基于轮询,跨平台兼容但效率低)
fs.watchFile('config.json', { interval: 1000 }, (curr, prev) => {
console.log(`文件大小: ${prev.size} → ${curr.size}`);
console.log(`修改时间: ${prev.mtime} → ${curr.mtime}`);
});
fs.unwatchFile('config.json');
// 使用 chokidar(第三方库,更稳定)
// npm install chokidar
const chokidar = require('chokidar');
const watcher2 = chokidar.watch('src', {
ignored: /node_modules/,
persistent: true,
ignoreInitial: true,
});
watcher2
.on('add', (path) => console.log(`文件添加: ${path}`))
.on('change', (path) => console.log(`文件修改: ${path}`))
.on('unlink', (path) => console.log(`文件删除: ${path}`))
.on('addDir', (path) => console.log(`目录添加: ${path}`))
.on('unlinkDir', (path) => console.log(`目录删除: ${path}`));
10.7 path 模块
const path = require('path');
// 基本路径操作
console.log(path.basename('/home/user/file.txt')); // 'file.txt'
console.log(path.basename('/home/user/file.txt', '.txt')); // 'file'
console.log(path.dirname('/home/user/file.txt')); // '/home/user'
console.log(path.extname('file.txt')); // '.txt'
console.log(path.extname('file.tar.gz')); // '.gz'
// 路径拼接
console.log(path.join('/home', 'user', 'file.txt'));
// '/home/user/file.txt'(规范化路径)
// 绝对路径解析
console.log(path.resolve('file.txt'));
// '/home/user/project/file.txt'(基于 cwd)
console.log(path.resolve('/a', '/b', 'c'));
// '/b/c'(绝对路径会重置)
// 相对路径
console.log(path.relative('/home/user/project', '/home/user/docs'));
// '../docs'
// 路径解析
const parsed = path.parse('/home/user/file.txt');
console.log(parsed);
// { root: '/', dir: '/home/user', base: 'file.txt', ext: '.txt', name: 'file' }
// 格式化路径
console.log(path.format(parsed));
// '/home/user/file.txt'
// 规范化路径
console.log(path.normalize('/home/user/../user/./file.txt'));
// '/home/user/file.txt'
// 跨平台路径分隔符
console.log(path.sep); // '/' (Unix) 或 '\\' (Windows)
console.log(path.delimiter); // ':' (Unix) 或 ';' (Windows)
跨平台路径处理
// ⚠️ 不要手动拼接路径
// const bad = dir + '/' + file; // Windows 上会有问题
// ✅ 使用 path.join
const good = path.join(dir, file);
// 使用 path.posix 或 path.win32 强制特定平台
console.log(path.posix.join('a', 'b')); // 'a/b'
console.log(path.win32.join('a', 'b')); // 'a\\b'
// URL 路径处理(推荐用于 URL)
const { fileURLToPath, pathToFileURL } = require('node:url');
// ESM 中获取 __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
10.8 实用工具函数
const fs = require('fs/promises');
const path = require('path');
// 确保目录存在
async function ensureDir(dir) {
await fs.mkdir(dir, { recursive: true });
}
// 安全读取 JSON
async function readJSON(filePath) {
try {
const content = await fs.readFile(filePath, 'utf8');
return JSON.parse(content);
} catch (err) {
if (err.code === 'ENOENT') return null;
throw err;
}
}
// 安全写入 JSON(先写临时文件再重命名,防止写入中断导致文件损坏)
async function writeJSON(filePath, data) {
const tmpPath = filePath + '.tmp';
await fs.writeFile(tmpPath, JSON.stringify(data, null, 2), 'utf8');
await fs.rename(tmpPath, filePath);
}
// 查找文件
async function findFiles(dir, pattern) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const results = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.name === 'node_modules' || entry.name === '.git') continue;
if (entry.isDirectory()) {
results.push(...(await findFiles(fullPath, pattern)));
} else if (pattern.test(entry.name)) {
results.push(fullPath);
}
}
return results;
}
// 用法:查找所有 .js 文件
const jsFiles = await findFiles('.', /\.js$/);
注意事项
⚠️ 生产环境避免同步 API:
readFileSync等会阻塞事件循环,在服务器中应使用异步版本。
⚠️ 大文件用流:
readFile会将整个文件加载到内存,大文件应使用createReadStream。
⚠️ 路径用
path.join:不要手动拼接路径字符串,path.join自动处理分隔符和规范化。
⚠️ 文件监听的平台差异:
fs.watch在不同操作系统上行为可能不一致,生产环境推荐chokidar。
⚠️ 并发写入安全:多个进程同时写入同一文件可能导致数据损坏,使用文件锁或临时文件 + 重命名。
业务场景
- 配置管理:读取 JSON/YAML 配置文件,支持热更新
- 日志收集:流式读取大量日志文件进行分析
- 文件上传/下载:处理用户上传的文件
- 静态资源服务:为 Web 应用提供静态文件
- 脚本工具:批量处理文件(重命名、转换、压缩)
扩展阅读
上一章:第 9 章 · Buffer 与二进制数据 下一章:第 11 章 · HTTP 服务与客户端 — 构建 HTTP 服务器、客户端、中间件和路由。