一、写在前面

欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:鸿蒙PC开发者社区:https://harmonypc.csdn.net/

项目开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_LXMusic

欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

环境搭建文章:https://blog.csdn.net/lbcyllqj/article/details/161286249?sharetype=blogdetail&sharerId=161286249&sharerefer=PC&sharesource=lbcyllqj&spm=1011.2480.3001.8118

这篇文章记录的是一次把 LX Music Desktop 适配到 OpenHarmony / HarmonyOS PC 的完整过程。

LX Music 是一个免费的音乐查找助手,桌面端基于 Electron、Vue、TypeScript、Webpack 构建。它不是一个只加载静态页面的普通 Electron 应用,而是同时包含主进程、渲染进程、歌词窗口、worker、播放器状态、下载任务、字体列表、自动更新、QQ 音乐加密歌词解码,以及本地 SQLite 数据库等能力。

因此,这次鸿蒙适配的目标不是简单让窗口打开,而是让原有 Vue 渲染层尽量保持不变,把 Electron 应用负载放入 OpenHarmony Electron 运行时壳工程中,再解决原生 Node 模块、数据库持久化、平台判断、自动更新、字体枚举等在鸿蒙侧不兼容的问题。

最终适配路线可以概括为:

LX Music 原 Electron 项目
        ↓
Webpack 构建 dist
        ↓
build-config/ohos/pack-app.js 组装应用负载
        ↓
ohos_hap/web_engine/src/main/resources/resfile/resources/app
        ↓
OpenHarmony Electron runtime 启动
        ↓
hvigor / DevEco Studio 打包 HAP
        ↓
鸿蒙 PC 真机安装运行

本次适配后的 HAP 已经可以完成签名构建,当前产物路径为:

ohos_hap/electron/build/default/outputs/default/electron-default-signed.hap

其中签名 HAP 大约 306 MB,鸿蒙运行时应用负载目录大约 130 MB。体积偏大的主要原因是 OpenHarmony Electron runtime、Chromium 资源、原生库以及 production 运行依赖闭包都被一起打进了 HAP。

在这里插入图片描述


二、项目背景

本次适配对象是 lx-music-desktop,项目版本为:

2.12.2

项目描述为:

一个免费的音乐查找助手

原项目主要技术栈包括:

  • Electron
  • Vue
  • Vue Router
  • TypeScript
  • Webpack
  • worker 线程
  • better-sqlite3
  • sql.js
  • electron-updater
  • font-list
  • qrc_decode 原生模块

从目录结构看,和鸿蒙适配关系最密切的目录主要有:

lx-music-desktop-master/
├── build-config/
│   ├── main/
│   ├── renderer/
│   └── ohos/
│       ├── pack-app.js
│       └── build-hap.js
├── src/
│   ├── common/
│   ├── main/
│   │   ├── modules/
│   │   ├── utils/
│   │   └── worker/dbService/
│   ├── renderer/
│   └── renderer-lyric/
├── dist/
├── ohos_hap/
├── package.json
└── OHOS_ADAPTATION.md

其中 ohos_hap/ 是新增的鸿蒙 HAP 壳工程,build-config/ohos/ 负责把 LX Music 的 Electron 运行资源同步到鸿蒙工程中。

这次适配的基础目标有六个:

  1. 不重写 LX Music 的 Vue 渲染层。
  2. 保留 Electron 主进程和渲染进程的原有组织方式。
  3. 构建产物可以被同步到 OpenHarmony Electron runtime 资源目录。
  4. HAP 可以通过 hvigor / DevEco Studio 构建并签名。
  5. 鸿蒙端启动时不因为原生 Node 模块加载失败而白屏或崩溃。
  6. 本地数据库可以在鸿蒙端完成初始化、读写和持久化。

在这里插入图片描述


三、为什么不能只把 dist 当静态页面塞进去

很多 Electron 项目迁到鸿蒙 PC 时,如果渲染层比较简单,可以把前端构建产物放进 HAP 资源目录,然后让窗口加载 index.html。LX Music 不能这么处理。

原因很直接:LX Music 的核心能力不只在页面上。

它至少依赖下面几类运行时能力:

  • 主进程负责窗口创建、应用生命周期、IPC、自动更新和本地能力封装。
  • 渲染进程负责音乐搜索、列表、播放器界面和设置页。
  • 歌词窗口有独立的 renderer-lyric 构建入口。
  • 下载、数据库、播放器状态等部分逻辑会使用 worker。
  • 本地数据依赖 SQLite 数据库。
  • 原项目使用 better-sqlite3 这类原生 .node 模块。
  • 字体枚举、QRC 歌词解码、自动更新等能力存在明显桌面平台假设。

也就是说,如果只把 dist/index.html 放进去,页面也许能出现,但主进程、数据库、worker 和运行依赖不完整,应用仍然不可用。

这次采用的是“应用负载”方案:先保留 LX Music 原有 Electron 架构,通过 webpack 构建 dist,再把运行所需的入口、清单、构建产物和 production 依赖同步到鸿蒙 Electron runtime 的资源目录中。

鸿蒙运行时真正加载的应用负载位于:

ohos_hap/web_engine/src/main/resources/resfile/resources/app

同步完成后的结构大致如下:

resources/app/
├── main.js
├── package.json
├── dist/
│   ├── main.js
│   ├── index.html
│   ├── lyric.html
│   ├── renderer.js
│   ├── renderer-lyric.js
│   ├── dbService.worker.js
│   └── ...
└── node_modules/

这里最关键的是 resources/app/main.js。它不是原项目的主进程源码,而是鸿蒙端启动引导文件,负责在真正加载 dist/main.js 之前注入鸿蒙运行标记、关闭硬件加速,并记录启动诊断日志。


四、构建和同步流程

项目新增了两个鸿蒙相关脚本:

{
  "ohos:pack": "node build-config/ohos/pack-app.js",
  "ohos:build": "node build-config/ohos/build-hap.js"
}

npm run ohos:pack 只负责组装应用负载,主要做这些事情:

  1. 检查 dist/main.jsdist/index.html 是否存在。
  2. 如果 dist 不存在,就自动执行 npm run build
  3. 清空鸿蒙 runtime 的应用负载目录。
  4. 拷贝 dist
  5. 写入鸿蒙启动引导 main.js
  6. 写入运行时 package.json
  7. 根据 package-lock.json 拷贝 production 依赖闭包。

核心输出目录是:

ohos_hap/web_engine/src/main/resources/resfile/resources/app

构建应用负载:

npm install
npm run ohos:pack

构建 HAP:

npm run ohos:build

build-config/ohos/build-hap.js 会先调用 pack-app.js,再自动查找 hvigorw 和 DevEco SDK,最后执行:

hvigorw --mode module -p product=default assembleHap

最终产物位于:

ohos_hap/electron/build/default/outputs/default/electron-default-signed.hap

如果首次更换了 bundleName,需要先在 DevEco Studio 中重新生成签名。操作路径是:

File > Project Structure > Signing Configs > Automatically generate signature

注意:启动命令中的 bundleName 一定以 ohos_hap/AppScope/app.json5 里的实际值为准。如果后续把包名改为 com.lxmusic.desktop,需要重新生成对应包名的调试签名。

五、第一处关键改造:给鸿蒙运行时注入平台标记

LX Music 原本主要面向 Windows、macOS、Linux 桌面端。OpenHarmony Electron runtime 中的 process.platform 并不一定稳定表现为桌面平台,也不能直接套用 Linux 桌面行为。

因此,适配时新增了一个鸿蒙运行标记:

export const isOpenHarmony = process.env.LX_OPENHARMONY === '1' ||
  /^(ohos|openharmony|harmony)$/i.test(process.platform)

这个判断放在:

src/common/utils/index.ts

对应地,build-config/ohos/pack-app.js 写入的鸿蒙启动引导会在加载原主进程之前设置环境变量:

process.env.LX_OPENHARMONY = '1'
process.env.NODE_ENV = process.env.NODE_ENV || 'production'

同时,为了避免鸿蒙 Electron runtime 在某些设备上出现 GPU / Chromium 渲染兼容问题,启动引导里还会提前关闭硬件加速:

try { app.disableHardwareAcceleration() } catch (err) { ... }

这一层启动引导还做了一个很实用的事情:把主进程未捕获异常、渲染进程加载失败、preload 错误、console 消息写入 lx-boot.log。这样真机上如果出现白屏,不需要只靠猜,可以先去看启动日志。

process.on('uncaughtException', ...)
process.on('unhandledRejection', ...)
wc.on('did-fail-load', ...)
wc.on('render-process-gone', ...)
wc.on('console-message', ...)

这一步的意义在于,把“当前是不是鸿蒙运行时”变成一个全局可判断的事实。后续数据库、字体、更新、原生模块降级,都可以围绕 isOpenHarmony 做清晰分支。

在这里插入图片描述


六、第二处关键改造:数据库从 better-sqlite3 切到 sql.js 适配层

这次适配里最关键的问题是数据库。

LX Music 桌面端使用 better-sqlite3。这个库性能很好,接口也是同步的,很适合 Electron 主进程或 worker 中使用。但它依赖原生 .node 绑定,实际运行时会加载类似下面的文件:

better_sqlite3.node

问题是,鸿蒙 PC 侧不能直接加载为 macOS / Windows / Linux 构建出来的 .node 文件。而 better-sqlite3 又基于 V8 / Node C++ API,当前适配环境下无法简单交叉编译出可用的鸿蒙 arm64 版本。

如果继续在模块顶层 import Database from 'better-sqlite3',鸿蒙端很容易在模块加载阶段就抛错,导致主进程或 worker 直接崩掉。

所以这里做了两个处理。

第一,db.ts 中只保留类型引用:

import type Database from 'better-sqlite3'

这样 TypeScript 编译阶段仍然有类型信息,但运行时不会立即加载 better-sqlite3

第二,运行时按平台分支:

if (isOpenHarmony) {
  db = await openSqljsDatabase(databasePath, { fileMustExist: true }) as unknown as Database.Database
} else {
  const Database = require('better-sqlite3')
  db = new Database(databasePath, ...)
}

鸿蒙端走 src/main/worker/dbService/sqljs/database.ts,桌面端仍然走原来的 better-sqlite3。这样 Windows、macOS、Linux 行为不会受影响。

需要注意的是,适配过程里一开始容易想到使用 sql.js 的 WASM 版本。但当前鸿蒙 Electron 运行时的 Node 侧 V8 环境并不适合直接依赖全局 WebAssembly 初始化,因此最终落地时使用的是:

const initSqlJs = require('sql.js/dist/sql-asm.js')

也就是 sql.js 的 asm.js 版本。它是纯 JS 初始化,不需要额外的 .node 原生模块,也不依赖鸿蒙侧能否加载 WASM。

为了让上层数据库 helper 尽量不用改,sqljs/database.ts 实现了一层与 better-sqlite3 子集兼容的接口:

db.prepare(sql)
stmt.run(...)
stmt.get(...)
stmt.all(...)
stmt.iterate(...)
db.exec(sql)
db.pragma(...)
db.transaction(fn)
db.close()

同时,sql.js 是内存数据库,所以还要处理持久化。当前实现会在数据变更后打 dirty 标记,并通过防抖方式把数据库导出为二进制,再原子写回 lx.data.db

const data = Buffer.from(this.rawDb.export())
const tmp = `${this.filePath}.tmp`
fs.writeFileSync(tmp, data)
fs.renameSync(tmp, this.filePath)

这一步解决了 LX Music 在鸿蒙端“能启动但数据库不可用”的核心问题。保留原 SQL、原表结构、原 dbHelper,大部分业务代码就不需要知道底层数据库已经从原生 SQLite 切到了纯 JS SQLite。

在这里插入图片描述


七、其他兼容处理:更新、字体和 QRC 原生模块

数据库是最大的问题,但不是唯一问题。

LX Music 中还有一些桌面端能力,在鸿蒙端需要明确边界。

1. 自动更新短路

原项目使用 electron-updater。但鸿蒙端没有对应的 Windows / macOS / Linux 安装包更新链路,如果照常触发更新检查,容易得到平台不支持或更新源不匹配的问题。

因此在:

src/main/modules/winMain/autoUpdate.ts

中增加了鸿蒙判断:

if (isOpenHarmony) {
  handleSendEvent({ type: WIN_MAIN_RENDERER_EVENT_NAME.update_not_available, info: { version: '' } as any })
  return
}

这样设置页或更新入口仍然能得到一个可处理的结果,但不会真的调用 autoUpdater.checkForUpdates()

2. font-list 懒加载

原项目会通过 font-list 获取系统字体。但 font-list 在 OpenHarmony 上可能会在 require 阶段直接抛错。

因此在:

src/main/utils/fontManage.ts

中改成按平台懒加载:

export const getFonts = async(): Promise<string[]> => {
  if (isOpenHarmony) return []
  const fontList = require('font-list')
  return fontList.getFonts()
}

这类能力不是 LX Music 启动的必需项,鸿蒙端返回空数组比主进程崩溃更合理。

3. qrc_decode 原生模块降级

LX Music 中的 QQ 音乐加密歌词解码依赖 qrc_decode.node。这个模块同样是原生绑定,鸿蒙端没有对应版本。

在:

src/main/modules/winMain/rendererEvent/tx_decodeLyric.ts

中,适配后的逻辑会尝试加载:

const addon = require('qrc_decode.node')

如果失败,就设置不可用标记,并返回空歌词结果:

qrcDecodeUnavailable = true
return { lyric: '', tlyric: '', rlyric: '' }

这样做的取舍很明确:QQ 音乐加密歌词解码这类增强能力可以暂时降级,但不能因为一个原生模块不可用影响整个播放器启动和普通播放链路。


八、HAP 构建、安装和启动验证

构建 HAP 的命令是:

npm run ohos:build

构建成功后可以看到:

ohos_hap/electron/build/default/outputs/default/electron-default-signed.hap

当前本地已经生成:

electron-default-signed.hap
electron-default-unsigned.hap

安装到设备:

hdc install -r ohos_hap/electron/build/default/outputs/default/electron-default-signed.hap

启动应用时,bundleName 以 ohos_hap/AppScope/app.json5 为准:

hdc shell aa start -a EntryAbility -b <bundleName> -m electron

如果要先停止再启动:

hdc shell aa force-stop <bundleName>
hdc shell aa start -a EntryAbility -b <bundleName> -m electron

如果真机启动后出现白屏,可以优先看启动引导写入的日志。鸿蒙端默认会尽量写到应用 userData 目录,兜底目录是:

/data/storage/el2/base/files/lx-boot.log

也可以先通过 HDC 确认设备连接:

hdc list targets

在这里插入图片描述


九、适配过程小结

这次 LX Music 的鸿蒙 PC 适配,表面上是把一个 Electron 应用放进 HAP,实际核心是把“桌面 Electron 默认成立的运行假设”逐个拆开。

最关键的经验有四点。

第一,Electron 应用不能只看页面是否显示。LX Music 的主进程、worker、数据库、歌词窗口和播放器状态都属于可用性的一部分。

第二,原生 .node 模块是迁移到鸿蒙 PC 时最需要优先排查的风险点。better-sqlite3qrc_decode.nodefont-list 这类模块如果在顶层直接加载,可能会在页面出现之前就导致主进程崩溃。

第三,对核心能力要尽量做等价替换。数据库不能简单跳过,所以这里实现了 sql.js 兼容层,让上层 dbHelper 保持原来的 prepare/run/get/all/transaction 使用方式。

第四,对非核心能力要允许降级。自动更新、字体枚举、QQ 音乐加密歌词解码这些能力在鸿蒙端可以先短路或返回空结果,保证主应用能启动、能搜索、能播放、能保存基础数据。

最终适配后的 LX Music 保留了原有 Electron + Vue 架构,通过 ohos_hap 壳工程和 build-config/ohos 脚本完成 HAP 构建。后续如果继续优化,重点可以放在三处:

  1. 裁剪 node_modules 运行依赖,降低 HAP 体积。
  2. qrc_decode.node 提供鸿蒙 arm64 原生实现,恢复 QQ 音乐加密歌词解码能力。
  3. 继续逐项验证托盘、全局快捷键、系统字体、下载目录选择、歌词窗口等桌面特性在鸿蒙 PC 上的实际体验。

整体来看,LX Music 这种项目适配鸿蒙 PC 的重点不是“把前端打进去”,而是保住 Electron 应用的完整运行链路:主进程能启动,渲染层能加载,worker 能跑,数据库能读写,原生模块不可用时能降级,最后再通过 HAP 安装到真机上验证真实使用路径。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐