发布于

Electron 桌面应用开发:使用 Web 技术构建跨平台桌面应用

作者

Electron 桌面应用开发:使用 Web 技术构建跨平台桌面应用

Electron 是一个使用 JavaScript、HTML 和 CSS 构建跨平台桌面应用的框架。本文将深入探讨 Electron 的架构原理、开发实践和性能优化。

Electron 架构基础

主进程与渲染进程

// main.js - 主进程
const { app, BrowserWindow, Menu, ipcMain, dialog } = require('electron')
const path = require('path')
const isDev = process.env.NODE_ENV === 'development'

class ElectronApp {
  constructor() {
    this.mainWindow = null
    this.init()
  }

  init() {
    // 应用准备就绪
    app.whenReady().then(() => {
      this.createWindow()
      this.createMenu()
      this.setupIPC()

      app.on('activate', () => {
        if (BrowserWindow.getAllWindows().length === 0) {
          this.createWindow()
        }
      })
    })

    // 所有窗口关闭
    app.on('window-all-closed', () => {
      if (process.platform !== 'darwin') {
        app.quit()
      }
    })

    // 应用即将退出
    app.on('before-quit', (event) => {
      if (this.mainWindow && !this.mainWindow.isDestroyed()) {
        event.preventDefault()
        this.mainWindow.webContents.send('app-closing')
      }
    })
  }

  createWindow() {
    this.mainWindow = new BrowserWindow({
      width: 1200,
      height: 800,
      minWidth: 800,
      minHeight: 600,
      show: false,
      icon: path.join(__dirname, 'assets/icon.png'),
      webPreferences: {
        nodeIntegration: false,
        contextIsolation: true,
        enableRemoteModule: false,
        preload: path.join(__dirname, 'preload.js'),
      },
    })

    // 加载应用
    if (isDev) {
      this.mainWindow.loadURL('http://localhost:3000')
      this.mainWindow.webContents.openDevTools()
    } else {
      this.mainWindow.loadFile('dist/index.html')
    }

    // 窗口事件
    this.mainWindow.once('ready-to-show', () => {
      this.mainWindow.show()

      if (isDev) {
        this.mainWindow.webContents.openDevTools()
      }
    })

    this.mainWindow.on('closed', () => {
      this.mainWindow = null
    })
  }

  createMenu() {
    const template = [
      {
        label: '文件',
        submenu: [
          {
            label: '新建',
            accelerator: 'CmdOrCtrl+N',
            click: () => {
              this.mainWindow.webContents.send('menu-new-file')
            },
          },
          {
            label: '打开',
            accelerator: 'CmdOrCtrl+O',
            click: async () => {
              const result = await dialog.showOpenDialog(this.mainWindow, {
                properties: ['openFile'],
                filters: [
                  { name: 'Text Files', extensions: ['txt', 'md'] },
                  { name: 'All Files', extensions: ['*'] },
                ],
              })

              if (!result.canceled) {
                this.mainWindow.webContents.send('menu-open-file', result.filePaths[0])
              }
            },
          },
          { type: 'separator' },
          {
            label: '退出',
            accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
            click: () => {
              app.quit()
            },
          },
        ],
      },
      {
        label: '编辑',
        submenu: [
          { role: 'undo', label: '撤销' },
          { role: 'redo', label: '重做' },
          { type: 'separator' },
          { role: 'cut', label: '剪切' },
          { role: 'copy', label: '复制' },
          { role: 'paste', label: '粘贴' },
        ],
      },
      {
        label: '视图',
        submenu: [
          { role: 'reload', label: '重新加载' },
          { role: 'forceReload', label: '强制重新加载' },
          { role: 'toggleDevTools', label: '开发者工具' },
          { type: 'separator' },
          { role: 'resetZoom', label: '实际大小' },
          { role: 'zoomIn', label: '放大' },
          { role: 'zoomOut', label: '缩小' },
          { type: 'separator' },
          { role: 'togglefullscreen', label: '全屏' },
        ],
      },
    ]

    const menu = Menu.buildFromTemplate(template)
    Menu.setApplicationMenu(menu)
  }

  setupIPC() {
    // 文件操作
    ipcMain.handle('read-file', async (event, filePath) => {
      try {
        const fs = require('fs').promises
        const content = await fs.readFile(filePath, 'utf8')
        return { success: true, content }
      } catch (error) {
        return { success: false, error: error.message }
      }
    })

    ipcMain.handle('write-file', async (event, filePath, content) => {
      try {
        const fs = require('fs').promises
        await fs.writeFile(filePath, content, 'utf8')
        return { success: true }
      } catch (error) {
        return { success: false, error: error.message }
      }
    })

    // 系统信息
    ipcMain.handle('get-system-info', () => {
      const os = require('os')
      return {
        platform: process.platform,
        arch: process.arch,
        version: process.getSystemVersion(),
        memory: os.totalmem(),
        cpus: os.cpus().length,
      }
    })

    // 应用信息
    ipcMain.handle('get-app-info', () => {
      return {
        name: app.getName(),
        version: app.getVersion(),
        path: app.getAppPath(),
      }
    })
  }
}

new ElectronApp()

预加载脚本

// preload.js - 预加载脚本
const { contextBridge, ipcRenderer } = require('electron')

// 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
  // 文件操作
  readFile: (filePath) => ipcRenderer.invoke('read-file', filePath),
  writeFile: (filePath, content) => ipcRenderer.invoke('write-file', filePath, content),

  // 系统信息
  getSystemInfo: () => ipcRenderer.invoke('get-system-info'),
  getAppInfo: () => ipcRenderer.invoke('get-app-info'),

  // 菜单事件监听
  onMenuNewFile: (callback) => ipcRenderer.on('menu-new-file', callback),
  onMenuOpenFile: (callback) => ipcRenderer.on('menu-open-file', callback),
  onAppClosing: (callback) => ipcRenderer.on('app-closing', callback),

  // 移除监听器
  removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel),

  // 窗口控制
  minimizeWindow: () => ipcRenderer.invoke('minimize-window'),
  maximizeWindow: () => ipcRenderer.invoke('maximize-window'),
  closeWindow: () => ipcRenderer.invoke('close-window'),

  // 通知
  showNotification: (title, body) => {
    new Notification(title, { body })
  },

  // 剪贴板
  writeToClipboard: (text) => {
    navigator.clipboard.writeText(text)
  },

  readFromClipboard: () => {
    return navigator.clipboard.readText()
  },
})

// 渲染进程中使用
// renderer.js
class App {
  constructor() {
    this.init()
  }

  async init() {
    // 获取系统信息
    const systemInfo = await window.electronAPI.getSystemInfo()
    console.log('系统信息:', systemInfo)

    // 监听菜单事件
    window.electronAPI.onMenuNewFile(() => {
      this.createNewFile()
    })

    window.electronAPI.onMenuOpenFile((event, filePath) => {
      this.openFile(filePath)
    })

    window.electronAPI.onAppClosing(() => {
      this.saveBeforeClose()
    })

    // 设置 UI 事件
    this.setupUI()
  }

  async createNewFile() {
    // 创建新文件逻辑
    document.getElementById('editor').value = ''
    document.title = '新文件 - 文本编辑器'
  }

  async openFile(filePath) {
    try {
      const result = await window.electronAPI.readFile(filePath)
      if (result.success) {
        document.getElementById('editor').value = result.content
        document.title = `${filePath} - 文本编辑器`
      } else {
        alert(`打开文件失败: ${result.error}`)
      }
    } catch (error) {
      alert(`打开文件失败: ${error.message}`)
    }
  }

  async saveFile(filePath, content) {
    try {
      const result = await window.electronAPI.writeFile(filePath, content)
      if (result.success) {
        window.electronAPI.showNotification('保存成功', '文件已保存')
      } else {
        alert(`保存文件失败: ${result.error}`)
      }
    } catch (error) {
      alert(`保存文件失败: ${error.message}`)
    }
  }

  saveBeforeClose() {
    const content = document.getElementById('editor').value
    if (content.trim()) {
      // 提示用户保存
      const save = confirm('是否保存当前文件?')
      if (save) {
        // 这里应该实现保存逻辑
      }
    }
  }

  setupUI() {
    // 快捷键
    document.addEventListener('keydown', (e) => {
      if (e.ctrlKey || e.metaKey) {
        switch (e.key) {
          case 's':
            e.preventDefault()
            this.saveCurrentFile()
            break
          case 'o':
            e.preventDefault()
            // 触发打开文件对话框
            break
        }
      }
    })

    // 拖拽文件
    document.addEventListener('dragover', (e) => {
      e.preventDefault()
    })

    document.addEventListener('drop', async (e) => {
      e.preventDefault()
      const files = Array.from(e.dataTransfer.files)
      if (files.length > 0) {
        await this.openFile(files[0].path)
      }
    })
  }
}

new App()

原生功能集成

系统集成

// main.js - 系统集成功能
const { app, BrowserWindow, Tray, nativeImage, powerMonitor, screen } = require('electron')

class SystemIntegration {
  constructor(mainWindow) {
    this.mainWindow = mainWindow
    this.tray = null
    this.setupSystemIntegration()
  }

  setupSystemIntegration() {
    // 系统托盘
    this.createTray()

    // 电源管理
    this.setupPowerMonitor()

    // 屏幕管理
    this.setupScreenMonitor()

    // 系统通知
    this.setupNotifications()
  }

  createTray() {
    const icon = nativeImage.createFromPath(path.join(__dirname, 'assets/tray-icon.png'))
    this.tray = new Tray(icon)

    const contextMenu = Menu.buildFromTemplate([
      {
        label: '显示窗口',
        click: () => {
          this.mainWindow.show()
        },
      },
      {
        label: '隐藏窗口',
        click: () => {
          this.mainWindow.hide()
        },
      },
      { type: 'separator' },
      {
        label: '退出',
        click: () => {
          app.quit()
        },
      },
    ])

    this.tray.setContextMenu(contextMenu)
    this.tray.setToolTip('我的应用')

    // 双击托盘图标显示窗口
    this.tray.on('double-click', () => {
      this.mainWindow.isVisible() ? this.mainWindow.hide() : this.mainWindow.show()
    })
  }

  setupPowerMonitor() {
    // 系统挂起
    powerMonitor.on('suspend', () => {
      console.log('系统即将挂起')
      this.mainWindow.webContents.send('system-suspend')
    })

    // 系统恢复
    powerMonitor.on('resume', () => {
      console.log('系统已恢复')
      this.mainWindow.webContents.send('system-resume')
    })

    // 电源状态变化
    powerMonitor.on('on-ac', () => {
      this.mainWindow.webContents.send('power-ac-connected')
    })

    powerMonitor.on('on-battery', () => {
      this.mainWindow.webContents.send('power-on-battery')
    })
  }

  setupScreenMonitor() {
    // 屏幕配置变化
    screen.on('display-added', (event, newDisplay) => {
      console.log('新显示器已连接:', newDisplay)
      this.mainWindow.webContents.send('display-added', newDisplay)
    })

    screen.on('display-removed', (event, oldDisplay) => {
      console.log('显示器已断开:', oldDisplay)
      this.mainWindow.webContents.send('display-removed', oldDisplay)
    })

    screen.on('display-metrics-changed', (event, display, changedMetrics) => {
      console.log('显示器配置已更改:', display, changedMetrics)
      this.mainWindow.webContents.send('display-metrics-changed', display, changedMetrics)
    })
  }

  setupNotifications() {
    // 处理通知点击
    ipcMain.on('show-notification', (event, { title, body, actions }) => {
      const notification = new Notification({
        title,
        body,
        actions: actions || [],
      })

      notification.on('click', () => {
        this.mainWindow.show()
        this.mainWindow.focus()
      })

      notification.on('action', (event, index) => {
        this.mainWindow.webContents.send('notification-action', index)
      })

      notification.show()
    })
  }
}

文件系统操作

// fileSystem.js - 文件系统操作
const { ipcMain, dialog } = require('electron')
const fs = require('fs').promises
const path = require('path')

class FileSystemManager {
  constructor() {
    this.setupFileOperations()
  }

  setupFileOperations() {
    // 选择文件
    ipcMain.handle('select-file', async (event, options = {}) => {
      const result = await dialog.showOpenDialog({
        properties: ['openFile'],
        filters: options.filters || [{ name: 'All Files', extensions: ['*'] }],
      })

      return result
    })

    // 选择文件夹
    ipcMain.handle('select-directory', async () => {
      const result = await dialog.showOpenDialog({
        properties: ['openDirectory'],
      })

      return result
    })

    // 保存文件对话框
    ipcMain.handle('save-file-dialog', async (event, options = {}) => {
      const result = await dialog.showSaveDialog({
        filters: options.filters || [{ name: 'All Files', extensions: ['*'] }],
        defaultPath: options.defaultPath,
      })

      return result
    })

    // 读取文件
    ipcMain.handle('read-file', async (event, filePath) => {
      try {
        const content = await fs.readFile(filePath, 'utf8')
        const stats = await fs.stat(filePath)

        return {
          success: true,
          content,
          stats: {
            size: stats.size,
            modified: stats.mtime,
            created: stats.birthtime,
          },
        }
      } catch (error) {
        return {
          success: false,
          error: error.message,
        }
      }
    })

    // 写入文件
    ipcMain.handle('write-file', async (event, filePath, content) => {
      try {
        await fs.writeFile(filePath, content, 'utf8')
        return { success: true }
      } catch (error) {
        return { success: false, error: error.message }
      }
    })

    // 读取目录
    ipcMain.handle('read-directory', async (event, dirPath) => {
      try {
        const items = await fs.readdir(dirPath, { withFileTypes: true })
        const result = []

        for (const item of items) {
          const fullPath = path.join(dirPath, item.name)
          const stats = await fs.stat(fullPath)

          result.push({
            name: item.name,
            path: fullPath,
            isDirectory: item.isDirectory(),
            isFile: item.isFile(),
            size: stats.size,
            modified: stats.mtime,
          })
        }

        return { success: true, items: result }
      } catch (error) {
        return { success: false, error: error.message }
      }
    })

    // 创建目录
    ipcMain.handle('create-directory', async (event, dirPath) => {
      try {
        await fs.mkdir(dirPath, { recursive: true })
        return { success: true }
      } catch (error) {
        return { success: false, error: error.message }
      }
    })

    // 删除文件/目录
    ipcMain.handle('delete-path', async (event, targetPath) => {
      try {
        const stats = await fs.stat(targetPath)

        if (stats.isDirectory()) {
          await fs.rmdir(targetPath, { recursive: true })
        } else {
          await fs.unlink(targetPath)
        }

        return { success: true }
      } catch (error) {
        return { success: false, error: error.message }
      }
    })

    // 监听文件变化
    ipcMain.handle('watch-file', (event, filePath) => {
      try {
        const watcher = fs.watch(filePath, (eventType, filename) => {
          event.sender.send('file-changed', {
            eventType,
            filename,
            filePath,
          })
        })

        // 存储 watcher 以便后续清理
        event.sender.on('destroyed', () => {
          watcher.close()
        })

        return { success: true }
      } catch (error) {
        return { success: false, error: error.message }
      }
    })
  }
}

new FileSystemManager()

性能优化

内存管理

// memoryManager.js - 内存管理
class MemoryManager {
  constructor() {
    this.setupMemoryMonitoring()
  }

  setupMemoryMonitoring() {
    // 定期检查内存使用
    setInterval(() => {
      const memoryUsage = process.memoryUsage()

      // 如果内存使用过高,触发垃圾回收
      if (memoryUsage.heapUsed > 100 * 1024 * 1024) {
        // 100MB
        if (global.gc) {
          global.gc()
          console.log('触发垃圾回收')
        }
      }

      // 发送内存使用情况到渲染进程
      if (this.mainWindow && !this.mainWindow.isDestroyed()) {
        this.mainWindow.webContents.send('memory-usage', {
          heapUsed: memoryUsage.heapUsed,
          heapTotal: memoryUsage.heapTotal,
          external: memoryUsage.external,
          rss: memoryUsage.rss,
        })
      }
    }, 30000) // 每30秒检查一次
  }

  // 清理未使用的资源
  cleanup() {
    // 清理事件监听器
    ipcMain.removeAllListeners()

    // 清理定时器
    // clearInterval(this.memoryCheckInterval)

    // 强制垃圾回收
    if (global.gc) {
      global.gc()
    }
  }
}

渲染进程优化

// renderer-optimization.js - 渲染进程优化
class RendererOptimization {
  constructor() {
    this.setupOptimizations()
  }

  setupOptimizations() {
    // 虚拟滚动
    this.setupVirtualScrolling()

    // 图片懒加载
    this.setupLazyLoading()

    // 防抖和节流
    this.setupDebounceThrottle()
  }

  setupVirtualScrolling() {
    // 虚拟滚动实现
    class VirtualList {
      constructor(container, itemHeight, items) {
        this.container = container
        this.itemHeight = itemHeight
        this.items = items
        this.visibleStart = 0
        this.visibleEnd = 0
        this.init()
      }

      init() {
        this.container.style.height = `${this.items.length * this.itemHeight}px`
        this.container.addEventListener('scroll', this.onScroll.bind(this))
        this.render()
      }

      onScroll() {
        const scrollTop = this.container.scrollTop
        const containerHeight = this.container.clientHeight

        this.visibleStart = Math.floor(scrollTop / this.itemHeight)
        this.visibleEnd = Math.min(
          this.visibleStart + Math.ceil(containerHeight / this.itemHeight) + 1,
          this.items.length
        )

        this.render()
      }

      render() {
        const fragment = document.createDocumentFragment()

        for (let i = this.visibleStart; i < this.visibleEnd; i++) {
          const item = document.createElement('div')
          item.style.height = `${this.itemHeight}px`
          item.style.position = 'absolute'
          item.style.top = `${i * this.itemHeight}px`
          item.textContent = this.items[i]
          fragment.appendChild(item)
        }

        this.container.innerHTML = ''
        this.container.appendChild(fragment)
      }
    }
  }

  setupLazyLoading() {
    // 图片懒加载
    const imageObserver = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const img = entry.target
          img.src = img.dataset.src
          img.classList.remove('lazy')
          imageObserver.unobserve(img)
        }
      })
    })

    document.querySelectorAll('img[data-src]').forEach((img) => {
      imageObserver.observe(img)
    })
  }

  setupDebounceThrottle() {
    // 防抖函数
    function debounce(func, wait) {
      let timeout
      return function executedFunction(...args) {
        const later = () => {
          clearTimeout(timeout)
          func(...args)
        }
        clearTimeout(timeout)
        timeout = setTimeout(later, wait)
      }
    }

    // 节流函数
    function throttle(func, limit) {
      let inThrottle
      return function (...args) {
        if (!inThrottle) {
          func.apply(this, args)
          inThrottle = true
          setTimeout(() => (inThrottle = false), limit)
        }
      }
    }

    // 使用示例
    const searchInput = document.getElementById('search')
    if (searchInput) {
      searchInput.addEventListener(
        'input',
        debounce((e) => {
          // 搜索逻辑
          console.log('搜索:', e.target.value)
        }, 300)
      )
    }

    const scrollContainer = document.getElementById('scroll-container')
    if (scrollContainer) {
      scrollContainer.addEventListener(
        'scroll',
        throttle(() => {
          // 滚动处理逻辑
          console.log('滚动位置:', scrollContainer.scrollTop)
        }, 100)
      )
    }
  }
}

new RendererOptimization()

应用打包与分发

构建配置

// package.json - 构建配置
{
  "name": "my-electron-app",
  "version": "1.0.0",
  "description": "我的 Electron 应用",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "dev": "NODE_ENV=development electron .",
    "build": "electron-builder",
    "build:win": "electron-builder --win",
    "build:mac": "electron-builder --mac",
    "build:linux": "electron-builder --linux",
    "dist": "npm run build -- --publish=never",
    "publish": "npm run build -- --publish=always"
  },
  "build": {
    "appId": "com.example.myapp",
    "productName": "我的应用",
    "directories": {
      "output": "dist"
    },
    "files": ["main.js", "preload.js", "renderer/", "assets/", "node_modules/", "package.json"],
    "extraResources": [
      {
        "from": "resources/",
        "to": "resources/",
        "filter": ["**/*"]
      }
    ],
    "win": {
      "target": [
        {
          "target": "nsis",
          "arch": ["x64", "ia32"]
        },
        {
          "target": "portable",
          "arch": ["x64"]
        }
      ],
      "icon": "assets/icon.ico"
    },
    "mac": {
      "target": [
        {
          "target": "dmg",
          "arch": ["x64", "arm64"]
        },
        {
          "target": "zip",
          "arch": ["x64", "arm64"]
        }
      ],
      "icon": "assets/icon.icns",
      "category": "public.app-category.productivity"
    },
    "linux": {
      "target": [
        {
          "target": "AppImage",
          "arch": ["x64"]
        },
        {
          "target": "deb",
          "arch": ["x64"]
        }
      ],
      "icon": "assets/icon.png",
      "category": "Office"
    },
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true,
      "createDesktopShortcut": true,
      "createStartMenuShortcut": true
    },
    "publish": [
      {
        "provider": "github",
        "owner": "your-username",
        "repo": "your-repo"
      }
    ]
  }
}

自动更新

// updater.js - 自动更新
const { autoUpdater } = require('electron-updater')
const { dialog } = require('electron')

class AutoUpdater {
  constructor(mainWindow) {
    this.mainWindow = mainWindow
    this.setupAutoUpdater()
  }

  setupAutoUpdater() {
    // 检查更新
    autoUpdater.checkForUpdatesAndNotify()

    // 发现更新
    autoUpdater.on('update-available', (info) => {
      console.log('发现新版本:', info.version)
      this.mainWindow.webContents.send('update-available', info)
    })

    // 没有更新
    autoUpdater.on('update-not-available', (info) => {
      console.log('当前已是最新版本')
    })

    // 下载进度
    autoUpdater.on('download-progress', (progressObj) => {
      this.mainWindow.webContents.send('download-progress', {
        percent: progressObj.percent,
        transferred: progressObj.transferred,
        total: progressObj.total,
      })
    })

    // 更新下载完成
    autoUpdater.on('update-downloaded', (info) => {
      console.log('更新下载完成')

      dialog
        .showMessageBox(this.mainWindow, {
          type: 'info',
          title: '更新已下载',
          message: '新版本已下载完成,是否立即重启应用以完成更新?',
          buttons: ['立即重启', '稍后重启'],
        })
        .then((result) => {
          if (result.response === 0) {
            autoUpdater.quitAndInstall()
          }
        })
    })

    // 更新错误
    autoUpdater.on('error', (error) => {
      console.error('更新错误:', error)
      this.mainWindow.webContents.send('update-error', error.message)
    })
  }

  checkForUpdates() {
    autoUpdater.checkForUpdatesAndNotify()
  }
}

module.exports = AutoUpdater

总结

Electron 为 Web 开发者提供了构建桌面应用的强大能力:

  1. 跨平台支持:一套代码运行在多个平台
  2. 丰富的 API:访问系统原生功能
  3. 灵活的架构:主进程与渲染进程分离
  4. 强大的生态:丰富的插件和工具链
  5. 便捷的分发:自动更新和多平台打包

掌握 Electron,你就能用熟悉的 Web 技术构建出功能强大的桌面应用!


Electron 是 Web 开发者进入桌面应用开发的最佳选择,值得深入学习和实践。