Appearance
第11章:基础实战
实战1:简易桌面记事本
11.1 需求分析
创建一个简易的桌面记事本应用,实现以下功能:
- 创建应用窗口
- 实现文本编辑功能
- 本地文件保存
- 文件打开功能
- 基本的编辑操作(新建、保存、打开)
11.2 核心实现
- BrowserWindow:创建应用窗口,设置窗口大小、标题等属性
- dialog模块:使用文件对话框选择文件
- fs模块:读写本地文件
- IPC通信:主进程与渲染进程之间的通信
11.3 实现步骤
创建项目结构
notepad-app/ ├── main.js ├── index.html ├── package.json └── assets/ └── icon.png修改 package.json
json{ "name": "notepad-app", "version": "1.0.0", "description": "简易桌面记事本", "main": "main.js", "scripts": { "start": "electron ." }, "devDependencies": { "electron": "^18.0.0" } }修改 main.js
javascriptconst { app, BrowserWindow, dialog, ipcMain, Menu } = require('electron') const fs = require('fs') const path = require('path') let mainWindow let currentFile = null function createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, contextIsolation: false } }) mainWindow.loadFile('index.html') mainWindow.webContents.openDevTools() // 创建菜单 createMenu() mainWindow.on('closed', () => { mainWindow = null }) } function createMenu() { const template = [ { label: '文件', submenu: [ { label: '新建', accelerator: 'CmdOrCtrl+N', click: () => { mainWindow.webContents.send('new-file') currentFile = null } }, { label: '打开', accelerator: 'CmdOrCtrl+O', click: openFile }, { label: '保存', accelerator: 'CmdOrCtrl+S', click: saveFile }, { label: '另存为', accelerator: 'CmdOrCtrl+Shift+S', click: saveAsFile }, { type: 'separator' }, { label: '退出', accelerator: 'CmdOrCtrl+Q', click: () => app.quit() } ] }, { label: '编辑', submenu: [ { role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, { role: 'selectall' } ] } ] const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) } function openFile() { dialog.showOpenDialog(mainWindow, { properties: ['openFile'], filters: [ { name: '文本文件', extensions: ['txt'] }, { name: '所有文件', extensions: ['*'] } ] }).then(result => { if (!result.canceled && result.filePaths.length > 0) { currentFile = result.filePaths[0] fs.readFile(currentFile, 'utf8', (err, data) => { if (err) { dialog.showMessageBox({ type: 'error', title: '错误', message: '读取文件失败', buttons: ['确定'] }) return } mainWindow.webContents.send('file-opened', data, currentFile) }) } }) } function saveFile() { if (currentFile) { mainWindow.webContents.send('get-content', (event, content) => { fs.writeFile(currentFile, content, 'utf8', (err) => { if (err) { dialog.showMessageBox({ type: 'error', title: '错误', message: '保存文件失败', buttons: ['确定'] }) return } dialog.showMessageBox({ type: 'info', title: '成功', message: '文件保存成功', buttons: ['确定'] }) }) }) } else { saveAsFile() } } function saveAsFile() { dialog.showSaveDialog(mainWindow, { filters: [ { name: '文本文件', extensions: ['txt'] }, { name: '所有文件', extensions: ['*'] } ] }).then(result => { if (!result.canceled && result.filePath) { currentFile = result.filePath mainWindow.webContents.send('get-content', (event, content) => { fs.writeFile(currentFile, content, 'utf8', (err) => { if (err) { dialog.showMessageBox({ type: 'error', title: '错误', message: '保存文件失败', buttons: ['确定'] }) return } dialog.showMessageBox({ type: 'info', title: '成功', message: '文件保存成功', buttons: ['确定'] }) }) }) } }) } // IPC 事件处理 ipcMain.on('new-file', () => { currentFile = null }) ipcMain.on('get-content-reply', (event, content) => { // 处理内容获取后的逻辑 }) app.whenReady().then(createWindow) app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() }) app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow() })修改 index.html
html<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>简易记事本</title> <style> body { margin: 0; padding: 0; font-family: Arial, sans-serif; } #editor { width: 100%; height: 100vh; border: none; outline: none; padding: 20px; font-size: 16px; resize: none; } </style> </head> <body> <textarea id="editor" placeholder="请输入内容..."></textarea> <script> const { ipcRenderer } = require('electron') const editor = document.getElementById('editor') // 监听新建文件事件 ipcRenderer.on('new-file', () => { editor.value = '' document.title = '未命名 - 记事本' }) // 监听文件打开事件 ipcRenderer.on('file-opened', (event, content, filePath) => { editor.value = content document.title = `${filePath} - 记事本` }) // 监听获取内容事件 ipcRenderer.on('get-content', (event) => { event.sender.send('get-content-reply', editor.value) }) // 监听快捷键 document.addEventListener('keydown', (e) => { // Ctrl/Cmd + S 保存 if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault() ipcRenderer.send('save-file') } // Ctrl/Cmd + O 打开 if ((e.ctrlKey || e.metaKey) && e.key === 'o') { e.preventDefault() ipcRenderer.send('open-file') } // Ctrl/Cmd + N 新建 if ((e.ctrlKey || e.metaKey) && e.key === 'n') { e.preventDefault() ipcRenderer.send('new-file') } }) </script> </body> </html>运行应用
bashnpm install npm start
实战2:系统通知工具
11.4 需求分析
创建一个系统通知工具,实现以下功能:
- 定时发送系统通知
- 自定义通知内容与图标
- 通知点击跳转功能
- 通知管理(添加、删除、编辑通知)
11.5 核心实现
- Notification模块:创建和管理系统通知
- 定时器:使用
setInterval或setTimeout实现定时功能 - 主进程控制:在主进程中管理通知逻辑
- IPC通信:主进程与渲染进程之间的通信
11.6 实现步骤
创建项目结构
notification-app/ ├── main.js ├── index.html ├── package.json └── assets/ └── icon.png修改 package.json
json{ "name": "notification-app", "version": "1.0.0", "description": "系统通知工具", "main": "main.js", "scripts": { "start": "electron ." }, "devDependencies": { "electron": "^18.0.0" } }修改 main.js
javascriptconst { app, BrowserWindow, Notification, ipcMain } = require('electron') const path = require('path') let mainWindow let notificationTimers = [] function createWindow() { mainWindow = new BrowserWindow({ width: 600, height: 400, webPreferences: { nodeIntegration: true, contextIsolation: false } }) mainWindow.loadFile('index.html') mainWindow.webContents.openDevTools() mainWindow.on('closed', () => { // 清理定时器 notificationTimers.forEach(timer => clearInterval(timer)) mainWindow = null }) } // 发送通知 function sendNotification(title, body, icon) { const notification = new Notification({ title: title, body: body, icon: icon || path.join(__dirname, 'assets', 'icon.png') }) notification.on('click', () => { mainWindow.show() }) notification.show() } // 设置定时通知 function setNotificationTimer(title, body, icon, interval) { // 立即发送一次通知 sendNotification(title, body, icon) // 设置定时器 const timer = setInterval(() => { sendNotification(title, body, icon) }, interval * 1000) notificationTimers.push(timer) return notificationTimers.length - 1 } // 取消定时通知 function cancelNotificationTimer(index) { if (notificationTimers[index]) { clearInterval(notificationTimers[index]) notificationTimers[index] = null return true } return false } // IPC 事件处理 ipcMain.handle('send-notification', (event, title, body, icon) => { sendNotification(title, body, icon) return '通知已发送' }) ipcMain.handle('set-notification-timer', (event, title, body, icon, interval) => { const index = setNotificationTimer(title, body, icon, interval) return index }) ipcMain.handle('cancel-notification-timer', (event, index) => { return cancelNotificationTimer(index) }) app.whenReady().then(createWindow) app.on('window-all-closed', function () { // 清理定时器 notificationTimers.forEach(timer => clearInterval(timer)) if (process.platform !== 'darwin') app.quit() }) app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow() })修改 index.html
html<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>系统通知工具</title> <style> body { font-family: Arial, sans-serif; margin: 20px; padding: 0; background-color: #f0f0f0; } .container { max-width: 500px; margin: 0 auto; background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } h1 { color: #333; text-align: center; } .form-group { margin: 15px 0; } label { display: block; margin-bottom: 5px; font-weight: bold; } input, textarea, select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; } textarea { resize: vertical; min-height: 100px; } button { padding: 10px 15px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px; } button:hover { background-color: #45a049; } .timers-list { margin-top: 20px; border-top: 1px solid #ddd; padding-top: 15px; } .timer-item { padding: 10px; background-color: #f9f9f9; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; } .timer-info { flex: 1; } .timer-actions { display: flex; gap: 5px; } .cancel-btn { background-color: #f44336; } .cancel-btn:hover { background-color: #d32f2f; } </style> </head> <body> <div class="container"> <h1>系统通知工具</h1> <div class="form-group"> <label for="title">通知标题</label> <input type="text" id="title" placeholder="请输入通知标题"> </div> <div class="form-group"> <label for="body">通知内容</label> <textarea id="body" placeholder="请输入通知内容"></textarea> </div> <div class="form-group"> <label for="interval">通知间隔(秒)</label> <input type="number" id="interval" min="1" value="60"> </div> <div style="text-align: center; margin: 20px 0;"> <button id="send-btn">立即发送通知</button> <button id="set-timer-btn">设置定时通知</button> </div> <div class="timers-list"> <h3>定时通知列表</h3> <div id="timers-container"> <!-- 定时通知项将在这里动态添加 --> </div> </div> </div> <script> const { ipcRenderer } = require('electron') const titleInput = document.getElementById('title') const bodyInput = document.getElementById('body') const intervalInput = document.getElementById('interval') const sendBtn = document.getElementById('send-btn') const setTimerBtn = document.getElementById('set-timer-btn') const timersContainer = document.getElementById('timers-container') let timers = [] // 发送通知 sendBtn.addEventListener('click', async () => { const title = titleInput.value const body = bodyInput.value if (!title || !body) { alert('请填写通知标题和内容') return } await ipcRenderer.invoke('send-notification', title, body) alert('通知已发送') }) // 设置定时通知 setTimerBtn.addEventListener('click', async () => { const title = titleInput.value const body = bodyInput.value const interval = parseInt(intervalInput.value) if (!title || !body || isNaN(interval)) { alert('请填写完整的通知信息') return } const index = await ipcRenderer.invoke('set-notification-timer', title, body, null, interval) // 添加到列表 addTimerToList(index, title, body, interval) alert('定时通知已设置') }) // 添加定时器到列表 function addTimerToList(index, title, body, interval) { const timerItem = document.createElement('div') timerItem.className = 'timer-item' timerItem.dataset.index = index timerItem.innerHTML = ` <div class="timer-info"> <strong>${title}</strong> <p>${body}</p> <small>间隔: ${interval}秒</small> </div> <div class="timer-actions"> <button class="cancel-btn" onclick="cancelTimer(${index})">取消</button> </div> ` timersContainer.appendChild(timerItem) timers.push({ index, title, body, interval }) } // 取消定时器 window.cancelTimer = async (index) => { const success = await ipcRenderer.invoke('cancel-notification-timer', index) if (success) { const timerItem = document.querySelector(`.timer-item[data-index="${index}"]`) if (timerItem) { timerItem.remove() } alert('定时通知已取消') } } </script> </body> </html>运行应用
bashnpm install npm start
实战3:网络请求工具
11.7 需求分析
创建一个网络请求工具,实现以下功能:
- 发送 GET/POST 请求
- 展示响应数据
- 本地存储请求记录
- 查看历史请求记录
11.8 核心实现
- axios:发送网络请求
- electron-store:本地存储请求记录
- IPC通信:主进程与渲染进程之间的通信
- dialog模块:保存响应数据到文件
11.9 实现步骤
创建项目结构
network-tool/ ├── main.js ├── index.html ├── package.json └── assets/ └── icon.png修改 package.json
json{ "name": "network-tool", "version": "1.0.0", "description": "网络请求工具", "main": "main.js", "scripts": { "start": "electron ." }, "devDependencies": { "electron": "^18.0.0" }, "dependencies": { "axios": "^0.27.2", "electron-store": "^8.0.1" } }修改 main.js
javascriptconst { app, BrowserWindow, ipcMain, dialog } = require('electron') const axios = require('axios') const Store = require('electron-store') const path = require('path') const fs = require('fs') // 创建存储实例 const store = new Store({ name: 'network-tool', defaults: { requestHistory: [] } }) let mainWindow function createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, contextIsolation: false } }) mainWindow.loadFile('index.html') mainWindow.webContents.openDevTools() mainWindow.on('closed', () => { mainWindow = null }) } // 发送 GET 请求 async function sendGetRequest(url, headers = {}) { try { const response = await axios.get(url, { headers }) return response } catch (error) { throw error } } // 发送 POST 请求 async function sendPostRequest(url, data = {}, headers = {}) { try { const response = await axios.post(url, data, { headers }) return response } catch (error) { throw error } } // 保存请求记录 function saveRequestHistory(request) { const history = store.get('requestHistory') history.unshift(request) // 只保留最近 50 条记录 if (history.length > 50) { history.splice(50) } store.set('requestHistory', history) } // 获取请求历史 function getRequestHistory() { return store.get('requestHistory') } // 清空请求历史 function clearRequestHistory() { store.set('requestHistory', []) } // 保存响应数据到文件 function saveResponseToFile(data) { dialog.showSaveDialog(mainWindow, { filters: [ { name: 'JSON 文件', extensions: ['json'] }, { name: '文本文件', extensions: ['txt'] }, { name: '所有文件', extensions: ['*'] } ] }).then(result => { if (!result.canceled && result.filePath) { fs.writeFile(result.filePath, JSON.stringify(data, null, 2), 'utf8', (err) => { if (err) { dialog.showMessageBox({ type: 'error', title: '错误', message: '保存文件失败', buttons: ['确定'] }) return } dialog.showMessageBox({ type: 'info', title: '成功', message: '文件保存成功', buttons: ['确定'] }) }) } }) } // IPC 事件处理 ipcMain.handle('send-get-request', async (event, url, headers) => { try { const response = await sendGetRequest(url, headers) const requestData = { type: 'GET', url: url, headers: headers, timestamp: new Date().toISOString(), response: { status: response.status, data: response.data, headers: response.headers } } saveRequestHistory(requestData) return requestData } catch (error) { throw error } }) ipcMain.handle('send-post-request', async (event, url, data, headers) => { try { const response = await sendPostRequest(url, data, headers) const requestData = { type: 'POST', url: url, data: data, headers: headers, timestamp: new Date().toISOString(), response: { status: response.status, data: response.data, headers: response.headers } } saveRequestHistory(requestData) return requestData } catch (error) { throw error } }) ipcMain.handle('get-request-history', () => { return getRequestHistory() }) ipcMain.handle('clear-request-history', () => { clearRequestHistory() return '历史记录已清空' }) ipcMain.handle('save-response', (event, data) => { saveResponseToFile(data) return '保存成功' }) app.whenReady().then(createWindow) app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() }) app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow() })修改 index.html
html<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>网络请求工具</title> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f0f0f0; } .container { display: flex; height: 100vh; } .left-panel { width: 300px; background-color: #f9f9f9; border-right: 1px solid #ddd; padding: 15px; overflow-y: auto; } .right-panel { flex: 1; padding: 20px; overflow-y: auto; } h1 { color: #333; text-align: center; margin-top: 0; } .form-group { margin: 15px 0; } label { display: block; margin-bottom: 5px; font-weight: bold; } input, textarea, select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; } textarea { resize: vertical; min-height: 100px; } .button-group { display: flex; gap: 10px; margin: 20px 0; } button { padding: 10px 15px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; flex: 1; } button:hover { background-color: #45a049; } .history-section { margin-top: 20px; } .history-item { padding: 10px; background-color: white; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 10px; cursor: pointer; } .history-item:hover { background-color: #f0f0f0; } .history-item .method { font-weight: bold; margin-right: 10px; } .history-item .url { font-size: 14px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .history-item .time { font-size: 12px; color: #999; margin-top: 5px; } .response-section { margin-top: 20px; padding: 15px; background-color: white; border: 1px solid #ddd; border-radius: 4px; } .response-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .response-status { font-weight: bold; } .response-body { background-color: #f9f9f9; padding: 15px; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; } .error { color: red; margin-top: 10px; } </style> </head> <body> <div class="container"> <!-- 左侧历史记录面板 --> <div class="left-panel"> <h1>请求历史</h1> <button onclick="clearHistory()">清空历史</button> <div class="history-section" id="history-container"> <!-- 历史记录将在这里动态添加 --> </div> </div> <!-- 右侧请求面板 --> <div class="right-panel"> <h1>网络请求工具</h1> <div class="form-group"> <label for="method">请求方法</label> <select id="method"> <option value="GET">GET</option> <option value="POST">POST</option> </select> </div> <div class="form-group"> <label for="url">请求 URL</label> <input type="text" id="url" placeholder="https://api.example.com" value="https://jsonplaceholder.typicode.com/todos/1"> </div> <div class="form-group"> <label for="headers">请求头(JSON 格式)</label> <textarea id="headers" placeholder='{"Content-Type": "application/json"}'>{"Content-Type": "application/json"}</textarea> </div> <div class="form-group"> <label for="data">请求数据(JSON 格式)</label> <textarea id="data" placeholder='{"key": "value"}'>{"title": "foo", "body": "bar", "userId": 1}</textarea> </div> <div class="button-group"> <button id="send-btn">发送请求</button> <button id="save-btn" disabled>保存响应</button> </div> <div class="response-section" id="response-section" style="display: none;"> <div class="response-header"> <div class="response-status" id="response-status"></div> <button onclick="saveResponse()">保存响应</button> </div> <div class="response-body" id="response-body"></div> </div> <div class="error" id="error-message"></div> </div> </div> <script> const { ipcRenderer } = require('electron') const methodSelect = document.getElementById('method') const urlInput = document.getElementById('url') const headersInput = document.getElementById('headers') const dataInput = document.getElementById('data') const sendBtn = document.getElementById('send-btn') const saveBtn = document.getElementById('save-btn') const responseSection = document.getElementById('response-section') const responseStatus = document.getElementById('response-status') const responseBody = document.getElementById('response-body') const errorMessage = document.getElementById('error-message') const historyContainer = document.getElementById('history-container') let currentResponse = null // 加载历史记录 async function loadHistory() { const history = await ipcRenderer.invoke('get-request-history') historyContainer.innerHTML = '' history.forEach(item => { const historyItem = document.createElement('div') historyItem.className = 'history-item' historyItem.onclick = () => loadHistoryItem(item) historyItem.innerHTML = ` <div> <span class="method">${item.type}</span> <span class="url">${item.url}</span> </div> <div class="time">${new Date(item.timestamp).toLocaleString()}</div> ` historyContainer.appendChild(historyItem) }) } // 加载历史记录项 function loadHistoryItem(item) { methodSelect.value = item.type urlInput.value = item.url headersInput.value = JSON.stringify(item.headers, null, 2) if (item.data) { dataInput.value = JSON.stringify(item.data, null, 2) } showResponse(item.response) } // 发送请求 sendBtn.addEventListener('click', async () => { const method = methodSelect.value const url = urlInput.value if (!url) { showError('请输入请求 URL') return } let headers = {} let data = {} try { headers = JSON.parse(headersInput.value) } catch (e) { showError('请求头格式错误') return } if (method === 'POST') { try { data = JSON.parse(dataInput.value) } catch (e) { showError('请求数据格式错误') return } } try { showError('') responseSection.style.display = 'none' let result if (method === 'GET') { result = await ipcRenderer.invoke('send-get-request', url, headers) } else { result = await ipcRenderer.invoke('send-post-request', url, data, headers) } showResponse(result.response) loadHistory() } catch (error) { showError(`请求失败: ${error.message}`) } }) // 显示响应 function showResponse(response) { currentResponse = response responseSection.style.display = 'block' responseStatus.textContent = `状态码: ${response.status}` responseBody.textContent = JSON.stringify(response.data, null, 2) saveBtn.disabled = false } // 显示错误 function showError(message) { errorMessage.textContent = message } // 保存响应 async function saveResponse() { if (currentResponse) { await ipcRenderer.invoke('save-response', currentResponse) } } // 清空历史 async function clearHistory() { if (confirm('确定要清空所有历史记录吗?')) { await ipcRenderer.invoke('clear-request-history') loadHistory() } } // 初始化加载历史记录 loadHistory() </script> </body> </html>运行应用
bashnpm install npm start
11.10 小结
通过本章的三个基础实战案例,你已经掌握了 Electron 应用开发的核心技能:
简易桌面记事本:学习了如何使用 BrowserWindow 创建窗口,使用 dialog 模块选择文件,使用 fs 模块读写文件,以及使用 IPC 通信实现主进程与渲染进程之间的交互。
系统通知工具:学习了如何使用 Notification 模块创建系统通知,使用定时器实现定时功能,以及管理通知的生命周期。
网络请求工具:学习了如何使用 axios 发送网络请求,使用 electron-store 存储请求历史,以及使用 dialog 模块保存响应数据。
这些实战案例覆盖了 Electron 开发的常见场景,帮助你巩固了前面章节所学的核心知识点。在接下来的章节中,我们将学习更复杂的进阶实战案例。
