文件系统
Electron 提供了多种方式来访问和操作文件系统,包括 Node.js 的 fs 模块和 Electron 的 dialog 模块。
系统架构
code
┌─────────────────────────────────────────────────────────┐
│ 文件系统访问层 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Dialog API │ │ fs 模块 │ │ path 模块 │ │
│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │
│ │ showOpenDialo│ │ readFile │ │ join │ │
│ │ showSaveDialo│ │ writeFile │ │ resolve │ │
│ │ showMessageBox│ │ mkdir │ │ dirname │ │
│ └──────────────┘ │ readdir │ │ basename │ │
│ │ unlink │ │ extname │ │
│ └──────────────┘ └──────────────┘ │
│ │
├─────────────────────────────────────────────────────────┤
│ 安全访问层 │
│ • IPC 通信 • 权限验证 • 路径过滤 │
│ • 沙箱隔离 • 文件类型检查 • 大小限制 │
└─────────────────────────────────────────────────────────┘Dialog API 完整说明
文件对话框参数
showOpenDialog(options)
打开文件/目录选择对话框。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
title | String | - | 对话框标题 |
defaultPath | String | - | 默认打开路径 |
buttonLabel | String | - | 确认按钮文本 |
filters | FileFilter[] | - | 文件类型过滤器 |
properties | String[] | - | 对话框属性 |
message | String | - | macOS: 对话框消息 |
securityScopedBookmarks | Boolean | false | macOS: 安全范围书签 |
properties 可选值:
| 属性 | 说明 |
|---|---|
openFile | 允许选择文件 |
openDirectory | 允许选择目录 |
multiSelections | 允许多选 |
showHiddenFiles | 显示隐藏文件 |
createDirectory | macOS: 允许创建目录 |
promptToCreate | Windows: 提示创建不存在的文件 |
noResolveAliases | macOS: 不解析别名 |
treatPackageAsDirectory | macOS: 将包视为目录 |
dontAddToRecent | Windows: 不添加到最近文件列表 |
showSaveDialog(options)
保存文件对话框,参数与 showOpenDialog 类似。
showMessageBox(options)
消息对话框。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
type | String | 'none' | 消息类型: 'none', 'info', 'error', 'question', 'warning' |
buttons | String[] | ['OK'] | 按钮文本数组 |
defaultId | Number | 0 | 默认选中按钮的索引 |
cancelId | Number | 0 | 取消按钮的索引 |
title | String | - | 对话框标题 |
message | String | - | 消息内容 |
detail | String | - | 详细信息 |
checkboxLabel | String | - | 复选框文本 |
checkboxChecked | Boolean | false | 复选框默认状态 |
icon | NativeImage | - | 对话框图标 |
noLink | Boolean | false | 不使用链接样式 |
文件对话框
打开文件
javascript
const { dialog, app } = require('electron')
// 选择单个文件
async function openFile() {
const result = await dialog.showOpenDialog({
title: '选择文件',
defaultPath: app.getPath('documents'),
filters: [
{ name: '文本文件', extensions: ['txt', 'md'] },
{ name: '图片', extensions: ['jpg', 'png', 'gif'] },
{ name: '所有文件', extensions: ['*'] }
],
properties: ['openFile']
})
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0]
}
return null
}
// 选择多个文件
async function openFiles() {
const result = await dialog.showOpenDialog({
title: '选择文件',
properties: ['openFile', 'multiSelections']
})
return result.canceled ? [] : result.filePaths
}
// 选择目录
async function selectDirectory() {
const result = await dialog.showOpenDialog({
title: '选择目录',
properties: ['openDirectory', 'createDirectory']
})
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0]
}
return null
}保存文件
javascript
async function saveFile(defaultName = 'untitled.txt') {
const result = await dialog.showSaveDialog({
title: '保存文件',
defaultPath: defaultName,
filters: [
{ name: '文本文件', extensions: ['txt'] },
{ name: 'Markdown', extensions: ['md'] },
{ name: '所有文件', extensions: ['*'] }
],
properties: ['createDirectory']
})
if (!result.canceled && result.filePath) {
return result.filePath
}
return null
}消息对话框
javascript
const { dialog } = require('electron')
// 确认对话框
async function confirmAction(message) {
const result = await dialog.showMessageBox({
type: 'question',
buttons: ['取消', '确定'],
defaultId: 1,
cancelId: 0,
title: '确认',
message: message
})
return result.response === 1 // true: 确定, false: 取消
}
// 错误提示
async function showError(title, message) {
await dialog.showMessageBox({
type: 'error',
title: title,
message: message
})
}
// 带复选框的对话框
async function showConfirmWithCheckbox() {
const result = await dialog.showMessageBox({
type: 'question',
buttons: ['取消', '确定'],
message: '确定要执行此操作吗?',
checkboxLabel: '不再提示',
checkboxChecked: false
})
return {
confirmed: result.response === 1,
dontAskAgain: result.checkboxChecked
}
}文件读写
Node.js fs 模块 API
常用方法
| 方法 | 同步版本 | 异步版本 | Promise 版本 | 说明 |
|---|---|---|---|---|
| 读取文件 | readFileSync | readFile | fs.promises.readFile | 读取文件内容 |
| 写入文件 | writeFileSync | writeFile | fs.promises.writeFile | 写入文件内容 |
| 追加文件 | appendFileSync | appendFile | fs.promises.appendFile | 追加内容到文件 |
| 创建目录 | mkdirSync | mkdir | fs.promises.mkdir | 创建目录 |
| 读取目录 | readdirSync | readdir | fs.promises.readdir | 读取目录内容 |
| 删除文件 | unlinkSync | unlink | fs.promises.unlink | 删除文件 |
| 删除目录 | rmdirSync | rmdir | fs.promises.rmdir | 删除空目录 |
| 复制文件 | copyFileSync | copyFile | fs.promises.copyFile | 复制文件 |
| 移动文件 | renameSync | rename | fs.promises.rename | 移动/重命名文件 |
| 文件状态 | statSync | stat | fs.promises.stat | 获取文件信息 |
| 检查存在 | existsSync | ❌ | ❌ | 检查文件是否存在 |
同步读写
javascript
const fs = require('fs')
const path = require('path')
// 读取文件
function readFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf-8')
return { success: true, content }
} catch (error) {
console.error('读取文件失败:', error.message)
return { success: false, error: error.message }
}
}
// 写入文件
function writeFile(filePath, content) {
try {
fs.writeFileSync(filePath, content, 'utf-8')
return { success: true }
} catch (error) {
console.error('写入文件失败:', error.message)
return { success: false, error: error.message }
}
}
// 读取 JSON 文件
function readJSON(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf-8')
return { success: true, data: JSON.parse(content) }
} catch (error) {
console.error('读取 JSON 失败:', error.message)
return { success: false, error: error.message }
}
}
// 写入 JSON 文件
function writeJSON(filePath, data, indent = 2) {
try {
const content = JSON.stringify(data, null, indent)
fs.writeFileSync(filePath, content, 'utf-8')
return { success: true }
} catch (error) {
console.error('写入 JSON 失败:', error.message)
return { success: false, error: error.message }
}
}异步读写(推荐)
javascript
const fs = require('fs/promises')
// 读取文件
async function readFileAsync(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8')
return { success: true, content }
} catch (error) {
console.error('读取文件失败:', error.message)
return { success: false, error: error.message }
}
}
// 写入文件
async function writeFileAsync(filePath, content) {
try {
await fs.writeFile(filePath, content, 'utf-8')
return { success: true }
} catch (error) {
console.error('写入文件失败:', error.message)
return { success: false, error: error.message }
}
}
// 追加内容
async function appendFileAsync(filePath, content) {
try {
await fs.appendFile(filePath, content, 'utf-8')
return { success: true }
} catch (error) {
console.error('追加文件失败:', error.message)
return { success: false, error: error.message }
}
}
// 读取 JSON 文件
async function readJSONAsync(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8')
return { success: true, data: JSON.parse(content) }
} catch (error) {
console.error('读取 JSON 失败:', error.message)
return { success: false, error: error.message }
}
}
// 写入 JSON 文件
async function writeJSONAsync(filePath, data, indent = 2) {
try {
const content = JSON.stringify(data, null, indent)
await fs.writeFile(filePath, content, 'utf-8')
return { success: true }
} catch (error) {
console.error('写入 JSON 失败:', error.message)
return { success: false, error: error.message }
}
}文件监听
使用 fs.watch
javascript
const fs = require('fs')
// 监听单个文件
function watchFile(filePath, callback) {
const watcher = fs.watch(filePath, (eventType, filename) => {
console.log(`文件 ${filename} 发生了 ${eventType} 事件`)
callback(eventType, filename)
})
return {
close: () => watcher.close()
}
}
// 监听目录
function watchDirectory(dirPath, callback) {
const watcher = fs.watch(dirPath, { recursive: false }, (eventType, filename) => {
console.log(`目录中的 ${filename} 发生了 ${eventType} 事件`)
callback(eventType, filename)
})
return {
close: () => watcher.close()
}
}使用 chokidar(推荐)
chokidar 是一个跨平台的文件监听库,解决了 fs.watch 的许多问题。
bash
npm install chokidarjavascript
const chokidar = require('chokidar')
// 监听文件或目录
function watch(path, options = {}) {
const defaultOptions = {
ignored: /(^|[\/\\])\../, // 忽略点文件
persistent: true,
ignoreInitial: false, // 是否忽略初始扫描
awaitWriteFinish: {
stabilityThreshold: 2000, // 文件稳定时间(毫秒)
pollInterval: 100 // 轮询间隔
}
}
const watcher = chokidar.watch(path, { ...defaultOptions, ...options })
watcher
.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}`))
.on('error', error => console.log(`监听错误: ${error}`))
.on('ready', () => console.log('初始扫描完成,开始监听'))
return watcher
}
// 监听多个路径
const watcher = chokidar.watch([
'/path/to/file1',
'/path/to/file2',
'/path/to/directory'
])
// 取消监听
watcher.close()文件操作
创建目录
javascript
const fs = require('fs')
// 创建单层目录
function createDir(dirPath) {
try {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath)
}
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
// 创建多层目录(推荐)
function createDirRecursive(dirPath) {
try {
fs.mkdirSync(dirPath, { recursive: true })
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
// 异步版本
async function createDirAsync(dirPath, recursive = true) {
try {
await fs.promises.mkdir(dirPath, { recursive })
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}删除文件/目录
javascript
const fs = require('fs')
// 删除文件
function deleteFile(filePath) {
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath)
}
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
// 删除空目录
function deleteEmptyDir(dirPath) {
try {
fs.rmdirSync(dirPath)
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
// 删除非空目录(递归删除)
function deleteDir(dirPath) {
try {
fs.rmSync(dirPath, { recursive: true, force: true })
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
// 异步版本
async function deleteDirAsync(dirPath) {
try {
await fs.promises.rm(dirPath, { recursive: true, force: true })
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}复制/移动文件
javascript
const fs = require('fs')
// 复制文件
function copyFile(src, dest) {
try {
// 确保目标目录存在
const destDir = path.dirname(dest)
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true })
}
fs.copyFileSync(src, dest)
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
// 复制目录(递归)
function copyDirectory(src, dest) {
try {
fs.cpSync(src, dest, { recursive: true })
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
// 移动/重命名文件
function moveFile(oldPath, newPath) {
try {
// 确保目标目录存在
const newDir = path.dirname(newPath)
if (!fs.existsSync(newDir)) {
fs.mkdirSync(newDir, { recursive: true })
}
fs.renameSync(oldPath, newPath)
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}获取文件信息
javascript
const fs = require('fs')
function getFileInfo(filePath) {
try {
const stats = fs.statSync(filePath)
return {
success: true,
info: {
exists: true,
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
size: stats.size,
sizeFormatted: formatFileSize(stats.size),
created: stats.birthtime,
modified: stats.mtime,
accessed: stats.atime,
permissions: stats.mode,
owner: stats.uid,
group: stats.gid
}
}
} catch (error) {
if (error.code === 'ENOENT') {
return { success: true, info: { exists: false } }
}
return { success: false, error: error.message }
}
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
}遍历目录
同步遍历
javascript
const fs = require('fs')
const path = require('path')
// 遍历目录(深度优先)
function walkDir(dir, callback) {
try {
const files = fs.readdirSync(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const stats = fs.statSync(filePath)
if (stats.isDirectory()) {
walkDir(filePath, callback)
} else {
callback(filePath, stats)
}
}
} catch (error) {
console.error(`遍历目录失败: ${dir}`, error.message)
}
}
// 获取所有文件列表
function getAllFiles(dir, fileList = []) {
const files = fs.readdirSync(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const stats = fs.statSync(filePath)
if (stats.isDirectory()) {
getAllFiles(filePath, fileList)
} else {
fileList.push({
path: filePath,
name: file,
size: stats.size,
modified: stats.mtime
})
}
}
return fileList
}异步遍历(推荐)
javascript
const fs = require('fs/promises')
const path = require('path')
// 异步遍历目录
async function walkDirAsync(dir, callback) {
try {
const files = await fs.readdir(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const stats = await fs.stat(filePath)
if (stats.isDirectory()) {
await walkDirAsync(filePath, callback)
} else {
await callback(filePath, stats)
}
}
} catch (error) {
console.error(`遍历目录失败: ${dir}`, error.message)
}
}
// 获取所有文件列表(异步)
async function getAllFilesAsync(dir) {
const results = []
const files = await fs.readdir(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const stats = await fs.stat(filePath)
if (stats.isDirectory()) {
const subResults = await getAllFilesAsync(filePath)
results.push(...subResults)
} else {
results.push({
path: filePath,
name: file,
size: stats.size,
modified: stats.mtime
})
}
}
return results
}路径处理
path 模块 API
javascript
const path = require('path')
// 路径拼接
const fullPath = path.join('/foo', 'bar', 'baz.txt')
// 结果: '/foo/bar/baz.txt'
// 解析绝对路径
const resolved = path.resolve('foo', 'bar', 'baz.txt')
// 结果: '/当前工作目录/foo/bar/baz.txt'
// 获取目录名
const dir = path.dirname('/foo/bar/baz.txt')
// 结果: '/foo/bar'
// 获取文件名
const base = path.basename('/foo/bar/baz.txt')
// 结果: 'baz.txt'
// 获取文件名(不含扩展名)
const name = path.basename('/foo/bar/baz.txt', '.txt')
// 结果: 'baz'
// 获取扩展名
const ext = path.extname('/foo/bar/baz.txt')
// 结果: '.txt'
// 解析路径
const parsed = path.parse('/foo/bar/baz.txt')
// 结果: { root: '/', dir: '/foo/bar', base: 'baz.txt', ext: '.txt', name: 'baz' }
// 格式化路径
const formatted = path.format({
dir: '/foo/bar',
base: 'baz.txt'
})
// 结果: '/foo/bar/baz.txt'
// 规范化路径
const normalized = path.normalize('/foo/bar/../baz/./qux')
// 结果: '/foo/baz/qux'
// 相对路径
const relative = path.relative('/foo/bar', '/foo/baz/qux')
// 结果: '../baz/qux'跨平台路径处理
javascript
// 路径分隔符
console.log(path.sep) // Windows: '\\', macOS/Linux: '/'
// 路径定界符
console.log(path.delimiter) // Windows: ';', macOS/Linux: ':'
// 跨平台路径拼接
const dataPath = path.join(
app.getPath('userData'),
'data',
'config.json'
)
// 处理不同平台的路径格式
function normalizePath(filePath) {
return filePath.replace(/[\\/]/g, path.sep)
}应用数据目录
获取系统路径
javascript
const { app } = require('electron')
// 常用路径
const paths = {
home: app.getPath('home'), // 用户主目录
appData: app.getPath('appData'), // 应用数据目录
userData: app.getPath('userData'), // 用户数据目录
temp: app.getPath('temp'), // 临时文件目录
desktop: app.getPath('desktop'), // 桌面
documents: app.getPath('documents'), // 文档目录
downloads: app.getPath('downloads'), // 下载目录
music: app.getPath('music'), // 音乐目录
pictures: app.getPath('pictures'), // 图片目录
videos: app.getPath('videos'), // 视频目录
logs: app.getPath('logs'), // 日志目录
exe: app.getPath('exe') // 应用可执行文件路径
}
console.log('应用数据目录:', paths.userData)
console.log('文档目录:', paths.documents)管理应用数据
javascript
const { app } = require('electron')
const path = require('path')
const fs = require('fs')
// 获取应用数据目录
function getAppDataDir() {
return app.getPath('userData')
}
// 确保数据目录存在
function ensureDataDir(subDir = '') {
const dataDir = path.join(getAppDataDir(), 'data', subDir)
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true })
}
return dataDir
}
// 获取配置文件路径
function getConfigPath(configName = 'config.json') {
return path.join(getAppDataDir(), configName)
}
// 加载配置
function loadConfig(defaultConfig = {}) {
const configPath = getConfigPath()
try {
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, 'utf-8')
return { ...defaultConfig, ...JSON.parse(content) }
}
} catch (error) {
console.error('加载配置失败:', error.message)
}
return defaultConfig
}
// 保存配置
function saveConfig(config) {
const configPath = getConfigPath()
try {
fs.writeFileSync(
configPath,
JSON.stringify(config, null, 2),
'utf-8'
)
return true
} catch (error) {
console.error('保存配置失败:', error.message)
return false
}
}临时文件处理
创建临时文件
javascript
const fs = require('fs')
const path = require('path')
const { app } = require('electron')
const crypto = require('crypto')
// 获取临时目录
function getTempDir() {
return app.getPath('temp')
}
// 创建临时文件
function createTempFile(prefix = 'temp-', extension = '.tmp') {
const tempDir = getTempDir()
const fileName = prefix + crypto.randomBytes(8).toString('hex') + extension
const tempPath = path.join(tempDir, fileName)
return tempPath
}
// 创建临时目录
function createTempDir(prefix = 'temp-') {
const tempDir = getTempDir()
const dirName = prefix + crypto.randomBytes(8).toString('hex')
const tempPath = path.join(tempDir, dirName)
fs.mkdirSync(tempPath, { recursive: true })
return tempPath
}
// 使用临时文件
async function withTempFile(callback) {
const tempPath = createTempFile()
try {
const result = await callback(tempPath)
return result
} finally {
// 清理临时文件
try {
fs.unlinkSync(tempPath)
} catch (error) {
console.error('清理临时文件失败:', error.message)
}
}
}
// 使用临时目录
async function withTempDir(callback) {
const tempPath = createTempDir()
try {
const result = await callback(tempPath)
return result
} finally {
// 清理临时目录
try {
fs.rmSync(tempPath, { recursive: true, force: true })
} catch (error) {
console.error('清理临时目录失败:', error.message)
}
}
}大文件处理
流式读写
javascript
const fs = require('fs')
const path = require('path')
// 大文件复制(使用流)
function copyLargeFile(src, dest, onProgress) {
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(src)
const writeStream = fs.createWriteStream(dest)
let copied = 0
const total = fs.statSync(src).size
readStream.on('data', (chunk) => {
copied += chunk.length
if (onProgress) {
onProgress(copied, total)
}
})
readStream.on('error', reject)
writeStream.on('error', reject)
writeStream.on('finish', resolve)
readStream.pipe(writeStream)
})
}
// 分块读取大文件
function readLargeFileInChunks(filePath, chunkSize = 1024 * 1024) {
const stats = fs.statSync(filePath)
const fileSize = stats.size
const chunks = []
for (let offset = 0; offset < fileSize; offset += chunkSize) {
const fd = fs.openSync(filePath, 'r')
const buffer = Buffer.alloc(Math.min(chunkSize, fileSize - offset))
fs.readSync(fd, buffer, 0, buffer.length, offset)
fs.closeSync(fd)
chunks.push(buffer)
}
return Buffer.concat(chunks)
}
// 行读取器(逐行读取大文件)
function readLines(filePath, callback) {
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(filePath, { encoding: 'utf-8' })
const readline = require('readline')
const rl = readline.createInterface({
input: readStream,
crlfDelay: Infinity
})
rl.on('line', (line) => {
callback(line)
})
rl.on('close', resolve)
rl.on('error', reject)
})
}渲染进程访问文件
通过 IPC 通信
javascript
// main.js
const { ipcMain, dialog } = require('electron')
const fs = require('fs')
// 打开文件对话框
ipcMain.handle('dialog:openFile', async (event, options = {}) => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
...options
})
return result.canceled ? null : result.filePaths[0]
})
// 保存文件对话框
ipcMain.handle('dialog:saveFile', async (event, options = {}) => {
const result = await dialog.showSaveDialog(options)
return result.canceled ? null : result.filePath
})
// 读取文件
ipcMain.handle('file:read', async (event, filePath) => {
try {
// 验证文件路径安全性
if (!isPathSafe(filePath)) {
throw new Error('文件路径不安全')
}
const content = await fs.promises.readFile(filePath, 'utf-8')
return { success: true, content }
} catch (error) {
return { success: false, error: error.message }
}
})
// 写入文件
ipcMain.handle('file:write', async (event, filePath, content) => {
try {
// 验证文件路径安全性
if (!isPathSafe(filePath)) {
throw new Error('文件路径不安全')
}
await fs.promises.writeFile(filePath, content, 'utf-8')
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
})
// 获取文件信息
ipcMain.handle('file:stat', async (event, filePath) => {
try {
const stats = await fs.promises.stat(filePath)
return {
success: true,
info: {
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
size: stats.size,
modified: stats.mtime
}
}
} catch (error) {
return { success: false, error: error.message }
}
})
// 路径安全检查
function isPathSafe(filePath) {
const normalized = path.normalize(filePath)
// 检查是否在允许的目录内
const allowedDirs = [
app.getPath('documents'),
app.getPath('downloads'),
app.getPath('desktop')
]
return allowedDirs.some(dir => normalized.startsWith(dir))
}javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('fileAPI', {
// 对话框
openFile: (options) => ipcRenderer.invoke('dialog:openFile', options),
saveFile: (options) => ipcRenderer.invoke('dialog:saveFile', options),
// 文件操作
readFile: (path) => ipcRenderer.invoke('file:read', path),
writeFile: (path, content) => ipcRenderer.invoke('file:write', path, content),
statFile: (path) => ipcRenderer.invoke('file:stat', path)
})javascript
// renderer.js
async function openAndReadFile() {
// 打开文件选择对话框
const filePath = await window.fileAPI.openFile({
filters: [
{ name: '文本文件', extensions: ['txt', 'md'] }
]
})
if (filePath) {
// 读取文件内容
const result = await window.fileAPI.readFile(filePath)
if (result.success) {
console.log('文件内容:', result.content)
} else {
console.error('读取失败:', result.error)
}
}
}
async function saveFile(content) {
const filePath = await window.fileAPI.saveFile({
defaultPath: 'untitled.txt',
filters: [
{ name: '文本文件', extensions: ['txt'] }
]
})
if (filePath) {
const result = await window.fileAPI.writeFile(filePath, content)
if (result.success) {
console.log('文件已保存')
} else {
console.error('保存失败:', result.error)
}
}
}错误处理
常见错误代码
javascript
// 错误代码对照表
const errorCodes = {
'ENOENT': '文件或目录不存在',
'EISDIR': '目标是一个目录',
'ENOTDIR': '目标不是一个目录',
'EACCES': '权限不足',
'EEXIST': '文件已存在',
'ENOSPC': '磁盘空间不足',
'EMFILE': '打开的文件过多',
'ELOOP': '符号链接层级过深',
'ENAMETOOLONG': '文件名过长'
}
// 错误处理函数
function handleFileError(error) {
const message = errorCodes[error.code] || error.message
switch (error.code) {
case 'ENOENT':
console.error('文件不存在:', message)
break
case 'EACCES':
console.error('权限不足:', message)
break
case 'ENOSPC':
console.error('磁盘空间不足')
break
default:
console.error('文件操作错误:', message)
}
}安全错误处理
javascript
// 安全的文件操作包装器
async function safeFileOperation(operation, ...args) {
try {
const result = await operation(...args)
return { success: true, result }
} catch (error) {
// 记录错误但不暴露敏感信息
console.error('文件操作失败:', error.code)
// 返回用户友好的错误信息
return {
success: false,
error: errorCodes[error.code] || '操作失败'
}
}
}性能优化
1. 使用异步操作
javascript
// ❌ 避免:同步操作会阻塞主线程
const content = fs.readFileSync('large-file.txt', 'utf-8')
// ✅ 推荐:异步操作不会阻塞
const content = await fs.promises.readFile('large-file.txt', 'utf-8')2. 批量操作
javascript
// ❌ 避免:多次单独操作
for (const file of files) {
await fs.promises.stat(file)
}
// ✅ 推荐:批量处理
const stats = await Promise.all(
files.map(file => fs.promises.stat(file))
)3. 流式处理大文件
javascript
// ❌ 避免:一次性读取大文件
const content = fs.readFileSync('large-file.txt', 'utf-8')
// ✅ 推荐:使用流处理
const readStream = fs.createReadStream('large-file.txt', { encoding: 'utf-8' })
readStream.on('data', (chunk) => {
// 处理数据块
})安全注意事项
1. 路径验证
javascript
// 验证路径是否在允许范围内
function isPathAllowed(filePath, allowedDirs) {
const resolved = path.resolve(filePath)
return allowedDirs.some(dir => resolved.startsWith(path.resolve(dir)))
}
// 防止路径遍历攻击
function sanitizePath(filePath) {
const normalized = path.normalize(filePath)
// 检查是否包含 ..
if (normalized.includes('..')) {
throw new Error('路径包含非法字符')
}
return normalized
}2. 文件类型验证
javascript
// 验证文件扩展名
function isAllowedExtension(filePath, allowedExtensions) {
const ext = path.extname(filePath).toLowerCase()
return allowedExtensions.includes(ext)
}
// 读取文件魔术数字验证类型
function getFileType(filePath) {
const fd = fs.openSync(filePath, 'r')
const buffer = Buffer.alloc(8)
fs.readSync(fd, buffer, 0, 8, 0)
fs.closeSync(fd)
// 检查文件头
if (buffer.toString('hex', 0, 4) === '89504e47') return 'png'
if (buffer.toString('hex', 0, 2) === 'ffd8') return 'jpeg'
if (buffer.toString('hex', 0, 4) === '47494638') return 'gif'
// ... 其他格式
return 'unknown'
}3. 文件大小限制
javascript
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
async function readFileWithLimit(filePath) {
const stats = await fs.promises.stat(filePath)
if (stats.size > MAX_FILE_SIZE) {
throw new Error(`文件过大,最大允许 ${MAX_FILE_SIZE / 1024 / 1024}MB`)
}
return fs.promises.readFile(filePath, 'utf-8')
}常见问题解答
1. 如何处理文件路径中的中文字符?
javascript
const path = require('path')
// Electron 会自动处理中文路径
// 确保使用 utf-8 编码读写文件
const content = fs.readFileSync(filePath, 'utf-8')
fs.writeFileSync(filePath, content, 'utf-8')2. 如何监听文件变化?
javascript
// 使用 chokidar 库(推荐)
const chokidar = require('chokidar')
const watcher = chokidar.watch('/path/to/file', {
awaitWriteFinish: {
stabilityThreshold: 2000,
pollInterval: 100
}
})
watcher.on('change', (path) => {
console.log('文件已修改:', path)
})3. 如何实现文件拖放?
javascript
// 主进程
mainWindow.webContents.on('will-navigate', (event, url) => {
event.preventDefault()
})
// 渲染进程
document.addEventListener('drop', (event) => {
event.preventDefault()
const files = event.dataTransfer.files
for (const file of files) {
console.log('拖放的文件:', file.path)
}
})
document.addEventListener('dragover', (event) => {
event.preventDefault()
})4. 如何处理文件编码问题?
javascript
const jschardet = require('jschardet')
const iconv = require('iconv-lite')
// 检测文件编码
function detectEncoding(buffer) {
const detected = jschardet.detect(buffer)
return detected.encoding || 'utf-8'
}
// 读取文件并自动检测编码
function readFileWithEncoding(filePath) {
const buffer = fs.readFileSync(filePath)
const encoding = detectEncoding(buffer)
if (encoding === 'utf-8') {
return buffer.toString('utf-8')
}
return iconv.decode(buffer, encoding)
}5. 如何实现文件搜索功能?
javascript
async function searchFiles(dir, pattern) {
const results = []
const files = await fs.promises.readdir(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const stats = await fs.promises.stat(filePath)
if (stats.isDirectory()) {
const subResults = await searchFiles(filePath, pattern)
results.push(...subResults)
} else if (file.includes(pattern)) {
results.push(filePath)
}
}
return results
}