Electron - 文件处理
Electron - 现代文件处理
Section titled “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 文档。