阿里云主机折上折
  • 微信号
Current Site:Index > With Electron desktop development

With Electron desktop development

Author:Chuan Chen 阅读数:7566人阅读 分类: TypeScript

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:

  1. Use contextBridge to expose limited APIs in preload scripts
  2. Validate all IPC communication data
  3. 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:

  1. Use webpack or vite to bundle renderer process code
  2. Lazy-load non-critical modules
  3. Use Worker threads for CPU-intensive tasks
  4. 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:

  1. Enable contextIsolation
  2. Disable nodeIntegration
  3. Use CSP (Content Security Policy)
  4. Validate all user input
  5. 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:

  1. Unit tests (Jest)
  2. Integration tests
  3. 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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.