Node.js 文件系统基础
Electron 的主进程运行在 Node.js 环境中,可以直接使用 Node.js 的 fs 模块:
const fs = require('fs');
const path = require('path');
同步 vs 异步
// 同步(阻塞)
const data = fs.readFileSync('/path/to/file.txt', 'utf8');
// 异步回调
fs.readFile('/path/to/file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
// 异步 Promise(Node.js 8+)
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
const data = await readFile('/path/to/file.txt', 'utf8');
// Node.js 10+ 原生 Promise API
const data = await fs.promises.readFile('/path/to/file.txt', 'utf8');
路径操作
const path = require('path');
// 路径拼接
const fullPath = path.join(__dirname, 'files', 'data.txt');
// 解析相对路径
const absolutePath = path.resolve('./data.txt');
// 获取文件扩展名
const ext = path.extname('document.pdf'); // '.pdf'
// 获取文件名
const basename = path.basename('/path/to/file.txt'); // 'file.txt'
const basenameNoExt = path.basename('/path/to/file.txt', '.txt'); // 'file'
// 获取目录名
const dirname = path.dirname('/path/to/file.txt'); // '/path/to'
// path.join 自动处理系统分隔符
const crossPlatformPath = path.join('folder', 'subfolder', 'file.txt');
读取文件
读取文本文件
async function readTextFile(filePath) {
try {
const content = await fs.promises.readFile(filePath, 'utf8');
return content;
} catch (err) {
console.error('读取文件失败:', err);
throw err;
}
}
读取二进制文件
async function readBinaryFile(filePath) {
const buffer = await fs.promises.readFile(filePath);
return buffer; // Buffer 对象
}
// 或者指定编码读取图片
async function readImageAsBase64(filePath) {
const buffer = await fs.promises.readFile(filePath);
return buffer.toString('base64');
}
逐行读取大文件
对于大文件,不应该一次性读取全部内容:
const readline = require('readline');
const { createInterface } = require('readline');
async function readFileLineByLine(filePath) {
const fileStream = fs.createReadStream(filePath, {
encoding: 'utf8',
highWaterMark: 1024 // 缓冲区大小
});
const rl = createInterface({
input: fileStream,
crlfDelay: Infinity // 识别 \n 和 \r\n
});
const lines = [];
for await (const line of rl) {
lines.push(line);
}
return lines;
}
// 处理大文件的另一种方式:流式处理
async function processLargeFile(filePath, processor) {
const fileStream = fs.createReadStream(filePath);
return new Promise((resolve, reject) => {
let lineNumber = 0;
const rl = createInterface({
input: fileStream,
crlfDelay: Infinity
});
rl.on('line', (line) => {
lineNumber++;
processor(line, lineNumber);
});
rl.on('close', resolve);
rl.on('error', reject);
});
}
监听文件变化
const chokidar = require('chokidar'); // 推荐使用 chokidar
// 监听文件变化
const watcher = chokidar.watch('/path/to/watch', {
persistent: true,
ignoreInitial: true
});
watcher
.on('add', path => console.log(`File added: ${path}`))
.on('change', path => console.log(`File changed: ${path}`))
.on('unlink', path => console.log(`File removed: ${path}`))
.on('error', error => console.error(`Watcher error: ${error}`));
// 停止监听
// watcher.close();
写入文件
写入文本文件
async function writeTextFile(filePath, content) {
// 如果目录不存在,需要先创建
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
// 写入文件
await fs.promises.writeFile(filePath, content, 'utf8');
}
// 或者使用 appendFile 追加内容
async function appendToFile(filePath, content) {
await fs.promises.appendFile(filePath, content, 'utf8');
}
写入二进制数据
async function writeBinaryFile(filePath, buffer) {
await fs.promises.writeFile(filePath, buffer);
}
// 从 base64 写入图片
async function writeBase64Image(filePath, base64Data) {
const buffer = Buffer.from(base64Data, 'base64');
await fs.promises.writeFile(filePath, buffer);
}
流式写入(处理大文件)
async function writeLargeFile(filePath, dataGenerator) {
const writeStream = fs.createWriteStream(filePath);
return new Promise((resolve, reject) => {
writeStream.on('finish', resolve);
writeStream.on('error', reject);
// dataGenerator 是一个异步生成器
(async () => {
for await (const chunk of dataGenerator()) {
writeStream.write(chunk);
}
writeStream.end();
})();
});
}
目录操作
创建目录
async function createDirectory(dirPath) {
// recursive: true 会自动创建所有中间目录
await fs.promises.mkdir(dirPath, { recursive: true });
}
// 例子:创建嵌套目录
await fs.promises.mkdir('/a/b/c/d', { recursive: true });
读取目录内容
async function readDirectory(dirPath) {
const entries = await fs.promises.readdir(dirPath, {
withFileTypes: true // 返回 DirEntry 对象,包含类型信息
});
return entries.map(entry => ({
name: entry.name,
isFile: entry.isFile(),
isDirectory: entry.isDirectory(),
isSymbolicLink: entry.isSymbolicLink(),
path: path.join(dirPath, entry.name)
}));
}
// 递归读取目录树
async function readDirectoryTree(dirPath, maxDepth = Infinity) {
const result = [];
async function traverse(currentPath, depth) {
if (depth > maxDepth) return;
const entries = await fs.promises.readdir(currentPath, {
withFileTypes: true
});
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
const item = {
name: entry.name,
path: fullPath,
isDirectory: entry.isDirectory()
};
if (entry.isDirectory() && depth < maxDepth) {
item.children = await traverse(fullPath, depth + 1);
}
result.push(item);
}
}
await traverse(dirPath, 0);
return result;
}
删除目录
async function deleteDirectory(dirPath) {
// recursive: true 会递归删除所有内容
await fs.promises.rm(dirPath, { recursive: true, force: true });
}
文件元数据
async function getFileStats(filePath) {
const stats = await fs.promises.stat(filePath);
return {
size: stats.size, // 文件大小(字节)
created: stats.birthtime, // 创建时间
modified: stats.mtime, // 修改时间
accessed: stats.atime, // 访问时间
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
isSymbolicLink: stats.isSymbolicLink()
};
}
// 判断文件是否存在
async function fileExists(filePath) {
try {
await fs.promises.access(filePath, fs.constants.F_OK);
return true;
} catch {
return false;
}
}
复制、移动、删除文件
复制文件
async function copyFile(source, destination) {
// 确保目标目录存在
await fs.promises.mkdir(path.dirname(destination), { recursive: true });
await fs.promises.copyFile(source, destination);
}
// 复制整个目录
async function copyDirectory(source, destination) {
await fs.promises.mkdir(destination, { recursive: true });
const entries = await fs.promises.readdir(source, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(source, entry.name);
const destPath = path.join(destination, entry.name);
if (entry.isDirectory()) {
await copyDirectory(srcPath, destPath);
} else {
await fs.promises.copyFile(srcPath, destPath);
}
}
}
移动文件
async function moveFile(source, destination) {
// 确保目标目录存在
await fs.promises.mkdir(path.dirname(destination), { recursive: true });
await fs.promises.rename(source, destination);
}
删除文件
async function deleteFile(filePath) {
await fs.promises.unlink(filePath);
}
// 安全删除:先检查再删除
async function safeDeleteFile(filePath) {
if (await fileExists(filePath)) {
await fs.promises.unlink(filePath);
}
}
Electron 中的安全文件访问
路径验证
主进程中的文件操作必须验证路径,防止路径穿越攻击:
const { app } = require('electron');
const path = require('path');
ipcMain.handle('file:read', async (event, userPath) => {
// 获取应用允许的目录
const allowedDir = app.getPath('userData');
// 解析用户输入的路径
const resolvedPath = path.resolve(userPath);
// 验证路径是否在允许目录内
if (!resolvedPath.startsWith(allowedDir)) {
throw new Error('Access denied: path outside allowed directory');
}
// 读取文件
return await fs.promises.readFile(resolvedPath, 'utf8');
});
危险的错误示例
// 危险:直接使用用户输入的路径
ipcMain.handle('file:read', async (event, filePath) => {
// 用户可能输入 ../../../etc/passwd
return await fs.promises.readFile(filePath, 'utf8');
});
// 危险:使用相对路径
ipcMain.handle('file:read', async (event, filePath) => {
// 用户可能输入 ../../../etc/passwd
return await fs.promises.readFile(path.join(__dirname, filePath), 'utf8');
});
使用 dialog 让用户选择文件
const { dialog } = require('electron');
ipcMain.handle('file:open-dialog', async (event) => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [
{ name: '文本文件', extensions: ['txt', 'md', 'json'] },
{ name: '所有文件', extensions: ['*'] }
]
});
if (result.canceled) {
return null;
}
return result.filePaths[0];
});
常见文件操作场景
1. 保存用户数据
const { app } = require('electron');
async function saveUserData(filename, data) {
const userDataPath = app.getPath('userData');
const filePath = path.join(userDataPath, filename);
await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
}
async function loadUserData(filename) {
const userDataPath = app.getPath('userData');
const filePath = path.join(userDataPath, filename);
try {
const content = await fs.promises.readFile(filePath, 'utf8');
return JSON.parse(content);
} catch (err) {
return null;
}
}
2. 导出文件
const { dialog } = require('electron');
async function exportToFile(content, defaultName) {
const result = await dialog.showSaveDialog({
title: '导出文件',
defaultPath: defaultName,
filters: [
{ name: 'JSON 文件', extensions: ['json'] },
{ name: '文本文件', extensions: ['txt'] },
{ name: 'CSV 文件', extensions: ['csv'] }
]
});
if (result.canceled) {
return false;
}
await fs.promises.writeFile(result.filePath, content, 'utf8');
return true;
}
3. 应用日志
const { app } = require('electron');
class Logger {
constructor(filename = 'app.log') {
this.logPath = path.join(app.getPath('logs'), filename);
}
async write(level, message, data = {}) {
const entry = {
timestamp: new Date().toISOString(),
level,
message,
...data
};
const line = JSON.stringify(entry) + '\n';
await fs.promises.appendFile(this.logPath, line, 'utf8');
}
info(message, data) { return this.write('INFO', message, data); }
warn(message, data) { return this.write('WARN', message, data); }
error(message, data) { return this.write('ERROR', message, data); }
}
const logger = new Logger();
这一章想说的
文件系统操作是 Electron 应用的基础:
- Node.js fs 模块:完整的文件系统 API,支持同步和异步
- 路径操作:使用
path.join和path.resolve处理路径 - 安全访问:主进程必须验证路径,防止路径穿越
- 常见场景:用户数据存储、文件导入导出、日志记录
合理使用文件 API,配合安全验证,可以构建安全可靠的桌面应用。