← 返回目录

第四章:IO 读写

文件系统操作是服务端开发的基础

1. fs/promises 模块

Node.js 的 fs/promises 模块提供了基于 Promise 的文件系统 API,是现代 TypeScript 项目的首选。

import { readFile, writeFile, mkdir, readdir, stat, unlink } from "fs/promises";

// 读取文本文件
const content = await readFile("./config.json", "utf-8");
console.log(content);

// 写入文件(不存在则创建,存在则覆盖)
await writeFile("./output.txt", "Hello, TypeScript!", "utf-8");

// 创建目录(recursive 类似 mkdir -p)
await mkdir("./logs/2024", { recursive: true });

// 读取目录内容
const files = await readdir("./src");
console.log(files); // ["index.ts", "utils.ts", ...]

// 获取文件信息
const info = await stat("./package.json");
console.log(info.size, info.isFile(), info.isDirectory());

// 删除文件
await unlink("./temp.txt");
🔄 对比 Python:readFile 类似 open().read()readdir 类似 os.listdir()stat 类似 os.stat()

2. 同步 vs 异步

Node.js 的 fs 模块同时提供同步和异步版本。在服务端场景中,应始终优先使用异步 API。

import { readFileSync } from "fs";
import { readFile } from "fs/promises";

// 同步读取 —— 会阻塞事件循环
const data = readFileSync("./config.json", "utf-8");

// 异步读取 —— 不阻塞
const dataAsync = await readFile("./config.json", "utf-8");

何时使用同步 API?

  • CLI 脚本或一次性工具
  • 应用启动阶段加载配置文件
  • 测试代码中的辅助操作

⚠️ 在 HTTP 服务器的请求处理中,绝不要使用同步 API——它会阻塞所有其他请求。

3. Stream 流式读写

处理大文件时,Stream 可以避免将整个文件加载到内存中。这和 Java 的 InputStream/OutputStream 理念一致。

import { createReadStream, createWriteStream } from "fs";
import { pipeline } from "stream/promises";

// 流式复制大文件
async function copyFile(src: string, dest: string): Promise<void> {
    const readStream = createReadStream(src);
    const writeStream = createWriteStream(dest);
    await pipeline(readStream, writeStream);
    console.log("复制完成");
}

// 逐块读取处理
const stream = createReadStream("./huge-log.txt", { encoding: "utf-8" });
let lineCount = 0;
for await (const chunk of stream) {
    lineCount += (chunk as string).split("\n").length;
}
console.log(`总行数: ${lineCount}`);

何时使用 Stream?

  • 文件大于 100MB
  • 需要逐行/逐块处理数据
  • 网络传输(HTTP 响应发送文件)

4. path 模块

永远不要手动拼接路径字符串。path 模块能正确处理不同操作系统的路径分隔符。

import path from "path";
import { fileURLToPath } from "url";

// 常用方法
path.join("/usr", "local", "bin");      // "/usr/local/bin"
path.resolve("src", "index.ts");         // 返回绝对路径
path.dirname("/app/src/index.ts");       // "/app/src"
path.basename("/app/src/index.ts");      // "index.ts"
path.extname("report.pdf");             // ".pdf"
path.parse("/app/src/index.ts");
// { root: "/", dir: "/app/src", base: "index.ts", ext: ".ts", name: "index" }

// ESM 模块中获取 __dirname(ESM 没有 __dirname 全局变量)
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// 实际用法:读取同目录下的配置文件
const configPath = path.join(__dirname, "config.json");

5. JSON 文件操作

JSON 是最常用的数据交换格式。TypeScript 可以结合类型系统来确保类型安全。

import { readFile, writeFile } from "fs/promises";

// 定义接口
interface AppConfig {
    port: number;
    host: string;
    debug: boolean;
}

// 读取并解析 JSON
async function loadConfig(filePath: string): Promise<AppConfig> {
    const raw = await readFile(filePath, "utf-8");
    return JSON.parse(raw) as AppConfig;
}

// 序列化并写入 JSON
async function saveConfig(filePath: string, config: AppConfig): Promise<void> {
    const json = JSON.stringify(config, null, 2);
    await writeFile(filePath, json, "utf-8");
}

使用 zod 进行运行时验证,确保外部数据真的符合预期类型:

import { z } from "zod";

const ConfigSchema = z.object({
    port: z.number().int().min(1).max(65535),
    host: z.string(),
    debug: z.boolean(),
});

type Config = z.infer<typeof ConfigSchema>;

const raw = JSON.parse(await readFile("config.json", "utf-8"));
const config = ConfigSchema.parse(raw); // 验证失败会抛出 ZodError

6. CSV 处理

处理 CSV 文件推荐使用 csv-parsecsv-stringify 库。

npm install csv-parse csv-stringify
import { parse } from "csv-parse/sync";
import { readFileSync } from "fs";

interface SalesRecord {
    date: string;
    product: string;
    amount: number;
}

const csvContent = readFileSync("./sales.csv", "utf-8");
const records = parse(csvContent, {
    columns: true,       // 首行作为列名
    skip_empty_lines: true,
    cast: (value, context) => {
        if (context.column === "amount") return Number(value);
        return value;
    },
}) as SalesRecord[];

records.forEach((r) => console.log(`${r.date}: ${r.product} - ¥${r.amount}`));

📝 本章要点

优先使用 fs/promises

基于 Promise 的异步 API 是现代标准

大文件用 Stream

避免内存溢出,用 pipeline() 管道连接

路径拼接用 path 模块

跨平台兼容,ESM 用 import.meta.url

JSON 解析加类型守卫

用 zod 做运行时验证更安全