Skip to content

Electron - 文件处理

与文件系统交互是桌面应用程序的常见需求。Electron 允许使用 Node.js 内置的 fs 模块来实现这一点。在现代 Electron 应用中,出于安全原因(contextIsolation: true,nodeIntegration: false),不鼓励从渲染进程直接访问文件系统。相反,文件操作应在主进程中处理,渲染进程通过预加载脚本促进的进程间通信(IPC)请求这些操作。

我们将创建一个应用来管理联系人列表(姓名和电子邮件),将它们保存到 JSON 文件并显示出来。此示例将使用 Bootstrap 进行样式设置(通过 npm install bootstrap 安装)。

首先,设置 main.js。这将处理文件操作的 IPC 调用:

const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
const fs = require('fs').promises; // 使用基于 Promise 的 fs 进行异步操作 (Using promise-based fs for async operations)
const contactsFilePath = path.join(app.getPath('userData'), 'contacts.json');
let mainWindow;
async function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 700,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
});
mainWindow.loadFile('index.html');
}
// 用于文件操作的 IPC 处理程序 (IPC Handlers for file operations)
ipcFutures = {
async getContacts() {
try {
await fs.access(contactsFilePath); // 检查文件是否存在 (Check if file exists)
const data = await fs.readFile(contactsFilePath, 'utf8');
return JSON.parse(data);
} catch (error) {
// 如果文件不存在或其他错误,返回空数组 (If file doesn't exist or other error, return empty array)
if (error.code === 'ENOENT') return [];
console.error('Failed to read contacts:', error);
throw error; // 重新抛出其他错误 (Re-throw other errors)
}
},
async saveContacts(contacts) {
try {
await fs.writeFile(contactsFilePath, JSON.stringify(contacts, null, 2), 'utf8');
return { success: true };
} catch (error) {
console.error('Failed to save contacts:', error);
return { success: false, error: error.message };
}
},
async showSaveDialog(defaultPath, content) {
const { filePath, canceled } = await dialog.showSaveDialog(mainWindow, {
defaultPath: defaultPath,
filters: [{ name: 'Text Files', extensions: ['txt'] }, { name: 'All Files', extensions: ['*'] }]
});
if (canceled || !filePath) return { canceled: true };
try {
await fs.writeFile(filePath, content, 'utf8');
return { success: true, filePath: filePath };
} catch (error) {
console.error('Failed to save file via dialog:', error);
return { success: false, error: error.message };
}
}
};
app.whenReady().then(() => {
ipcMain.handle('get-contacts', ipcFutures.getContacts);
ipcMain.handle('save-contacts', (event, contacts) => ipcFutures.saveContacts(contacts));
ipcMain.handle('show-save-dialog', (event, defaultPath, content) => ipcFutures.showSaveDialog(defaultPath, content));
createWindow();
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});

接下来,创建 preload.js 来安全地暴露这些 IPC 通道:

const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronFileAPI', {
getContacts: () => ipcRenderer.invoke('get-contacts'),
saveContacts: (contacts) => ipcRenderer.invoke('save-contacts', contacts),
showSaveDialog: (defaultPath, content) => ipcRenderer.invoke('show-save-dialog', defaultPath, content)
});

现在,创建 index.html 用于 UI:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Contact Manager</title>
<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
<style>
body { padding: 20px; }
#contact-list-container { max-height: 300px; overflow-y: auto; margin-top: 15px; border: 1px solid #ccc; }
</style>
</head>
<body>
<div class="container">
<h2>Contact List</h2>
<form id="contact-form">
<div class="mb-3">
<label for="Name" class="form-label">Name</label>
<input type="text" class="form-control" id="Name" required>
</div>
<div class="mb-3">
<label for="Email" class="form-label">Email</label>
<input type="email" class="form-control" id="Email" required>
</div>
<button type="submit" class="btn btn-primary">Add Contact</button>
<button type="button" class="btn btn-info" id="export-contacts">Export Contacts (TXT)</button>
</form>
<div id="contact-list-container" class="mt-4">
<table class="table table-striped table-hover">
<thead>
<tr><th>#</th><th>Name</th><th>Email</th><th>Actions</th></tr>
</thead>
<tbody id="contact-table-body"></tbody>
</table>
</div>
</div>
<script src="./renderer.js"></script>
</body>
</html>

最后,创建 renderer.js 来处理 UI 逻辑并与预加载脚本暴露的 API 交互:

document.addEventListener('DOMContentLoaded', () => {
const contactForm = document.getElementById('contact-form');
const nameInput = document.getElementById('Name');
const emailInput = document.getElementById('Email');
const contactTableBody = document.getElementById('contact-table-body');
const exportButton = document.getElementById('export-contacts');
let contacts = [];
async function loadAndDisplayContacts() {
try {
contacts = await window.electronFileAPI.getContacts();
renderContacts();
} catch (error) {
console.error('Error loading contacts:', error);
alert('Could not load contacts. Check console.');
}
}
function renderContacts() {
contactTableBody.innerHTML = ''; // 清空现有行 (Clear existing rows)
contacts.forEach((contact, index) => {
const row = contactTableBody.insertRow();
row.insertCell().textContent = index + 1;
row.insertCell().textContent = contact.name;
row.insertCell().textContent = contact.email;
const actionsCell = row.insertCell();
const deleteButton = document.createElement('button');
deleteButton.textContent = 'Delete';
deleteButton.className = 'btn btn-danger btn-sm';
deleteButton.onclick = () => deleteContact(index);
actionsCell.appendChild(deleteButton);
});
}
async function addContact(name, email) {
contacts.push({ name, email });
await saveContactsToFile();
renderContacts();
}
async function deleteContact(index) {
contacts.splice(index, 1);
await saveContactsToFile();
renderContacts();
}
async function saveContactsToFile() {
try {
const result = await window.electronFileAPI.saveContacts(contacts);
if (!result.success) {
console.error('Error saving contacts:', result.error);
alert('Failed to save contacts: ' + result.error);
}
} catch (error) {
console.error('IPC error saving contacts:', error);
alert('IPC error saving contacts. Check console.');
}
}
contactForm.addEventListener('submit', async (event) => {
event.preventDefault();
const name = nameInput.value.trim();
const email = emailInput.value.trim();
if (name && email) {
await addContact(name, email);
contactForm.reset();
nameInput.focus();
} else {
alert('Name and Email cannot be empty.');
}
});
exportButton.addEventListener('click', async () => {
if (contacts.length === 0) {
alert('No contacts to export.');
return;
}
let content = 'Contacts:\n\n';
contacts.forEach(c => {
content += `Name: ${c.name}, Email: ${c.email}\n`;
});
try {
const result = await window.electronFileAPI.showSaveDialog('contacts-export.txt', content);
if (result.success) {
alert('Contacts exported successfully to: ' + result.filePath);
} else if (!result.canceled) {
alert('Failed to export contacts: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('Error exporting contacts:', error);
alert('Error exporting contacts. See console for details.');
}
});
loadAndDisplayContacts(); // 初次加载 (Initial load)
});

使用 npx electron .(或如果已在 package.json 中配置了则使用 npm start)运行应用。

应用程序将显示一个用于输入姓名和电子邮件地址的表单。添加的联系人会显示在下面的表格中。这些联系人会保存到你的应用程序用户数据目录中的 contacts.json 文件(例如,在 Windows 上:C:\Users\[你的用户名]\AppData\Roaming\[你的应用名称]\contacts.json)。数据在应用重启后仍然存在。还有一个“导出联系人 (TXT)”按钮,它使用本地保存对话框。

此示例演示了 Electron 中文件处理的现代安全方法,使用了异步操作、IPC 和本地对话框。对于更复杂的场景,考虑使用轻量级数据库,如 SQLite(通过 sqlite3 npm 包),以实现更健壮的数据存储,仍需通过 IPC 调用在主进程中管理。

有关 ipcMain、ipcRenderer 和 dialog 模块的更多信息,请参阅官方 Electron 文档。