Electron
Electron 是一个前端框架,可用于构建跨平台的桌面应用程序,桌面应用程序指的是可以在电脑上安装的软件(如QQ、浏览器、酷狗音乐等)。 与 开发者可使用 JavaScript 、 HTML 和 CSS 等前端基础技术,结合 Node.js 进行开发。

查看更多相关内容
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?**A: 正常使用 React Router,但需要配置 history 为 createHashHistory 或使用 MemoryHistory。
**Q: 如何处理 Electron 和 Web 环境的差异?**A: 使用环境变量和条件判断,区分不同环境的代码执行。
**Q: 如何优化 Electron 应用的打包体积?**A: 使用代码分割、Tree Shaking、压缩资源等技术优化打包体积。
**Q: 如何在 Electron 中使用 Service Worker?**A: 在主进程中注册 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?**A: 因为这会暴露 Node.js API 给网页代码,可能被恶意代码利用来访问系统资源。
**Q: contextIsolation 是如何工作的?**A: 它将预加载脚本和渲染进程的 JavaScript 上下文完全隔离,防止渲染进程访问预加载脚本中的对象。
**Q: 如何处理用户上传的文件?**A: 验证文件类型和大小,将文件存储在隔离的目录中,使用安全的文件名,避免路径遍历攻击。
**Q: 是否应该使用 remote 模块?**A: 不应该,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?**A: 出于安全考虑,防止恶意网页代码访问系统资源,提高应用安全性。
**Q: 一个应用可以有多个主进程吗?**A: 不可以,每个 Electron 应用只能有一个主进程,但可以有多个渲染进程。
**Q: 如何在渲染进程中使用 Node.js 功能?**A: 通过 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: 如何防止窗口被关闭?**A: 监听 beforeunload 事件:
```javascript
// renderer.js
window.addEventListener('beforeunload', (event) => {
if (hasUnsavedChanges()) {
event.preventDefault()
event.returnValue = false
}
})
```
**Q: 如何在窗口间传递大量数据?**A: 使用主进程作为中介,或者使用共享存储如 IndexedDB。
**Q: 如何实现窗口拖拽?**A: 使用 -webkit-app-region CSS 属性:
```css
.title-bar {
-webkit-app-region: drag;
}
```
**Q: 如何实现窗口透明效果?**A: 设置 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: 如何减小应用体积?**A: 排除不必要的文件、使用生产依赖、启用压缩、考虑使用 electron-forge 或 electron-builder 的优化选项。
**Q: macOS 公证失败怎么办?**A: 确保使用有效的开发者证书、检查 entitlements 配置、使用正确的 app-specific-password。
**Q: 如何实现增量更新?**A: electron-updater 默认支持增量更新,只需配置正确的发布服务器和版本号。
**Q: Windows 签名证书在哪里购买?**A: 可以从 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 应用内存占用过高怎么办?**A: 检查是否有内存泄漏、使用延迟加载、限制缓存大小、及时清理不再使用的对象。
**Q: 如何提高应用启动速度?**A: 延迟加载非关键资源、预加载常用数据、使用代码分割、优化初始化逻辑。
**Q: 虚拟列表如何实现?**A: 使用 react-window 或 react-virtualized 等库,只渲染可视区域内的元素。
**Q: 如何检测内存泄漏?**A: 使用 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?**A: 主进程中的 console.log 会输出到终端,可以在终端中查看日志。
**Q: 如何调试多个窗口?**A: 每个窗口都有独立的 DevTools,可以分别为每个窗口打开 DevTools。
**Q: 如何在生产环境中禁用 DevTools?**A: 设置 webPreferences.devTools 为 false,并移除打开 DevTools 的代码。
**Q: 如何远程调试 Electron 应用?**A: 使用 --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 中无法加载怎么办?**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 测试用例调用原生模块进行测试。
服务端 · 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: 如何实现增量更新?**A: electron-updater 默认支持增量更新,只需确保服务器配置正确,使用相同的发布流程即可。
**Q: 更新失败后如何重试?**A: 监听 error 事件,实现重试逻辑:
```javascript
let retryCount = 0
const MAX_RETRIES = 3
autoUpdater.on('error', (err) => {
if (retryCount < MAX_RETRIES) {
retryCount++
setTimeout(() => {
autoUpdater.checkForUpdates()
}, 5000 * retryCount)
}
})
```
**Q: 如何跳过某个版本?**A: 在应用设置中保存跳过的版本号,检查更新时进行比较:
```javascript
const skippedVersion = getSkippedVersion()
if (newVersion !== skippedVersion) {
// 显示更新提示
}
```
**Q: 更新后如何迁移用户数据?**A: 在主进程中监听 before-quit 事件,在更新前执行数据迁移逻辑。
服务端 · 2月18日 10:36