Skip to content

Latest commit

 

History

History
1093 lines (879 loc) · 31.9 KB

01-node-core-modules.md

File metadata and controls

1093 lines (879 loc) · 31.9 KB

Node.js 核心模块

面向 Java 工程师的 Node.js 核心模块详解


概览

Node.js 提供了一组内置核心模块,无需安装即可使用。这些模块类似 Java 的标准库(java.io、java.nio、java.lang 等),但 API 风格截然不同——以异步和回调为主。

导入核心模块时推荐使用 node: 前缀:

// 推荐写法:加 node: 前缀,明确表示这是内置模块
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { exec } from 'node:child_process';

// 也能工作,但不推荐(容易与 npm 包混淆)
import { readFile } from 'fs/promises';

核心模块与 Java 标准库对照:

Node.js 模块 Java 等价物 用途
fs/promises java.nio.file.Files 文件读写
path java.nio.file.Path / Paths 路径处理
child_process ProcessBuilder / Runtime.exec() 子进程
process System / ProcessHandle 进程信息
os System.getProperty("os.*") 操作系统信息
http HttpServer (JDK 18+) HTTP 服务
crypto java.security.* 加密/哈希
stream java.io.InputStream/OutputStream 流处理

1. fs/promises — 异步文件操作

fs 模块是 Node.js 中最常用的核心模块之一,提供文件系统操作。推荐使用 fs/promises 版本,它返回 Promise,可配合 async/await 使用。

Java NIO vs Node.js fs/promises

操作 Java NIO (java.nio.file) Node.js (fs/promises)
读文件 Files.readString(path) readFile(path, 'utf-8')
写文件 Files.writeString(path, content) writeFile(path, content)
列目录 Files.list(dir) readdir(dir)
创建目录 Files.createDirectories(dir) mkdir(dir, { recursive: true })
文件信息 Files.getAttribute(path, attr) stat(path)
删除文件 Files.delete(path) unlink(path)
判断存在 Files.exists(path) access(path) (try/catch)

1.1 读文件 readFile

import { readFile } from 'node:fs/promises';

// 读取文本文件(指定编码返回 string)
async function readTextFile(filePath: string): Promise<string> {
  const content = await readFile(filePath, 'utf-8');
  return content;
}

// 读取 JSON 文件
async function readJsonFile<T>(filePath: string): Promise<T> {
  const content = await readFile(filePath, 'utf-8');
  return JSON.parse(content) as T;
}

// 读取二进制文件(不指定编码返回 Buffer)
async function readBinaryFile(filePath: string): Promise<Buffer> {
  const buffer = await readFile(filePath);
  console.log(`文件大小: ${buffer.length} 字节`);
  return buffer;
}

// 使用示例
const text = await readTextFile('./config.txt');
console.log(text);

interface PackageJson {
  name: string;
  version: string;
  dependencies?: Record<string, string>;
}

const pkg = await readJsonFile<PackageJson>('./package.json');
console.log(`项目名: ${pkg.name}, 版本: ${pkg.version}`);

Java 对比:

// Java NIO 读文件
String content = Files.readString(Path.of("config.txt"));

// Java 读 JSON(需要 Jackson/Gson)
ObjectMapper mapper = new ObjectMapper();
PackageJson pkg = mapper.readValue(new File("package.json"), PackageJson.class);

1.2 写文件 writeFile

import { writeFile, appendFile } from 'node:fs/promises';

// 写入文本文件(覆盖写入)
async function writeTextFile(filePath: string, content: string): Promise<void> {
  await writeFile(filePath, content, 'utf-8');
  console.log(`已写入: ${filePath}`);
}

// 写入 JSON 文件(格式化输出)
async function writeJsonFile(filePath: string, data: unknown): Promise<void> {
  const json = JSON.stringify(data, null, 2);
  await writeFile(filePath, json, 'utf-8');
}

// 追加写入
async function appendToFile(filePath: string, line: string): Promise<void> {
  await appendFile(filePath, line + '\n', 'utf-8');
}

// 使用示例
await writeTextFile('./output.txt', 'Hello, Node.js!');

await writeJsonFile('./config.json', {
  name: 'my-app',
  port: 3000,
  debug: true,
});

// 追加日志
await appendToFile('./app.log', `[${new Date().toISOString()}] 服务启动`);
await appendToFile('./app.log', `[${new Date().toISOString()}] 收到请求`);

Java 对比:

// Java NIO 写文件
Files.writeString(Path.of("output.txt"), "Hello, Java!");

// Java 追加写入
Files.writeString(Path.of("app.log"), "日志行\n",
    StandardOpenOption.CREATE, StandardOpenOption.APPEND);

1.3 目录操作 readdir、mkdir、stat

import { readdir, mkdir, stat, rm } from 'node:fs/promises';
import path from 'node:path';

// 列出目录内容
async function listDirectory(dirPath: string): Promise<void> {
  // 基础用法:返回文件名数组
  const names = await readdir(dirPath);
  console.log('文件列表:', names);
  // 输出: ['file1.txt', 'file2.js', 'subdir']

  // 带文件类型信息
  const entries = await readdir(dirPath, { withFileTypes: true });
  for (const entry of entries) {
    const type = entry.isDirectory() ? '目录' : '文件';
    console.log(`  ${type}: ${entry.name}`);
  }
}

// 递归列出所有文件
async function listAllFiles(dirPath: string): Promise<string[]> {
  const files: string[] = [];
  const entries = await readdir(dirPath, { withFileTypes: true });

  for (const entry of entries) {
    const fullPath = path.join(dirPath, entry.name);
    if (entry.isDirectory()) {
      // 递归进入子目录
      const subFiles = await listAllFiles(fullPath);
      files.push(...subFiles);
    } else {
      files.push(fullPath);
    }
  }

  return files;
}

// 递归创建目录(类似 Java 的 Files.createDirectories)
async function ensureDir(dirPath: string): Promise<void> {
  await mkdir(dirPath, { recursive: true });
  // recursive: true 等价于 Java 的 Files.createDirectories()
  // 如果目录已存在也不会报错
}

// 获取文件信息
async function getFileInfo(filePath: string): Promise<void> {
  const stats = await stat(filePath);
  console.log(`文件: ${filePath}`);
  console.log(`  大小: ${stats.size} 字节`);
  console.log(`  是否目录: ${stats.isDirectory()}`);
  console.log(`  是否文件: ${stats.isFile()}`);
  console.log(`  创建时间: ${stats.birthtime.toISOString()}`);
  console.log(`  修改时间: ${stats.mtime.toISOString()}`);
}

// 删除目录(递归)
async function removeDir(dirPath: string): Promise<void> {
  await rm(dirPath, { recursive: true, force: true });
  // force: true 表示即使不存在也不报错
}

// 判断文件是否存在
async function fileExists(filePath: string): Promise<boolean> {
  try {
    await stat(filePath);
    return true;
  } catch {
    return false;
  }
}

// 使用示例
await ensureDir('./output/reports/2024');
await listDirectory('./src');
const allFiles = await listAllFiles('./src');
console.log('所有文件:', allFiles);
await getFileInfo('./package.json');
console.log('文件存在:', await fileExists('./package.json'));

Java 对比:

// Java 列目录
try (Stream<Path> stream = Files.list(Path.of("src"))) {
    stream.forEach(p -> System.out.println(p.getFileName()));
}

// Java 递归列所有文件
try (Stream<Path> stream = Files.walk(Path.of("src"))) {
    stream.filter(Files::isRegularFile)
          .forEach(System.out::println);
}

// Java 创建目录
Files.createDirectories(Path.of("output/reports/2024"));

// Java 获取文件信息
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
System.out.println("大小: " + attrs.size());
System.out.println("修改时间: " + attrs.lastModifiedTime());

1.4 文件流 createReadStream / createWriteStream

对于大文件,一次性读入内存不现实,需要使用流(Stream)。这与 Java 的 InputStream / OutputStream 概念相同。

import { createReadStream, createWriteStream } from 'node:fs';
import { createInterface } from 'node:readline';
import { pipeline } from 'node:stream/promises';
import { createGzip, createGunzip } from 'node:zlib';

// 逐行读取大文件(类似 Java 的 BufferedReader.readLine())
async function readLargeFileByLine(filePath: string): Promise<void> {
  const fileStream = createReadStream(filePath, 'utf-8');
  const rl = createInterface({
    input: fileStream,
    crlfDelay: Infinity, // 处理 Windows 换行符
  });

  let lineNumber = 0;
  for await (const line of rl) {
    lineNumber++;
    // 处理每一行
    if (line.includes('ERROR')) {
      console.log(`第 ${lineNumber} 行: ${line}`);
    }
  }
  console.log(`总行数: ${lineNumber}`);
}

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

// 流式压缩文件(gzip)
async function compressFile(src: string, dest: string): Promise<void> {
  const readStream = createReadStream(src);
  const gzip = createGzip();
  const writeStream = createWriteStream(dest);
  await pipeline(readStream, gzip, writeStream);
  console.log(`已压缩: ${src} -> ${dest}`);
}

// 流式解压文件
async function decompressFile(src: string, dest: string): Promise<void> {
  const readStream = createReadStream(src);
  const gunzip = createGunzip();
  const writeStream = createWriteStream(dest);
  await pipeline(readStream, gunzip, writeStream);
  console.log(`已解压: ${src} -> ${dest}`);
}

// 使用示例
await readLargeFileByLine('./server.log');
await copyFile('./data.csv', './data-backup.csv');
await compressFile('./data.csv', './data.csv.gz');

Java 对比:

// Java 逐行读取(BufferedReader)
try (BufferedReader reader = Files.newBufferedReader(Path.of("server.log"))) {
    String line;
    int lineNumber = 0;
    while ((line = reader.readLine()) != null) {
        lineNumber++;
        if (line.contains("ERROR")) {
            System.out.printf("第 %d 行: %s%n", lineNumber, line);
        }
    }
}

// Java 流式复制
try (InputStream in = Files.newInputStream(src);
     OutputStream out = Files.newOutputStream(dest)) {
    in.transferTo(out);
}

// Java gzip 压缩
try (InputStream in = Files.newInputStream(src);
     GZIPOutputStream gzOut = new GZIPOutputStream(Files.newOutputStream(dest))) {
    in.transferTo(gzOut);
}

2. path — 路径处理

path 模块提供跨平台的文件路径操作,类似 Java 的 java.nio.file.Path 和 Paths。

Java Path vs Node.js path

操作 Java Node.js
拼接路径 path1.resolve(path2) path.join(a, b)
绝对路径 path.toAbsolutePath() path.resolve(p)
父目录 path.getParent() path.dirname(p)
文件名 path.getFileName() path.basename(p)
扩展名 手动处理 path.extname(p)
分隔符 File.separator path.sep

2.1 常用方法详解

import path from 'node:path';

// ========== path.join — 拼接路径 ==========
// 智能拼接,自动处理分隔符
const filePath = path.join('/home', 'user', 'documents', 'file.txt');
console.log(filePath);
// Linux/macOS: /home/user/documents/file.txt
// Windows:     \home\user\documents\file.txt

// 自动处理多余的分隔符和 ..
const cleaned = path.join('/home/user/', '../admin/', './config.json');
console.log(cleaned);  // /home/admin/config.json

// ========== path.resolve — 解析为绝对路径 ==========
// 从右到左处理,遇到绝对路径就停止
const abs1 = path.resolve('src', 'index.ts');
console.log(abs1);  // /当前工作目录/src/index.ts

const abs2 = path.resolve('/home/user', 'src', 'index.ts');
console.log(abs2);  // /home/user/src/index.ts

// ========== path.dirname — 获取父目录 ==========
const dir = path.dirname('/home/user/project/src/index.ts');
console.log(dir);  // /home/user/project/src

// ========== path.basename — 获取文件名 ==========
const filename = path.basename('/home/user/project/src/index.ts');
console.log(filename);  // index.ts

// 去掉扩展名
const nameOnly = path.basename('/home/user/project/src/index.ts', '.ts');
console.log(nameOnly);  // index

// ========== path.extname — 获取扩展名 ==========
console.log(path.extname('index.ts'));       // .ts
console.log(path.extname('README.md'));      // .md
console.log(path.extname('Makefile'));       // '' (空字符串)
console.log(path.extname('.gitignore'));     // '' (空字符串)
console.log(path.extname('archive.tar.gz')); // .gz

// ========== path.parse — 完整解析路径 ==========
const parsed = path.parse('/home/user/project/src/index.ts');
console.log(parsed);
// {
//   root: '/',
//   dir: '/home/user/project/src',
//   base: 'index.ts',
//   ext: '.ts',
//   name: 'index'
// }

// ========== path.format — 从对象构造路径 ==========
const formatted = path.format({
  dir: '/home/user/project/src',
  name: 'index',
  ext: '.ts',
});
console.log(formatted);  // /home/user/project/src/index.ts

// ========== path.relative — 计算相对路径 ==========
const rel = path.relative('/home/user/project', '/home/user/project/src/utils/file.ts');
console.log(rel);  // src/utils/file.ts

const rel2 = path.relative('/home/user/project/src', '/home/user/project/dist');
console.log(rel2);  // ../dist

// ========== path.isAbsolute — 是否为绝对路径 ==========
console.log(path.isAbsolute('/home/user'));   // true
console.log(path.isAbsolute('./src'));         // false
console.log(path.isAbsolute('src/index.ts')); // false

2.2 跨平台路径处理

import path from 'node:path';
import { fileURLToPath } from 'node:url';

// ========== 获取当前文件目录(ESM 中没有 __dirname) ==========
// CommonJS 中有 __dirname 和 __filename,ESM 中需要手动计算
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

console.log('当前文件:', __filename);
console.log('当前目录:', __dirname);

// 基于当前文件定位其他文件
const configPath = path.join(__dirname, '..', 'config', 'default.json');
const templatesDir = path.resolve(__dirname, '../templates');

// ========== 路径分隔符 ==========
console.log(path.sep);       // '/' (macOS/Linux) 或 '\\' (Windows)
console.log(path.delimiter);  // ':' (macOS/Linux) 或 ';' (Windows)

// ========== 规范化路径 ==========
const messy = '/home/user//project/./src/../src/index.ts';
const clean = path.normalize(messy);
console.log(clean);  // /home/user/project/src/index.ts

Java 对比:

// Java 获取当前文件目录
Path currentDir = Path.of("").toAbsolutePath();

// Java 路径拼接
Path configPath = currentDir.resolve("config").resolve("default.json");

// Java 相对路径
Path rel = Path.of("/home/user/project").relativize(Path.of("/home/user/project/src"));
// 结果: src

// Java 规范化
Path normalized = Path.of("/home/user//project/./src/../src/index.ts").normalize();

3. child_process — 子进程

child_process 模块用于创建子进程、执行外部命令。类似 Java 的 ProcessBuilder 和 Runtime.exec()。

Java ProcessBuilder vs Node.js child_process

操作 Java Node.js
执行命令(等待完成) ProcessBuilder.start() + waitFor() execSync() / execFile()
执行命令(异步) ProcessBuilder.start() exec() / spawn()
获取输出 process.getInputStream() 回调 / Promise
设置工作目录 pb.directory(dir) { cwd: dir }
设置环境变量 pb.environment().put(k, v) { env: { ...process.env, k: v } }

3.1 exec — 执行 shell 命令

exec 在 shell 中执行命令,缓冲整个输出后返回。适合输出较少的短命令。

import { exec } from 'node:child_process';
import { promisify } from 'node:util';

// 将 exec 转为 Promise 版本
const execAsync = promisify(exec);

// 基本用法:执行命令并获取输出
async function runCommand(command: string): Promise<string> {
  const { stdout, stderr } = await execAsync(command);
  if (stderr) {
    console.warn('标准错误:', stderr);
  }
  return stdout.trim();
}

// 使用示例
const nodeVersion = await runCommand('node --version');
console.log('Node 版本:', nodeVersion);  // v22.x.x

const files = await runCommand('ls -la');
console.log('文件列表:\n', files);

// 获取 Git 信息
const gitBranch = await runCommand('git branch --show-current');
console.log('当前分支:', gitBranch);

const gitLog = await runCommand('git log --oneline -5');
console.log('最近提交:\n', gitLog);

// 指定工作目录和环境变量
const { stdout } = await execAsync('ls', {
  cwd: '/tmp',                           // 工作目录
  env: { ...process.env, NODE_ENV: 'production' },  // 环境变量
  timeout: 5000,                          // 超时时间(毫秒)
  maxBuffer: 1024 * 1024,                 // 最大输出缓冲(默认 1MB)
});

3.2 execSync — 同步执行

import { execSync } from 'node:child_process';

// 同步执行命令(阻塞当前线程)
// 适合在脚本或 CLI 工具中使用
function runSync(command: string): string {
  try {
    const output = execSync(command, {
      encoding: 'utf-8',  // 返回字符串而非 Buffer
      timeout: 10000,     // 10 秒超时
    });
    return output.trim();
  } catch (error) {
    // 命令执行失败时 execSync 会抛出异常
    const err = error as { stderr?: string; status?: number };
    console.error(`命令失败 (退出码 ${err.status}):`, err.stderr);
    throw error;
  }
}

// 使用示例
const whoami = runSync('whoami');
console.log('当前用户:', whoami);

const diskUsage = runSync('df -h .');
console.log('磁盘使用:\n', diskUsage);

3.3 spawn — 流式处理子进程

spawn 不缓冲输出,而是通过流(Stream)实时传递。适合长时间运行或输出量大的命令。

import { spawn } from 'node:child_process';

// 基本用法:流式获取输出
function spawnCommand(command: string, args: string[]): Promise<string> {
  return new Promise((resolve, reject) => {
    const child = spawn(command, args);
    let stdout = '';
    let stderr = '';

    // 实时接收标准输出
    child.stdout.on('data', (data: Buffer) => {
      stdout += data.toString();
      // 可以在这里实时处理输出
    });

    // 实时接收标准错误
    child.stderr.on('data', (data: Buffer) => {
      stderr += data.toString();
    });

    // 进程结束
    child.on('close', (code) => {
      if (code === 0) {
        resolve(stdout.trim());
      } else {
        reject(new Error(`进程退出码 ${code}: ${stderr}`));
      }
    });

    // 进程启动失败
    child.on('error', (err) => {
      reject(new Error(`启动失败: ${err.message}`));
    });
  });
}

// 使用示例:实时输出
function runWithLiveOutput(command: string, args: string[]): Promise<number> {
  return new Promise((resolve, reject) => {
    const child = spawn(command, args, {
      stdio: 'inherit',  // 直接继承父进程的 stdin/stdout/stderr
      // 命令输出会直接打印到终端
    });

    child.on('close', (code) => resolve(code ?? 1));
    child.on('error', reject);
  });
}

// 使用示例
const result = await spawnCommand('find', ['.', '-name', '*.ts', '-maxdepth', '3']);
console.log('找到的 TS 文件:\n', result);

// 实时输出(例如运行测试或构建)
const exitCode = await runWithLiveOutput('npm', ['test']);
console.log('测试退出码:', exitCode);

Java 对比:

// Java ProcessBuilder
ProcessBuilder pb = new ProcessBuilder("git", "log", "--oneline", "-5");
pb.directory(new File("."));
pb.redirectErrorStream(true);

Process process = pb.start();
String output = new String(process.getInputStream().readAllBytes());
int exitCode = process.waitFor();

System.out.println("输出: " + output);
System.out.println("退出码: " + exitCode);

3.4 实用封装:命令执行工具

import { exec } from 'node:child_process';
import { promisify } from 'node:util';

const execAsync = promisify(exec);

// 执行结果接口
interface CommandResult {
  stdout: string;
  stderr: string;
  exitCode: number;
}

// 执行选项接口
interface CommandOptions {
  cwd?: string;
  timeout?: number;
  env?: Record<string, string>;
}

// 通用命令执行函数
async function executeCommand(
  command: string,
  options: CommandOptions = {}
): Promise<CommandResult> {
  try {
    const { stdout, stderr } = await execAsync(command, {
      cwd: options.cwd,
      timeout: options.timeout ?? 30000,
      env: options.env ? { ...process.env, ...options.env } : undefined,
      maxBuffer: 10 * 1024 * 1024, // 10MB
    });
    return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 };
  } catch (error) {
    const err = error as { stdout?: string; stderr?: string; code?: number };
    return {
      stdout: (err.stdout ?? '').trim(),
      stderr: (err.stderr ?? '').trim(),
      exitCode: err.code ?? 1,
    };
  }
}

// 使用示例
const gitResult = await executeCommand('git status --porcelain');
if (gitResult.exitCode === 0) {
  const changedFiles = gitResult.stdout.split('\n').filter(Boolean);
  console.log(`有 ${changedFiles.length} 个文件变更`);
} else {
  console.error('不在 Git 仓库中');
}

// 在指定目录执行
const npmResult = await executeCommand('npm ls --depth=0', {
  cwd: '/path/to/project',
  timeout: 10000,
});
console.log('已安装的依赖:\n', npmResult.stdout);

4. process — 进程信息

process 是一个全局对象,无需导入,提供当前 Node.js 进程的信息和控制。类似 Java 的 System 类。

Java System vs Node.js process

操作 Java Node.js
环境变量 System.getenv("KEY") process.env.KEY
命令行参数 args (main 参数) process.argv
当前目录 System.getProperty("user.dir") process.cwd()
退出程序 System.exit(code) process.exit(code)
标准输出 System.out.println() process.stdout.write()
标准输入 System.in process.stdin

4.1 process.env — 环境变量

// 读取环境变量
const nodeEnv = process.env.NODE_ENV ?? 'development';
const port = parseInt(process.env.PORT ?? '3000', 10);
const dbUrl = process.env.DATABASE_URL;

console.log(`环境: ${nodeEnv}`);
console.log(`端口: ${port}`);

if (!dbUrl) {
  console.error('错误: 未设置 DATABASE_URL 环境变量');
  process.exit(1);
}

// 类型安全的环境变量读取
interface EnvConfig {
  NODE_ENV: 'development' | 'production' | 'test';
  PORT: number;
  DATABASE_URL: string;
  LOG_LEVEL: 'debug' | 'info' | 'warn' | 'error';
}

function getEnvConfig(): EnvConfig {
  const required = ['DATABASE_URL'];
  for (const key of required) {
    if (!process.env[key]) {
      throw new Error(`缺少必需的环境变量: ${key}`);
    }
  }

  return {
    NODE_ENV: (process.env.NODE_ENV as EnvConfig['NODE_ENV']) ?? 'development',
    PORT: parseInt(process.env.PORT ?? '3000', 10),
    DATABASE_URL: process.env.DATABASE_URL!,
    LOG_LEVEL: (process.env.LOG_LEVEL as EnvConfig['LOG_LEVEL']) ?? 'info',
  };
}

// 列出所有环境变量
for (const [key, value] of Object.entries(process.env)) {
  if (key.startsWith('NODE_')) {
    console.log(`${key} = ${value}`);
  }
}

Java 对比:

// Java 读取环境变量
String nodeEnv = System.getenv().getOrDefault("NODE_ENV", "development");
int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "3000"));
String dbUrl = System.getenv("DATABASE_URL");
if (dbUrl == null) {
    System.err.println("错误: 未设置 DATABASE_URL");
    System.exit(1);
}

4.2 process.argv — 命令行参数

// process.argv 是一个字符串数组
// argv[0] = node 可执行文件路径
// argv[1] = 脚本文件路径
// argv[2...] = 用户传入的参数

// 运行: node script.ts --name=hello --verbose -p 3000
console.log(process.argv);
// [
//   '/usr/local/bin/node',
//   '/path/to/script.ts',
//   '--name=hello',
//   '--verbose',
//   '-p',
//   '3000'
// ]

// 获取用户参数(跳过前两个)
const userArgs = process.argv.slice(2);
console.log('用户参数:', userArgs);
// ['--name=hello', '--verbose', '-p', '3000']

// 简单的参数解析
function parseArgs(args: string[]): Record<string, string | boolean> {
  const result: Record<string, string | boolean> = {};

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];

    if (arg.startsWith('--')) {
      // --key=value 或 --flag
      const [key, value] = arg.slice(2).split('=');
      result[key] = value ?? true;
    } else if (arg.startsWith('-')) {
      // -k value
      const key = arg.slice(1);
      const nextArg = args[i + 1];
      if (nextArg && !nextArg.startsWith('-')) {
        result[key] = nextArg;
        i++; // 跳过下一个参数
      } else {
        result[key] = true;
      }
    }
  }

  return result;
}

const parsed = parseArgs(userArgs);
console.log(parsed);
// { name: 'hello', verbose: true, p: '3000' }

Java 对比:

// Java main 方法的 args
public static void main(String[] args) {
    // args 直接是用户参数,不包含 java/类名
    // 运行: java App --name=hello --verbose
    // args = ["--name=hello", "--verbose"]
}

4.3 process.cwd() 与 process.exit()

// 当前工作目录
const currentDir = process.cwd();
console.log('工作目录:', currentDir);
// 等价于 Java 的 System.getProperty("user.dir")

// 改变工作目录(很少用)
// process.chdir('/tmp');

// 进程 ID
console.log('PID:', process.pid);

// Node.js 版本
console.log('Node 版本:', process.version);

// 平台信息
console.log('平台:', process.platform);  // 'darwin', 'linux', 'win32'
console.log('架构:', process.arch);      // 'x64', 'arm64'

// 内存使用
const mem = process.memoryUsage();
console.log('内存使用:');
console.log(`  堆已用: ${Math.round(mem.heapUsed / 1024 / 1024)} MB`);
console.log(`  堆总量: ${Math.round(mem.heapTotal / 1024 / 1024)} MB`);
console.log(`  RSS:    ${Math.round(mem.rss / 1024 / 1024)} MB`);

// 正常退出
process.exit(0);

// 异常退出
process.exit(1);

// 监听退出事件(做清理工作)
process.on('exit', (code) => {
  console.log(`进程即将退出,退出码: ${code}`);
  // 注意:此处只能执行同步操作
});

// 监听未捕获的异常
process.on('uncaughtException', (err) => {
  console.error('未捕获的异常:', err);
  process.exit(1);
});

// 监听未处理的 Promise 拒绝
process.on('unhandledRejection', (reason) => {
  console.error('未处理的 Promise 拒绝:', reason);
  process.exit(1);
});

// 监听中断信号(Ctrl+C)
process.on('SIGINT', () => {
  console.log('\n收到 SIGINT,正在清理...');
  // 执行清理工作
  process.exit(0);
});

5. 综合实战:文件统计工具

结合以上所有核心模块,实现一个实用的文件统计工具:

import { readdir, stat, readFile } from 'node:fs/promises';
import path from 'node:path';

// 文件统计结果
interface FileStats {
  extension: string;
  count: number;
  totalSize: number;
  totalLines: number;
}

// 扫描目录并统计文件信息
async function scanDirectory(
  dirPath: string,
  excludeDirs: string[] = ['node_modules', '.git', 'dist']
): Promise<Map<string, FileStats>> {
  const stats = new Map<string, FileStats>();

  async function walk(currentPath: string): Promise<void> {
    const entries = await readdir(currentPath, { withFileTypes: true });

    for (const entry of entries) {
      const fullPath = path.join(currentPath, entry.name);

      if (entry.isDirectory()) {
        // 跳过排除的目录
        if (excludeDirs.includes(entry.name)) continue;
        await walk(fullPath);
      } else if (entry.isFile()) {
        const ext = path.extname(entry.name) || '(无扩展名)';
        const fileStat = await stat(fullPath);

        // 尝试统计行数(只对文本文件)
        let lines = 0;
        try {
          const content = await readFile(fullPath, 'utf-8');
          lines = content.split('\n').length;
        } catch {
          // 二进制文件读取可能失败,忽略
        }

        const existing = stats.get(ext) ?? {
          extension: ext,
          count: 0,
          totalSize: 0,
          totalLines: 0,
        };
        existing.count++;
        existing.totalSize += fileStat.size;
        existing.totalLines += lines;
        stats.set(ext, existing);
      }
    }
  }

  await walk(dirPath);
  return stats;
}

// 格式化输出
function formatReport(stats: Map<string, FileStats>): string {
  const sorted = [...stats.values()].sort((a, b) => b.count - a.count);

  const lines: string[] = [];
  lines.push('='.repeat(60));
  lines.push('文件统计报告');
  lines.push('='.repeat(60));
  lines.push(
    '扩展名'.padEnd(12) +
    '文件数'.padStart(8) +
    '总行数'.padStart(10) +
    '总大小'.padStart(12)
  );
  lines.push('-'.repeat(60));

  let totalFiles = 0;
  let totalLines = 0;
  let totalSize = 0;

  for (const s of sorted) {
    totalFiles += s.count;
    totalLines += s.totalLines;
    totalSize += s.totalSize;

    const sizeStr = formatSize(s.totalSize);
    lines.push(
      s.extension.padEnd(12) +
      String(s.count).padStart(8) +
      String(s.totalLines).padStart(10) +
      sizeStr.padStart(12)
    );
  }

  lines.push('-'.repeat(60));
  lines.push(
    '合计'.padEnd(12) +
    String(totalFiles).padStart(8) +
    String(totalLines).padStart(10) +
    formatSize(totalSize).padStart(12)
  );
  lines.push('='.repeat(60));

  return lines.join('\n');
}

// 格式化文件大小
function formatSize(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}

// 主函数
async function main(): Promise<void> {
  const targetDir = process.argv[2] ?? process.cwd();

  console.log(`扫描目录: ${path.resolve(targetDir)}\n`);

  const stats = await scanDirectory(targetDir);
  const report = formatReport(stats);
  console.log(report);
}

main().catch(console.error);

运行效果:

扫描目录: /home/user/my-project

============================================================
文件统计报告
============================================================
扩展名          文件数      总行数        总大小
------------------------------------------------------------
.ts                24      1856      45.2 KB
.json               5       120       3.8 KB
.md                 3       240       8.1 KB
.css                2        85       2.1 KB
------------------------------------------------------------
合计               34      2301      59.2 KB
============================================================

总结对照表

概念 Java Node.js
读文件 Files.readString() readFile()
写文件 Files.writeString() writeFile()
列目录 Files.list() / Files.walk() readdir()
流式读取 BufferedReader createReadStream + readline
路径拼接 Path.resolve() path.join() / path.resolve()
获取文件名 path.getFileName() path.basename()
执行命令 ProcessBuilder exec() / spawn()
环境变量 System.getenv() process.env
命令行参数 main(String[] args) process.argv.slice(2)
退出进程 System.exit() process.exit()

动手练习

  1. 编写一个脚本,递归扫描指定目录,找出所有超过 100KB 的文件并列出
  2. 编写一个脚本,调用 git log 获取最近 10 次提交的提交者和提交信息
  3. 编写一个简单的文件复制工具,从命令行参数读取源文件和目标文件路径
  4. 使用流式读取,逐行处理一个大文件,统计每个单词出现的次数