Back

Node.js 文件写入详解:关于 fs.writeFileSync() 的所有知识

Node.js 文件写入详解:关于 fs.writeFileSync() 的所有知识

文件操作是许多 Node.js 应用程序的基本部分。无论是创建配置文件、记录数据还是生成报告,了解如何高效地写入文件都是至关重要的。fs.writeFileSync() 方法提供了一种在 Node.js 中处理同步文件写入操作的直接方式。

这份全面指南涵盖了有效使用 fs.writeFileSync() 的所有知识,从基本用法到高级技术和最佳实践。

关键要点

  • fs.writeFileSync() 同步写入数据到文件,在完成前会阻塞事件循环
  • 始终使用 try-catch 块来处理潜在错误
  • 使用适当的标志来控制文件创建和写入行为
  • 考虑在高并发应用中的性能影响
  • 对于较大文件或生产环境,考虑使用异步替代方案
  • 注意安全问题,特别是处理用户提供的路径时

什么是 fs.writeFileSync()?

fs.writeFileSync() 是 Node.js 文件系统(fs)模块中的一个内置方法,用于同步写入数据到文件。与其异步对应方法不同,这个方法会阻塞代码执行,直到文件操作完成。

方法签名为:

fs.writeFileSync(file, data[, options])
  • file:文件路径(字符串、Buffer、URL 或文件描述符)
  • data:要写入的内容(字符串、Buffer、TypedArray 或 DataView)
  • options:可选配置参数(字符串或对象)

fs.writeFileSync() 的基本用法

写入简单文本文件

const fs = require('fs');

try {
  fs.writeFileSync('example.txt', 'Hello, Node.js!');
  console.log('File written successfully');
} catch (err) {
  console.error('Error writing file:', err);
}

这段代码在当前工作目录中创建一个名为 example.txt 的文件,内容为 ""Hello, Node.js!""。

写入 JSON 数据

const fs = require('fs');

const user = {
  name: 'John Doe',
  age: 30,
  email: 'john@example.com'
};

try {
  fs.writeFileSync('user.json', JSON.stringify(user, null, 2));
  console.log('JSON file written successfully');
} catch (err) {
  console.error('Error writing JSON file:', err);
}

理解参数

文件参数

第一个参数指定数据将被写入的文件路径:

// 写入当前目录
fs.writeFileSync('data.txt', 'Some content');

// 写入特定路径
fs.writeFileSync('/var/logs/app.log', 'Log entry');

// 使用文件描述符
const fd = fs.openSync('config.json', 'w');
fs.writeFileSync(fd, '{""setting"": ""value""}');
fs.closeSync(fd);

重要:如果指定的目录不存在,Node.js 将抛出错误。fs.writeFileSync() 方法不能创建目录。

数据参数

第二个参数包含你想要写入的内容:

// 写入字符串
fs.writeFileSync('file.txt', 'Plain text content');

// 写入 Buffer
const buffer = Buffer.from('Binary content');
fs.writeFileSync('binary.dat', buffer);

// 写入 TypedArray
const uint8Array = new Uint8Array([72, 101, 108, 108, 111]);  // ASCII 码中的 ""Hello""
fs.writeFileSync('typed-array.txt', uint8Array);

使用 Buffer

Buffer 在处理二进制数据时特别有用:

const fs = require('fs');

// 从字符串创建 buffer
const stringData = 'Hello World';
const buffer = Buffer.from(stringData, 'utf8');
fs.writeFileSync('buffer-example.txt', buffer);

// 从 JSON 创建 buffer
const userData = { name: 'Alice', age: 28 };
const jsonBuffer = Buffer.from(JSON.stringify(userData));
fs.writeFileSync('user-data.json', jsonBuffer);

选项参数

第三个参数允许你自定义文件写入行为:

fs.writeFileSync('config.txt', 'Configuration data', {
  encoding: 'utf8',    // 字符编码(默认:'utf8')
  mode: 0o666,         // 文件权限(默认:0o666)
  flag: 'w'            // 文件系统标志(默认:'w')
});

常用标志选项

  • 'w':打开文件用于写入,如果不存在则创建,如果存在则截断(默认)
  • 'a':打开文件用于追加,如果不存在则创建
  • 'wx':类似 ‘w’,但如果路径存在则失败
  • 'ax':类似 ‘a’,但如果路径存在则失败
  • 'r+':打开文件用于读写,文件必须存在
// 追加到文件而不是覆盖
fs.writeFileSync('log.txt', 'New log entryn', { flag: 'a' });

// 仅当文件不存在时创建新文件
try {
  fs.writeFileSync('config.json', '{}', { flag: 'wx' });
  console.log('New config file created');
} catch (err) {
  if (err.code === 'EEXIST') {
    console.log('Config file already exists');
  } else {
    console.error('Error:', err);
  }
}

fs.writeFileSync() 的错误处理

由于 fs.writeFileSync() 是同步的,错误会直接抛出。始终将其包装在 try-catch 块中:

try {
  fs.writeFileSync('/path/to/file.txt', 'Content');
} catch (error) {
  // 处理特定错误类型
  if (error.code === 'ENOENT') {
    console.error('Directory does not exist');
  } else if (error.code === 'EACCES') {
    console.error('Permission denied');
  } else {
    console.error('Unexpected error:', error);
  }
}

常见错误代码

  • ENOENT:无此文件或目录(通常是父目录不存在)
  • EACCES:权限被拒绝
  • EISDIR:是一个目录(尝试写入目录)
  • EMFILE:打开的文件过多
  • EEXIST:文件已存在(使用 ‘wx’ 或 ‘ax’ 标志时)

实际用例

创建配置文件

const fs = require('fs');

function createDefaultConfig() {
  const config = {
    apiKey: '',
    debug: false,
    logLevel: 'info',
    maxRetries: 3
  };
  
  try {
    fs.writeFileSync('config.json', JSON.stringify(config, null, 2));
    console.log('Default configuration created');
  } catch (err) {
    console.error('Failed to create config file:', err);
  }
}

// 检查配置是否存在,不存在则创建
try {
  fs.accessSync('config.json', fs.constants.F_OK);
  console.log('Config file already exists');
} catch (err) {
  createDefaultConfig();
}

简单日志记录

function logMessage(message) {
  const timestamp = new Date().toISOString();
  const logEntry = `[${timestamp}] ${message}n`;
  
  try {
    fs.writeFileSync('app.log', logEntry, { flag: 'a' });
  } catch (err) {
    console.error('Failed to write to log file:', err);
  }
}

logMessage('Application started');
// 执行某些操作
logMessage('Operation completed');

保存用户输入

const readline = require('readline-sync');
const fs = require('fs');

function saveUserData() {
  const username = readline.question('Enter username: ');
  
  if (!username) {
    console.log('Username cannot be empty');
    return saveUserData();
  }
  
  try {
    const userData = {
      username,
      createdAt: new Date().toISOString()
    };
    
    fs.writeFileSync(`${username}.json`, JSON.stringify(userData, null, 2));
    console.log(`User data saved to ${username}.json`);
  } catch (err) {
    console.error('Failed to save user data:', err);
  }
}

saveUserData();

性能考虑

同步与异步操作

fs.writeFileSync() 会阻塞事件循环直到操作完成,这可能会影响高并发应用程序的性能:

// 同步(阻塞事件循环)
console.time('writeFileSync');
fs.writeFileSync('large-file.txt', 'X'.repeat(1000000));
console.timeEnd('writeFileSync');

// 异步(不阻塞事件循环)
console.time('writeFile');
fs.writeFile('large-file-async.txt', 'X'.repeat(1000000), () => {
  console.timeEnd('writeFile');
});
console.log('This runs immediately while file is being written');

何时使用 fs.writeFileSync()

在以下情况使用 fs.writeFileSync()

  • 写入小文件时
  • 在脚本或 CLI 工具中工作时
  • 文件必须在继续执行前写入时
  • 在启动/初始化阶段时

避免在以下情况使用 fs.writeFileSync()

  • 处理大文件时
  • 在高并发 Web 服务器中
  • 在性能关键的代码路径中
  • 同时处理多个文件操作时

现代替代方案

使用 ES 模块语法

// ESM 语法(需要 Node.js 12+)
import { writeFileSync } from 'fs';
import { join } from 'path';

const filePath = join(process.cwd(), 'data.txt');
writeFileSync(filePath, 'Content using ESM syntax');

使用 fs/promises API

对于保持同步行为但使用更现代的 Promise 方式:

// 使用 fs/promises 和顶级 await(Node.js 14.8+ 与 ESM)
import { writeFile } from 'fs/promises';

try {
  // 这仍然是异步的,但语法更清晰
  await writeFile('example.txt', 'Content with promises');
  console.log('File written successfully');
} catch (err) {
  console.error('Error writing file:', err);
}

安全考虑

文件权限

写入敏感数据时要注意文件权限:

// 为敏感文件设置限制性权限
fs.writeFileSync('credentials.json', JSON.stringify(credentials), {
  mode: 0o600  // 仅所有者可读写
});

路径遍历漏洞

始终验证和净化文件路径,特别是当它们来自用户输入时:

const path = require('path');
const fs = require('fs');

function safeWriteFile(filename, content) {
  // 净化文件名以防止路径遍历
  const safeName = path.basename(filename);
  const safePath = path.join('./uploads', safeName);
  
  try {
    fs.writeFileSync(safePath, content);
    return true;
  } catch (err) {
    console.error('Error writing file:', err);
    return false;
  }
}

常见问题排查

""ENOENT: no such file or directory""

这通常意味着目录不存在:

const fs = require('fs');
const path = require('path');

function writeFileWithDirectoryCreation(filePath, content) {
  const directory = path.dirname(filePath);
  
  try {
    // 如果目录不存在则创建
    if (!fs.existsSync(directory)) {
      fs.mkdirSync(directory, { recursive: true });
    }
    
    // 现在写入文件
    fs.writeFileSync(filePath, content);
    return true;
  } catch (err) {
    console.error('Error:', err);
    return false;
  }
}

writeFileWithDirectoryCreation('logs/app/data.log', 'Log entry');

""EACCES: permission denied""

检查文件和目录权限:

try {
  fs.writeFileSync('/var/log/app.log', 'Log entry');
} catch (err) {
  if (err.code === 'EACCES') {
    // 尝试写入不同位置
    const homeDir = require('os').homedir();
    fs.writeFileSync(path.join(homeDir, 'app.log'), 'Log entry');
    console.log('Wrote to home directory instead');
  }
}

结论

fs.writeFileSync() 方法是 Node.js 应用程序中处理文件操作的强大工具。它提供了一种直接的方式来同步写入数据到文件,非常适合需要在继续执行前确保完成的场景。通过理解其参数、错误处理和性能影响,你可以在项目中有效地使用这个方法,同时避免常见陷阱。

记住要考虑这个方法的同步特性,并根据应用程序的需求适当使用。对于高并发应用或处理大文件时,考虑使用异步替代方案如 fs.writeFile() 或流。

常见问题

当你需要在继续执行前确保完成时使用 fs.writeFileSync(),通常在脚本、CLI 工具或初始化代码中。在大多数其他情况下使用 fs.writeFile(),特别是在性能重要的服务器或应用程序中。

在选项参数中使用 'a' 标志:fs.writeFileSync('log.txt', 'New entryn', { flag: 'a' });

不能,它只能创建文件。你需要先使用 fs.mkdirSync() 来创建目录。

使用 Buffer 对象:const buffer = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // 'Hello' fs.writeFileSync('binary.dat', buffer);

API 中没有特定限制,但同步写入非常大的文件可能会阻塞事件循环太长时间。对于大于几 MB 的文件,考虑使用流或异步方法。

设置 mode 选项:fs.writeFileSync('sensitive.txt', 'private data', { mode: 0o600 });

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers