乐闻世界logo
搜索文章和话题

面试题手册

Electron 与 Web 技术的集成

Electron 的核心优势在于能够无缝集成各种 Web 技术和框架。本文将详细介绍如何在 Electron 中集成和使用各种 Web 技术。前端框架集成1. React 集成# 创建 React 应用npx create-react-app my-electron-appcd my-electron-app# 安装 Electronnpm install --save-dev electron electron-builder# 修改 package.json{ "main": "public/electron.js", "homepage": "./", "scripts": { "electron": "electron .", "electron-dev": "concurrently \"npm start\" \"wait-on http://localhost:3000 && electron .\"", "electron-pack": "electron-builder", "preelectron-pack": "npm run build" }}// public/electron.jsconst { app, BrowserWindow } = require('electron')const path = require('path')let mainWindowfunction createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }) // 开发环境加载开发服务器 const startUrl = process.env.ELECTRON_START_URL || url.format({ pathname: path.join(__dirname, '../build/index.html'), protocol: 'file:', slashes: true }) mainWindow.loadURL(startUrl) if (process.env.ELECTRON_START_URL) { mainWindow.webContents.openDevTools() }}app.whenReady().then(createWindow)2. Vue 集成# 创建 Vue 应用npm create vue@latest my-electron-appcd my-electron-app# 安装 Electronnpm install --save-dev electron electron-builder# 修改 package.json{ "main": "electron/main.js", "scripts": { "electron": "electron .", "electron:dev": "vite & electron .", "electron:build": "vite build && electron-builder" }}// electron/main.jsconst { app, BrowserWindow } = require('electron')const path = require('path')let mainWindowfunction createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }) // 开发环境 if (process.env.NODE_ENV === 'development') { mainWindow.loadURL('http://localhost:5173') mainWindow.webContents.openDevTools() } else { mainWindow.loadFile(path.join(__dirname, '../dist/index.html')) }}app.whenReady().then(createWindow)3. Angular 集成# 创建 Angular 应用ng new my-electron-appcd my-electron-app# 安装 Electronnpm install --save-dev electron electron-builder# 修改 package.json{ "main": "electron/main.js", "scripts": { "electron": "electron .", "electron:dev": "ng build --watch & electron .", "electron:build": "ng build && electron-builder" }}// electron/main.jsconst { app, BrowserWindow } = require('electron')const path = require('path')let mainWindowfunction createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }) // 开发环境 if (process.env.NODE_ENV === 'development') { mainWindow.loadURL('http://localhost:4200') mainWindow.webContents.openDevTools() } else { mainWindow.loadFile(path.join(__dirname, '../dist/my-electron-app/index.html')) }}app.whenReady().then(createWindow)状态管理集成1. Redux 集成npm install redux react-redux @reduxjs/toolkit// store/index.jsimport { configureStore } from '@reduxjs/toolkit'import rootReducer from './reducers'const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { ignoredActions: ['persist/PERSIST'] } })})export default store// preload.jsconst { contextBridge, ipcRenderer } = require('electron')contextBridge.exposeInMainWorld('electron', { store: { getState: () => ipcRenderer.invoke('store:getState'), dispatch: (action) => ipcRenderer.invoke('store:dispatch', action) }})// main.jsconst { ipcMain } = require('electron')const store = require('./store')ipcMain.handle('store:getState', () => { return store.getState()})ipcMain.handle('store:dispatch', (event, action) => { store.dispatch(action)})2. Vuex 集成// store/index.jsimport { createStore } from 'vuex'export default createStore({ state: { count: 0 }, mutations: { increment(state) { state.count++ } }, actions: { increment({ commit }) { commit('increment') } }})// preload.jsconst { contextBridge, ipcRenderer } = require('electron')contextBridge.exposeInMainWorld('electron', { store: { getState: () => ipcRenderer.invoke('store:getState'), dispatch: (action) => ipcRenderer.invoke('store:dispatch', action) }})3. Pinia 集成// stores/counter.jsimport { defineStore } from 'pinia'export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), actions: { increment() { this.count++ } }})UI 组件库集成1. Material-UI 集成npm install @mui/material @emotion/react @emotion/styled// App.jsimport React from 'react'import { Button, TextField, Container, Typography } from '@mui/material'import { createTheme, ThemeProvider } from '@mui/material/styles'const theme = createTheme({ palette: { primary: { main: '#1976d2', }, },})function App() { return ( <ThemeProvider theme={theme}> <Container maxWidth="sm"> <Typography variant="h4" component="h1" gutterBottom> Electron + Material-UI </Typography> <Button variant="contained" color="primary"> Click Me </Button> <TextField fullWidth label="Email" variant="outlined" margin="normal" /> </Container> </ThemeProvider> )}export default App2. Ant Design 集成npm install antd// App.jsimport React from 'react'import { Button, Input, Typography, Card } from 'antd'import 'antd/dist/reset.css'const { Title } = Typographyfunction App() { return ( <Card style={{ width: 400, margin: '100px auto' }}> <Title level={3}>Electron + Ant Design</Title> <Button type="primary">Click Me</Button> <Input placeholder="Enter your email" style={{ marginTop: 16 }} /> </Card> )}export default App3. Element Plus 集成npm install element-plus// main.jsimport { createApp } from 'vue'import ElementPlus from 'element-plus'import 'element-plus/dist/index.css'import App from './App.vue'const app = createApp(App)app.use(ElementPlus)app.mount('#app')<!-- App.vue --><template> <el-card style="width: 400px; margin: 100px auto;"> <h3>Electron + Element Plus</h3> <el-button type="primary">Click Me</el-button> <el-input v-model="email" placeholder="Enter your email" style="margin-top: 16px" /> </el-card></template><script>export default { data() { return { email: '' } }}</script>构建工具集成1. Webpack 集成// webpack.config.jsconst path = require('path')module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader' } }, { test: /\.css$/, use: ['style-loader', 'css-loader'] } ] }, resolve: { extensions: ['.js', '.jsx'] }, target: 'electron-renderer'}2. Vite 集成// vite.config.jsimport { defineConfig } from 'vite'import react from '@vitejs/plugin-react'export default defineConfig({ plugins: [react()], base: './', build: { outDir: 'dist', assetsDir: 'assets' }})3. Parcel 集成// .parcelrc{ "extends": "@parcel/config-default", "targets": { "default": { "distDir": "dist" } }}CSS 框架集成1. Tailwind CSS 集成npm install -D tailwindcss postcss autoprefixernpx tailwindcss init -p// tailwind.config.jsmodule.exports = { content: [ "./src/**/*.{js,jsx,ts,tsx}", "./public/index.html" ], theme: { extend: {}, }, plugins: [],}/* src/index.css */@tailwind base;@tailwind components;@tailwind utilities;// src/App.jsfunction App() { return ( <div className="min-h-screen bg-gray-100 flex items-center justify-center"> <div className="bg-white p-8 rounded-lg shadow-lg"> <h1 className="text-2xl font-bold mb-4">Electron + Tailwind CSS</h1> <button className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"> Click Me </button> </div> </div> )}2. Bootstrap 集成npm install bootstrap// src/index.jsimport 'bootstrap/dist/css/bootstrap.min.css'import 'bootstrap/dist/js/bootstrap.bundle.min.js'// src/App.jsfunction App() { return ( <div className="container mt-5"> <div className="card"> <div className="card-body"> <h1 className="card-title">Electron + Bootstrap</h1> <button className="btn btn-primary">Click Me</button> <input type="email" className="form-control mt-3" placeholder="Enter your email" /> </div> </div> </div> )}图表库集成1. Chart.js 集成npm install chart.js// src/ChartComponent.jsimport React, { useEffect, useRef } from 'react'import Chart from 'chart.js/auto'function ChartComponent() { const chartRef = useRef(null) const chartInstance = useRef(null) useEffect(() => { if (chartRef.current) { const ctx = chartRef.current.getContext('2d') chartInstance.current = new Chart(ctx, { type: 'bar', data: { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [{ label: '# of Votes', data: [12, 19, 3, 5, 2, 3], backgroundColor: [ 'rgba(255, 99, 132, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(255, 206, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(255, 159, 64, 0.2)' ], borderColor: [ 'rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)' ], borderWidth: 1 }] }, options: { scales: { y: { beginAtZero: true } } } }) } return () => { if (chartInstance.current) { chartInstance.current.destroy() } } }, []) return <canvas ref={chartRef}></canvas>}export default ChartComponent2. ECharts 集成npm install echarts// src/EChartsComponent.jsimport React, { useEffect, useRef } from 'react'import * as echarts from 'echarts'function EChartsComponent() { const chartRef = useRef(null) const chartInstance = useRef(null) useEffect(() => { if (chartRef.current) { chartInstance.current = echarts.init(chartRef.current) const option = { title: { text: 'ECharts Example' }, tooltip: {}, xAxis: { data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: {}, series: [{ name: 'Sales', type: 'bar', data: [5, 20, 36, 10, 10, 20, 30] }] } chartInstance.current.setOption(option) } return () => { if (chartInstance.current) { chartInstance.current.dispose() } } }, []) return <div ref={chartRef} style={{ width: '600px', height: '400px' }}></div>}export default EChartsComponent动画库集成1. Framer Motion 集成npm install framer-motion// src/AnimatedComponent.jsimport React from 'react'import { motion } from 'framer-motion'function AnimatedComponent() { return ( <motion.div initial={{ opacity: 0, scale: 0.5 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.5 }} style={{ width: 200, height: 200, backgroundColor: '#1976d2', borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'white' }} > Animated Box </motion.div> )}export default AnimatedComponent2. GSAP 集成npm install gsap// src/GSAPComponent.jsimport React, { useEffect, useRef } from 'react'import gsap from 'gsap'function GSAPComponent() { const boxRef = useRef(null) useEffect(() => { gsap.to(boxRef.current, { rotation: 360, duration: 2, repeat: -1, ease: 'linear' }) }, []) return ( <div ref={boxRef} style={{ width: 100, height: 100, backgroundColor: '#1976d2', borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'white' }} > GSAP Box </div> )}export default GSAPComponent最佳实践1. 环境变量管理// .env.developmentELECTRON_START_URL=http://localhost:3000API_URL=http://localhost:5000// .env.productionAPI_URL=https://api.example.com// electron/main.jsconst startUrl = process.env.ELECTRON_START_URL || url.format({ pathname: path.join(__dirname, '../build/index.html'), protocol: 'file:', slashes: true })2. 热重载配置npm install --save-dev electron-reload// electron/main.jsif (process.env.NODE_ENV === 'development') { require('electron-reload')(__dirname, { electron: path.join(__dirname, '..', 'node_modules', '.bin', 'electron'), hardResetMethod: 'exit' })}3. 代码分割// 使用 React.lazyconst LazyComponent = React.lazy(() => import('./LazyComponent'))function App() { return ( <React.Suspense fallback={<div>Loading...</div>}> <LazyComponent /> </React.Suspense> )}常见问题Q: 如何在 Electron 中使用 React Router?A: 正常使用 React Router,但需要配置 history 为 createHashHistory 或使用 MemoryHistory。Q: 如何处理 Electron 和 Web 环境的差异?A: 使用环境变量和条件判断,区分不同环境的代码执行。Q: 如何优化 Electron 应用的打包体积?A: 使用代码分割、Tree Shaking、压缩资源等技术优化打包体积。Q: 如何在 Electron 中使用 Service Worker?A: 在主进程中注册 Service Worker,确保在正确的上下文中使用。
阅读 0·2月18日 10:43

Electron 安全性最佳实践

Electron 应用的安全性至关重要,因为它结合了 Web 技术和 Node.js 的强大功能。如果不正确配置,可能会暴露系统资源给恶意代码。核心安全配置1. 禁用 nodeIntegrationnodeIntegration 允许渲染进程直接访问 Node.js API,这是最大的安全风险。// main.jsconst mainWindow = new BrowserWindow({ webPreferences: { nodeIntegration: false, // 必须设置为 false contextIsolation: true, // 必须设置为 true enableRemoteModule: false // 必须设置为 false }})2. 启用 contextIsolationcontextIsolation 将预加载脚本与渲染进程隔离,防止恶意代码访问 Node.js API。// main.jswebPreferences: { contextIsolation: true, preload: path.join(__dirname, 'preload.js')}3. 使用 preload 脚本preload 脚本是在渲染进程加载之前运行的脚本,可以安全地暴露 API。// preload.jsconst { contextBridge, ipcRenderer } = require('electron')contextBridge.exposeInMainWorld('electronAPI', { // 只暴露必要的 API readFile: (filePath) => ipcRenderer.invoke('read-file', filePath), writeFile: (filePath, content) => ipcRenderer.invoke('write-file', filePath, content), getVersion: () => ipcRenderer.invoke('get-version')})// renderer.js// 渲染进程只能访问暴露的 APIwindow.electronAPI.readFile('path/to/file.txt') .then(content => console.log(content))内容安全策略(CSP)CSP 是一个额外的安全层,帮助防止跨站脚本攻击(XSS)。<!-- index.html --><meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;">CSP 配置说明// main.jsmainWindow = new BrowserWindow({ webPreferences: { webSecurity: true // 启用 Web 安全策略 }})// 为远程内容设置 CSPsession.defaultSession.webRequest.onHeadersReceived((details, callback) => { callback({ responseHeaders: { ...details.responseHeaders, 'Content-Security-Policy': ["default-src 'none'"] } })})加载内容的安全1. 只加载可信内容// 好的做法 - 加载本地文件mainWindow.loadFile('index.html')// 好的做法 - 加载可信的 HTTPS 网站mainWindow.loadURL('https://example.com')// 不好的做法 - 加载不可信的 HTTP 网站mainWindow.loadURL('http://untrusted-site.com')2. 验证加载的 URL// main.jsapp.on('web-contents-created', (event, contents) => { contents.on('will-navigate', (event, navigationUrl) => { const parsedUrl = new URL(navigationUrl) // 只允许导航到白名单域名 if (['localhost', 'example.com'].includes(parsedUrl.hostname)) { return } event.preventDefault() }) contents.on('new-window', (event, navigationUrl) => { event.preventDefault() // 使用默认浏览器打开外部链接 shell.openExternal(navigationUrl) })})IPC 通信安全1. 验证输入数据// main.jsipcMain.handle('read-file', async (event, filePath) => { // 验证文件路径 if (!isValidFilePath(filePath)) { throw new Error('Invalid file path') } // 确保路径在允许的目录内 const allowedDir = path.join(app.getPath('userData'), 'files') const fullPath = path.resolve(filePath) if (!fullPath.startsWith(allowedDir)) { throw new Error('Access denied') } return fs.promises.readFile(filePath, 'utf-8')})function isValidFilePath(filePath) { // 实现路径验证逻辑 return typeof filePath === 'string' && filePath.length > 0}2. 限制 IPC 通道// main.jsconst allowedChannels = ['read-file', 'write-file', 'get-version']ipcMain.on('channel-name', (event, ...args) => { // 验证通道名称 if (!allowedChannels.includes('channel-name')) { console.warn('Unauthorized IPC channel:', 'channel-name') return } // 处理消息})3. 使用白名单// preload.jsconst { contextBridge, ipcRenderer } = require('electron')const allowedChannels = ['read-file', 'write-file']contextBridge.exposeInMainWorld('electronAPI', { invoke: (channel, ...args) => { if (allowedChannels.includes(channel)) { return ipcRenderer.invoke(channel, ...args) } throw new Error(`Unauthorized channel: ${channel}`) }})权限管理1. 限制系统权限// main.jsconst mainWindow = new BrowserWindow({ webPreferences: { // 禁用不必要的权限 sandbox: false, // 根据需求启用沙盒 webSecurity: true, allowRunningInsecureContent: false, experimentalFeatures: false, plugins: false }})2. 使用沙盒模式// main.jsconst mainWindow = new BrowserWindow({ webPreferences: { sandbox: true, // 启用沙盒模式 nodeIntegration: false, contextIsolation: true }})数据保护1. 敏感数据加密// main.jsconst crypto = require('crypto')function encrypt(text, key) { const algorithm = 'aes-256-cbc' const iv = crypto.randomBytes(16) const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv) let encrypted = cipher.update(text, 'utf8', 'hex') encrypted += cipher.final('hex') return iv.toString('hex') + ':' + encrypted}function decrypt(text, key) { const algorithm = 'aes-256-cbc' const parts = text.split(':') const iv = Buffer.from(parts.shift(), 'hex') const encrypted = parts.join(':') const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), iv) let decrypted = decipher.update(encrypted, 'hex', 'utf8') decrypted += decipher.final('utf8') return decrypted}2. 安全存储凭据// 使用 keytar 存储敏感信息const keytar = require('keytar')async function saveCredentials(username, password) { await keytar.setPassword('MyApp', username, password)}async function getCredentials(username) { return await keytar.getPassword('MyApp', username)}更新安全1. 验证更新包// main.jsconst { autoUpdater } = require('electron-updater')autoUpdater.setFeedURL({ url: 'https://your-server.com/updates', headers: { 'Authorization': 'Bearer your-token' }})autoUpdater.on('update-downloaded', (info) => { // 验证更新包的签名 if (verifyUpdateSignature(info)) { autoUpdater.quitAndInstall() }})2. 使用 HTTPS// 确保所有网络请求使用 HTTPSconst protocol = 'https:'const updateUrl = `${protocol}//your-server.com/updates`开发与生产环境分离// config.jsconst isDev = process.env.NODE_ENV === 'development'module.exports = { isDev, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js'), // 开发环境可以启用 DevTools devTools: isDev }}安全检查清单[ ] 禁用 nodeIntegration[ ] 启用 contextIsolation[ ] 使用 preload 脚本[ ] 配置 CSP[ ] 验证所有输入数据[ ] 限制 IPC 通道[ ] 使用 HTTPS[ ] 加密敏感数据[ ] 验证更新包[ ] 定期更新依赖项[ ] 使用沙盒模式(如适用)[ ] 禁用不必要的权限常见安全问题Q: 为什么不能在渲染进程中直接使用 require?A: 因为这会暴露 Node.js API 给网页代码,可能被恶意代码利用来访问系统资源。Q: contextIsolation 是如何工作的?A: 它将预加载脚本和渲染进程的 JavaScript 上下文完全隔离,防止渲染进程访问预加载脚本中的对象。Q: 如何处理用户上传的文件?A: 验证文件类型和大小,将文件存储在隔离的目录中,使用安全的文件名,避免路径遍历攻击。Q: 是否应该使用 remote 模块?A: 不应该,remote 模块已被弃用,因为它会绕过安全隔离。使用 IPC 代替。
阅读 0·2月18日 10:42

Electron 与原生应用的对比及选择

在选择桌面应用开发技术时,开发者经常需要在 Electron 和原生开发之间做出选择。本文将详细对比这两种方案的优缺点,帮助开发者做出合适的技术选型。Electron 优势1. 跨平台支持Electron 最大的优势是一次编写,多平台运行。// 同一套代码可以在 Windows、macOS、Linux 上运行const { app, BrowserWindow } = require('electron')app.whenReady().then(() => { const mainWindow = new BrowserWindow({ width: 800, height: 600 }) mainWindow.loadFile('index.html')})优势:开发效率高,只需维护一套代码快速覆盖多个平台统一的用户体验2. 开发成本低使用熟悉的 Web 技术栈,降低学习成本。// 使用 React/Vue 等 Web 框架import React from 'react'import ReactDOM from 'react-dom'function App() { return <div>Hello Electron</div>}ReactDOM.render(<App />, document.getElementById('root'))优势:前端开发者可以快速上手丰富的 Web 生态系统热重载等开发工具支持3. 快速迭代Web 技术的灵活性使得快速迭代成为可能。// 实时预览和热更新if (process.env.NODE_ENV === 'development') { mainWindow.webContents.openDevTools() mainWindow.loadURL('http://localhost:3000')}优势:快速部署更新A/B 测试容易实现灰度发布简单4. 丰富的 UI 组件库可以使用成熟的 Web UI 组件库。// 使用 Material-UI、Ant Design 等import { Button, TextField } from '@mui/material'function LoginForm() { return ( <div> <TextField label="Username" /> <Button variant="contained">Login</Button> </div> )}Electron 劣势1. 应用体积大Electron 应用包含了完整的 Chromium 和 Node.js 运行时。典型 Electron 应用体积:- Windows: ~100-200 MB- macOS: ~150-250 MB- Linux: ~150-250 MB影响:下载时间长占用磁盘空间大不适合网络环境差的用户2. 内存占用高由于集成了完整的浏览器引擎,内存占用相对较高。// 监控内存使用setInterval(() => { const memoryUsage = process.memoryUsage() console.log('Memory:', { heapUsed: `${Math.round(memoryUsage.heapUsed / 1024 / 1024)} MB`, rss: `${Math.round(memoryUsage.rss / 1024 / 1024)} MB` })}, 5000)影响:在低配置设备上运行不流畅可能影响其他应用的性能电池消耗增加3. 启动速度慢需要加载完整的浏览器环境。// 优化启动速度app.whenReady().then(() => { const mainWindow = new BrowserWindow({ show: false // 初始不显示 }) mainWindow.loadFile('index.html') // 页面加载完成后再显示 mainWindow.once('ready-to-show', () => { mainWindow.show() })})影响:用户体验不佳不适合需要快速启动的应用4. 安全性考虑Web 技术的安全性需要额外关注。// 必须正确配置安全选项const mainWindow = new BrowserWindow({ webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: true }})影响:需要额外的安全配置可能存在 XSS 等安全风险敏感数据处理需要谨慎原生开发优势1. 性能优越原生应用可以直接访问系统资源,性能更优。// C++ 原生代码示例void processData() { // 直接操作内存 int* data = new int[1000000]; for (int i = 0; i < 1000000; i++) { data[i] = i * 2; } delete[] data;}优势:启动速度快内存占用低CPU 使用效率高2. 更好的系统集成原生应用可以深度集成系统功能。// macOS 原生代码示例import Cocoaclass AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { // 直接访问 macOS API let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) statusItem.button?.title = "My App" }}优势:完整的系统 API 访问更好的原生体验系统通知和集成3. 更小的应用体积原生应用不需要打包运行时环境。典型原生应用体积:- Windows: ~5-50 MB- macOS: ~10-80 MB- Linux: ~5-50 MB优势:下载快速占用空间小适合网络环境差的用户4. 更好的安全性原生应用有更严格的安全模型。// Java 原生代码示例public class SecureApp { public void processData() { // Java 安全管理器 SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new FilePermission("data.txt", "read")); } }}优势:系统级别的安全保护更少的攻击面符合平台安全规范原生开发劣势1. 开发成本高需要为每个平台单独开发。// Windows 平台代码#include <windows.h>void createWindow() { HWND hwnd = CreateWindow(...);}// macOS 平台代码#import <Cocoa/Cocoa.h>void createWindow() { NSWindow* window = [[NSWindow alloc] init];}// Linux 平台代码#include <gtk/gtk.h>void createWindow() { GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL);}劣势:需要多套代码开发周期长维护成本高2. 学习曲线陡峭需要掌握平台特定的语言和 API。不同平台的技术栈:- Windows: C++, C#, Win32 API- macOS: Swift, Objective-C, Cocoa- Linux: C++, Qt, GTK劣势:开发者需要学习多种语言招聘难度大知识迁移成本高3. 迭代速度慢原生应用的编译和部署相对复杂。# 原生应用构建流程# 1. 编译代码gcc -o app main.c# 2. 打包资源# 3. 签名# 4. 打包安装程序# 5. 发布劣势:编译时间长测试复杂发布流程繁琐4. UI 开发复杂原生 UI 开发相对复杂。// C++ Windows UI 代码HWND createButton(HWND parent, const char* text) { return CreateWindow( "BUTTON", text, WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON, 10, 10, 100, 30, parent, NULL, GetModuleHandle(NULL), NULL );}劣势:UI 开发代码量大样式定制复杂跨平台 UI 一致性差技术选型决策树是否需要跨平台支持?├─ 是 → 选择 Electron└─ 否 → 继续判断性能要求是否极高?├─ 是 → 考虑原生开发└─ 否 → 继续判断开发团队是否熟悉 Web 技术?├─ 是 → 选择 Electron└─ 否 → 考虑原生开发应用体积是否是关键因素?├─ 是 → 考虑原生开发└─ 否 → 选择 Electron是否需要深度系统集成?├─ 是 → 考虑原生开发└─ 否 → 选择 Electron典型应用场景适合 Electron 的场景内容展示型应用文档编辑器(VS Code)笔记应用(Notion)阅读应用Web 应用包装企业内部工具管理后台数据可视化快速原型开发MVP 产品概念验证快速迭代跨平台工具应用开发工具效率工具通讯应用(Discord, Slack)适合原生开发的场景高性能应用游戏引擎视频编辑器3D 建模工具系统集成应用系统工具驱动程序安全软件资源受限环境嵌入式系统低配置设备移动设备严格安全要求金融应用政府应用企业级安全软件混合方案1. Electron + 原生模块// 使用原生模块提升性能const nativeModule = require('./build/Release/native-module')const result = nativeModule.performHeavyComputation(data)2. Web 技术包装原生核心// 主进程使用原生代码const nativeCore = require('./native-core')// 渲染进程使用 Web 技术ipcMain.handle('process-data', async (event, data) => { return nativeCore.process(data)})成本分析开发成本对比| 项目 | Electron | 原生开发 || ----- | -------- | ---- || 初始开发 | 低 | 高 || 跨平台支持 | 低成本 | 高成本 || 维护成本 | 低 | 高 || 学习成本 | 低 | 高 || 总体成本 | 低-中 | 高 |性能成本对比| 项目 | Electron | 原生开发 || ------ | -------- | ---- || 启动速度 | 慢 | 快 || 内存占用 | 高 | 低 || CPU 使用 | 中-高 | 低 || 应用体积 | 大 | 小 |结论选择 Electron 如果:需要跨平台支持开发团队熟悉 Web 技术快速迭代是优先考虑性能要求不是极高选择原生开发如果:性能是关键因素需要深度系统集成应用体积是关键考虑有严格的性能要求对于大多数现代桌面应用,Electron 提供了足够的性能和更好的开发效率,是更实用的选择。但对于性能要求极高的应用,原生开发仍然是最佳选择。
阅读 0·2月18日 10:41

Electron 主进程和渲染进程的区别

在 Electron 开发中,理解主进程(Main Process)和渲染进程(Renderer Process)的区别至关重要,它们各自承担不同的职责和运行环境。主进程(Main Process)特点每个 Electron 应用只有一个主进程运行在 Node.js 环境中作为应用程序的入口点,通常在 package.json 的 main 字段中指定拥有完整的 Node.js API 访问权限职责创建和管理 BrowserWindow 实例控制应用程序的生命周期(启动、退出等)处理系统级事件和菜单管理原生窗口和对话框通过 ipcMain 监听来自渲染进程的消息访问操作系统底层功能(文件系统、网络等)示例代码// main.jsconst { app, BrowserWindow, ipcMain } = require('electron')let mainWindowapp.whenReady().then(() => { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }) mainWindow.loadFile('index.html')})ipcMain.on('message-from-renderer', (event, data) => { console.log('Received from renderer:', data) event.reply('message-from-main', 'Hello from main process')})渲染进程(Renderer Process)特点每个 BrowserWindow 都有一个独立的渲染进程运行在 Chromium 浏览器环境中默认情况下无法直接访问 Node.js API使用标准的 Web 技术(HTML、CSS、JavaScript)构建用户界面职责渲染和显示用户界面处理用户交互事件通过 ipcRenderer 与主进程通信执行前端业务逻辑访问 DOM 和浏览器 API示例代码// renderer.jsconst { ipcRenderer } = require('electron')// 发送消息到主进程ipcRenderer.send('message-from-renderer', { data: 'Hello from renderer' })// 监听来自主进程的消息ipcRenderer.on('message-from-main', (event, data) => { console.log('Received from main:', data)})主要区别对比| 特性 | 主进程 | 渲染进程 || ----------- | ---------------- | ----------- || 数量 | 单个 | 多个(每个窗口一个) || 运行环境 | Node.js | Chromium || Node.js API | 完全支持 | 默认不支持 || DOM 访问 | 不支持 | 完全支持 || 主要职责 | 应用生命周期、窗口管理、系统交互 | UI 渲染、用户交互 || 通信方式 | ipcMain | ipcRenderer |进程间通信(IPC)主进程和渲染进程之间通过 IPC 进行通信,这是 Electron 架构的核心机制:从渲染进程到主进程// 渲染进程ipcRenderer.send('channel-name', data)// 主进程ipcMain.on('channel-name', (event, data) => { // 处理消息})从主进程到渲染进程// 主进程mainWindow.webContents.send('channel-name', data)// 渲染进程ipcRenderer.on('channel-name', (event, data) => { // 处理消息})安全最佳实践禁用 nodeIntegration: 在 webPreferences 中设置 nodeIntegration: false启用 contextIsolation: 设置 contextIsolation: true 隔离预加载脚本使用 preload 脚本: 通过 preload 脚本安全地暴露必要的 API使用 contextBridge: 安全地将 API 暴露给渲染进程// preload.jsconst { contextBridge, ipcRenderer } = require('electron')contextBridge.exposeInMainWorld('electronAPI', { sendMessage: (channel, data) => ipcRenderer.send(channel, data), onMessage: (channel, callback) => ipcRenderer.on(channel, callback)})常见问题Q: 为什么渲染进程默认不能访问 Node.js API?A: 出于安全考虑,防止恶意网页代码访问系统资源,提高应用安全性。Q: 一个应用可以有多个主进程吗?A: 不可以,每个 Electron 应用只能有一个主进程,但可以有多个渲染进程。Q: 如何在渲染进程中使用 Node.js 功能?A: 通过 preload 脚本和 contextBridge 安全地暴露必要的 API,或通过 IPC 与主进程通信。
阅读 0·2月18日 10:41

Electron 多窗口管理和通信

Electron 应用经常需要创建多个窗口来实现复杂的功能,如主窗口、设置窗口、弹窗等。本文将详细介绍 Electron 中的多窗口管理和窗口间通信。创建多个窗口1. 基本窗口创建// main.jsconst { app, BrowserWindow } = require('electron')let mainWindowlet settingsWindowapp.whenReady().then(() => { createMainWindow()})function createMainWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }) mainWindow.loadFile('index.html') mainWindow.on('closed', () => { mainWindow = null })}function createSettingsWindow() { if (settingsWindow) { settingsWindow.focus() return } settingsWindow = new BrowserWindow({ width: 600, height: 400, parent: mainWindow, // 设置为主窗口的子窗口 modal: true, // 模态窗口 webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }) settingsWindow.loadFile('settings.html') settingsWindow.on('closed', () => { settingsWindow = null })}2. 窗口类型// 主窗口const mainWindow = new BrowserWindow({ width: 1200, height: 800, title: 'My App', icon: path.join(__dirname, 'icon.png')})// 设置窗口const settingsWindow = new BrowserWindow({ width: 600, height: 400, title: 'Settings', parent: mainWindow, modal: true})// 弹出窗口const popupWindow = new BrowserWindow({ width: 400, height: 300, parent: mainWindow, show: false, // 初始不显示 autoHideMenuBar: true})// 工具窗口const toolWindow = new BrowserWindow({ width: 300, height: 200, frame: false, // 无边框 alwaysOnTop: true, // 始终置顶 transparent: true // 透明背景})窗口管理1. 窗口引用管理// main.jsconst windows = new Map()function createWindow(id, options) { const win = new BrowserWindow(options) windows.set(id, win) win.on('closed', () => { windows.delete(id) }) return win}function getWindow(id) { return windows.get(id)}function closeWindow(id) { const win = getWindow(id) if (win) { win.close() }}function closeAllWindows() { windows.forEach((win, id) => { win.close() }) windows.clear()}2. 窗口状态管理// main.jsconst windowState = new Map()function saveWindowState(id, win) { const [width, height] = win.getSize() const [x, y] = win.getPosition() const isMaximized = win.isMaximized() const isFullScreen = win.isFullScreen() windowState.set(id, { width, height, x, y, isMaximized, isFullScreen })}function restoreWindowState(id, win) { const state = windowState.get(id) if (state) { win.setSize(state.width, state.height) win.setPosition(state.x, state.y) if (state.isMaximized) { win.maximize() } if (state.isFullScreen) { win.setFullScreen(true) } }}// 使用app.whenReady().then(() => { const win = createWindow('main', { width: 1200, height: 800 }) restoreWindowState('main', win) win.on('close', () => { saveWindowState('main', win) })})3. 窗口生命周期管理// main.jsapp.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() }})app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createMainWindow() }})// 防止意外关闭app.on('before-quit', (event) => { const windows = BrowserWindow.getAllWindows() const hasUnsavedChanges = windows.some(win => win.webContents.executeJavaScript('hasUnsavedChanges()') ) if (hasUnsavedChanges) { event.preventDefault() // 提示用户保存 }})窗口间通信1. 主进程作为中介// main.jsconst { ipcMain } = require('electron')// 从窗口 A 发送到窗口 BipcMain.on('send-to-window-b', (event, data) => { const windowB = getWindow('window-b') if (windowB) { windowB.webContents.send('message-from-a', data) }})// 广播到所有窗口ipcMain.on('broadcast', (event, data) => { const windows = BrowserWindow.getAllWindows() windows.forEach(win => { if (win !== BrowserWindow.fromWebContents(event.sender)) { win.webContents.send('broadcast-message', data) } })})2. 直接窗口通信// main.js// 获取特定窗口function sendToWindow(windowId, channel, data) { const win = getWindow(windowId) if (win) { win.webContents.send(channel, data) }}// 从渲染进程调用ipcMain.handle('send-to-window', (event, { windowId, channel, data }) => { sendToWindow(windowId, channel, data)})3. 使用事件总线// main.jsconst EventEmitter = require('events')const eventBus = new EventEmitter()// 订阅事件eventBus.on('window-event', (data) => { console.log('Received event:', data)})// 发布事件eventBus.emit('window-event', { message: 'Hello' })// 在窗口间使用ipcMain.on('window-a-event', (event, data) => { eventBus.emit('window-a-event', data)})ipcMain.on('window-b-event', (event, data) => { eventBus.emit('window-b-event', data)})窗口数据共享1. 使用主进程存储// main.jsconst sharedData = new Map()ipcMain.handle('set-shared-data', (event, { key, value }) => { sharedData.set(key, value) return true})ipcMain.handle('get-shared-data', (event, key) => { return sharedData.get(key)})ipcMain.handle('delete-shared-data', (event, key) => { return sharedData.delete(key)})2. 使用 localStorage// renderer.js// 设置数据localStorage.setItem('shared-key', JSON.stringify(data))// 获取数据const data = JSON.parse(localStorage.getItem('shared-key'))// 监听变化window.addEventListener('storage', (event) => { if (event.key === 'shared-key') { const newData = JSON.parse(event.newValue) // 处理数据变化 }})3. 使用 IndexedDB// renderer.jsconst request = indexedDB.open('SharedDB', 1)request.onupgradeneeded = (event) => { const db = event.target.result const objectStore = db.createObjectStore('sharedData', { keyPath: 'key' })}request.onsuccess = (event) => { const db = event.target.result // 添加数据 const transaction = db.transaction(['sharedData'], 'readwrite') const objectStore = transaction.objectStore('sharedData') objectStore.add({ key: 'myKey', value: 'myValue' }) // 获取数据 const getRequest = objectStore.get('myKey') getRequest.onsuccess = (event) => { console.log(event.target.result) }}窗口同步1. 状态同步// main.jsconst windowStates = new Map()ipcMain.on('window-state-change', (event, state) => { const windowId = getWindowId(event.sender) windowStates.set(windowId, state) // 通知其他窗口 const windows = BrowserWindow.getAllWindows() windows.forEach(win => { if (win !== BrowserWindow.fromWebContents(event.sender)) { win.webContents.send('window-state-update', { windowId, state }) } })})2. 数据同步// main.jsipcMain.on('data-update', (event, data) => { // 保存数据 saveData(data) // 通知所有窗口 const windows = BrowserWindow.getAllWindows() windows.forEach(win => { win.webContents.send('data-updated', data) })})3. 操作同步// main.jsipcMain.on('perform-action', (event, action) => { // 执行操作 const result = performAction(action) // 通知所有窗口 const windows = BrowserWindow.getAllWindows() windows.forEach(win => { win.webContents.send('action-performed', { action, result }) })})窗口布局管理1. 窗口位置管理// main.jsfunction arrangeWindows() { const windows = BrowserWindow.getAllWindows() const screenWidth = require('electron').screen.getPrimaryDisplay().workAreaSize.width const screenHeight = require('electron').screen.getPrimaryDisplay().workAreaSize.height windows.forEach((win, index) => { const [width, height] = win.getSize() const x = (index % 2) * (screenWidth / 2) const y = Math.floor(index / 2) * (screenHeight / 2) win.setPosition(x, y) })}2. 窗口大小管理// main.jsfunction resizeWindows(width, height) { const windows = BrowserWindow.getAllWindows() windows.forEach(win => { win.setSize(width, height) })}function maximizeAllWindows() { const windows = BrowserWindow.getAllWindows() windows.forEach(win => { win.maximize() })}function minimizeAllWindows() { const windows = BrowserWindow.getAllWindows() windows.forEach(win => { win.minimize() })}3. 窗口焦点管理// main.jsfunction focusWindow(windowId) { const win = getWindow(windowId) if (win) { win.focus() }}function focusNextWindow() { const windows = BrowserWindow.getAllWindows() const focusedWindow = BrowserWindow.getFocusedWindow() if (focusedWindow) { const currentIndex = windows.indexOf(focusedWindow) const nextIndex = (currentIndex + 1) % windows.length windows[nextIndex].focus() }}最佳实践1. 窗口创建优化// main.js// 延迟创建窗口function createWindowLazy(id, options) { if (getWindow(id)) { return getWindow(id) } const win = new BrowserWindow({ show: false, // 初始不显示 ...options }) win.once('ready-to-show', () => { win.show() }) windows.set(id, win) return win}2. 内存管理// main.js// 关闭不活跃的窗口function cleanupInactiveWindows() { const windows = BrowserWindow.getAllWindows() const now = Date.now() windows.forEach(win => { const lastActive = win.getLastFocusedWebContents()?.getLastActiveTime() || 0 const inactiveTime = now - lastActive if (inactiveTime > 30 * 60 * 1000) { // 30分钟 win.close() } })}// 定期清理setInterval(cleanupInactiveWindows, 5 * 60 * 1000) // 5分钟3. 错误处理// main.jsfunction safeCreateWindow(id, options) { try { const win = new BrowserWindow(options) windows.set(id, win) win.on('unresponsive', () => { console.error(`Window ${id} became unresponsive`) }) win.on('responsive', () => { console.log(`Window ${id} is responsive again`) }) win.on('crashed', (event, killed) => { console.error(`Window ${id} crashed. Killed: ${killed}`) // 尝试恢复窗口 recreateWindow(id, options) }) return win } catch (error) { console.error(`Failed to create window ${id}:`, error) return null }}常见问题Q: 如何防止窗口被关闭?A: 监听 beforeunload 事件:// renderer.jswindow.addEventListener('beforeunload', (event) => { if (hasUnsavedChanges()) { event.preventDefault() event.returnValue = false }})Q: 如何在窗口间传递大量数据?A: 使用主进程作为中介,或者使用共享存储如 IndexedDB。Q: 如何实现窗口拖拽?A: 使用 -webkit-app-region CSS 属性:.title-bar { -webkit-app-region: drag;}Q: 如何实现窗口透明效果?A: 设置 transparent: true 并使用 CSS:const win = new BrowserWindow({ transparent: true, frame: false})​
阅读 0·2月18日 10:40

Electron 应用打包和分发的最佳实践

将 Electron 应用打包并分发给用户是开发流程的重要环节。本文将介绍 Electron 应用的打包、签名、发布等最佳实践。打包工具选择1. electron-builder (推荐)electron-builder 是目前最流行的 Electron 打包工具,支持多平台打包和自动更新。安装npm install --save-dev electron-builder配置示例// package.json{ "build": { "appId": "com.example.myapp", "productName": "MyApp", "directories": { "output": "dist" }, "files": [ "build/**/*", "node_modules/**/*", "package.json" ], "win": { "target": [ { "target": "nsis", "arch": ["x64", "ia32"] } ], "icon": "build/icon.ico" }, "mac": { "target": [ { "target": "dmg", "arch": ["x64", "arm64"] } ], "icon": "build/icon.icns", "category": "public.app-category.productivity" }, "linux": { "target": [ { "target": "AppImage", "arch": ["x64"] }, { "target": "deb", "arch": ["x64"] } ], "icon": "build/icon.png", "category": "Utility" } }, "scripts": { "build": "electron-builder", "build:win": "electron-builder --win", "build:mac": "electron-builder --mac", "build:linux": "electron-builder --linux" }}2. electron-packagerelectron-packager 是另一个打包工具,功能相对简单。npm install --save-dev electron-packager// package.json{ "scripts": { "package": "electron-packager . MyApp --platform=all --arch=x64 --out=dist/" }}Windows 平台打包NSIS 安装程序配置{ "build": { "win": { "target": [ { "target": "nsis", "arch": ["x64"] } ], "artifactName": "${productName}-${version}-${arch}.${ext}" }, "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true, "createDesktopShortcut": true, "createStartMenuShortcut": true, "shortcutName": "MyApp", "installerIcon": "build/icon.ico", "uninstallerIcon": "build/icon.ico", "installerHeader": "build/header.bmp", "installerSidebar": "build/sidebar.bmp", "deleteAppDataOnUninstall": false } }}代码签名Windows 应用签名需要购买代码签名证书。{ "build": { "win": { "certificateFile": "path/to/certificate.pfx", "certificatePassword": "your-password" } }}macOS 平台打包DMG 配置{ "build": { "mac": { "target": "dmg", "icon": "build/icon.icns", "category": "public.app-category.productivity", "hardenedRuntime": true, "gatekeeperAssess": false, "entitlements": "build/entitlements.mac.plist", "entitlementsInherit": "build/entitlements.mac.plist" }, "dmg": { "contents": [ { "x": 130, "y": 220 }, { "x": 410, "y": 220, "type": "link", "path": "/Applications" } ], "window": { "width": 540, "height": 380 } } }}代码签名和公证macOS 应用需要签名和公证才能在较新版本上正常运行。# 签名codesign --deep --force --verify --verbose --sign "Developer ID Application: Your Name" dist/MyApp.app# 公证xcrun notarytool submit dist/MyApp.dmg --apple-id "your@email.com" --password "app-specific-password" --team-id "TEAMID" --wait{ "build": { "mac": { "identity": "Developer ID Application: Your Name", "hardenedRuntime": true, "entitlements": "build/entitlements.mac.plist", "entitlementsInherit": "build/entitlements.mac.plist" } }}Linux 平台打包AppImage 配置{ "build": { "linux": { "target": "AppImage", "icon": "build/icon.png", "category": "Utility", "maintainer": "Your Name", "vendor": "Your Company", "synopsis": "My Application" } }}DEB 包配置{ "build": { "linux": { "target": "deb", "icon": "build/icon.png", "category": "Utility", "depends": ["gconf2", "gconf-service", "libnotify4", "libappindicator1", "libxtst6", "libnss3"] } }}自动更新electron-updater 配置npm install electron-updater// main.jsconst { autoUpdater } = require('electron-updater')app.whenReady().then(() => { autoUpdater.checkForUpdatesAndNotify()})autoUpdater.on('update-available', (info) => { console.log('Update available:', info)})autoUpdater.on('update-downloaded', (info) => { autoUpdater.quitAndInstall()})// package.json{ "build": { "publish": { "provider": "github", "owner": "your-username", "repo": "your-repo" } }}优化应用体积1. 排除不必要的文件{ "build": { "files": [ "build/**/*", "node_modules/**/*", "package.json" ], "asarUnpack": [ "node_modules/some-module/**/*" ] }}2. 使用生产依赖npm install --production3. 压缩资源{ "build": { "compression": "maximum", "fileAssociations": [ { "ext": "myfile", "name": "My File", "role": "Editor" } ] }}CI/CD 集成GitHub Actions 示例name: Build and Releaseon: push: tags: - 'v*'jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, windows-latest, ubuntu-latest] steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: '16' - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Release uses: softprops/action-gh-release@v1 with: files: dist/* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}安全最佳实践1. 禁用开发者工具(生产环境)// main.jsif (process.env.NODE_ENV === 'production') { mainWindow.webContents.closeDevTools()}2. 使用 CSP(Content Security Policy)<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">3. 验证更新包autoUpdater.setFeedURL({ url: 'https://your-server.com/updates', headers: { 'Authorization': 'Bearer your-token' }})常见问题Q: 如何减小应用体积?A: 排除不必要的文件、使用生产依赖、启用压缩、考虑使用 electron-forge 或 electron-builder 的优化选项。Q: macOS 公证失败怎么办?A: 确保使用有效的开发者证书、检查 entitlements 配置、使用正确的 app-specific-password。Q: 如何实现增量更新?A: electron-updater 默认支持增量更新,只需配置正确的发布服务器和版本号。Q: Windows 签名证书在哪里购买?A: 可以从 DigiCert、Sectigo、GlobalSign 等受信任的证书颁发机构购买。
阅读 0·2月18日 10:39

Electron 性能优化技巧

Electron 应用由于集成了 Chromium 和 Node.js,默认情况下会占用较多系统资源。通过合理的优化策略,可以显著提升应用性能和用户体验。减少应用体积1. 排除不必要的文件// package.json{ "build": { "files": [ "build/**/*", "node_modules/**/*", "package.json" ], "asar": true, "asarUnpack": [ "node_modules/some-native-module/**/*" ] }}2. 使用生产依赖# 开发环境npm install --save-dev electron electron-builder# 生产环境只安装必要的依赖npm install --production3. 压缩资源{ "build": { "compression": "maximum", "files": [ "!**/node_modules/*/{TEST,test,tests,__tests__,examples,example}/**", "!**/node_modules/.bin", "!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}", "!.editorconfig", "!**/._*", "!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}", "!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}", "!**/{appveyor.yml,.travis.yml,circle.yml}", "!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}" ] }}内存优化1. 延迟加载模块// 不好的做法 - 在文件顶部加载所有模块const heavyModule = require('heavy-module')function useHeavyModule() { heavyModule.doSomething()}// 好的做法 - 按需加载function useHeavyModule() { const heavyModule = require('heavy-module') heavyModule.doSomething()}2. 使用动态 import// 渲染进程async function loadFeature() { const { feature } = await import('./heavy-feature.js') feature.init()}3. 清理不再使用的对象// 及时清理事件监听器function setupListeners() { const handler = () => console.log('Event') window.addEventListener('resize', handler) // 组件卸载时清理 return () => window.removeEventListener('resize', handler)}// 使用const cleanup = setupListeners()// 不再需要时cleanup()4. 限制缓存大小// 使用 LRU 缓存const LRU = require('lru-cache')const cache = new LRU({ max: 500, // 最大缓存项数 maxAge: 1000 * 60 * 5 // 5分钟过期})function getData(key) { const cached = cache.get(key) if (cached) return cached const data = fetchData(key) cache.set(key, data) return data}渲染性能优化1. 使用虚拟列表// 使用 react-window 或 react-virtualizedimport { FixedSizeList as List } from 'react-window'const Row = ({ index, style }) => ( <div style={style}>Row {index}</div>)const VirtualList = () => ( <List height={600} itemCount={10000} itemSize={35} width={300} > {Row} </List>)2. 避免频繁的 DOM 操作// 不好的做法for (let i = 0; i < 1000; i++) { document.body.appendChild(createElement(i))}// 好的做法 - 使用文档片段const fragment = document.createDocumentFragment()for (let i = 0; i < 1000; i++) { fragment.appendChild(createElement(i))}document.body.appendChild(fragment)3. 使用 requestAnimationFrame// 不好的做法function animate() { element.style.left = position + 'px' position += 1 setTimeout(animate, 16)}// 好的做法function animate() { element.style.left = position + 'px' position += 1 requestAnimationFrame(animate)}4. 优化图片加载// 使用懒加载const img = new Image()img.loading = 'lazy'img.src = 'image.jpg'// 使用 WebP 格式const supportsWebP = document.createElement('canvas') .toDataURL('image/webp') .indexOf('data:image/webp') === 0const imageFormat = supportsWebP ? 'webp' : 'jpg'img.src = `image.${imageFormat}`进程优化1. 使用多窗口时共享资源// main.jsconst sharedSession = session.defaultSessionconst window1 = new BrowserWindow({ webPreferences: { session: sharedSession }})const window2 = new BrowserWindow({ webPreferences: { session: sharedSession }})2. 使用 Worker Threads 处理密集任务// main.jsconst { Worker } = require('worker_threads')ipcMain.handle('heavy-computation', async (event, data) => { return new Promise((resolve, reject) => { const worker = new Worker('./worker.js', { workerData: data }) worker.on('message', resolve) worker.on('error', reject) worker.on('exit', (code) => { if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`)) }) })})// worker.jsconst { parentPort, workerData } = require('worker_threads')const result = performHeavyComputation(workerData)parentPort.postMessage(result)3. 使用子进程处理独立任务// main.jsconst { spawn } = require('child_process')function runTask(data) { return new Promise((resolve, reject) => { const child = spawn('node', ['task.js', JSON.stringify(data)]) let output = '' child.stdout.on('data', (data) => { output += data.toString() }) child.on('close', (code) => { if (code === 0) { resolve(JSON.parse(output)) } else { reject(new Error(`Process exited with code ${code}`)) } }) })}网络优化1. 使用 Service Worker 缓存// sw.jsself.addEventListener('install', (event) => { event.waitUntil( caches.open('v1').then((cache) => { return cache.addAll([ '/', '/styles/main.css', '/scripts/main.js' ]) }) )})self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request).then((response) => { return response || fetch(event.request) }) )})2. 使用 HTTP/2// main.jsapp.on('ready', () => { session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { details.requestHeaders['Upgrade-Insecure-Requests'] = '1' callback({ requestHeaders: details.requestHeaders }) })})3. 优化 API 请求// 使用请求批处理const batchRequests = []function scheduleRequest(request) { batchRequests.push(request) if (batchRequests.length >= 10) { flushRequests() }}function flushRequests() { const requests = batchRequests.splice(0, batchRequests.length) ipcRenderer.invoke('batch-request', requests)}// 定期刷新setInterval(flushRequests, 100)启动优化1. 延迟加载非关键资源// main.jsapp.whenReady().then(() => { const mainWindow = new BrowserWindow({ show: false // 初始不显示 }) mainWindow.loadFile('index.html') // 页面加载完成后再显示 mainWindow.once('ready-to-show', () => { mainWindow.show() })})2. 预加载常用数据// preload.jsconst { contextBridge, ipcRenderer } = require('electron')contextBridge.exposeInMainWorld('electronAPI', { getInitialData: () => ipcRenderer.invoke('get-initial-data')})// renderer.jswindow.addEventListener('DOMContentLoaded', async () => { const initialData = await window.electronAPI.getInitialData() // 使用初始数据})3. 使用代码分割// 使用动态 import 进行代码分割async function loadFeature() { const { default: Feature } = await import('./features/feature.js') new Feature()}监控和调试1. 使用 Chrome DevTools// main.jsconst mainWindow = new BrowserWindow({ webPreferences: { devTools: true }})// 开发环境自动打开 DevToolsif (process.env.NODE_ENV === 'development') { mainWindow.webContents.openDevTools()}2. 性能分析// main.jsmainWindow.webContents.on('did-finish-load', () => { mainWindow.webContents.executeJavaScript(` const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log(entry.name, entry.duration) } }) observer.observe({ entryTypes: ['measure'] }) `)})3. 内存监控// main.jssetInterval(() => { const memoryUsage = process.memoryUsage() console.log('Memory Usage:', { rss: `${Math.round(memoryUsage.rss / 1024 / 1024)} MB`, heapTotal: `${Math.round(memoryUsage.heapTotal / 1024 / 1024)} MB`, heapUsed: `${Math.round(memoryUsage.heapUsed / 1024 / 1024)} MB`, external: `${Math.round(memoryUsage.external / 1024 / 1024)} MB` })}, 30000)最佳实践总结减少应用体积: 排除不必要文件、使用生产依赖、压缩资源内存优化: 延迟加载、及时清理、限制缓存渲染优化: 虚拟列表、减少 DOM 操作、使用 RAF进程优化: 共享资源、使用 Worker Threads网络优化: Service Worker、HTTP/2、请求批处理启动优化: 延迟加载、预加载数据、代码分割监控调试: DevTools、性能分析、内存监控常见问题Q: Electron 应用内存占用过高怎么办?A: 检查是否有内存泄漏、使用延迟加载、限制缓存大小、及时清理不再使用的对象。Q: 如何提高应用启动速度?A: 延迟加载非关键资源、预加载常用数据、使用代码分割、优化初始化逻辑。Q: 虚拟列表如何实现?A: 使用 react-window 或 react-virtualized 等库,只渲染可视区域内的元素。Q: 如何检测内存泄漏?A: 使用 Chrome DevTools 的 Memory 面板、定期监控内存使用情况、检查事件监听器和定时器是否正确清理。
阅读 0·2月18日 10:39

Electron 调试技巧和工具

调试是 Electron 开发中的重要环节,掌握有效的调试技巧和工具可以大大提高开发效率。本文将详细介绍 Electron 的调试方法和工具。开发者工具1. 启用开发者工具// main.jsconst { app, BrowserWindow } = require('electron')let mainWindowapp.whenReady().then(() => { mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { devTools: true // 确保启用开发者工具 } }) mainWindow.loadFile('index.html') // 自动打开开发者工具 if (process.env.NODE_ENV === 'development') { mainWindow.webContents.openDevTools() }})2. 快捷键Electron 提供了默认的快捷键来打开开发者工具:Windows/Linux: Ctrl+Shift+ImacOS: Cmd+Option+I3. 自定义快捷键// main.jsconst { app, BrowserWindow, globalShortcut } = require('electron')app.whenReady().then(() => { // 注册自定义快捷键 globalShortcut.register('CommandOrControl+Shift+D', () => { const focusedWindow = BrowserWindow.getFocusedWindow() if (focusedWindow) { if (focusedWindow.webContents.isDevToolsOpened()) { focusedWindow.webContents.closeDevTools() } else { focusedWindow.webContents.openDevTools() } } })})app.on('will-quit', () => { // 注销所有快捷键 globalShortcut.unregisterAll()})主进程调试1. 使用 VS Code 调试创建 .vscode/launch.json 文件:{ "version": "0.2.0", "configurations": [ { "name": "Debug Main Process", "type": "node", "request": "launch", "cwd": "${workspaceFolder}", "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", "windows": { "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" }, "args": ["."], "outputCapture": "std" } ]}2. 使用 Chrome DevTools// main.jsconst { app, BrowserWindow } = require('electron')app.whenReady().then(() => { const mainWindow = new BrowserWindow({ width: 1200, height: 800 }) mainWindow.loadFile('index.html') // 在主进程中使用 console.log console.log('Main process started') console.log('Electron version:', process.versions.electron) console.log('Node.js version:', process.versions.node) console.log('Chrome version:', process.versions.chrome)})3. 使用 debugger 语句// main.jsconst { app, BrowserWindow } = require('electron')function initializeApp() { debugger // 在这里设置断点 const mainWindow = new BrowserWindow({ width: 1200, height: 800 }) mainWindow.loadFile('index.html')}app.whenReady().then(initializeApp)渲染进程调试1. 使用 Chrome DevTools渲染进程可以直接使用 Chrome DevTools 进行调试:// renderer.jsconsole.log('Renderer process started')// 使用 debugger 语句function handleClick() { debugger // 在这里设置断点 console.log('Button clicked')}document.getElementById('myButton').addEventListener('click', handleClick)2. React DevToolsnpm install --save-dev react-devtools// main.jsconst { app, BrowserWindow } = require('electron')app.whenReady().then(() => { // 安装 React DevTools const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer') try { installExtension(REACT_DEVELOPER_TOOLS) console.log('React DevTools installed') } catch (error) { console.error('Failed to install React DevTools:', error) } const mainWindow = new BrowserWindow({ width: 1200, height: 800 }) mainWindow.loadFile('index.html')})3. Vue DevToolsnpm install --save-dev vue-devtools// main.jsconst { app, BrowserWindow } = require('electron')app.whenReady().then(() => { // 安装 Vue DevTools const { default: installExtension, VUEJS_DEVTOOLS } = require('electron-devtools-installer') try { installExtension(VUEJS_DEVTOOLS) console.log('Vue DevTools installed') } catch (error) { console.error('Failed to install Vue DevTools:', error) } const mainWindow = new BrowserWindow({ width: 1200, height: 800 }) mainWindow.loadFile('index.html')})性能分析1. 使用 Performance 面板// renderer.js// 开始性能记录performance.mark('start')// 执行一些操作function performOperation() { // 复杂的计算或渲染操作}// 结束性能记录performance.mark('end')// 测量性能performance.measure('operation', 'start', 'end')const measure = performance.getEntriesByName('operation')[0]console.log(`Operation took ${measure.duration}ms`)2. 使用 Chrome Performance API// renderer.js// 使用 Performance Observerconst observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log(`${entry.name}: ${entry.duration}ms`) }})observer.observe({ entryTypes: ['measure', 'navigation', 'resource'] })// 测量特定操作function measureFunction(fn) { const start = performance.now() fn() const end = performance.now() console.log(`Function took ${end - start}ms`)}3. 内存分析// main.js// 监控内存使用setInterval(() => { const memoryUsage = process.memoryUsage() console.log('Memory Usage:', { rss: `${Math.round(memoryUsage.rss / 1024 / 1024)} MB`, heapTotal: `${Math.round(memoryUsage.heapTotal / 1024 / 1024)} MB`, heapUsed: `${Math.round(memoryUsage.heapUsed / 1024 / 1024)} MB`, external: `${Math.round(memoryUsage.external / 1024 / 1024)} MB` })}, 30000)// renderer.js// 使用 Chrome Memory Profilerfunction takeHeapSnapshot() { if (window.performance && window.performance.memory) { console.log('Memory Info:', { usedJSHeapSize: `${Math.round(window.performance.memory.usedJSHeapSize / 1024 / 1024)} MB`, totalJSHeapSize: `${Math.round(window.performance.memory.totalJSHeapSize / 1024 / 1024)} MB`, jsHeapSizeLimit: `${Math.round(window.performance.memory.jsHeapSizeLimit / 1024 / 1024)} MB` }) }}// 定期检查内存setInterval(takeHeapSnapshot, 10000)网络调试1. 使用 Network 面板// main.jsconst { session } = require('electron')app.whenReady().then(() => { // 监听网络请求 session.defaultSession.webRequest.onBeforeRequest((details, callback) => { console.log('Request:', details.url) callback({}) }) session.defaultSession.webRequest.onCompleted((details) => { console.log('Response:', details.url, details.statusCode) }) session.defaultSession.webRequest.onErrorOccurred((details) => { console.error('Error:', details.url, details.error) })})2. 拦截和修改请求// main.jsconst { session } = require('electron')app.whenReady().then(() => { // 拦截请求 session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { // 添加自定义请求头 details.requestHeaders['X-Custom-Header'] = 'CustomValue' callback({ requestHeaders: details.requestHeaders }) }) // 修改响应 session.defaultSession.webRequest.onHeadersReceived((details, callback) => { // 添加自定义响应头 details.responseHeaders['X-Custom-Response'] = ['CustomResponse'] callback({ responseHeaders: details.responseHeaders }) })})日志记录1. 使用 electron-lognpm install electron-log// main.jsconst log = require('electron-log')// 配置日志log.transports.file.level = 'debug'log.transports.console.level = 'debug'// 记录日志log.info('Application started')log.debug('Debug information')log.warn('Warning message')log.error('Error occurred')// 记录对象log.info('User data:', { name: 'John', age: 30 })// 记录错误堆栈try { // 可能出错的代码} catch (error) { log.error('Error:', error)}2. 自定义日志系统// logger.jsconst fs = require('fs').promisesconst path = require('path')const { app } = require('electron')class Logger { constructor() { this.logPath = path.join(app.getPath('userData'), 'logs') this.currentLogFile = path.join(this.logPath, this.getLogFileName()) } getLogFileName() { const date = new Date() return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}.log` } async log(level, message, data = null) { const timestamp = new Date().toISOString() const logEntry = `[${timestamp}] [${level}] ${message}` if (data) { console.log(logEntry, data) await this.writeToFile(logEntry + ' ' + JSON.stringify(data)) } else { console.log(logEntry) await this.writeToFile(logEntry) } } async writeToFile(content) { try { await fs.mkdir(this.logPath, { recursive: true }) await fs.appendFile(this.currentLogFile, content + '\n') } catch (error) { console.error('Failed to write log:', error) } } info(message, data) { this.log('INFO', message, data) } debug(message, data) { this.log('DEBUG', message, data) } warn(message, data) { this.log('WARN', message, data) } error(message, data) { this.log('ERROR', message, data) }}module.exports = new Logger()错误处理1. 全局错误处理// main.jsconst { app, dialog } = require('electron')// 捕获未处理的异常process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error) dialog.showErrorBox('Error', `An error occurred: ${error.message}`) // 记录错误 log.error('Uncaught Exception:', error)})// 捕获未处理的 Promise 拒绝process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection:', reason) dialog.showErrorBox('Error', `An error occurred: ${reason}`) // 记录错误 log.error('Unhandled Rejection:', reason)})// 捕获渲染进程错误app.on('render-process-gone', (event, webContents, details) => { console.error('Render process gone:', details) dialog.showErrorBox('Error', 'The application encountered an error and needs to restart.') // 重新加载页面 if (webContents) { webContents.reload() }})2. 渲染进程错误处理// renderer.js// 捕获全局错误window.addEventListener('error', (event) => { console.error('Global error:', event.error) // 发送错误到主进程 const { ipcRenderer } = require('electron') ipcRenderer.send('renderer-error', { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, stack: event.error?.stack })})// 捕获未处理的 Promise 拒绝window.addEventListener('unhandledrejection', (event) => { console.error('Unhandled promise rejection:', event.reason) // 发送错误到主进程 const { ipcRenderer } = require('electron') ipcRenderer.send('renderer-error', { reason: event.reason })})// main.jsconst { ipcMain } = require('electron')ipcMain.on('renderer-error', (event, errorData) => { console.error('Renderer error:', errorData) // 记录错误 log.error('Renderer error:', errorData) // 显示错误对话框 const { dialog } = require('electron') dialog.showErrorBox('Error', 'An error occurred in the renderer process')})测试工具1. 使用 Spectronnpm install --save-dev spectron// test/app.test.jsconst Application = require('spectron').Applicationconst path = require('path')describe('Application launch', function() { this.timeout(10000) beforeEach(async function() { this.app = new Application({ path: path.join(__dirname, '..', 'node_modules', '.bin', 'electron'), args: [path.join(__dirname, '..')] }) await this.app.start() }) afterEach(async function() { if (this.app && this.app.isRunning()) { await this.app.stop() } }) it('shows an initial window', async function() { const windowCount = await this.app.client.getWindowCount() assert.equal(windowCount, 1) }) it('window title is correct', async function() { const title = await this.app.client.getTitle() assert.equal(title, 'My Electron App') })})2. 使用 Playwrightnpm install --save-dev @playwright/test// test/app.spec.jsconst { test, expect } = require('@playwright/test')const { _electron: electron } = require('playwright')test('launch app', async () => { const app = await electron.launch({ path: require('electron') }) const window = await app.firstWindow() const title = await window.title() expect(title).toBe('My Electron App') await app.close()})最佳实践1. 开发环境配置// config.jsconst isDevelopment = process.env.NODE_ENV === 'development'module.exports = { isDevelopment, devTools: isDevelopment, logLevel: isDevelopment ? 'debug' : 'info'}2. 条件编译// main.jsconst { isDevelopment } = require('./config')app.whenReady().then(() => { const mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { devTools: isDevelopment } }) mainWindow.loadFile('index.html') if (isDevelopment) { mainWindow.webContents.openDevTools() }})3. 错误上报// main.jsconst Sentry = require('@sentry/electron')Sentry.init({ dsn: 'your-sentry-dsn', environment: process.env.NODE_ENV})// 自动捕获错误Sentry.captureException(error)常见问题Q: 如何在主进程中使用 console.log?A: 主进程中的 console.log 会输出到终端,可以在终端中查看日志。Q: 如何调试多个窗口?A: 每个窗口都有独立的 DevTools,可以分别为每个窗口打开 DevTools。Q: 如何在生产环境中禁用 DevTools?A: 设置 webPreferences.devTools 为 false,并移除打开 DevTools 的代码。Q: 如何远程调试 Electron 应用?A: 使用 --remote-debugging-port 参数启动应用,然后使用 Chrome 连接到该端口。
阅读 0·2月18日 10:37

Electron 中如何实现原生模块

原生模块(Native Module)允许 Electron 应用使用 C/C++ 等原生代码,可以显著提升性能或访问系统底层功能。本文将详细介绍如何在 Electron 中实现原生模块。原生模块概述原生模块是使用 C/C++ 编写的 Node.js 模块,通过 Node.js 的 N-API(Node API)或 NAN(Node.js Native Abstractions for Node.js)与 JavaScript 交互。为什么需要原生模块性能优化: 处理密集计算任务系统访问: 访问操作系统底层 API硬件交互: 与硬件设备通信现有库集成: 使用现有的 C/C++ 库使用 N-API 创建原生模块N-API 是 Node.js 提供的稳定 ABI(Application Binary Interface)接口,推荐使用。1. 项目结构native-module/├── binding.gyp├── package.json├── src/│ └── addon.cpp└── index.js2. 配置 binding.gyp# binding.gyp{ "targets": [ { "target_name": "addon", "sources": [ "src/addon.cpp" ], "include_dirs": [ "<!(node -e \"require('nan')\")" ] } ]}3. 编写 C++ 代码// src/addon.cpp#include <node_api.h>#include <string>napi_value Add(napi_env env, napi_callback_info info) { size_t argc = 2; napi_value args[2]; napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); double a, b; napi_get_value_double(env, args[0], &a); napi_get_value_double(env, args[1], &b); napi_value sum; napi_create_double(env, a + b, &sum); return sum;}napi_value Init(napi_env env, napi_value exports) { napi_value add_fn; napi_create_function(env, nullptr, 0, Add, nullptr, &add_fn); napi_set_named_property(env, exports, "add", add_fn); return exports;}NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)4. 编译原生模块# 安装依赖npm install node-gyp --save-dev# 编译node-gyp configurenode-gyp build# 或使用 npm scriptsnpm run build5. 在 Electron 中使用// index.jsconst addon = require('./build/Release/addon')const result = addon.add(10, 20)console.log(result) // 30使用 NAN 创建原生模块NAN(Node.js Native Abstractions for Node.js)提供了更简单的 API,但不如 N-API 稳定。1. 安装 NANnpm install nan --save-dev2. 编写 C++ 代码// src/addon.cpp#include <nan.h>using namespace v8;void Add(const Nan::FunctionCallbackInfo<Value>& info) { Isolate* isolate = info.GetIsolate(); if (info.Length() < 2) { isolate->ThrowException(Exception::TypeError( String::NewFromUtf8(isolate, "Wrong number of arguments").ToLocalChecked())); return; } if (!info[0]->IsNumber() || !info[1]->IsNumber()) { isolate->ThrowException(Exception::TypeError( String::NewFromUtf8(isolate, "Wrong arguments").ToLocalChecked())); return; } double a = info[0].As<Number>()->Value(); double b = info[1].As<Number>()->Value(); double sum = a + b; info.GetReturnValue().Set(sum);}void Init(Local<Object> exports) { Nan::SetMethod(exports, "add", Add);}NODE_MODULE(NODE_GYP_MODULE_NAME, Init)在 Electron 中使用原生模块1. 配置 package.json{ "name": "my-electron-app", "version": "1.0.0", "main": "main.js", "dependencies": { "electron-rebuild": "^3.2.9" }, "scripts": { "rebuild": "electron-rebuild", "start": "electron ." }}2. 重新编译原生模块# 安装 electron-rebuildnpm install --save-dev electron-rebuild# 重新编译原生模块以匹配 Electron 版本npm run rebuild3. 在主进程中使用// main.jsconst { app, BrowserWindow, ipcMain } = require('electron')const addon = require('./build/Release/addon')let mainWindowapp.whenReady().then(() => { mainWindow = new BrowserWindow({ width: 800, height: 600 }) mainWindow.loadFile('index.html')})// 使用原生模块处理计算密集型任务ipcMain.handle('heavy-computation', async (event, data) => { const result = addon.performComputation(data) return result})4. 在渲染进程中使用// renderer.jsconst { ipcRenderer } = require('electron')async function performHeavyComputation(data) { try { const result = await ipcRenderer.invoke('heavy-computation', data) return result } catch (error) { console.error('Computation failed:', error) }}// 使用document.getElementById('compute').addEventListener('click', async () => { const data = { /* computation data */ } const result = await performHeavyComputation(data) console.log('Result:', result)})原生模块最佳实践1. 错误处理// src/addon.cppvoid SafeOperation(const Nan::FunctionCallbackInfo<Value>& info) { Isolate* isolate = info.GetIsolate(); try { // 执行可能失败的操作 double result = performOperation(); info.GetReturnValue().Set(result); } catch (const std::exception& e) { isolate->ThrowException(Exception::Error( String::NewFromUtf8(isolate, e.what()).ToLocalChecked())); }}2. 内存管理// src/addon.cppvoid ProcessData(const Nan::FunctionCallbackInfo<Value>& info) { Isolate* isolate = info.GetIsolate(); // 获取输入数据 Local<Array> inputArray = Local<Array>::Cast(info[0]); size_t length = inputArray->Length(); // 分配内存 double* buffer = new double[length]; // 处理数据 for (size_t i = 0; i < length; i++) { Local<Value> element = inputArray->Get(i); buffer[i] = element->NumberValue(isolate->GetCurrentContext()).ToChecked(); } // 执行计算 double result = compute(buffer, length); // 释放内存 delete[] buffer; info.GetReturnValue().Set(result);}3. 异步操作// src/addon.cpp#include <uv.h>struct AsyncData { uv_work_t request; Nan::Persistent<Function> callback; double input; double result;};void AsyncWork(uv_work_t* req) { AsyncData* data = static_cast<AsyncData*>(req->data); // 执行耗时操作 data->result = performHeavyComputation(data->input);}void AsyncComplete(uv_work_t* req, int status) { Nan::HandleScope scope; AsyncData* data = static_cast<AsyncData*>(req->data); Local<Value> argv[] = { Nan::Null(), Nan::New(data->result) }; Local<Function> callback = Nan::New(data->callback); callback->Call(Nan::Null(), 2, argv); delete data;}void AsyncComputation(const Nan::FunctionCallbackInfo<Value>& info) { AsyncData* data = new AsyncData(); data->request.data = data; data->input = info[0]->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); data->callback.Reset(info[1].As<Function>()); uv_queue_work(uv_default_loop(), &data->request, AsyncWork, AsyncComplete);}常见应用场景1. 图像处理#include <opencv2/opencv.hpp>void ProcessImage(const Nan::FunctionCallbackInfo<Value>& info) { // 使用 OpenCV 处理图像 cv::Mat image = cv::imread("input.jpg"); cv::GaussianBlur(image, image, cv::Size(5, 5), 0); cv::imwrite("output.jpg", image); info.GetReturnValue().Set(Nan::New("Image processed").ToLocalChecked());}2. 文件系统操作#include <fstream>void ReadFile(const Nan::FunctionCallbackInfo<Value>& info) { String::Utf8Value path(info[0]); std::ifstream file(*path, std::ios::binary); std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); info.GetReturnValue().Set(Nan::New(content).ToLocalChecked());}3. 加密解密#include <openssl/aes.h>void Encrypt(const Nan::FunctionCallbackInfo<Value>& info) { // 使用 OpenSSL 进行加密 unsigned char key[16] = { /* 16字节密钥 */ }; unsigned char iv[16] = { /* 16字节IV */ }; // 执行加密操作 // ... info.GetReturnValue().Set(Nan::New("Encrypted").ToLocalChecked());}调试原生模块1. 使用 GDB 调试# 编译调试版本node-gyp configure --debugnode-gyp build --debug# 使用 GDB 调试gdb --args electron .2. 添加日志#include <iostream>void DebugFunction(const Nan::FunctionCallbackInfo<Value>& info) { std::cout << "Debug: Function called" << std::endl; // 执行操作 std::cout << "Debug: Function completed" << std::endl;}3. 使用 Node.js 调试器// main.jsconst addon = require('./build/Release/addon')// 使用 debugger 语句debuggerconst result = addon.add(10, 20)跨平台兼容性1. 条件编译// src/addon.cpp#if defined(_WIN32) #include <windows.h>#elif defined(__APPLE__) #include <CoreFoundation/CoreFoundation.h>#elif defined(__linux__) #include <unistd.h>#endifvoid PlatformSpecificFunction(const Nan::FunctionCallbackInfo<Value>& info) {#if defined(_WIN32) // Windows 特定代码#elif defined(__APPLE__) // macOS 特定代码#elif defined(__linux__) // Linux 特定代码#endif}2. 统一接口// 提供跨平台统一的接口void GetSystemInfo(const Nan::FunctionCallbackInfo<Value>& info) { Isolate* isolate = info.GetIsolate(); Local<Object> result = Object::New(isolate);#if defined(_WIN32) SYSTEM_INFO sysInfo; GetSystemInfo(&sysInfo); result->Set(isolate->GetCurrentContext(), Nan::New("processors").ToLocalChecked(), Nan::New(sysInfo.dwNumberOfProcessors));#elif defined(__APPLE__) || defined(__linux__) long processors = sysconf(_SC_NPROCESSORS_ONLN); result->Set(isolate->GetCurrentContext(), Nan::New("processors").ToLocalChecked(), Nan::New(processors));#endif info.GetReturnValue().Set(result);}性能优化1. 减少数据拷贝// 避免不必要的数据拷贝void ProcessArray(const Nan::FunctionCallbackInfo<Value>& info) { Local<Array> array = Local<Array>::Cast(info[0]); // 直接访问数组元素,避免拷贝 for (uint32_t i = 0; i < array->Length(); i++) { Local<Value> element = array->Get(i); // 处理元素 }}2. 使用缓存// 缓存计算结果static std::unordered_map<std::string, double> cache;void CachedComputation(const Nan::FunctionCallbackInfo<Value>& info) { String::Utf8Value key(info[0]); auto it = cache.find(*key); if (it != cache.end()) { info.GetReturnValue().Set(it->second); return; } double result = performComputation(*key); cache[*key] = result; info.GetReturnValue().Set(result);}常见问题Q: 原生模块在 Electron 中无法加载怎么办?A: 确保使用 electron-rebuild 重新编译原生模块,使其匹配 Electron 的 Node.js 版本。Q: 如何在多个 Electron 版本间共享原生模块?A: 使用 N-API 创建原生模块,N-API 提供稳定的 ABI,可以在不同 Node.js 版本间共享。Q: 原生模块会导致应用体积增加吗?A: 会增加,但通常增加量不大。可以通过优化代码和减少依赖来控制体积。Q: 如何测试原生模块?A: 使用 Node.js 原生测试框架如 node-test-runner,或编写 JavaScript 测试用例调用原生模块进行测试。
阅读 0·2月18日 10:36

Electron 自动更新机制的实现

自动更新是桌面应用的重要功能,可以让用户获得最新的功能和修复。Electron 提供了多种自动更新方案,本文将详细介绍如何实现自动更新。electron-updater 基础electron-updater 是最常用的 Electron 自动更新解决方案。安装npm install electron-updater基本配置// main.jsconst { app, BrowserWindow } = require('electron')const { autoUpdater } = require('electron-updater')let mainWindowapp.whenReady().then(() => { createWindow() // 检查更新 autoUpdater.checkForUpdatesAndNotify()})function createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600 }) mainWindow.loadFile('index.html')}配置更新服务器1. GitHub Releases (推荐)// package.json{ "build": { "publish": { "provider": "github", "owner": "your-username", "repo": "your-repo" } }}// main.jsautoUpdater.setFeedURL({ provider: 'github', owner: 'your-username', repo: 'your-repo'})2. 自定义服务器// main.jsautoUpdater.setFeedURL({ url: 'https://your-server.com/updates', headers: { 'Authorization': 'Bearer your-token' }})3. S3 存储// package.json{ "build": { "publish": { "provider": "s3", "bucket": "your-bucket-name", "path": 'updates' } }}更新事件监听// main.js// 检查到更新autoUpdater.on('update-available', (info) => { console.log('Update available:', info.version) sendStatusToWindow('Update available')})// 更新已下载autoUpdater.on('update-downloaded', (info) => { console.log('Update downloaded:', info.version) sendStatusToWindow('Update downloaded') // 提示用户重启应用 dialog.showMessageBox(mainWindow, { type: 'info', title: 'Update Available', message: 'A new version has been downloaded. Restart the application to apply the update.', buttons: ['Restart', 'Later'] }).then((result) => { if (result.response === 0) { autoUpdater.quitAndInstall() } })})// 更新不可用autoUpdater.on('update-not-available', (info) => { console.log('Update not available') sendStatusToWindow('Update not available')})// 更新错误autoUpdater.on('error', (err) => { console.error('Update error:', err) sendStatusToWindow('Update error: ' + err.message)})// 下载进度autoUpdater.on('download-progress', (progressObj) => { let log_message = "Download speed: " + progressObj.bytesPerSecond log_message = log_message + ' - Downloaded ' + progressObj.percent + '%' log_message = log_message + ' (' + progressObj.transferred + "/" + progressObj.total + ')' sendStatusToWindow(log_message)})function sendStatusToWindow(text) { mainWindow.webContents.send('update-status', text)}渲染进程处理// renderer.jsconst { ipcRenderer } = require('electron')// 监听更新状态ipcRenderer.on('update-status', (event, message) => { console.log('Update status:', message) updateStatusElement.textContent = message})// 手动检查更新document.getElementById('check-update').addEventListener('click', () => { ipcRenderer.send('check-for-updates')})// 主进程处理ipcMain.on('check-for-updates', () => { autoUpdater.checkForUpdates()})高级配置1. 定时检查更新// main.jsconst CHECK_UPDATE_INTERVAL = 24 * 60 * 60 * 1000 // 24小时app.whenReady().then(() => { // 应用启动时检查 autoUpdater.checkForUpdatesAndNotify() // 定时检查 setInterval(() => { autoUpdater.checkForUpdates() }, CHECK_UPDATE_INTERVAL)})2. 静默更新// main.jsautoUpdater.autoDownload = trueautoUpdater.autoInstallOnAppQuit = trueapp.on('before-quit', () => { if (autoUpdater.isUpdateDownloaded()) { autoUpdater.quitAndInstall() }})3. 自定义更新检查// main.jsasync function checkForUpdates() { try { const updateCheckResult = await autoUpdater.checkForUpdates() if (updateCheckResult.updateInfo.version !== app.getVersion()) { // 有新版本 return { hasUpdate: true, version: updateCheckResult.updateInfo.version, releaseNotes: updateCheckResult.updateInfo.releaseNotes } } else { // 没有新版本 return { hasUpdate: false } } } catch (error) { console.error('Update check failed:', error) return { hasUpdate: false, error: error.message } }}版本管理1. 版本号格式// package.json{ "version": "1.0.0"}遵循语义化版本规范(SemVer):主版本号.次版本号.修订号 (MAJOR.MINOR.PATCH)例如: 1.0.0, 1.2.3, 2.0.02. 发布新版本# 更新版本号npm version patch # 1.0.0 -> 1.0.1npm version minor # 1.0.0 -> 1.1.0npm version major # 1.0.0 -> 2.0.0# 构建应用npm run build# 发布到 GitHubgit push --follow-tags3. 发布说明在 GitHub Releases 中添加发布说明:## Version 1.1.0### New Features- Added feature X- Added feature Y### Bug Fixes- Fixed bug A- Fixed bug B### Improvements- Improved performance- Enhanced user experience安全考虑1. 验证更新包// main.jsautoUpdater.on('update-downloaded', (info) => { // 验证更新包签名 if (verifyUpdateSignature(info)) { autoUpdater.quitAndInstall() } else { console.error('Update signature verification failed') }})function verifyUpdateSignature(info) { // 实现签名验证逻辑 return true}2. 使用 HTTPS// main.jsautoUpdater.setFeedURL({ url: 'https://your-server.com/updates', headers: { 'Authorization': 'Bearer your-token' }})3. 限制更新频率// main.jslet lastUpdateCheck = 0const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000 // 24小时async function checkForUpdatesWithRateLimit() { const now = Date.now() if (now - lastUpdateCheck < UPDATE_CHECK_INTERVAL) { console.log('Update check skipped - too soon') return } lastUpdateCheck = now return await autoUpdater.checkForUpdates()}错误处理1. 网络错误// main.jsautoUpdater.on('error', (err) => { if (err.message.includes('ERR_INTERNET_DISCONNECTED')) { console.error('Network disconnected') sendStatusToWindow('Network disconnected. Please check your internet connection.') } else if (err.message.includes('ERR_CONNECTION_REFUSED')) { console.error('Connection refused') sendStatusToWindow('Cannot connect to update server.') } else { console.error('Update error:', err) sendStatusToWindow('Update failed: ' + err.message) }})2. 磁盘空间不足// main.jsautoUpdater.on('error', (err) => { if (err.message.includes('ENOSPC')) { console.error('No disk space') sendStatusToWindow('Not enough disk space to download update.') }})最佳实践1. 用户体验// main.js// 在应用空闲时检查更新app.on('ready', () => { setTimeout(() => { autoUpdater.checkForUpdatesAndNotify() }, 5000) // 延迟5秒,避免影响启动速度})// 提供更新进度反馈autoUpdater.on('download-progress', (progress) => { const percentage = Math.round(progress.percent) sendStatusToWindow(`Downloading update: ${percentage}%`)})2. 回滚机制// main.js// 保留旧版本const { app } = require('electron')app.on('before-quit', () => { // 在更新前备份当前版本 const currentVersion = app.getVersion() const backupPath = path.join(app.getPath('userData'), 'backup', currentVersion) // 实现备份逻辑})3. 测试更新// 开发环境测试if (process.env.NODE_ENV === 'development') { autoUpdater.setFeedURL({ url: 'http://localhost:3000/updates' })}常见问题Q: 如何实现增量更新?A: electron-updater 默认支持增量更新,只需确保服务器配置正确,使用相同的发布流程即可。Q: 更新失败后如何重试?A: 监听 error 事件,实现重试逻辑:let retryCount = 0const MAX_RETRIES = 3autoUpdater.on('error', (err) => { if (retryCount < MAX_RETRIES) { retryCount++ setTimeout(() => { autoUpdater.checkForUpdates() }, 5000 * retryCount) }})Q: 如何跳过某个版本?A: 在应用设置中保存跳过的版本号,检查更新时进行比较:const skippedVersion = getSkippedVersion()if (newVersion !== skippedVersion) { // 显示更新提示}Q: 更新后如何迁移用户数据?A: 在主进程中监听 before-quit 事件,在更新前执行数据迁移逻辑。
阅读 0·2月18日 10:36