Appearance
第15章:Electron 进阶技巧
15.1 应用自动更新
15.1.1 autoUpdater 模块
Electron 提供了 autoUpdater 模块,用于实现应用的自动更新功能。
核心功能:
- 检查更新
- 下载更新
- 安装更新
- 重启应用
15.1.2 实现步骤
安装依赖
bashnpm install electron-updater配置 package.json
json{ "build": { "publish": { "provider": "github", "repo": "username/repository", "owner": "username", "private": false, "releaseType": "release" } } }实现自动更新逻辑
javascriptconst { app, autoUpdater, dialog } = require('electron') const { autoUpdater: electronUpdater } = require('electron-updater') // 配置自动更新 function setupAutoUpdater() { // 设置更新服务器地址 electronUpdater.setFeedURL({ provider: 'github', owner: 'username', repo: 'repository', releaseType: 'release' }) // 检查更新 electronUpdater.checkForUpdates() // 监听更新事件 electronUpdater.on('update-available', () => { console.log('有新版本可用') }) electronUpdater.on('update-downloaded', () => { dialog.showMessageBox({ type: 'info', title: '更新可用', message: '新版本已下载完成,是否立即重启应用?', buttons: ['是', '否'] }).then((result) => { if (result.response === 0) { electronUpdater.quitAndInstall() } }) }) electronUpdater.on('error', (error) => { console.error('更新失败:', error) }) } // 在应用就绪后设置自动更新 app.whenReady().then(() => { setupAutoUpdater() // 创建窗口等其他逻辑 })
15.2 自定义应用图标、启动页
15.2.1 应用图标
不同平台的图标要求:
- Windows:
.ico格式,推荐尺寸 256x256 - macOS:
.icns格式,包含多种尺寸 - Linux:
.png格式,推荐尺寸 512x512
配置方法:
json
{
"build": {
"mac": {
"icon": "assets/icon.icns"
},
"win": {
"icon": "assets/icon.ico"
},
"linux": {
"icon": "assets/icon.png"
}
}
}15.2.2 启动页
实现方法:
创建启动页 HTML
html<!-- splash.html --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>启动中</title> <style> body { margin: 0; padding: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; background-color: #f0f0f0; } .logo { font-size: 24px; font-weight: bold; margin-bottom: 20px; } .progress { width: 200px; height: 10px; background-color: #e0e0e0; border-radius: 5px; overflow: hidden; } .progress-bar { width: 0; height: 100%; background-color: #4CAF50; transition: width 0.3s ease; } </style> </head> <body> <div class="logo">我的应用</div> <div class="progress"> <div class="progress-bar" id="progress-bar"></div> </div> <script> // 模拟加载过程 let progress = 0; const progressBar = document.getElementById('progress-bar'); const interval = setInterval(() => { progress += 10; progressBar.style.width = progress + '%'; if (progress >= 100) { clearInterval(interval); // 通知主进程加载完成 require('electron').ipcRenderer.send('splash-loaded'); } }, 200); </script> </body> </html>在主进程中实现启动页
javascriptconst { app, BrowserWindow, ipcMain } = require('electron') const path = require('path') let splashWindow let mainWindow function createSplashWindow() { splashWindow = new BrowserWindow({ width: 400, height: 300, frame: false, transparent: true, alwaysOnTop: true, webPreferences: { nodeIntegration: true, contextIsolation: false } }) splashWindow.loadFile('splash.html') } function createMainWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, contextIsolation: false }, show: false // 先隐藏主窗口 }) mainWindow.loadFile('index.html') mainWindow.once('ready-to-show', () => { // 主窗口准备就绪后显示 mainWindow.show() // 关闭启动页 if (splashWindow) { splashWindow.close() } }) } app.whenReady().then(() => { createSplashWindow() // 模拟加载时间 setTimeout(createMainWindow, 2000) }) // 监听启动页加载完成事件 ipcMain.on('splash-loaded', () => { console.log('启动页加载完成') })
15.3 主进程与渲染进程权限控制
15.3.1 安全最佳实践
主进程权限:
- 可以访问所有 Electron API
- 可以访问 Node.js API
- 可以访问文件系统
渲染进程权限:
- 默认情况下,不应该直接访问 Node.js API
- 应该通过 IPC 与主进程通信,由主进程执行敏感操作
15.3.2 上下文隔离
启用上下文隔离:
javascript
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})创建预加载脚本:
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
// 向渲染进程暴露安全的 API
contextBridge.exposeInMainWorld('electronAPI', {
// 暴露方法
sendMessage: (message) => ipcRenderer.send('message', message),
onMessage: (callback) => ipcRenderer.on('message', (event, ...args) => callback(...args)),
// 暴露属性
appVersion: process.version
})在渲染进程中使用:
javascript
// 渲染进程中
window.electronAPI.sendMessage('Hello from renderer')
window.electronAPI.onMessage((message) => {
console.log('收到消息:', message)
})
console.log('应用版本:', window.electronAPI.appVersion)15.4 第三方插件使用
15.4.1 常用插件
15.4.1.1 electron-store
功能:持久化存储应用配置和用户数据
安装:
bash
npm install electron-store使用:
javascript
const Store = require('electron-store')
const store = new Store({
name: 'app-config',
defaults: {
theme: 'light',
language: 'zh-CN'
}
})
// 存储数据
store.set('user', { name: 'John', email: 'john@example.com' })
// 获取数据
const user = store.get('user')
// 删除数据
store.delete('user')15.4.1.2 electron-log
功能:日志管理
安装:
bash
npm install electron-log使用:
javascript
const log = require('electron-log')
// 配置日志
log.transports.file.level = 'info'
log.transports.console.level = 'debug'
// 使用日志
log.info('应用启动')
log.warn('警告信息')
log.error('错误信息')
log.debug('调试信息')15.4.1.3 electron-updater
功能:应用自动更新
安装:
bash
npm install electron-updater使用:
javascript
const { autoUpdater } = require('electron-updater')
// 检查更新
autoUpdater.checkForUpdatesAndNotify()
// 监听更新事件
autoUpdater.on('update-available', () => {
console.log('有新版本可用')
})
autoUpdater.on('update-downloaded', () => {
console.log('更新已下载完成')
// 提示用户重启应用
})15.5 Electron 版本升级与兼容性处理
15.5.1 版本升级注意事项
主要版本升级:
- 可能包含 breaking changes
- 需要更新依赖
- 需要测试应用功能
次要版本升级:
- 通常包含新功能和 bug 修复
- 兼容性较好
补丁版本升级:
- 主要包含 bug 修复
- 兼容性最好
15.5.2 兼容性处理
处理方法:
使用特性检测:
javascriptconst { app } = require('electron') if (app.isPackaged) { // 生产环境逻辑 } else { // 开发环境逻辑 }版本检测:
javascriptconst { app } = require('electron') const electronVersion = process.versions.electron if (parseInt(electronVersion.split('.')[0]) >= 18) { // 新版本逻辑 } else { // 旧版本逻辑 }使用 polyfill:
- 对于已废弃的 API,提供替代实现
- 对于新 API,提供降级方案
15.6 实操案例:高级 Electron 应用
15.6.1 场景描述
创建一个具有以下功能的 Electron 应用:
- 自动更新
- 自定义启动页
- 安全的 IPC 通信
- 持久化存储
- 日志管理
15.6.2 实现步骤
创建项目结构
advanced-app/ ├── main.js ├── preload.js ├── splash.html ├── index.html ├── package.json └── assets/ ├── icon.ico ├── icon.icns └── icon.png修改 package.json
json{ "name": "advanced-electron-app", "version": "1.0.0", "description": "高级 Electron 应用示例", "main": "main.js", "scripts": { "start": "electron .", "build": "electron-builder" }, "devDependencies": { "electron": "^18.0.0", "electron-builder": "^22.14.5" }, "dependencies": { "electron-store": "^8.0.1", "electron-log": "^4.4.8", "electron-updater": "^4.6.5" }, "build": { "appId": "com.example.advanced-app", "productName": "高级 Electron 应用", "copyright": "Copyright © 2023 Example", "directories": { "output": "dist" }, "files": [ "main.js", "preload.js", "splash.html", "index.html", "package.json", "assets/**/*" ], "mac": { "category": "public.app-category.utilities", "icon": "assets/icon.icns" }, "win": { "target": [ "nsis", "portable" ], "icon": "assets/icon.ico" }, "linux": { "target": [ "deb", "rpm", "AppImage" ], "icon": "assets/icon.png" }, "publish": { "provider": "github", "repo": "username/repository", "owner": "username", "private": false, "releaseType": "release" } } }修改 main.js
javascriptconst { app, BrowserWindow, ipcMain, dialog } = require('electron') const { autoUpdater } = require('electron-updater') const Store = require('electron-store') const log = require('electron-log') const path = require('path') // 配置日志 log.transports.file.level = 'info' log.transports.console.level = 'debug' // 创建存储实例 const store = new Store({ name: 'app-config', defaults: { theme: 'light', language: 'zh-CN', lastWindowState: { width: 800, height: 600 } } }) let splashWindow let mainWindow // 设置自动更新 function setupAutoUpdater() { autoUpdater.logger = log autoUpdater.checkForUpdatesAndNotify() autoUpdater.on('update-available', () => { log.info('有新版本可用') }) autoUpdater.on('update-downloaded', () => { dialog.showMessageBox({ type: 'info', title: '更新可用', message: '新版本已下载完成,是否立即重启应用?', buttons: ['是', '否'] }).then((result) => { if (result.response === 0) { autoUpdater.quitAndInstall() } }) }) autoUpdater.on('error', (error) => { log.error('更新失败:', error) }) } // 创建启动页 function createSplashWindow() { splashWindow = new BrowserWindow({ width: 400, height: 300, frame: false, transparent: true, alwaysOnTop: true, webPreferences: { nodeIntegration: true, contextIsolation: false } }) splashWindow.loadFile('splash.html') } // 创建主窗口 function createMainWindow() { const lastWindowState = store.get('lastWindowState') mainWindow = new BrowserWindow({ width: lastWindowState.width, height: lastWindowState.height, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') }, show: false }) mainWindow.loadFile('index.html') mainWindow.once('ready-to-show', () => { mainWindow.show() if (splashWindow) { splashWindow.close() } }) mainWindow.on('close', () => { // 保存窗口状态 store.set('lastWindowState', { width: mainWindow.getBounds().width, height: mainWindow.getBounds().height }) }) mainWindow.on('closed', () => { mainWindow = null }) } // IPC 事件处理 ipcMain.handle('get-settings', () => { return store.store }) ipcMain.handle('update-settings', (event, settings) => { store.set(settings) return '设置已更新' }) ipcMain.handle('get-app-version', () => { return app.getVersion() }) // 全局错误捕获 process.on('uncaughtException', (error) => { log.error('未捕获异常:', error) }) process.on('unhandledRejection', (reason) => { log.error('未处理的 Promise 拒绝:', reason) }) app.whenReady().then(() => { log.info('应用启动') createSplashWindow() setTimeout(() => { createMainWindow() setupAutoUpdater() }, 2000) }) app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() }) app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) { createSplashWindow() setTimeout(() => { createMainWindow() }, 2000) } })修改 preload.js
javascriptconst { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electronAPI', { // 设置相关 getSettings: () => ipcRenderer.invoke('get-settings'), updateSettings: (settings) => ipcRenderer.invoke('update-settings', settings), // 应用相关 getAppVersion: () => ipcRenderer.invoke('get-app-version'), // 消息传递 sendMessage: (message) => ipcRenderer.send('message', message), onMessage: (callback) => ipcRenderer.on('message', (event, ...args) => callback(...args)) })修改 splash.html
html<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>启动中</title> <style> body { margin: 0; padding: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; background-color: #f0f0f0; } .logo { font-size: 24px; font-weight: bold; margin-bottom: 20px; } .progress { width: 200px; height: 10px; background-color: #e0e0e0; border-radius: 5px; overflow: hidden; } .progress-bar { width: 0; height: 100%; background-color: #4CAF50; transition: width 0.3s ease; } </style> </head> <body> <div class="logo">高级 Electron 应用</div> <div class="progress"> <div class="progress-bar" id="progress-bar"></div> </div> <script> let progress = 0; const progressBar = document.getElementById('progress-bar'); const interval = setInterval(() => { progress += 10; progressBar.style.width = progress + '%'; if (progress >= 100) { clearInterval(interval); } }, 200); </script> </body> </html>修改 index.html
html<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>高级 Electron 应用</title> <style> body { font-family: Arial, sans-serif; margin: 20px; padding: 0; background-color: #f0f0f0; } .container { max-width: 700px; 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; } .info-section { margin: 20px 0; padding: 15px; background-color: #f9f9f9; border-radius: 4px; } .setting-item { margin: 10px 0; } label { display: block; margin-bottom: 5px; font-weight: bold; } select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .version { text-align: center; margin-top: 30px; font-size: 14px; color: #999; } </style> </head> <body> <div class="container"> <h1>高级 Electron 应用</h1> <div class="info-section"> <h3>应用信息</h3> <div id="app-version"></div> </div> <div class="info-section"> <h3>设置</h3> <div class="setting-item"> <label for="theme">主题</label> <select id="theme"> <option value="light">浅色</option> <option value="dark">深色</option> </select> </div> <div class="setting-item"> <label for="language">语言</label> <select id="language"> <option value="zh-CN">中文</option> <option value="en-US">英文</option> </select> </div> </div> <div class="version">版本:<span id="version"></span></div> </div> <script> const themeSelect = document.getElementById('theme') const languageSelect = document.getElementById('language') const versionElement = document.getElementById('version') const appVersionElement = document.getElementById('app-version') // 加载设置 async function loadSettings() { const settings = await window.electronAPI.getSettings() themeSelect.value = settings.theme languageSelect.value = settings.language } // 保存设置 async function saveSettings() { const settings = { theme: themeSelect.value, language: languageSelect.value } await window.electronAPI.updateSettings(settings) } // 加载应用版本 async function loadAppVersion() { const version = await window.electronAPI.getAppVersion() versionElement.textContent = version appVersionElement.textContent = `应用版本:${version}` } // 事件监听 themeSelect.addEventListener('change', saveSettings) languageSelect.addEventListener('change', saveSettings) // 初始化 loadSettings() loadAppVersion() </script> </body> </html>运行应用
bashnpm install npm start
15.7 小结
通过本章的学习,你已经掌握了 Electron 开发的进阶技巧:
- 应用自动更新:使用 autoUpdater 模块实现应用的自动更新功能
- 自定义应用图标和启动页:为应用添加专业的图标和启动页
- 主进程与渲染进程权限控制:使用上下文隔离和预加载脚本提高应用安全性
- 第三方插件使用:使用 electron-store、electron-log 等插件增强应用功能
- 版本升级与兼容性处理:处理不同版本的 Electron 兼容性问题
这些进阶技巧将帮助你开发更加专业、安全和功能丰富的 Electron 应用。在接下来的章节中,我们将学习 Electron 的高频面试题和拓展学习方向。
