With Electron desktop development
What is Electron
Electron is a framework for building cross-platform desktop applications using JavaScript, HTML, and CSS. It combines Chromium and Node.js into a single runtime environment, allowing developers to create native applications using web technologies. Electron apps can be packaged into executable files for Windows, macOS, and Linux, achieving the goal of "write once, run anywhere."
// A simplest Electron app example
import { app, BrowserWindow } from 'electron'
let mainWindow: BrowserWindow | null = null
app.whenReady().then(() => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
mainWindow.loadFile('index.html')
})
Combining TypeScript with Electron
TypeScript brings type safety and a better development experience to Electron development. Through type definitions, many common runtime errors can be avoided. Install the necessary dependencies:
npm install electron typescript @types/node @types/electron -D
Configure tsconfig.json:
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Main Process and Renderer Process
An Electron app consists of a main process and renderer processes. The main process manages the app lifecycle and creates browser windows, while renderer processes display web pages. They communicate via IPC (Inter-Process Communication).
Main process example:
// src/main.ts
import { app, BrowserWindow, ipcMain } from 'electron'
import path from 'path'
let mainWindow: BrowserWindow
app.on('ready', () => {
mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true
}
})
ipcMain.handle('perform-action', (event, data) => {
console.log('Received data from renderer:', data)
return { status: 'success' }
})
mainWindow.loadFile('renderer/index.html')
})
Renderer process communication example:
// src/renderer/app.ts
import { ipcRenderer } from 'electron'
async function sendDataToMain() {
const response = await ipcRenderer.invoke('perform-action', {
message: 'Hello from renderer'
})
console.log('Response from main:', response)
}
Best Practices for Inter-Process Communication
For security and maintainability, you should:
- Use contextBridge to expose limited APIs in preload scripts
- Validate all IPC communication data
- Use enums to define IPC channel names
Preload script example:
// src/preload.ts
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('electronAPI', {
sendData: (data: unknown) => ipcRenderer.invoke('perform-action', data),
onUpdate: (callback: (data: unknown) => void) =>
ipcRenderer.on('update-data', (event, data) => callback(data))
})
State Management and Data Persistence
Electron apps often need to manage complex state and persist data. You can use state management libraries like Redux or MobX, combined with electron-store for local storage.
// src/store/configStore.ts
import Store from 'electron-store'
interface Config {
theme: 'light' | 'dark'
fontSize: number
lastOpenedFiles: string[]
}
const schema = {
theme: {
type: 'string',
enum: ['light', 'dark'],
default: 'light'
},
fontSize: {
type: 'number',
minimum: 12,
maximum: 24,
default: 14
}
} as const
const configStore = new Store<Config>({ schema })
export function getConfig(): Config {
return {
theme: configStore.get('theme'),
fontSize: configStore.get('fontSize'),
lastOpenedFiles: configStore.get('lastOpenedFiles', [])
}
}
export function updateConfig(updates: Partial<Config>) {
configStore.set(updates)
}
Native Feature Integration
Electron allows access to native OS features like the file system, menus, and tray icons.
File operations example:
// src/native/fileOperations.ts
import { dialog, ipcMain } from 'electron'
import fs from 'fs'
import path from 'path'
ipcMain.handle('open-file-dialog', async (event) => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'All Files', extensions: ['*'] }
]
})
if (!result.canceled && result.filePaths.length > 0) {
const filePath = result.filePaths[0]
const content = fs.readFileSync(filePath, 'utf-8')
return { filePath, content }
}
return null
})
System tray example:
// src/native/tray.ts
import { Tray, Menu, nativeImage } from 'electron'
import path from 'path'
export function createTray(iconPath: string) {
const icon = nativeImage.createFromPath(iconPath)
const tray = new Tray(icon)
const contextMenu = Menu.buildFromTemplate([
{ label: 'Open', click: () => mainWindow.show() },
{ label: 'Quit', click: () => app.quit() }
])
tray.setToolTip('My Electron App')
tray.setContextMenu(contextMenu)
return tray
}
Packaging and Distribution
Use electron-builder to package the app:
// package.json configuration
{
"build": {
"appId": "com.example.myapp",
"productName": "MyApp",
"directories": {
"output": "release"
},
"files": ["dist/**/*", "package.json"],
"win": {
"target": "nsis",
"icon": "build/icon.ico"
},
"mac": {
"target": "dmg",
"icon": "build/icon.icns"
},
"linux": {
"target": "AppImage",
"icon": "build/icon.png"
}
}
}
Packaging command:
npm run build && electron-builder --win --x64
Debugging and Performance Optimization
Debug Electron apps using Chrome DevTools:
// Open DevTools in development mode
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools({ mode: 'detach' })
}
Performance optimization recommendations:
- Use webpack or vite to bundle renderer process code
- Lazy-load non-critical modules
- Use Worker threads for CPU-intensive tasks
- Optimize DOM operations and rendering performance
Webpack configuration example:
// webpack.renderer.config.js
module.exports = {
entry: './src/renderer/index.ts',
output: {
filename: 'renderer.js',
path: path.resolve(__dirname, 'dist/renderer')
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js']
},
target: 'electron-renderer'
}
Security Best Practices
Electron app security considerations:
- Enable contextIsolation
- Disable nodeIntegration
- Use CSP (Content Security Policy)
- Validate all user input
- Keep Electron and dependencies updated
Security configuration example:
new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true
}
})
CSP setup example:
<!-- Add to HTML head -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
">
Testing Strategy
Electron app testing should include:
- Unit tests (Jest)
- Integration tests
- E2E tests (Spectron or Playwright)
Jest test example:
// __tests__/configStore.test.ts
import { getConfig, updateConfig } from '../store/configStore'
describe('configStore', () => {
it('should return default config', () => {
const config = getConfig()
expect(config.theme).toBe('light')
expect(config.fontSize).toBe(14)
})
it('should update config', () => {
updateConfig({ theme: 'dark' })
expect(getConfig().theme).toBe('dark')
})
})
Playwright E2E test example:
// tests/app.spec.ts
import { test, expect } from '@playwright/test'
test('should open window', async () => {
const electronApp = await playwright._electron.launch({
args: ['dist/main.js']
})
const window = await electronApp.firstWindow()
await expect(window).toHaveTitle('My Electron App')
await electronApp.close()
})
Update Mechanism
Implement auto-update functionality:
// src/updater.ts
import { autoUpdater } from 'electron-updater'
import { ipcMain } from 'electron'
export function initAutoUpdate() {
if (process.env.NODE_ENV === 'development') {
autoUpdater.autoDownload = false
}
autoUpdater.on('update-available', () => {
mainWindow.webContents.send('update-available')
})
autoUpdater.on('update-downloaded', () => {
mainWindow.webContents.send('update-downloaded')
})
ipcMain.handle('check-for-updates', () => {
autoUpdater.checkForUpdates()
})
ipcMain.handle('install-update', () => {
autoUpdater.quitAndInstall()
})
}
Multi-Window Management
Complex apps may need to manage multiple windows:
// src/windowManager.ts
import { BrowserWindow } from 'electron'
import path from 'path'
const windows = new Set<BrowserWindow>()
export function createWindow(options = {}) {
const window = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true
},
...options
})
windows.add(window)
window.on('closed', () => {
windows.delete(window)
})
return window
}
export function getWindows() {
return Array.from(windows)
}
Native Menus and Shortcuts
Customize app menus and shortcuts:
// src/menu.ts
import { Menu, MenuItem } from 'electron'
export function createMenu() {
const template: (MenuItemConstructorOptions | MenuItem)[] = [
{
label: 'File',
submenu: [
{
label: 'Open',
accelerator: 'CmdOrCtrl+O',
click: () => openFileDialog()
},
{ type: 'separator' },
{
label: 'Quit',
role: 'quit'
}
]
},
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' }
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
Error Handling and Logging
Implement robust error handling and logging:
// src/logger.ts
import { app } from 'electron'
import fs from 'fs'
import path from 'path'
const logFilePath = path.join(app.getPath('logs'), 'app.log')
export function logError(error: Error) {
const timestamp = new Date().toISOString()
const message = `[${timestamp}] ERROR: ${error.stack || error.message}\n`
fs.appendFile(logFilePath, message, (err) => {
if (err) console.error('Failed to write to log file:', err)
})
console.error(message)
}
// Global error handling
process.on('uncaughtException', (error) => {
logError(error)
})
Native Module Integration
Use Node.js native modules or write your own native addons:
// Compile native modules with node-gyp
// binding.gyp
{
"targets": [
{
"target_name": "my_native_module",
"sources": ["src/native/module.cc"],
"include_dirs": ["<!(node -e \"require('node-addon-api').include\")"],
"dependencies": ["<!(node -e \"require('node-addon-api').gyp\")"],
"defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"]
}
]
}
TypeScript declaration file:
// typings/my-native-module.d.ts
declare module 'my-native-module' {
export function calculate(input: number): number
}
Usage example:
import { calculate } from 'my-native-module'
const result = calculate(42)
console.log('Native module result:', result)
Cross-Platform Compatibility Handling
Handle differences between operating systems:
// src/utils/platform.ts
import { platform } from 'os'
export function getPlatformSpecificConfig() {
switch (platform()) {
case 'darwin':
return {
menuStyle: 'macOS',
shortcutModifier: 'Cmd'
}
case 'win32':
return {
menuStyle: 'windows',
shortcutModifier: 'Ctrl'
}
case 'linux':
return {
menuStyle: 'linux',
shortcutModifier: 'Ctrl'
}
default:
return {
menuStyle: 'default',
shortcutModifier: 'Ctrl'
}
}
}
Performance Monitoring
Implement app performance monitoring:
// src/performance.ts
import { performance, PerformanceObserver } from 'perf_hooks'
import { ipcMain } from 'electron'
const obs = new PerformanceObserver((items) => {
const entries = items.getEntries()
for (const entry of entries) {
console.log(`${entry.name}: ${entry.duration}ms`)
}
})
obs.observe({ entryTypes: ['measure'] })
export function startMeasure(name: string) {
performance.mark(`${name}-start`)
}
export function endMeasure(name: string) {
performance.mark(`${name}-end`)
performance.measure(name, `${name}-start`, `${name}-end`)
}
// IPC performance monitoring
ipcMain.on('ipc-message', (event, message) => {
startMeasure(`ipc-${message.type}`)
// Process message...
endMeasure(`ipc-${message.type}`)
})
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:与WebAssembly