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

Electron

Electron 是一个前端框架,可用于构建跨平台的桌面应用程序,桌面应用程序指的是可以在电脑上安装的软件(如QQ、浏览器、酷狗音乐等)。 与 开发者可使用 JavaScript 、 HTML 和 CSS 等前端基础技术,结合 Node.js 进行开发。
Electron
查看更多相关内容
Electron 与 Web 技术的集成Electron 的核心优势在于能够无缝集成各种 Web 技术和框架。本文将详细介绍如何在 Electron 中集成和使用各种 Web 技术。 ## 前端框架集成 ### 1. React 集成 ```bash # 创建 React 应用 npx create-react-app my-electron-app cd my-electron-app # 安装 Electron npm 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" } } ``` ```javascript // public/electron.js const { app, BrowserWindow } = require('electron') const path = require('path') let mainWindow function 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 集成 ```bash # 创建 Vue 应用 npm create vue@latest my-electron-app cd my-electron-app # 安装 Electron npm 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" } } ``` ```javascript // electron/main.js const { app, BrowserWindow } = require('electron') const path = require('path') let mainWindow function 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 集成 ```bash # 创建 Angular 应用 ng new my-electron-app cd my-electron-app # 安装 Electron npm 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" } } ``` ```javascript // electron/main.js const { app, BrowserWindow } = require('electron') const path = require('path') let mainWindow function 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 集成 ```bash npm install redux react-redux @reduxjs/toolkit ``` ```javascript // store/index.js import { configureStore } from '@reduxjs/toolkit' import rootReducer from './reducers' const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { ignoredActions: ['persist/PERSIST'] } }) }) export default store ``` ```javascript // preload.js const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electron', { store: { getState: () => ipcRenderer.invoke('store:getState'), dispatch: (action) => ipcRenderer.invoke('store:dispatch', action) } }) ``` ```javascript // main.js const { 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 集成 ```javascript // store/index.js import { createStore } from 'vuex' export default createStore({ state: { count: 0 }, mutations: { increment(state) { state.count++ } }, actions: { increment({ commit }) { commit('increment') } } }) ``` ```javascript // preload.js const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electron', { store: { getState: () => ipcRenderer.invoke('store:getState'), dispatch: (action) => ipcRenderer.invoke('store:dispatch', action) } }) ``` ### 3. Pinia 集成 ```javascript // stores/counter.js import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), actions: { increment() { this.count++ } } }) ``` ## UI 组件库集成 ### 1. Material-UI 集成 ```bash npm install @mui/material @emotion/react @emotion/styled ``` ```javascript // App.js import 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 App ``` ### 2. Ant Design 集成 ```bash npm install antd ``` ```javascript // App.js import React from 'react' import { Button, Input, Typography, Card } from 'antd' import 'antd/dist/reset.css' const { Title } = Typography function 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 App ``` ### 3. Element Plus 集成 ```bash npm install element-plus ``` ```javascript // main.js import { 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') ``` ```vue <!-- 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 集成 ```javascript // webpack.config.js const 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 集成 ```javascript // vite.config.js import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], base: './', build: { outDir: 'dist', assetsDir: 'assets' } }) ``` ### 3. Parcel 集成 ```javascript // .parcelrc { "extends": "@parcel/config-default", "targets": { "default": { "distDir": "dist" } } } ``` ## CSS 框架集成 ### 1. Tailwind CSS 集成 ```bash npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p ``` ```javascript // tailwind.config.js module.exports = { content: [ "./src/**/*.{js,jsx,ts,tsx}", "./public/index.html" ], theme: { extend: {}, }, plugins: [], } ``` ```css /* src/index.css */ @tailwind base; @tailwind components; @tailwind utilities; ``` ```jsx // src/App.js function 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 集成 ```bash npm install bootstrap ``` ```javascript // src/index.js import 'bootstrap/dist/css/bootstrap.min.css' import 'bootstrap/dist/js/bootstrap.bundle.min.js' ``` ```jsx // src/App.js function 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 集成 ```bash npm install chart.js ``` ```javascript // src/ChartComponent.js import 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 ChartComponent ``` ### 2. ECharts 集成 ```bash npm install echarts ``` ```javascript // src/EChartsComponent.js import 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 集成 ```bash npm install framer-motion ``` ```javascript // src/AnimatedComponent.js import 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 AnimatedComponent ``` ### 2. GSAP 集成 ```bash npm install gsap ``` ```javascript // src/GSAPComponent.js import 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. 环境变量管理 ```javascript // .env.development ELECTRON_START_URL=http://localhost:3000 API_URL=http://localhost:5000 ``` ```javascript // .env.production API_URL=https://api.example.com ``` ```javascript // electron/main.js const startUrl = process.env.ELECTRON_START_URL || url.format({ pathname: path.join(__dirname, '../build/index.html'), protocol: 'file:', slashes: true }) ``` ### 2. 热重载配置 ```bash npm install --save-dev electron-reload ``` ```javascript // electron/main.js if (process.env.NODE_ENV === 'development') { require('electron-reload')(__dirname, { electron: path.join(__dirname, '..', 'node_modules', '.bin', 'electron'), hardResetMethod: 'exit' }) } ``` ### 3. 代码分割 ```javascript // 使用 React.lazy const LazyComponent = React.lazy(() => import('./LazyComponent')) function App() { return ( <React.Suspense fallback={<div>Loading...</div>}> <LazyComponent /> </React.Suspense> ) } ``` ## 常见问题 **Q: 如何在 Electron 中使用 React Router?**&#x41;: 正常使用 React Router,但需要配置 history 为 createHashHistory 或使用 MemoryHistory。 **Q: 如何处理 Electron 和 Web 环境的差异?**&#x41;: 使用环境变量和条件判断,区分不同环境的代码执行。 **Q: 如何优化 Electron 应用的打包体积?**&#x41;: 使用代码分割、Tree Shaking、压缩资源等技术优化打包体积。 **Q: 如何在 Electron 中使用 Service Worker?**&#x41;: 在主进程中注册 Service Worker,确保在正确的上下文中使用。
服务端 · 2月18日 10:43
Electron 安全性最佳实践Electron 应用的安全性至关重要,因为它结合了 Web 技术和 Node.js 的强大功能。如果不正确配置,可能会暴露系统资源给恶意代码。 ## 核心安全配置 ### 1. 禁用 nodeIntegration nodeIntegration 允许渲染进程直接访问 Node.js API,这是最大的安全风险。 ```javascript // main.js const mainWindow = new BrowserWindow({ webPreferences: { nodeIntegration: false, // 必须设置为 false contextIsolation: true, // 必须设置为 true enableRemoteModule: false // 必须设置为 false } }) ``` ### 2. 启用 contextIsolation contextIsolation 将预加载脚本与渲染进程隔离,防止恶意代码访问 Node.js API。 ```javascript // main.js webPreferences: { contextIsolation: true, preload: path.join(__dirname, 'preload.js') } ``` ### 3. 使用 preload 脚本 preload 脚本是在渲染进程加载之前运行的脚本,可以安全地暴露 API。 ```javascript // preload.js const { 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') }) ``` ```javascript // renderer.js // 渲染进程只能访问暴露的 API window.electronAPI.readFile('path/to/file.txt') .then(content => console.log(content)) ``` ## 内容安全策略(CSP) CSP 是一个额外的安全层,帮助防止跨站脚本攻击(XSS)。 ```html <!-- 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 配置说明 ```javascript // main.js mainWindow = new BrowserWindow({ webPreferences: { webSecurity: true // 启用 Web 安全策略 } }) // 为远程内容设置 CSP session.defaultSession.webRequest.onHeadersReceived((details, callback) => { callback({ responseHeaders: { ...details.responseHeaders, 'Content-Security-Policy': ["default-src 'none'"] } }) }) ``` ## 加载内容的安全 ### 1. 只加载可信内容 ```javascript // 好的做法 - 加载本地文件 mainWindow.loadFile('index.html') // 好的做法 - 加载可信的 HTTPS 网站 mainWindow.loadURL('https://example.com') // 不好的做法 - 加载不可信的 HTTP 网站 mainWindow.loadURL('http://untrusted-site.com') ``` ### 2. 验证加载的 URL ```javascript // main.js app.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. 验证输入数据 ```javascript // main.js ipcMain.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 通道 ```javascript // main.js const 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. 使用白名单 ```javascript // preload.js const { 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. 限制系统权限 ```javascript // main.js const mainWindow = new BrowserWindow({ webPreferences: { // 禁用不必要的权限 sandbox: false, // 根据需求启用沙盒 webSecurity: true, allowRunningInsecureContent: false, experimentalFeatures: false, plugins: false } }) ``` ### 2. 使用沙盒模式 ```javascript // main.js const mainWindow = new BrowserWindow({ webPreferences: { sandbox: true, // 启用沙盒模式 nodeIntegration: false, contextIsolation: true } }) ``` ## 数据保护 ### 1. 敏感数据加密 ```javascript // main.js const 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. 安全存储凭据 ```javascript // 使用 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. 验证更新包 ```javascript // main.js const { 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 ```javascript // 确保所有网络请求使用 HTTPS const protocol = 'https:' const updateUrl = `${protocol}//your-server.com/updates` ``` ## 开发与生产环境分离 ```javascript // config.js const 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?**&#x41;: 因为这会暴露 Node.js API 给网页代码,可能被恶意代码利用来访问系统资源。 **Q: contextIsolation 是如何工作的?**&#x41;: 它将预加载脚本和渲染进程的 JavaScript 上下文完全隔离,防止渲染进程访问预加载脚本中的对象。 **Q: 如何处理用户上传的文件?**&#x41;: 验证文件类型和大小,将文件存储在隔离的目录中,使用安全的文件名,避免路径遍历攻击。 **Q: 是否应该使用 remote 模块?**&#x41;: 不应该,remote 模块已被弃用,因为它会绕过安全隔离。使用 IPC 代替。
服务端 · 2月18日 10:42
Electron 与原生应用的对比及选择在选择桌面应用开发技术时,开发者经常需要在 Electron 和原生开发之间做出选择。本文将详细对比这两种方案的优缺点,帮助开发者做出合适的技术选型。 ## Electron 优势 ### 1. 跨平台支持 Electron 最大的优势是一次编写,多平台运行。 ```javascript // 同一套代码可以在 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 技术栈,降低学习成本。 ```javascript // 使用 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 技术的灵活性使得快速迭代成为可能。 ```javascript // 实时预览和热更新 if (process.env.NODE_ENV === 'development') { mainWindow.webContents.openDevTools() mainWindow.loadURL('http://localhost:3000') } ``` **优势**: * 快速部署更新 * A/B 测试容易实现 * 灰度发布简单 ### 4. 丰富的 UI 组件库 可以使用成熟的 Web UI 组件库。 ```javascript // 使用 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. 内存占用高 由于集成了完整的浏览器引擎,内存占用相对较高。 ```javascript // 监控内存使用 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. 启动速度慢 需要加载完整的浏览器环境。 ```javascript // 优化启动速度 app.whenReady().then(() => { const mainWindow = new BrowserWindow({ show: false // 初始不显示 }) mainWindow.loadFile('index.html') // 页面加载完成后再显示 mainWindow.once('ready-to-show', () => { mainWindow.show() }) }) ``` **影响**: * 用户体验不佳 * 不适合需要快速启动的应用 ### 4. 安全性考虑 Web 技术的安全性需要额外关注。 ```javascript // 必须正确配置安全选项 const mainWindow = new BrowserWindow({ webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: true } }) ``` **影响**: * 需要额外的安全配置 * 可能存在 XSS 等安全风险 * 敏感数据处理需要谨慎 ## 原生开发优势 ### 1. 性能优越 原生应用可以直接访问系统资源,性能更优。 ```cpp // C++ 原生代码示例 void processData() { // 直接操作内存 int* data = new int[1000000]; for (int i = 0; i < 1000000; i++) { data[i] = i * 2; } delete[] data; } ``` **优势**: * 启动速度快 * 内存占用低 * CPU 使用效率高 ### 2. 更好的系统集成 原生应用可以深度集成系统功能。 ```swift // macOS 原生代码示例 import Cocoa class 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 // Java 原生代码示例 public class SecureApp { public void processData() { // Java 安全管理器 SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new FilePermission("data.txt", "read")); } } } ``` **优势**: * 系统级别的安全保护 * 更少的攻击面 * 符合平台安全规范 ## 原生开发劣势 ### 1. 开发成本高 需要为每个平台单独开发。 ```cpp // 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. 迭代速度慢 原生应用的编译和部署相对复杂。 ```bash # 原生应用构建流程 # 1. 编译代码 gcc -o app main.c # 2. 打包资源 # 3. 签名 # 4. 打包安装程序 # 5. 发布 ``` **劣势**: * 编译时间长 * 测试复杂 * 发布流程繁琐 ### 4. UI 开发复杂 原生 UI 开发相对复杂。 ```cpp // 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 的场景 1. **内容展示型应用** * 文档编辑器(VS Code) * 笔记应用(Notion) * 阅读应用 2. **Web 应用包装** * 企业内部工具 * 管理后台 * 数据可视化 3. **快速原型开发** * MVP 产品 * 概念验证 * 快速迭代 4. **跨平台工具应用** * 开发工具 * 效率工具 * 通讯应用(Discord, Slack) ### 适合原生开发的场景 1. **高性能应用** * 游戏引擎 * 视频编辑器 * 3D 建模工具 2. **系统集成应用** * 系统工具 * 驱动程序 * 安全软件 3. **资源受限环境** * 嵌入式系统 * 低配置设备 * 移动设备 4. **严格安全要求** * 金融应用 * 政府应用 * 企业级安全软件 ## 混合方案 ### 1. Electron + 原生模块 ```javascript // 使用原生模块提升性能 const nativeModule = require('./build/Release/native-module') const result = nativeModule.performHeavyComputation(data) ``` ### 2. Web 技术包装原生核心 ```javascript // 主进程使用原生代码 const nativeCore = require('./native-core') // 渲染进程使用 Web 技术 ipcMain.handle('process-data', async (event, data) => { return nativeCore.process(data) }) ``` ## 成本分析 ### 开发成本对比 | 项目 | Electron | 原生开发 | | ----- | -------- | ---- | | 初始开发 | 低 | 高 | | 跨平台支持 | 低成本 | 高成本 | | 维护成本 | 低 | 高 | | 学习成本 | 低 | 高 | | 总体成本 | 低-中 | 高 | ### 性能成本对比 | 项目 | Electron | 原生开发 | | ------ | -------- | ---- | | 启动速度 | 慢 | 快 | | 内存占用 | 高 | 低 | | CPU 使用 | 中-高 | 低 | | 应用体积 | 大 | 小 | ## 结论 选择 Electron 如果: * 需要跨平台支持 * 开发团队熟悉 Web 技术 * 快速迭代是优先考虑 * 性能要求不是极高 选择原生开发如果: * 性能是关键因素 * 需要深度系统集成 * 应用体积是关键考虑 * 有严格的性能要求 对于大多数现代桌面应用,Electron 提供了足够的性能和更好的开发效率,是更实用的选择。但对于性能要求极高的应用,原生开发仍然是最佳选择。
服务端 · 2月18日 10:41
Electron 主进程和渲染进程的区别在 Electron 开发中,理解主进程(Main Process)和渲染进程(Renderer Process)的区别至关重要,它们各自承担不同的职责和运行环境。 ## 主进程(Main Process) ### 特点 * 每个 Electron 应用只有一个主进程 * 运行在 Node.js 环境中 * 作为应用程序的入口点,通常在 package.json 的 main 字段中指定 * 拥有完整的 Node.js API 访问权限 ### 职责 * 创建和管理 BrowserWindow 实例 * 控制应用程序的生命周期(启动、退出等) * 处理系统级事件和菜单 * 管理原生窗口和对话框 * 通过 ipcMain 监听来自渲染进程的消息 * 访问操作系统底层功能(文件系统、网络等) ### 示例代码 ```javascript // main.js const { app, BrowserWindow, ipcMain } = require('electron') let mainWindow app.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 ### 示例代码 ```javascript // renderer.js const { 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 架构的核心机制: ### 从渲染进程到主进程 ```javascript // 渲染进程 ipcRenderer.send('channel-name', data) // 主进程 ipcMain.on('channel-name', (event, data) => { // 处理消息 }) ``` ### 从主进程到渲染进程 ```javascript // 主进程 mainWindow.webContents.send('channel-name', data) // 渲染进程 ipcRenderer.on('channel-name', (event, data) => { // 处理消息 }) ``` ## 安全最佳实践 1. **禁用 nodeIntegration**: 在 webPreferences 中设置 `nodeIntegration: false` 2. **启用 contextIsolation**: 设置 `contextIsolation: true` 隔离预加载脚本 3. **使用 preload 脚本**: 通过 preload 脚本安全地暴露必要的 API 4. **使用 contextBridge**: 安全地将 API 暴露给渲染进程 ```javascript // preload.js const { 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?**&#x41;: 出于安全考虑,防止恶意网页代码访问系统资源,提高应用安全性。 **Q: 一个应用可以有多个主进程吗?**&#x41;: 不可以,每个 Electron 应用只能有一个主进程,但可以有多个渲染进程。 **Q: 如何在渲染进程中使用 Node.js 功能?**&#x41;: 通过 preload 脚本和 contextBridge 安全地暴露必要的 API,或通过 IPC 与主进程通信。
服务端 · 2月18日 10:41
Electron 多窗口管理和通信Electron 应用经常需要创建多个窗口来实现复杂的功能,如主窗口、设置窗口、弹窗等。本文将详细介绍 Electron 中的多窗口管理和窗口间通信。 ## 创建多个窗口 ### 1. 基本窗口创建 ```javascript // main.js const { app, BrowserWindow } = require('electron') let mainWindow let settingsWindow app.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. 窗口类型 ```javascript // 主窗口 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. 窗口引用管理 ```javascript // main.js const 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. 窗口状态管理 ```javascript // main.js const 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. 窗口生命周期管理 ```javascript // main.js app.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. 主进程作为中介 ```javascript // main.js const { ipcMain } = require('electron') // 从窗口 A 发送到窗口 B ipcMain.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. 直接窗口通信 ```javascript // 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. 使用事件总线 ```javascript // main.js const 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. 使用主进程存储 ```javascript // main.js const 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 ```javascript // 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 ```javascript // renderer.js const 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. 状态同步 ```javascript // main.js const 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. 数据同步 ```javascript // main.js ipcMain.on('data-update', (event, data) => { // 保存数据 saveData(data) // 通知所有窗口 const windows = BrowserWindow.getAllWindows() windows.forEach(win => { win.webContents.send('data-updated', data) }) }) ``` ### 3. 操作同步 ```javascript // main.js ipcMain.on('perform-action', (event, action) => { // 执行操作 const result = performAction(action) // 通知所有窗口 const windows = BrowserWindow.getAllWindows() windows.forEach(win => { win.webContents.send('action-performed', { action, result }) }) }) ``` ## 窗口布局管理 ### 1. 窗口位置管理 ```javascript // main.js function 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. 窗口大小管理 ```javascript // main.js function 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. 窗口焦点管理 ```javascript // main.js function 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. 窗口创建优化 ```javascript // 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. 内存管理 ```javascript // 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. 错误处理 ```javascript // main.js function 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: 如何防止窗口被关闭?**&#x41;: 监听 beforeunload 事件: ```javascript // renderer.js window.addEventListener('beforeunload', (event) => { if (hasUnsavedChanges()) { event.preventDefault() event.returnValue = false } }) ``` **Q: 如何在窗口间传递大量数据?**&#x41;: 使用主进程作为中介,或者使用共享存储如 IndexedDB。 **Q: 如何实现窗口拖拽?**&#x41;: 使用 -webkit-app-region CSS 属性: ```css .title-bar { -webkit-app-region: drag; } ``` **Q: 如何实现窗口透明效果?**&#x41;: 设置 transparent: true 并使用 CSS: ```javascript const win = new BrowserWindow({ transparent: true, frame: false }) ``` ​
服务端 · 2月18日 10:40
Electron 应用打包和分发的最佳实践将 Electron 应用打包并分发给用户是开发流程的重要环节。本文将介绍 Electron 应用的打包、签名、发布等最佳实践。 ## 打包工具选择 ### 1. electron-builder (推荐) electron-builder 是目前最流行的 Electron 打包工具,支持多平台打包和自动更新。 #### 安装 ```bash npm install --save-dev electron-builder ``` #### 配置示例 ```json // 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-packager electron-packager 是另一个打包工具,功能相对简单。 ```bash npm install --save-dev electron-packager ``` ```javascript // package.json { "scripts": { "package": "electron-packager . MyApp --platform=all --arch=x64 --out=dist/" } } ``` ## Windows 平台打包 ### NSIS 安装程序配置 ```json { "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 应用签名需要购买代码签名证书。 ```json { "build": { "win": { "certificateFile": "path/to/certificate.pfx", "certificatePassword": "your-password" } } } ``` ## macOS 平台打包 ### DMG 配置 ```json { "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 应用需要签名和公证才能在较新版本上正常运行。 ```bash # 签名 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 ``` ```json { "build": { "mac": { "identity": "Developer ID Application: Your Name", "hardenedRuntime": true, "entitlements": "build/entitlements.mac.plist", "entitlementsInherit": "build/entitlements.mac.plist" } } } ``` ## Linux 平台打包 ### AppImage 配置 ```json { "build": { "linux": { "target": "AppImage", "icon": "build/icon.png", "category": "Utility", "maintainer": "Your Name", "vendor": "Your Company", "synopsis": "My Application" } } } ``` ### DEB 包配置 ```json { "build": { "linux": { "target": "deb", "icon": "build/icon.png", "category": "Utility", "depends": ["gconf2", "gconf-service", "libnotify4", "libappindicator1", "libxtst6", "libnss3"] } } } ``` ## 自动更新 ### electron-updater 配置 ```bash npm install electron-updater ``` ```javascript // main.js const { 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() }) ``` ```json // package.json { "build": { "publish": { "provider": "github", "owner": "your-username", "repo": "your-repo" } } } ``` ## 优化应用体积 ### 1. 排除不必要的文件 ```json { "build": { "files": [ "build/**/*", "node_modules/**/*", "package.json" ], "asarUnpack": [ "node_modules/some-module/**/*" ] } } ``` ### 2. 使用生产依赖 ```bash npm install --production ``` ### 3. 压缩资源 ```json { "build": { "compression": "maximum", "fileAssociations": [ { "ext": "myfile", "name": "My File", "role": "Editor" } ] } } ``` ## CI/CD 集成 ### GitHub Actions 示例 ```yaml name: Build and Release on: 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. 禁用开发者工具(生产环境) ```javascript // main.js if (process.env.NODE_ENV === 'production') { mainWindow.webContents.closeDevTools() } ``` ### 2. 使用 CSP(Content Security Policy) ```html <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"> ``` ### 3. 验证更新包 ```javascript autoUpdater.setFeedURL({ url: 'https://your-server.com/updates', headers: { 'Authorization': 'Bearer your-token' } }) ``` ## 常见问题 **Q: 如何减小应用体积?**&#x41;: 排除不必要的文件、使用生产依赖、启用压缩、考虑使用 electron-forge 或 electron-builder 的优化选项。 **Q: macOS 公证失败怎么办?**&#x41;: 确保使用有效的开发者证书、检查 entitlements 配置、使用正确的 app-specific-password。 **Q: 如何实现增量更新?**&#x41;: electron-updater 默认支持增量更新,只需配置正确的发布服务器和版本号。 **Q: Windows 签名证书在哪里购买?**&#x41;: 可以从 DigiCert、Sectigo、GlobalSign 等受信任的证书颁发机构购买。
服务端 · 2月18日 10:39
Electron 性能优化技巧Electron 应用由于集成了 Chromium 和 Node.js,默认情况下会占用较多系统资源。通过合理的优化策略,可以显著提升应用性能和用户体验。 ## 减少应用体积 ### 1. 排除不必要的文件 ```json // package.json { "build": { "files": [ "build/**/*", "node_modules/**/*", "package.json" ], "asar": true, "asarUnpack": [ "node_modules/some-native-module/**/*" ] } } ``` ### 2. 使用生产依赖 ```bash # 开发环境 npm install --save-dev electron electron-builder # 生产环境只安装必要的依赖 npm install --production ``` ### 3. 压缩资源 ```json { "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. 延迟加载模块 ```javascript // 不好的做法 - 在文件顶部加载所有模块 const heavyModule = require('heavy-module') function useHeavyModule() { heavyModule.doSomething() } // 好的做法 - 按需加载 function useHeavyModule() { const heavyModule = require('heavy-module') heavyModule.doSomething() } ``` ### 2. 使用动态 import ```javascript // 渲染进程 async function loadFeature() { const { feature } = await import('./heavy-feature.js') feature.init() } ``` ### 3. 清理不再使用的对象 ```javascript // 及时清理事件监听器 function setupListeners() { const handler = () => console.log('Event') window.addEventListener('resize', handler) // 组件卸载时清理 return () => window.removeEventListener('resize', handler) } // 使用 const cleanup = setupListeners() // 不再需要时 cleanup() ``` ### 4. 限制缓存大小 ```javascript // 使用 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. 使用虚拟列表 ```javascript // 使用 react-window 或 react-virtualized import { 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 操作 ```javascript // 不好的做法 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 ```javascript // 不好的做法 function animate() { element.style.left = position + 'px' position += 1 setTimeout(animate, 16) } // 好的做法 function animate() { element.style.left = position + 'px' position += 1 requestAnimationFrame(animate) } ``` ### 4. 优化图片加载 ```javascript // 使用懒加载 const img = new Image() img.loading = 'lazy' img.src = 'image.jpg' // 使用 WebP 格式 const supportsWebP = document.createElement('canvas') .toDataURL('image/webp') .indexOf('data:image/webp') === 0 const imageFormat = supportsWebP ? 'webp' : 'jpg' img.src = `image.${imageFormat}` ``` ## 进程优化 ### 1. 使用多窗口时共享资源 ```javascript // main.js const sharedSession = session.defaultSession const window1 = new BrowserWindow({ webPreferences: { session: sharedSession } }) const window2 = new BrowserWindow({ webPreferences: { session: sharedSession } }) ``` ### 2. 使用 Worker Threads 处理密集任务 ```javascript // main.js const { 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.js const { parentPort, workerData } = require('worker_threads') const result = performHeavyComputation(workerData) parentPort.postMessage(result) ``` ### 3. 使用子进程处理独立任务 ```javascript // main.js const { 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 缓存 ```javascript // sw.js self.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 ```javascript // main.js app.on('ready', () => { session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { details.requestHeaders['Upgrade-Insecure-Requests'] = '1' callback({ requestHeaders: details.requestHeaders }) }) }) ``` ### 3. 优化 API 请求 ```javascript // 使用请求批处理 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. 延迟加载非关键资源 ```javascript // main.js app.whenReady().then(() => { const mainWindow = new BrowserWindow({ show: false // 初始不显示 }) mainWindow.loadFile('index.html') // 页面加载完成后再显示 mainWindow.once('ready-to-show', () => { mainWindow.show() }) }) ``` ### 2. 预加载常用数据 ```javascript // preload.js const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electronAPI', { getInitialData: () => ipcRenderer.invoke('get-initial-data') }) // renderer.js window.addEventListener('DOMContentLoaded', async () => { const initialData = await window.electronAPI.getInitialData() // 使用初始数据 }) ``` ### 3. 使用代码分割 ```javascript // 使用动态 import 进行代码分割 async function loadFeature() { const { default: Feature } = await import('./features/feature.js') new Feature() } ``` ## 监控和调试 ### 1. 使用 Chrome DevTools ```javascript // main.js const mainWindow = new BrowserWindow({ webPreferences: { devTools: true } }) // 开发环境自动打开 DevTools if (process.env.NODE_ENV === 'development') { mainWindow.webContents.openDevTools() } ``` ### 2. 性能分析 ```javascript // main.js mainWindow.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. 内存监控 ```javascript // 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) ``` ## 最佳实践总结 1. **减少应用体积**: 排除不必要文件、使用生产依赖、压缩资源 2. **内存优化**: 延迟加载、及时清理、限制缓存 3. **渲染优化**: 虚拟列表、减少 DOM 操作、使用 RAF 4. **进程优化**: 共享资源、使用 Worker Threads 5. **网络优化**: Service Worker、HTTP/2、请求批处理 6. **启动优化**: 延迟加载、预加载数据、代码分割 7. **监控调试**: DevTools、性能分析、内存监控 ## 常见问题 **Q: Electron 应用内存占用过高怎么办?**&#x41;: 检查是否有内存泄漏、使用延迟加载、限制缓存大小、及时清理不再使用的对象。 **Q: 如何提高应用启动速度?**&#x41;: 延迟加载非关键资源、预加载常用数据、使用代码分割、优化初始化逻辑。 **Q: 虚拟列表如何实现?**&#x41;: 使用 react-window 或 react-virtualized 等库,只渲染可视区域内的元素。 **Q: 如何检测内存泄漏?**&#x41;: 使用 Chrome DevTools 的 Memory 面板、定期监控内存使用情况、检查事件监听器和定时器是否正确清理。
服务端 · 2月18日 10:39
Electron 调试技巧和工具调试是 Electron 开发中的重要环节,掌握有效的调试技巧和工具可以大大提高开发效率。本文将详细介绍 Electron 的调试方法和工具。 ## 开发者工具 ### 1. 启用开发者工具 ```javascript // main.js const { app, BrowserWindow } = require('electron') let mainWindow app.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+I` * **macOS**: `Cmd+Option+I` ### 3. 自定义快捷键 ```javascript // main.js const { 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` 文件: ```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 ```javascript // main.js const { 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 语句 ```javascript // main.js const { 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 进行调试: ```javascript // renderer.js console.log('Renderer process started') // 使用 debugger 语句 function handleClick() { debugger // 在这里设置断点 console.log('Button clicked') } document.getElementById('myButton').addEventListener('click', handleClick) ``` ### 2. React DevTools ```bash npm install --save-dev react-devtools ``` ```javascript // main.js const { 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 DevTools ```bash npm install --save-dev vue-devtools ``` ```javascript // main.js const { 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 面板 ```javascript // 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 ```javascript // renderer.js // 使用 Performance Observer const 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. 内存分析 ```javascript // 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) ``` ```javascript // renderer.js // 使用 Chrome Memory Profiler function 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 面板 ```javascript // main.js const { 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. 拦截和修改请求 ```javascript // main.js const { 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-log ```bash npm install electron-log ``` ```javascript // main.js const 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. 自定义日志系统 ```javascript // logger.js const fs = require('fs').promises const 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. 全局错误处理 ```javascript // main.js const { 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. 渲染进程错误处理 ```javascript // 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 }) }) ``` ```javascript // main.js const { 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. 使用 Spectron ```bash npm install --save-dev spectron ``` ```javascript // test/app.test.js const Application = require('spectron').Application const 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. 使用 Playwright ```bash npm install --save-dev @playwright/test ``` ```javascript // test/app.spec.js const { 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. 开发环境配置 ```javascript // config.js const isDevelopment = process.env.NODE_ENV === 'development' module.exports = { isDevelopment, devTools: isDevelopment, logLevel: isDevelopment ? 'debug' : 'info' } ``` ### 2. 条件编译 ```javascript // main.js const { 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. 错误上报 ```javascript // main.js const Sentry = require('@sentry/electron') Sentry.init({ dsn: 'your-sentry-dsn', environment: process.env.NODE_ENV }) // 自动捕获错误 Sentry.captureException(error) ``` ## 常见问题 **Q: 如何在主进程中使用 console.log?**&#x41;: 主进程中的 console.log 会输出到终端,可以在终端中查看日志。 **Q: 如何调试多个窗口?**&#x41;: 每个窗口都有独立的 DevTools,可以分别为每个窗口打开 DevTools。 **Q: 如何在生产环境中禁用 DevTools?**&#x41;: 设置 webPreferences.devTools 为 false,并移除打开 DevTools 的代码。 **Q: 如何远程调试 Electron 应用?**&#x41;: 使用 --remote-debugging-port 参数启动应用,然后使用 Chrome 连接到该端口。
服务端 · 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 交互。 ### 为什么需要原生模块 1. **性能优化**: 处理密集计算任务 2. **系统访问**: 访问操作系统底层 API 3. **硬件交互**: 与硬件设备通信 4. **现有库集成**: 使用现有的 C/C++ 库 ## 使用 N-API 创建原生模块 N-API 是 Node.js 提供的稳定 ABI(Application Binary Interface)接口,推荐使用。 ### 1. 项目结构 ``` native-module/ ├── binding.gyp ├── package.json ├── src/ │ └── addon.cpp └── index.js ``` ### 2. 配置 binding.gyp ```python # binding.gyp { "targets": [ { "target_name": "addon", "sources": [ "src/addon.cpp" ], "include_dirs": [ "<!(node -e \"require('nan')\")" ] } ] } ``` ### 3. 编写 C++ 代码 ```cpp // 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. 编译原生模块 ```bash # 安装依赖 npm install node-gyp --save-dev # 编译 node-gyp configure node-gyp build # 或使用 npm scripts npm run build ``` ### 5. 在 Electron 中使用 ```javascript // index.js const 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. 安装 NAN ```bash npm install nan --save-dev ``` ### 2. 编写 C++ 代码 ```cpp // 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 ```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. 重新编译原生模块 ```bash # 安装 electron-rebuild npm install --save-dev electron-rebuild # 重新编译原生模块以匹配 Electron 版本 npm run rebuild ``` ### 3. 在主进程中使用 ```javascript // main.js const { app, BrowserWindow, ipcMain } = require('electron') const addon = require('./build/Release/addon') let mainWindow app.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. 在渲染进程中使用 ```javascript // renderer.js const { 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. 错误处理 ```cpp // src/addon.cpp void 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. 内存管理 ```cpp // src/addon.cpp void 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. 异步操作 ```cpp // 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. 图像处理 ```cpp #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. 文件系统操作 ```cpp #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. 加密解密 ```cpp #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 调试 ```bash # 编译调试版本 node-gyp configure --debug node-gyp build --debug # 使用 GDB 调试 gdb --args electron . ``` ### 2. 添加日志 ```cpp #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 调试器 ```javascript // main.js const addon = require('./build/Release/addon') // 使用 debugger 语句 debugger const result = addon.add(10, 20) ``` ## 跨平台兼容性 ### 1. 条件编译 ```cpp // src/addon.cpp #if defined(_WIN32) #include <windows.h> #elif defined(__APPLE__) #include <CoreFoundation/CoreFoundation.h> #elif defined(__linux__) #include <unistd.h> #endif void PlatformSpecificFunction(const Nan::FunctionCallbackInfo<Value>& info) { #if defined(_WIN32) // Windows 特定代码 #elif defined(__APPLE__) // macOS 特定代码 #elif defined(__linux__) // Linux 特定代码 #endif } ``` ### 2. 统一接口 ```cpp // 提供跨平台统一的接口 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. 减少数据拷贝 ```cpp // 避免不必要的数据拷贝 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. 使用缓存 ```cpp // 缓存计算结果 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 中无法加载怎么办?**&#x41;: 确保使用 electron-rebuild 重新编译原生模块,使其匹配 Electron 的 Node.js 版本。 **Q: 如何在多个 Electron 版本间共享原生模块?**&#x41;: 使用 N-API 创建原生模块,N-API 提供稳定的 ABI,可以在不同 Node.js 版本间共享。 **Q: 原生模块会导致应用体积增加吗?**&#x41;: 会增加,但通常增加量不大。可以通过优化代码和减少依赖来控制体积。 **Q: 如何测试原生模块?**&#x41;: 使用 Node.js 原生测试框架如 node-test-runner,或编写 JavaScript 测试用例调用原生模块进行测试。
服务端 · 2月18日 10:36
Electron 自动更新机制的实现自动更新是桌面应用的重要功能,可以让用户获得最新的功能和修复。Electron 提供了多种自动更新方案,本文将详细介绍如何实现自动更新。 ## electron-updater 基础 electron-updater 是最常用的 Electron 自动更新解决方案。 ### 安装 ```bash npm install electron-updater ``` ### 基本配置 ```javascript // main.js const { app, BrowserWindow } = require('electron') const { autoUpdater } = require('electron-updater') let mainWindow app.whenReady().then(() => { createWindow() // 检查更新 autoUpdater.checkForUpdatesAndNotify() }) function createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600 }) mainWindow.loadFile('index.html') } ``` ## 配置更新服务器 ### 1. GitHub Releases (推荐) ```json // package.json { "build": { "publish": { "provider": "github", "owner": "your-username", "repo": "your-repo" } } } ``` ```javascript // main.js autoUpdater.setFeedURL({ provider: 'github', owner: 'your-username', repo: 'your-repo' }) ``` ### 2. 自定义服务器 ```javascript // main.js autoUpdater.setFeedURL({ url: 'https://your-server.com/updates', headers: { 'Authorization': 'Bearer your-token' } }) ``` ### 3. S3 存储 ```json // package.json { "build": { "publish": { "provider": "s3", "bucket": "your-bucket-name", "path": 'updates' } } } ``` ## 更新事件监听 ```javascript // 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) } ``` ## 渲染进程处理 ```javascript // renderer.js const { 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. 定时检查更新 ```javascript // main.js const CHECK_UPDATE_INTERVAL = 24 * 60 * 60 * 1000 // 24小时 app.whenReady().then(() => { // 应用启动时检查 autoUpdater.checkForUpdatesAndNotify() // 定时检查 setInterval(() => { autoUpdater.checkForUpdates() }, CHECK_UPDATE_INTERVAL) }) ``` ### 2. 静默更新 ```javascript // main.js autoUpdater.autoDownload = true autoUpdater.autoInstallOnAppQuit = true app.on('before-quit', () => { if (autoUpdater.isUpdateDownloaded()) { autoUpdater.quitAndInstall() } }) ``` ### 3. 自定义更新检查 ```javascript // main.js async 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. 版本号格式 ```json // package.json { "version": "1.0.0" } ``` 遵循语义化版本规范(SemVer): * 主版本号.次版本号.修订号 (MAJOR.MINOR.PATCH) * 例如: 1.0.0, 1.2.3, 2.0.0 ### 2. 发布新版本 ```bash # 更新版本号 npm version patch # 1.0.0 -> 1.0.1 npm version minor # 1.0.0 -> 1.1.0 npm version major # 1.0.0 -> 2.0.0 # 构建应用 npm run build # 发布到 GitHub git push --follow-tags ``` ### 3. 发布说明 在 GitHub Releases 中添加发布说明: ```markdown ## 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. 验证更新包 ```javascript // main.js autoUpdater.on('update-downloaded', (info) => { // 验证更新包签名 if (verifyUpdateSignature(info)) { autoUpdater.quitAndInstall() } else { console.error('Update signature verification failed') } }) function verifyUpdateSignature(info) { // 实现签名验证逻辑 return true } ``` ### 2. 使用 HTTPS ```javascript // main.js autoUpdater.setFeedURL({ url: 'https://your-server.com/updates', headers: { 'Authorization': 'Bearer your-token' } }) ``` ### 3. 限制更新频率 ```javascript // main.js let lastUpdateCheck = 0 const 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. 网络错误 ```javascript // main.js autoUpdater.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. 磁盘空间不足 ```javascript // main.js autoUpdater.on('error', (err) => { if (err.message.includes('ENOSPC')) { console.error('No disk space') sendStatusToWindow('Not enough disk space to download update.') } }) ``` ## 最佳实践 ### 1. 用户体验 ```javascript // 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. 回滚机制 ```javascript // main.js // 保留旧版本 const { app } = require('electron') app.on('before-quit', () => { // 在更新前备份当前版本 const currentVersion = app.getVersion() const backupPath = path.join(app.getPath('userData'), 'backup', currentVersion) // 实现备份逻辑 }) ``` ### 3. 测试更新 ```javascript // 开发环境测试 if (process.env.NODE_ENV === 'development') { autoUpdater.setFeedURL({ url: 'http://localhost:3000/updates' }) } ``` ## 常见问题 **Q: 如何实现增量更新?**&#x41;: electron-updater 默认支持增量更新,只需确保服务器配置正确,使用相同的发布流程即可。 **Q: 更新失败后如何重试?**&#x41;: 监听 error 事件,实现重试逻辑: ```javascript let retryCount = 0 const MAX_RETRIES = 3 autoUpdater.on('error', (err) => { if (retryCount < MAX_RETRIES) { retryCount++ setTimeout(() => { autoUpdater.checkForUpdates() }, 5000 * retryCount) } }) ``` **Q: 如何跳过某个版本?**&#x41;: 在应用设置中保存跳过的版本号,检查更新时进行比较: ```javascript const skippedVersion = getSkippedVersion() if (newVersion !== skippedVersion) { // 显示更新提示 } ``` **Q: 更新后如何迁移用户数据?**&#x41;: 在主进程中监听 before-quit 事件,在更新前执行数据迁移逻辑。
服务端 · 2月18日 10:36