Electron鸿蒙PC适配全攻略:从环境搭建到打包发布

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

本文是Electron应用迁移到鸿蒙PC的完整实战指南,涵盖环境搭建、API适配、原生模块编译、签名打包、踩坑排错等全链路内容,帮助Electron开发者快速将应用迁移到鸿蒙PC平台。
在这里插入图片描述

引言:Web技术栈开发者的鸿蒙之路

在PC应用开发领域,Electron凭借"JavaScript+HTML+CSS"的技术栈,让Web开发者能够构建跨平台桌面应用。VS Code、Slack、Figma、Discord——这些大名鼎鼎的应用都基于Electron。

随着鸿蒙PC的推出,华为与Electron社区合作,提供了鸿蒙PC版的Electron运行时。这意味着:

  • 你现有的Electron应用可以通过少量修改,在鸿蒙PC上运行
  • Web前端团队的技能可以直接复用到鸿蒙PC开发
  • 数以万计的npm包可以继续使用

本文将带你完整走过这条迁移之路。
在这里插入图片描述

第一章:Electron鸿蒙PC版概述

1.1 架构差异对比

标准Electron架构:

┌─────────────────────────────┐
│     Renderer Process        │
│    (HTML + CSS + JS)        │
│    ┌───────────────────┐    │
│    │  Chromium Engine   │    │
│    └───────────────────┘    │
├─────────────────────────────┤
│     Main Process (Node.js)  │
│    ┌───────────────────┐    │
│    │  Native Modules    │    │
│    └───────────────────┘    │
├─────────────────────────────┤
│   Operating System Layer    │
│  (Windows / macOS / Linux)  │
└─────────────────────────────┘

鸿蒙PC版Electron架构:
在这里插入图片描述

1.2 关键差异

特性 标准Electron 鸿蒙PC Electron
渲染引擎 Chromium ArkWeb
原生API访问 Node.js Addon NAPI (Node-API)
包格式 asar + exe/dmg/AppImage HNP / HAP
进程模型 Main + Renderer 相同
签名 代码签名证书 binary-sign-tool
分发 网站 / 应用商店 鸿蒙应用商店

第二章:环境搭建

2.1 开发环境准备

系统要求:

# 开发主机(任选其一)
# - Windows 10/11
# - macOS 10.15+
# - Ubuntu 20.04+

# 目标环境
# - 鸿蒙PC(API 12+)

# Node.js版本
# - 推荐 v18.x 或 v20.x LTS

安装Node.js:

# 使用nvm管理Node版本
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.bashrc

nvm install 20
nvm use 20

node --version  # 应显示 v20.x.x
npm --version   # 应显示 10.x.x

2.2 安装鸿蒙版Electron

# 创建新项目
mkdir my-electron-ohos
cd my-electron-ohos
npm init -y

# 安装鸿蒙版Electron
npm install electron@ohos-latest

# 或者在package.json中指定
{
  "devDependencies": {
    "electron": "npm:@ohos/electron@latest"
  }
}

2.3 项目结构

my-electron-ohos/
├── package.json                    # 项目配置
├── electron-builder.yml            # 打包配置
├── src/
│   ├── main/                       # 主进程
│   │   ├── main.js                 # 入口文件
│   │   ├── menu.js                 # 菜单配置
│   │   ├── tray.js                 # 系统托盘
│   │   └── native/                 # 原生模块
│   │       └── binding.gyp
│   └── renderer/                   # 渲染进程
│       ├── index.html
│       ├── styles/
│       ├── scripts/
│       └── assets/
├── resources/                      # 平台资源
│   ├── icon.png
│   └── ohos/
│       ├── icon.png
│       └── config.json
└── scripts/                        # 构建脚本
    ├── build.sh
    └── sign.sh

第三章:主进程适配

3.1 入口文件适配

// src/main/main.js
const { app, BrowserWindow, Menu, Tray, dialog, shell } = require('electron');
const path = require('path');

// 检测运行平台
const isOHOS = process.platform === 'ohos';

let mainWindow = null;

function createWindow() {
    const windowOptions = {
        width: 1280,
        height: 800,
        minWidth: 800,
        minHeight: 600,
        webPreferences: {
            nodeIntegration: false,
            contextIsolation: true,
            preload: path.join(__dirname, 'preload.js'),
            // 鸿蒙PC特殊配置
            ...(isOHOS ? {
                sandbox: true,
                webSecurity: true,
                // ArkWeb特有配置
                additionalArguments: ['--enable-ark-web-features'],
            } : {})
        },
        // 窗口外观
        titleBarStyle: isOHOS ? 'default' : 'hiddenInset',
        frame: !isOHOS,  // OHOS使用系统标题栏
        show: false,
        backgroundColor: '#ffffff',
        
        // 鸿蒙特有窗口属性
        ...(isOHOS ? {
            resizable: true,
            maximizable: true,
            minimizable: true,
            fullscreenable: true,
        } : {})
    };
    
    mainWindow = new BrowserWindow(windowOptions);
    
    // 加载页面
    if (process.env.NODE_ENV === 'development') {
        mainWindow.loadURL('http://localhost:5173');
    } else {
        mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
    }
    
    // 窗口准备好后显示
    mainWindow.once('ready-to-show', () => {
        mainWindow.show();
        
        // 鸿蒙PC:最大化启动
        if (isOHOS && app.getLoginItemSettings().wasOpenedAtLogin) {
            mainWindow.maximize();
        }
    });
    
    // 鸿蒙PC:处理窗口关闭事件
    if (isOHOS) {
        mainWindow.on('close', (event) => {
            // 最小化到托盘而不是退出
            if (tray) {
                event.preventDefault();
                mainWindow.hide();
            }
        });
    }
    
    return mainWindow;
}

3.2 菜单适配

// src/main/menu.js
function createMenu() {
    const isOHOS = process.platform === 'ohos';
    const isMac = process.platform === 'darwin';
    
    const template = [
        // 应用菜单(macOS/OHOS)
        ...(isMac || isOHOS ? [{
            label: app.getName(),
            submenu: [
                { role: 'about' },
                { type: 'separator' },
                { role: 'services' },
                { type: 'separator' },
                { role: 'hide' },
                { role: 'hideOthers' },
                { role: 'unhide' },
                { type: 'separator' },
                { role: 'quit' }
            ]
        }] : []),
        
        // 文件菜单
        {
            label: '文件',
            submenu: [
                {
                    label: '打开文件',
                    accelerator: isMac ? 'Cmd+O' : 'Ctrl+O',
                    click: async () => {
                        const result = await dialog.showOpenDialog({
                            properties: ['openFile'],
                            filters: [
                                { name: '所有文件', extensions: ['*'] }
                            ]
                        });
                        if (!result.canceled) {
                            mainWindow.webContents.send('file-opened', result.filePaths[0]);
                        }
                    }
                },
                {
                    label: '保存',
                    accelerator: isMac ? 'Cmd+S' : 'Ctrl+S',
                    click: () => {
                        mainWindow.webContents.send('save-file');
                    }
                },
                { type: 'separator' },
                ...(isOHOS ? [] : [{ role: 'quit' }])
            ]
        },
        
        // 编辑菜单
        {
            label: '编辑',
            submenu: [
                { role: 'undo' },
                { role: 'redo' },
                { type: 'separator' },
                { role: 'cut' },
                { role: 'copy' },
                { role: 'paste' },
                { role: 'selectAll' }
            ]
        },
        
        // 视图菜单
        {
            label: '视图',
            submenu: [
                { role: 'reload' },
                { role: 'forceReload' },
                { role: 'toggleDevTools' },
                { type: 'separator' },
                { role: 'resetZoom' },
                { role: 'zoomIn' },
                { role: 'zoomOut' },
                { type: 'separator' },
                { role: 'togglefullscreen' }
            ]
        },
        
        // 帮助菜单
        {
            label: '帮助',
            submenu: [
                {
                    label: '关于',
                    click: () => {
                        dialog.showMessageBox({
                            type: 'info',
                            title: '关于',
                            message: `My App v${app.getVersion()}`,
                            detail: isOHOS ? '运行在鸿蒙PC' : '桌面版'
                        });
                    }
                }
            ]
        }
    ];
    
    const menu = Menu.buildFromTemplate(template);
    Menu.setApplicationMenu(menu);
}

3.3 系统托盘适配

// src/main/tray.js
function createTray() {
    let trayIcon;
    
    if (process.platform === 'ohos') {
        // 鸿蒙PC托盘图标(需要PNG格式)
        trayIcon = path.join(__dirname, '../../resources/ohos/tray-icon.png');
    } else if (process.platform === 'darwin') {
        // macOS使用Template图像
        trayIcon = path.join(__dirname, '../../resources/tray-iconTemplate.png');
    } else {
        trayIcon = path.join(__dirname, '../../resources/tray-icon.png');
    }
    
    tray = new Tray(trayIcon);
    
    const contextMenu = Menu.buildFromTemplate([
        {
            label: '显示窗口',
            click: () => mainWindow.show()
        },
        {
            label: '设置',
            click: () => {
                mainWindow.show();
                mainWindow.webContents.send('open-settings');
            }
        },
        { type: 'separator' },
        {
            label: '退出',
            click: () => {
                app.isQuitting = true;
                app.quit();
            }
        }
    ]);
    
    tray.setToolTip('My Electron App');
    tray.setContextMenu(contextMenu);
    
    tray.on('click', () => {
        mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
    });
}

第四章:原生模块处理

4.1 Node.js原生模块编译

鸿蒙PC需要重新编译Node.js原生模块(C++ Addon):

// src/main/native/binding.gyp
{
    "targets": [
        {
            "target_name": "native_module",
            "sources": [
                "native_addon.cc"
            ],
            "conditions": [
                ["OS=='ohos'", {
                    "cflags": [
                        "-fPIC",
                        "-D__MUSL__=1",
                        "-D__OHOS__"
                    ],
                    "ldflags": [
                        "-fuse-ld=lld"
                    ],
                    "include_dirs": [
                        "<!@(node -p \"require('node-addon-api').include\")"
                    ]
                }]
            ],
            "include_dirs": [
                "<!@(node -p \"require('node-addon-api').include\")"
            ]
        }
    ]
}

4.2 NAPI桥接实现

// src/main/native/native_addon.cc
#include <napi.h>
#include <string>

// 简单示例:获取鸿蒙系统信息
#ifdef __OHOS__
#include <string>
#include <fstream>

std::string getOHOSVersion() {
    std::ifstream file("/proc/version");
    std::string line;
    if (std::getline(file, line)) {
        return line;
    }
    return "Unknown";
}
#endif

// NAPI函数:获取系统信息
Napi::Value GetSystemInfo(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    Napi::Object result = Napi::Object::New(env);
    
    result.Set("platform", Napi::String::New(env, 
#ifdef __OHOS__
        "ohos"
#elif defined(_WIN32)
        "win32"
#elif defined(__APPLE__)
        "darwin"
#else
        "linux"
#endif
    ));
    
#ifdef __OHOS__
    result.Set("ohosVersion", Napi::String::New(env, getOHOSVersion()));
    result.Set("isOHOS", Napi::Boolean::New(env, true));
#else
    result.Set("isOHOS", Napi::Boolean::New(env, false));
#endif
    
    return result;
}

// 模块初始化
Napi::Object Init(Napi::Env env, Napi::Object exports) {
    exports.Set("getSystemInfo", Napi::Function::New(env, GetSystemInfo));
    return exports;
}

NODE_API_MODULE(native_module, Init)

第五章:渲染进程适配

5.1 预加载脚本

// src/main/preload.js
const { contextBridge, ipcRenderer } = require('electron');

// 安全地暴露API到渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
    // 平台信息
    platform: process.platform,
    isOHOS: process.platform === 'ohos',
    
    // IPC通信
    send: (channel, data) => {
        const validChannels = ['save-file', 'open-settings'];
        if (validChannels.includes(channel)) {
            ipcRenderer.send(channel, data);
        }
    },
    
    receive: (channel, callback) => {
        const validChannels = ['file-opened', 'update-available'];
        if (validChannels.includes(channel)) {
            ipcRenderer.on(channel, (event, ...args) => callback(...args));
        }
    },
    
    // 文件对话框
    openFileDialog: async (options) => {
        return await ipcRenderer.invoke('dialog:openFile', options);
    },
    
    // 窗口控制
    windowControl: {
        minimize: () => ipcRenderer.send('window:minimize'),
        maximize: () => ipcRenderer.send('window:maximize'),
        close: () => ipcRenderer.send('window:close'),
    }
});

5.2 响应式布局适配

<!-- src/renderer/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="Content-Security-Policy" 
          content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">
    <title>My Electron App</title>
    <link rel="stylesheet" href="styles/main.css">
</head>
<body>
    <div id="app">
        <!-- 侧边栏(PC可见) -->
        <aside class="sidebar" data-platform-visible="pc,ohos">
            <nav class="nav-menu">
                <a href="#home" class="nav-item active">
                    <span class="icon">🏠</span>
                    <span class="label">首页</span>
                </a>
                <a href="#projects" class="nav-item">
                    <span class="icon">📁</span>
                    <span class="label">项目</span>
                </a>
                <a href="#settings" class="nav-item">
                    <span class="icon">⚙️</span>
                    <span class="label">设置</span>
                </a>
            </nav>
        </aside>
        
        <!-- 主内容区 -->
        <main class="content">
            <header class="topbar">
                <h1 id="page-title">首页</h1>
                <div class="window-controls ohos-only">
                    <button onclick="window.electronAPI.windowControl.minimize()"></button>
                    <button onclick="window.electronAPI.windowControl.maximize()"></button>
                    <button onclick="window.electronAPI.windowControl.close()"></button>
                </div>
            </header>
            
            <div class="main-content">
                <!-- 动态内容 -->
            </div>
        </main>
    </div>
    
    <script src="scripts/app.js"></script>
</body>
</html>
/* src/renderer/styles/main.css */
:root {
    --sidebar-width: 240px;
    --topbar-height: 48px;
    
    /* 平台变量 */
    --platform-gap: 16px;
    --platform-radius: 8px;
    --platform-font-size: 14px;
}

/* 鸿蒙PC特殊样式 */
[data-platform="ohos"] {
    --platform-gap: 12px;
    --platform-radius: 10px;
    --platform-font-size: 13px;
    
    /* 使用鸿蒙设计变量 */
    --color-primary: #007AFF;
    --color-background: #F5F5F5;
    --color-surface: #FFFFFF;
    --color-text: #1A1A1A;
    --color-text-secondary: #666666;
}

/* 侧边栏 */
.sidebar {
    width: var(--sidebar-width);
    height: 100vh;
    background: var(--color-surface);
    border-right: 1px solid #e0e0e0;
    display: flex;
    flex-direction: column;
    padding: 12px 0;
    user-select: none;
}

/* 鸿蒙PC上的隐藏控制 */
.ohos-only {
    display: none;
}

[data-platform="ohos"] .ohos-only {
    display: flex;
    gap: 4px;
}

/* 窗口控制按钮 */
.window-controls button {
    width: 32px;
    height: 28px;
    border: none;
    background: transparent;
    cursor: pointer;
    border-radius: 4px;
    color: var(--color-text);
}

.window-controls button:hover {
    background: rgba(0, 0, 0, 0.05);
}

.window-controls button:last-child:hover {
    background: #E81123;
    color: white;
}

第六章:打包与签名

6.1 electron-builder配置

# electron-builder.yml
appId: com.example.my-electron-app
productName: MyApp
copyright: Copyright © 2024

directories:
  output: dist
  buildResources: resources

files:
  - src/main/**/*
  - src/renderer/**/*
  - package.json
  - "!node_modules/**/*"

ohos:
  target:
    - hnp
  icon: resources/ohos/icon.png
  category: Utility
  packageName: com.example.myapp
  permissions:
    - ohos.permission.INTERNET
    - ohos.permission.FILE_READ_WRITE
    - ohos.permission.NOTIFICATION
  
mac:
  target:
    - dmg
    - zip
  icon: resources/icon.icns
  category: public.app-category.utilities
  hardenedRuntime: true
  entitlements: build/entitlements.mac.plist

win:
  target:
    - nsis
    - portable
  icon: resources/icon.ico

6.2 鸿蒙PC签名脚本

#!/bin/bash
# scripts/sign.sh - 鸿蒙PC签名脚本

PACKAGE_DIR="./dist/ohos/MyApp"
OUTPUT_DIR="./dist/ohos/signed"

mkdir -p $OUTPUT_DIR

echo "=== 开始签名 ==="

# 签名可执行文件
echo "签名主程序..."
binary-sign-tool sign \
    -inFile "$PACKAGE_DIR/MyApp" \
    -outFile "$OUTPUT_DIR/MyApp" \
    -selfSign "1"

# 签名所有 .so 文件
echo "签名动态库..."
find "$PACKAGE_DIR" -name "*.so" -type f | while read lib; do
    rel_path="${lib#$PACKAGE_DIR/}"
    output_path="$OUTPUT_DIR/$rel_path"
    mkdir -p "$(dirname "$output_path")"
    
    binary-sign-tool sign \
        -inFile "$lib" \
        -outFile "$output_path" \
        -selfSign "1"
    echo "  已签名: $rel_path"
done

# 签名 Node.js 原生模块
echo "签名 Node.js 原生模块..."
find "$PACKAGE_DIR" -name "*.node" -type f | while read addon; do
    rel_path="${addon#$PACKAGE_DIR/}"
    output_path="$OUTPUT_DIR/$rel_path"
    mkdir -p "$(dirname "$output_path")"
    
    binary-sign-tool sign \
        -inFile "$addon" \
        -outFile "$output_path" \
        -selfSign "1"
    echo "  已签名: $rel_path"
done

# 复制非二进制文件
echo "复制静态资源..."
rsync -av --exclude='*.so' --exclude='*.node' --exclude='MyApp' \
    "$PACKAGE_DIR/" "$OUTPUT_DIR/"

# 设置权限
chmod -R 755 "$OUTPUT_DIR"

echo "=== 签名完成 ==="
echo "产物路径: $OUTPUT_DIR"

6.3 打包为HNP格式

// hnp.json
{
    "type": "hnp-config",
    "name": "my-electron-app",
    "version": "1.0.0",
    "description": "基于Electron的鸿蒙PC应用",
    "license": "MIT",
    "arch": "arm64-v8a",
    "install": {
        "bin": ["usr/bin/my-electron-app"],
        "lib": ["usr/lib/*.so"],
        "share": ["usr/share/my-electron-app"]
    }
}

第七章:性能优化

7.1 启动优化

// 延迟加载非关键模块
const loadNonCriticalModules = () => {
    setTimeout(() => {
        require('./updater');
        require('./analytics');
        require('./crashReporter');
    }, 1000);
};

app.whenReady().then(() => {
    createWindow();
    createMenu();
    loadNonCriticalModules();  // 延迟1秒加载
});

7.2 内存管理

// 监控内存使用
setInterval(() => {
    const memoryInfo = process.getProcessMemoryInfo();
    if (memoryInfo.private > 500 * 1024 * 1024) {
        console.warn('内存使用超过500MB');
        // 触发GC
        if (global.gc) {
            global.gc();
        }
    }
}, 30000);

第八章:踩坑合集(Electron专属)

8.1 常见问题与解决

问题 原因 解决方案
ArkWeb渲染异常 CSS属性不支持 检查CSS兼容性,使用WebKit前缀
原生模块编译失败 musl libc差异 添加__MUSL__宏
打包后启动闪退 签名缺失或版本so缺失 检查所有.so是否签名
托盘图标不显示 图标格式错误 使用PNG格式,尺寸16x16或24x24
快捷键冲突 系统快捷键抢占 使用非标准组合键或自定义
preload.js报错 contextIsolation配置 检查contextBridge用法

8.2 调试技巧

# 开启Electron调试
electron --inspect=9229 .

# 查看进程日志
hdc hilog | grep "ELECTRON"

# 检查签名状态
binary-sign-tool verify -inFile /path/to/binary

# 查看依赖库
llvm-readelf -d /path/to/binary | grep NEEDED

结语

Electron+鸿蒙PC的组合,为Web开发者打开了一扇通往新平台的大门。虽然目前还处于早期阶段,存在一些兼容性问题,但随着社区贡献和官方推进,这条路会越来越通畅。

如果你是一个Electron开发者,现在就是最好的时机来尝试鸿蒙PC适配。因为你不仅能将自己的应用带到新平台,还能在这个过程中成为生态建设的先行者。


参考资源:

  • Electron官方文档:https://www.electronjs.org/docs
  • 鸿蒙PC Electron适配:https://harmonypc.csdn.net/
  • 三方库移植经验:https://blog.csdn.net/qq8864
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐