Electron+鸿蒙桌面应用实战:跨平台开发完全指南

Electron+鸿蒙桌面应用实战:跨平台开发完全指南

本文详细介绍如何使用Electron框架开发鸿蒙桌面应用,从环境搭建到上架发布,包含完整可运行代码示例。

前言

随着鸿蒙生态的快速发展,越来越多的开发者开始关注鸿蒙桌面应用的开发。虽然鸿蒙原生支持ArkTS/JS开发,但在某些场景下,我们可能需要:

  • 复用现有的Web技术栈
  • 快速迁移现有Electron应用
  • 利用Electron丰富的生态库

本文将手把手教你使用Electron开发鸿蒙桌面应用,实现一套代码同时运行在Windows、macOS、Linux和鸿蒙桌面系统上。

一、环境准备

1.1 系统环境要求

开发环境:

  • Node.js 16.x 或更高版本
  • npm 8.x 或更高版本
  • Electron 25.x 或更高版本
  • 鸿蒙DevEco Studio 3.1+(用于测试)

目标平台:

  • 鸿蒙OS 3.0+ 桌面版
  • Windows 10+
  • macOS 10.15+
  • Linux Ubuntu 18.04+

1.2 项目初始化

bash 复制代码
# 创建项目目录
mkdir electron-harmonyos-demo
cd electron-harmonyos-demo

# 初始化package.json
npm init -y

# 安装Electron
npm install electron --save-dev

# 安装必要依赖
npm install electron-builder --save-dev
npm install electron-packager --save-dev

1.3 项目结构设计

复制代码
electron-harmonyos-demo/
├── src/
│   ├── main/              # 主进程代码
│   │   └── main.js
│   ├── renderer/         # 渲染进程代码
│   │   ├── index.html
│   │   ├── renderer.js
│   │   └── style.css
│   └── preload/          # 预加载脚本
│       └── preload.js
├── assets/               # 静态资源
│   ├── icons/
│   └── images/
├── dist/                 # 打包输出目录
├── package.json
└── electron-builder.yml  # 打包配置文件

二、主进程开发

2.1 主进程入口文件

创建 src/main/main.js

javascript 复制代码
const { app, BrowserWindow, ipcMain, Tray, Menu } = require('electron')
const path = require('path')
const { autoUpdater } = require('electron-updater')

// 保持对window对象的全局引用
let mainWindow
let tray

// 创建浏览器窗口
function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    minWidth: 800,
    minHeight: 600,
    icon: path.join(__dirname, '../../assets/icons/icon.png'),
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      preload: path.join(__dirname, '../preload/preload.js')
    }
  })

  // 加载应用
  if (process.env.NODE_ENV === 'development') {
    mainWindow.loadURL('http://localhost:3000')
    mainWindow.webContents.openDevTools()
  } else {
    mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
  }

  // 窗口关闭事件
  mainWindow.on('closed', function () {
    mainWindow = null
  })
}

// Electron初始化完成
app.whenReady().then(() => {
  createWindow()
  
  // 创建系统托盘
  createTray()
  
  // 检查更新
  checkForUpdates()
  
  app.on('activate', function () {
    if (mainWindow === null) createWindow()
  })
})

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

// 创建系统托盘
function createTray() {
  tray = new Tray(path.join(__dirname, '../../assets/icons/tray.png'))
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '显示主窗口',
      click: () => {
        if (mainWindow) {
          mainWindow.show()
          mainWindow.focus()
        }
      }
    },
    {
      label: '关于',
      click: () => {
        // 显示关于对话框
      }
    },
    {
      label: '退出',
      click: () => {
        app.quit()
      }
    }
  ])
  tray.setToolTip('Electron鸿蒙应用')
  tray.setContextMenu(contextMenu)
}

// 检查更新
function checkForUpdates() {
  if (process.env.NODE_ENV === 'production') {
    autoUpdater.checkForUpdatesAndNotify()
    
    autoUpdater.on('update-available', () => {
      mainWindow.webContents.send('update-available')
    })
    
    autoUpdater.on('update-downloaded', () => {
      mainWindow.webContents.send('update-downloaded')
    })
  }
}

2.2 预加载脚本

创建 src/preload/preload.js

javascript 复制代码
const { contextBridge, ipcRenderer } = require('electron')

// 通过contextBridge暴露安全的API给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
  // 窗口控制
  minimize: () => ipcRenderer.invoke('window-minimize'),
  maximize: () => ipcRenderer.invoke('window-maximize'),
  close: () => ipcRenderer.invoke('window-close'),
  
  // 文件系统
  readFile: (path) => ipcRenderer.invoke('file-read', path),
  writeFile: (path, content) => ipcRenderer.invoke('file-write', path, content),
  
  // 系统信息
  getSystemInfo: () => ipcRenderer.invoke('system-info'),
  
  // 更新相关
  onUpdateAvailable: (callback) => ipcRenderer.on('update-available', callback),
  onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', callback),
  
  // 鸿蒙特有API
  harmonyOS: {
    getDeviceInfo: () => ipcRenderer.invoke('harmony-device-info'),
    shareFile: (filePath) => ipcRenderer.invoke('harmony-share', filePath)
  }
})

三、渲染进程开发

3.1 HTML主页面

创建 src/renderer/index.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Electron鸿蒙桌面应用</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="app">
        <!-- 标题栏 -->
        <div class="titlebar">
            <div class="titlebar-title">Electron鸿蒙桌面应用</div>
            <div class="titlebar-controls">
                <button id="minimize-btn" class="titlebar-btn">−</button>
                <button id="maximize-btn" class="titlebar-btn">□</button>
                <button id="close-btn" class="titlebar-btn close">×</button>
            </div>
        </div>
        
        <!-- 主内容区 -->
        <div class="container">
            <header>
                <h1>🚀 Electron + 鸿蒙开发实战</h1>
                <p class="subtitle">跨平台桌面应用开发完全指南</p>
            </header>
            
            <main>
                <section class="card">
                    <h2>系统信息</h2>
                    <div id="system-info" class="info-box">
                        加载中...
                    </div>
                    <button id="refresh-info" class="btn primary">刷新信息</button>
                </section>
                
                <section class="card">
                    <h2>文件操作</h2>
                    <div class="file-operations">
                        <input type="file" id="file-input" class="file-input">
                        <button id="read-file" class="btn">读取文件</button>
                        <button id="write-file" class="btn">写入文件</button>
                    </div>
                    <div id="file-content" class="file-content"></div>
                </section>
                
                <section class="card">
                    <h2>鸿蒙特性</h2>
                    <div class="harmony-features">
                        <button id="device-info" class="btn harmony">获取设备信息</button>
                        <button id="share-file" class="btn harmony">分享文件</button>
                    </div>
                </section>
            </main>
            
            <footer>
                <p>Powered by Electron & HarmonyOS | 版本 1.0.0</p>
            </footer>
        </div>
    </div>
    
    <script src="renderer.js"></script>
</body>
</html>

3.2 渲染进程JavaScript

创建 src/renderer/renderer.js

javascript 复制代码
document.addEventListener('DOMContentLoaded', () => {
  // 窗口控制按钮
  document.getElementById('minimize-btn').addEventListener('click', () => {
    window.electronAPI.minimize()
  })
  
  document.getElementById('maximize-btn').addEventListener('click', () => {
    window.electronAPI.maximize()
  })
  
  document.getElementById('close-btn').addEventListener('click', () => {
    window.electronAPI.close()
  })
  
  // 系统信息
  const loadSystemInfo = async () => {
    try {
      const info = await window.electronAPI.getSystemInfo()
      document.getElementById('system-info').innerHTML = `
        <p><strong>操作系统:</strong> ${info.platform}</p>
        <p><strong>架构:</strong> ${info.arch}</p>
        <p><strong>内存:</strong> ${Math.round(info.totalMemory / 1024 / 1024 / 1024)} GB</p>
        <p><strong>CPU:</strong> ${info.cpus[0].model}</p>
        <p><strong>鸿蒙版本:</strong> ${info.harmonyVersion || 'N/A'}</p>
      `
    } catch (error) {
      console.error('获取系统信息失败:', error)
    }
  }
  
  document.getElementById('refresh-info').addEventListener('click', loadSystemInfo)
  loadSystemInfo()
  
  // 文件操作
  document.getElementById('read-file').addEventListener('click', async () => {
    const filePath = document.getElementById('file-input').value
    if (!filePath) {
      alert('请输入文件路径')
      return
    }
    
    try {
      const content = await window.electronAPI.readFile(filePath)
      document.getElementById('file-content').textContent = content
    } catch (error) {
      console.error('读取文件失败:', error)
      alert('读取文件失败: ' + error.message)
    }
  })
  
  document.getElementById('write-file').addEventListener('click', async () => {
    const filePath = prompt('请输入保存路径:', 'output.txt')
    const content = document.getElementById('file-content').textContent
    
    if (!filePath) return
    
    try {
      await window.electronAPI.writeFile(filePath, content)
      alert('文件保存成功!')
    } catch (error) {
      console.error('写入文件失败:', error)
      alert('写入文件失败: ' + error.message)
    }
  })
  
  // 鸿蒙特性
  document.getElementById('device-info').addEventListener('click', async () => {
    try {
      const deviceInfo = await window.electronAPI.harmonyOS.getDeviceInfo()
      alert(`设备信息:\n${JSON.stringify(deviceInfo, null, 2)}`)
    } catch (error) {
      console.error('获取设备信息失败:', error)
    }
  })
  
  document.getElementById('share-file').addEventListener('click', async () => {
    const filePath = prompt('请输入要分享的文件路径:', '')
    if (!filePath) return
    
    try {
      await window.electronAPI.harmonyOS.shareFile(filePath)
      alert('文件分享成功!')
    } catch (error) {
      console.error('文件分享失败:', error)
    }
  })
})

3.3 CSS样式

创建 src/renderer/style.css

css 复制代码
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #333;
  height: 100vh;
  overflow: hidden;
}

#app {
  height: 100vh;
  display: flex;
  flex-direction: column;
}

/* 标题栏样式 */
.titlebar {
  height: 32px;
  background: rgba(255, 255, 255, 0.95);
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 10px;
  -webkit-app-region: drag;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}

.titlebar-title {
  font-size: 14px;
  font-weight: 600;
  color: #333;
}

.titlebar-controls {
  display: flex;
  -webkit-app-region: no-drag;
}

.titlebar-btn {
  width: 46px;
  height: 32px;
  border: none;
  background: transparent;
  font-size: 16px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.titlebar-btn:hover {
  background: rgba(0, 0, 0, 0.1);
}

.titlebar-btn.close:hover {
  background: #e74c3c;
  color: white;
}

/* 主容器 */
.container {
  flex: 1;
  padding: 20px;
  overflow-y: auto;
  background: rgba(255, 255, 255, 0.9);
  backdrop-filter: blur(10px);
}

header {
  text-align: center;
  margin-bottom: 30px;
  padding-bottom: 20px;
  border-bottom: 2px solid #667eea;
}

header h1 {
  color: #333;
  font-size: 2.5rem;
  margin-bottom: 10px;
}

.subtitle {
  color: #666;
  font-size: 1.2rem;
}

/* 卡片样式 */
.card {
  background: white;
  border-radius: 12px;
  padding: 20px;
  margin-bottom: 20px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.card:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
}

.card h2 {
  color: #667eea;
  margin-bottom: 15px;
  font-size: 1.5rem;
}

/* 信息框 */
.info-box {
  background: #f8f9fa;
  border-radius: 8px;
  padding: 15px;
  margin-bottom: 15px;
  font-family: 'Consolas', monospace;
  line-height: 1.6;
}

/* 按钮样式 */
.btn {
  background: #667eea;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s ease;
}

.btn:hover {
  background: #764ba2;
  transform: translateY(-2px);
}

.btn.primary {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.btn.harmony {
  background: linear-gradient(135deg, #ff6b35 0%, #f7c948 100%);
}

/* 文件操作区域 */
.file-operations {
  display: flex;
  gap: 10px;
  margin-bottom: 15px;
  flex-wrap: wrap;
}

.file-input {
  flex: 1;
  min-width: 200px;
  padding: 8px;
  border: 2px dashed #ddd;
  border-radius: 6px;
  background: #f8f9fa;
}

.file-content {
  background: #f8f9fa;
  border-radius: 8px;
  padding: 15px;
  min-height: 100px;
  max-height: 300px;
  overflow-y: auto;
  font-family: 'Consolas', monospace;
  white-space: pre-wrap;
  word-break: break-all;
}

/* 鸿蒙特性区域 */
.harmony-features {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

/* 页脚 */
footer {
  text-align: center;
  margin-top: 30px;
  padding-top: 20px;
  border-top: 1px solid #eee;
  color: #666;
  font-size: 14px;
}

/* 滚动条样式 */
::-webkit-scrollbar {
  width: 8px;
}

::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 4px;
}

::-webkit-scrollbar-thumb {
  background: #888;
  border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
  background: #555;
}

四、鸿蒙平台适配

4.1 鸿蒙特有API封装

创建 src/main/harmony-bridge.js

javascript 复制代码
const { ipcMain } = require('electron')
const { exec } = require('child_process')
const util = require('util')
const execPromise = util.promisify(exec)

// 鸿蒙设备信息获取
ipcMain.handle('harmony-device-info', async (event) => {
  try {
    // 检测是否为鸿蒙系统
    if (process.platform === 'linux' && process.env.DESKTOP_SESSION) {
      const { stdout } = await execPromise('getprop hw_sc.build.platform.Version')
      return {
        platform: 'HarmonyOS',
        version: stdout.trim(),
        deviceType: 'Desktop',
        architecture: process.arch
      }
    }
    
    return {
      platform: process.platform,
      version: 'N/A',
      deviceType: 'Unknown',
      architecture: process.arch
    }
  } catch (error) {
    console.error('获取鸿蒙设备信息失败:', error)
    return null
  }
})

// 鸿蒙文件分享
ipcMain.handle('harmony-share', async (event, filePath) => {
  try {
    if (process.platform === 'linux' && process.env.DESKTOP_SESSION) {
      // 调用鸿蒙分享API
      await execPromise(`hm share file "${filePath}"`)
      return true
    }
    return false
  } catch (error) {
    console.error('鸿蒙文件分享失败:', error)
    throw error
  }
})

4.2 鸿蒙应用打包配置

更新 package.json

json 复制代码
{
  "name": "electron-harmonyos-demo",
  "version": "1.0.0",
  "description": "Electron鸿蒙桌面应用实战",
  "main": "src/main/main.js",
  "scripts": {
    "start": "electron .",
    "build": "electron-builder",
    "build:harmony": "electron-builder --platform linux --arch arm64",
    "build:win": "electron-builder --platform win32",
    "build:mac": "electron-builder --platform darwin",
    "pack": "electron-packager . --platform=linux --arch=arm64 --out=dist/"
  },
  "build": {
    "appId": "com.example.electronharmonyos",
    "productName": "Electron鸿蒙应用",
    "directories": {
      "output": "dist"
    },
    "files": [
      "src/**/*",
      "assets/**/*",
      "package.json"
    ],
    "linux": {
      "target": "AppImage",
      "arch": ["arm64", "x64"],
      "category": "Utility",
      "icon": "assets/icons/icon.png",
      "synopsis": "Electron开发的鸿蒙桌面应用",
      "description": "使用Electron框架开发的跨平台鸿蒙桌面应用"
    },
    "harmonyos": {
      "target": "hap",
      "arch": ["arm64"],
      "ability": {
        "name": ".MainAbility",
        "icon": "$media:icon",
        "label": "$string:app_name",
        "launchType": "standard",
        "description": "$string:description_main_ability",
        "visible": true
      }
    }
  }
}

五、打包与发布

5.1 打包为鸿蒙应用

bash 复制代码
# 安装打包工具
npm install -g electron-builder

# 打包鸿蒙版本
npm run build:harmony

# 输出目录: dist/
# 生成文件:
# - dist/linux-arm64/Electron鸿蒙应用-1.0.0-arm64.AppImage
# - dist/linux-arm64/Electron鸿蒙应用-1.0.0-arm64.deb
# - dist/linux-arm64/Electron鸿蒙应用-1.0.0-arm64.rpm

5.2 鸿蒙应用签名

bash 复制代码
# 生成密钥库
keytool -genkey -alias harmony-app -keyalg RSA -keysize 2048 -validity 10000 -keystore harmony-keystore.jks

# 签名应用
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore harmony-keystore.jks dist/linux-arm64/Electron鸿蒙应用-1.0.0-arm64.apk harmony-app

5.3 上架鸿蒙应用市场

  1. 注册开发者账号: 访问华为开发者联盟官网注册
  2. 创建应用: 在AppGallery Connect中创建应用
  3. 上传应用包: 上传签名后的.hap或.apk文件
  4. 填写应用信息: 包括应用名称、描述、截图等
  5. 提交审核: 等待华为审核通过

六、性能优化

6.1 启动速度优化

javascript 复制代码
// 使用V8快照加速启动
const snapshot = require('v8-heap-snapshot')

app.on('ready', () => {
  // 创建快照
  const snap = snapshot.createSnapshot()
  
  // 使用快照恢复状态
  if (snap) {
    snapshot.restoreSnapshot(snap)
  }
  
  createWindow()
})

6.2 内存优化

javascript 复制代码
// 监听内存警告
process.on('warning', (warning) => {
  if (warning.name === 'MaxListenersExceededWarning') {
    // 清理事件监听器
    mainWindow.webContents.removeAllListeners()
  }
})

// 定期垃圾回收
setInterval(() => {
  if (global.gc) {
    global.gc()
  }
}, 300000) // 5分钟

七、调试技巧

7.1 开发工具配置

javascript 复制代码
// 开发模式下打开DevTools
if (process.env.NODE_ENV === 'development') {
  mainWindow.webContents.openDevTools({
    mode: 'detach',
    activate: true
  })
  
  // 热重载
  require('electron-reload')(__dirname, {
    electron: require(`${__dirname}/node_modules/electron`)
  })
}

7.2 日志记录

javascript 复制代码
const fs = require('fs')
const path = require('path')

// 创建日志目录
const logDir = path.join(app.getPath('userData'), 'logs')
if (!fs.existsSync(logDir)) {
  fs.mkdirSync(logDir, { recursive: true })
}

// 日志写入函数
function writeLog(level, message) {
  const timestamp = new Date().toISOString()
  const logMessage = `[${timestamp}] [${level}] ${message}\n`
  
  fs.appendFileSync(
    path.join(logDir, `app-${new Date().toISOString().split('T')[0]}.log`),
    logMessage
  )
}

// 重写console方法
console.log = (...args) => {
  writeLog('INFO', args.join(' '))
  process.stdout.write(`[INFO] ${args.join(' ')}\n`)
}

console.error = (...args) => {
  writeLog('ERROR', args.join(' '))
  process.stderr.write(`[ERROR] ${args.join(' ')}\n`)
}

八、实战案例:文件管理器

8.1 功能实现

javascript 复制代码
// src/renderer/file-manager.js
class FileManager {
  constructor() {
    this.currentPath = app.getPath('home')
    this.init()
  }
  
  init() {
    this.renderFileList()
    this.bindEvents()
  }
  
  async renderFileList() {
    const files = await window.electronAPI.readDirectory(this.currentPath)
    const fileList = document.getElementById('file-list')
    
    fileList.innerHTML = files.map(file => `
      <div class="file-item ${file.isDirectory ? 'directory' : 'file'}" data-path="${file.path}">
        <img src="${file.isDirectory ? 'folder.png' : 'file.png'}" class="file-icon">
        <span class="file-name">${file.name}</span>
        <span class="file-size">${this.formatSize(file.size)}</span>
      </div>
    `).join('')
  }
  
  formatSize(bytes) {
    if (bytes === 0) return '0 B'
    const k = 1024
    const sizes = ['B', 'KB', 'MB', 'GB']
    const i = Math.floor(Math.log(bytes) / Math.log(k))
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
  }
  
  bindEvents() {
    document.getElementById('file-list').addEventListener('click', (e) => {
      const fileItem = e.target.closest('.file-item')
      if (fileItem) {
        const path = fileItem.dataset.path
        this.openFile(path)
      }
    })
  }
  
  async openFile(filePath) {
    const stat = await window.electronAPI.getFileStats(filePath)
    
    if (stat.isDirectory) {
      this.currentPath = filePath
      this.renderFileList()
    } else {
      // 使用默认应用打开文件
      window.electronAPI.openFile(filePath)
    }
  }
}

九、总结

本文详细介绍了使用Electron开发鸿蒙桌面应用的完整流程,包括:

  1. 环境搭建: Node.js + Electron + 鸿蒙DevEco Studio
  2. 核心开发: 主进程、渲染进程、预加载脚本
  3. 鸿蒙适配: 特有API封装、平台检测
  4. 打包发布: 多平台打包、应用签名、上架流程
  5. 性能优化: 启动加速、内存管理
  6. 调试技巧: 开发工具、日志记录

通过Electron,我们可以利用Web技术栈快速开发跨平台桌面应用,同时针对鸿蒙系统进行深度适配,为用户提供原生体验。

参考资料


作者简介: 资深全栈开发者,专注Electron、鸿蒙应用开发,热爱分享技术干货。

版权声明: 本文为原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。

相关推荐
求学中--1 小时前
鸿蒙实战:用状态管理实现一个完整的Todo应用,从@State到@Provide全链路打通
华为·harmonyos·组件化开发·完整项目
HwJack2010 小时前
HarmonyOS APP开发玩转鸿蒙 HSP:打造高复用“乐高模块”的底层逻辑
华为·harmonyos
:mnong11 小时前
附图报价系统设计分析5
electron·pdf·vue·cad·dwg·定额
●VON17 小时前
28个Token重构鸿蒙App:企业级设计系统的搭建实践
华为·重构·harmonyos
会周易的程序员17 小时前
aiDgeScanner 工业设备网络扫描与管理工具
网络·c++·物联网·架构·electron·node.js·iot
求学中--18 小时前
AppStorage和LocalStorage有什么区别?鸿蒙全局状态管理方案选型指南
华为·harmonyos
求学中--21 小时前
鸿蒙状态管理一文通:@State/@Prop/@Link/@Provide四大装饰器,15分钟彻底搞懂
华为·harmonyos
阿钱真强道1 天前
19 小凌派 rk2206 鸿蒙 LiteOS-M 任务详解
华为·鸿蒙·任务·liteos·详解·rk2206·小凌派
阿钱真强道1 天前
18 小凌派 rk2206 鸿蒙 liteos 如何通过修改配置文件,编译不通的案例
华为·鸿蒙·编译·案例·liteos·rk2206