一、写在前面
开源项目地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_yesplaymusic
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
这篇文章记录的是 YesPlayMusic 在 OpenHarmony / HarmonyOS Electron 运行环境中的一次迁移和适配过程。
YesPlayMusic 是一个第三方网易云音乐播放器,原始项目基于 Electron + Vue 2 构建。和普通 Web 项目不同,它不是只要页面能打开就算完成迁移。音乐播放器真正要验证的是一整条使用链路:
- 首页、发现、音乐库、搜索这些页面要能正常切换
- 本地网易云 API 服务要能启动
- 歌曲详情、歌单、专辑、歌手信息要能请求
- 播放、暂停、切歌、进度更新要稳定
- 外部链接、Last.fm 授权、设置页要符合鸿蒙端使用习惯
- 窗口按钮、应用图标这些原生壳能力要接到鸿蒙工程里
针对鸿蒙适配,这次没有把它当成一个"静态页面打包"问题处理,而是选择了一条更接近原桌面端运行方式的路线:
保留 YesPlayMusic 原有 Vue 业务界面,通过 OpenHarmony Electron runtime 启动本地 Node 服务和本地 Web 服务,再由鸿蒙窗口加载
http://127.0.0.1:27232。
这样做的核心目标,是让 YesPlayMusic 在鸿蒙端不只是"页面展示出来",而是具备一个播放器应用的基本可用状态。
本文会按照实际适配步骤展开:先接入 HAP 工程,再处理构建脚本和运行入口,然后逐步解决本地 API、播放链路、窗口按钮、设置项、外部链接和图标这些问题。
先说一点个人判断:YesPlayMusic 这种项目不能只看"窗口里有没有内容"。首页能显示,只能说明 Vue 页面进来了;搜索能请求、歌曲能播放、设置项不误导用户、窗口按钮能正常显示,才说明鸿蒙端具备了可用基础。
所以这次的策略是把 YesPlayMusic 原有 Vue 界面、本地网易云 API、本地 HTTP 服务和鸿蒙 Electron runtime 串成一条完整链路。后面所有脚本和代码调整,也都围绕这个目标展开:确保资源能进 HAP,确保 API 能被前端访问,确保播放器失败时有恢复逻辑,确保桌面端能力在鸿蒙端有明确边界。

二、项目背景
YesPlayMusic 项目版本为:
text
0.4.10
项目描述是:
text
A third party music player for Netease Music
原项目主要技术栈包括:
- Electron
- Vue 2
- Vue Router
- Vuex
- Howler
- 本地网易云 API 服务
- Last.fm 授权
- Electron IPC
- 桌面端托盘、快捷键、MPRIS、系统菜单等能力
从目录结构看,原项目的核心代码主要分布在:
text
src/
├── api/
├── components/
├── electron/
├── store/
├── utils/
└── views/
其中 src/electron 和 src/utils/Player.js 对桌面 Electron 环境依赖较明显。比如 renderer 中会使用 window.require('electron'),播放器状态会同步到托盘和 MPRIS,设置页中也保留了很多桌面平台专属选项。
这类项目迁移到鸿蒙时,不能简单按 Web 项目处理。因为它的运行过程至少包含三层:
- Electron 主进程负责窗口、本地服务、IPC 和外部链接。
- Vue renderer 负责页面、路由、播放器 UI 和业务状态。
- 本地网易云 API 服务负责音乐数据和播放链路。
所以这次适配的重点,是把这三层重新组织到鸿蒙 HAP 工程中。
三、总体变化概览
3.1 新增鸿蒙工程壳
适配后的项目新增:
text
ohos_hap/
├── AppScope/
├── electron/
├── web_engine/
└── build-profile.json5
鸿蒙 Electron runtime 最终读取的应用资源目录是:
text
ohos_hap/web_engine/src/main/resources/resfile/resources/app
最终同步进去的运行资源大致包括:
text
resources/app/
├── main.js
├── package.json
├── ncmModDef.js
├── dist/
└── node_modules/

3.2 新增构建脚本
项目新增:
text
scripts/build-ohos-package.js
scripts/build-ohos-hap.js
并在 package.json 中增加:
json
{
"ohos:sync": "OHOS_YESPLAYMUSIC_OUT=ohos_hap/web_engine/src/main/resources/resfile/resources/app node scripts/build-ohos-package.js",
"ohos:build": "node scripts/build-ohos-hap.js"
}
这两个命令分别负责:
ohos:sync:构建 Vue 产物,生成鸿蒙 runtime 入口,同步运行资源和依赖。ohos:build:同步资源、补齐 OpenHarmony 依赖、同步图标、调用 Hvigor 构建 HAP。
3.3 新增鸿蒙 Electron 启动引导
scripts/build-ohos-package.js 会写入:
text
resources/app/main.js
这个入口负责:
- 设置
YESPLAYMUSIC_OPENHARMONY=1 - 创建 YesPlayMusic 主窗口
- 启动本地网易云 API 服务
- 启动本地 Express 静态服务
- 将
/api代理到127.0.0.1:10754 - 拦截外部链接和 Last.fm 授权窗口
- 注册窗口控制、设置同步、代理配置等 IPC
四、为什么 YesPlayMusic 不能只按静态页面迁移
很多前端应用迁到鸿蒙 Electron 时,可以选择直接加载 dist/index.html。但 YesPlayMusic 不太适合这么做。
原因很简单:它是播放器,不是展示页。
如果直接把 Vue 构建产物放进 HAP,可能会出现:
- 页面可以打开,但
/api请求没有后端承接 - 首页能显示局部内容,但搜索、歌单、播放请求失败
- Last.fm 授权无法回到应用内
window.open打开外部链接行为不稳定- 设置页还显示桌面端功能,但鸿蒙端实际无法使用
- 播放失败时没有良好的恢复路径
因此这次没有选择纯静态 file:// 方案,而是在鸿蒙 Electron 主进程中启动一个本地服务:
text
http://127.0.0.1:27232
其中:
/加载 Vue 构建后的dist/api代理到本地网易云 API/player暴露当前播放状态
这样做以后,YesPlayMusic 前端仍然运行在熟悉的 HTTP 环境里,路由、资源、接口和播放器状态都更容易稳定下来。
这条路线可以概括成:
dist前端产物 + 本地网易云 API + Express 同源代理 + 鸿蒙 Electron 窗口。
它比纯静态方案多了一层本地服务,但换来的好处是播放器链路更接近原 Electron 桌面端,也更方便逐步验证首页、搜索、歌单和播放这些核心路径。
五、鸿蒙适配核心路径
5.1 第一步:构建 Vue 产物并修正资源路径
scripts/build-ohos-package.js 首先检查:
text
dist/index.html
如果产物不存在,或者发现里面仍然包含根路径资源,就会重新执行构建。
脚本中有一个资源路径检查:
js
const hasRootRelativeAssets = file => {
const html = fs.readFileSync(file, 'utf8');
return /(?:src|href|content)="\/(?:css|js|img|fonts|favicon\.ico|manifest\.json)/.test(html);
};
构建时会设置:
text
PUBLIC_PATH=./
YESPLAYMUSIC_OHOS_BUILD=1
NODE_OPTIONS=--openssl-legacy-provider
这里的几个变量都比较关键:
PUBLIC_PATH=./保证构建产物可以在鸿蒙 runtime 中正确加载资源。YESPLAYMUSIC_OHOS_BUILD=1用来告诉前端当前是鸿蒙构建。NODE_OPTIONS=--openssl-legacy-provider解决 Vue CLI / webpack 4 在较新 Node 环境下的 OpenSSL 兼容问题。
5.2 第二步:给前端注入鸿蒙运行标识
vue.config.js 中增加了鸿蒙构建判断:
js
const isOhosBuild = process.env.YESPLAYMUSIC_OHOS_BUILD === '1';
鸿蒙构建时会注入:
js
process.env.IS_ELECTRON = true;
process.env.IS_OHOS = true;
这样前端可以明确区分三种环境:
- 普通 Web
- 桌面 Electron
- 鸿蒙 Electron
后续设置页裁剪、播放器 IPC、安全兜底、标题栏行为,都是基于这个判断来做的。
5.3 第三步:生成鸿蒙 runtime 入口
鸿蒙端使用脚本生成的 main.js,而不是直接照搬桌面端主进程。
它会启动本地网易云 API:
js
await server.serveNcmApi({
port: 10754,
moduleDefs: require('./ncmModDef')
});
再启动本地应用服务:
js
expressApp.use('/', express.static(path.join(__dirname, 'dist')));
expressApp.use('/api', expressProxy('http://127.0.0.1:10754'));
appServer = expressApp.listen(27232, '127.0.0.1');
最后创建窗口:
js
win.loadURL('http://127.0.0.1:27232/');
这一步完成了从 Vue 页面到本地音乐 API 的闭环。
5.4 第四步:复制运行时依赖
YesPlayMusic 鸿蒙运行时需要一部分 Node 依赖进入 HAP:
js
const runtimeDependencyEntrypoints = [
'@neteaseapireborn',
'axios',
'express',
'express-fileupload',
'express-http-proxy'
];
脚本会递归解析这些依赖,并复制到:
text
resources/app/node_modules
这一步很重要。只复制 dist 没有意义,因为本地 API 服务和 Express 代理都需要运行时依赖。
5.5 第五步:收口 Electron IPC
原项目中不少文件会直接使用:
js
window.require('electron')
在桌面端这通常没有问题,但鸿蒙适配时更容易遇到运行时 API 不一致、模块加载顺序不同、某些能力不可用等情况。
因此新增:
text
src/utils/electron.js
统一提供:
js
getElectron()
getIpcRenderer()
sendIpc()
invokeIpc()
onIpc()
前端发送 IPC 时不再直接访问 window.require,而是先走安全封装。这样即使某些鸿蒙端不支持的 Electron 能力不可用,也不会在页面初始化阶段直接崩掉。
5.6 第六步:调整播放器播放失败恢复逻辑
播放器核心在:
text
src/utils/Player.js
鸿蒙端需要处理两个问题:
第一,桌面端的托盘、MPRIS、UnblockNeteaseMusic 等能力不能默认启用。
第二,音频加载失败时不能反复使用同一个缓存音源,否则用户看到的就是"点了播放但没反应"。
适配后做了几件事:
- 鸿蒙端跳过 MPRIS 和托盘更新。
- 鸿蒙端不启用 UnblockNeteaseMusic 桌面链路。
- Howler
loaderror后跳过缓存重新取音源。 - 同一首歌同一音源失败后进入下一首,避免播放器卡住。
- 播放失败时给出 toast 提示。

5.7 第七步:裁剪鸿蒙端不适合的设置项
设置页在:
text
src/views/settings.vue
适配中增加了:
js
isOhos() {
return process.env.IS_OHOS === true;
},
isDesktopElectron() {
return this.isElectron && !this.isOhos;
}
鸿蒙端隐藏或固定这些桌面专属能力:
- 托盘图标主题
- 音频输出设备选择
- UnblockNeteaseMusic 配置
- Discord Rich Presence
- Linux 自定义标题栏
- 全局快捷键
- OS 桌面歌词等能力
同时通过 normalizeOhosSettings() 把不适合鸿蒙端的设置写回安全值:
js
enableUnblockNeteaseMusic: false
enableDiscordRichPresence: false
enableGlobalShortcut: false
enableOsdlyricsSupport: false
linuxEnableCustomTitlebar: false
outputDevice: 'default'
这样用户在鸿蒙端看到的是可用配置,而不是一堆点了没有效果的桌面选项。

5.8 第八步:处理窗口按钮和顶部点击区域
窗口按钮不能放在 Vue 页面里模拟。
YesPlayMusic 原本有 Win32 / Linux 自定义标题栏组件,但鸿蒙端应该使用原生窗口装饰。主进程创建窗口时设置:
js
frame: true,
resizable: true,
minimizable: true,
maximizable: true,
closable: true,
titleBarStyle: 'default'
并调用:
js
setUseNativeFrame(true)
setWindowButtonVisibility(true)
setMinimizable(true)
setMaximizable(true)
setClosable(true)
setResizable(true)
同时,Vue 顶部导航栏在鸿蒙端要禁用桌面 Electron 的拖动区域:
css
nav.is-ohos {
-webkit-app-region: no-drag;
}
这个修正很关键。否则顶部导航可能被当成窗口拖动区域,导致首页、发现、音乐库、搜索、头像菜单这些按钮点击无效。
5.9 第九步:处理外部链接和 Last.fm 授权
新增:
text
src/utils/openExternal.js
用于统一处理外部链接:
- GitHub
- 网易云专辑 / 歌手 / MV 页面
- Last.fm 授权页面
鸿蒙端通过 IPC 发给主进程:
js
sendIpc('open-url', normalizedUrl)
这里踩过一个比较隐蔽的问题。Vue Router 在 Electron 模式下使用 hash 路由,站内链接会解析成:
text
http://127.0.0.1:27232/#/explore
http://127.0.0.1:27232/#/library
如果外链拦截器只判断 http,就会把站内路由也拦截掉,导致顶部"发现""音乐库"点击没反应。最终修成只拦截跨域链接:
js
new URL(url, window.location.href).origin !== window.location.origin
主进程也增加保护:如果收到的是 127.0.0.1:27232/#/...,只在主窗口导航,不再新开窗口。

5.10 第十步:同步原项目图标
YesPlayMusic 原项目图标位于:
text
build/icons/icon.png
build/icons/512x512.png
build/icons/32x32.png
鸿蒙端应用图标来自:
text
ohos_hap/AppScope/resources/base/media/app_icon.png
启动图和小图标还包括:
text
startIcon.png
product_logo_32.png
scripts/build-ohos-hap.js 中增加 syncOhosIcons(),每次构建 HAP 前自动同步:
text
app_icon.png <- build/icons/512x512.png
product_logo_32.png <- build/icons/32x32.png
startIcon.png <- build/icons/icon.png 生成 192x192
这样可以保证桌面图标、启动图标和项目原 Electron 图标保持一致。
六、问题困难解决
这次 YesPlayMusic 适配中最核心的问题是:
如何把一个依赖 Electron 主进程、本地 API 和播放器状态的桌面音乐应用,整理成鸿蒙端可用的 HAP 应用。
具体看,主要有下面几类问题。
6.1 困难一:页面能打开不代表接口能用
YesPlayMusic 不适合只加载静态 dist。
因为播放器数据来自本地网易云 API,如果没有本地 API 服务,首页、搜索、歌单和播放都会受影响。
解决方式是在 runtime main.js 中启动:
@neteaseapireborn/api/server- Express 静态服务
/api本地代理
最终前端只需要访问同源 /api,不用关心鸿蒙端本地服务细节。
6.2 困难二:桌面 Electron API 不能散落在 renderer 里
原项目中有大量 Electron 调用散落在组件和工具文件中。鸿蒙端如果直接照搬,容易出现某个 API 不存在就导致页面初始化失败。
解决方式是新增 src/utils/electron.js,把 IPC 和 Electron 模块访问统一收口。
这样播放器、设置页、标题栏组件、store 插件都可以通过 sendIpc()、invokeIpc()、onIpc() 访问主进程,失败时也有统一兜底。
6.3 困难三:播放器失败恢复要更清楚
音乐播放器的失败状态比普通工具应用更常见。歌曲可能因为版权、格式、缓存音源失效等原因无法播放。
适配中调整了 Player.js:
loaderror后跳过缓存重取音源- 同一音源失败后不无限重试
- 失败时提示用户
- 必要时自动进入下一首
这样用户不会卡在一首不可播放的歌上。
6.4 困难四:鸿蒙端需要主动做功能减法
托盘、MPRIS、Linux 自定义标题栏、Discord Rich Presence、全局快捷键等能力,在传统桌面端合理,但在当前鸿蒙 Electron 运行环境中并不适合作为默认能力展示。
适配方式不是强行模拟,而是:
- 桌面端继续保留。
- 鸿蒙端隐藏对应设置项。
- store 中把不适合的设置规范化为安全值。
- 播放器逻辑中跳过这些桌面集成。
这能减少用户点击无效功能的情况。
6.5 困难五:窗口按钮必须回到原生壳
最初容易误判的一点是,把最大化、最小化、关闭按钮做在 Vue 页面里。
但鸿蒙 Electron 的窗口按钮属于原生窗口装饰,应该在壳层处理。
最终做法是在主进程和鸿蒙 WebAbility 相关逻辑中启用原生窗口按钮,并在 Vue 侧关闭鸿蒙端自定义标题栏。
同时处理 -webkit-app-region: drag 对顶部导航点击的影响,让顶部导航回到普通可点击区域。
6.6 困难六:外链拦截不能误伤站内 hash 路由
外部链接适配时,最容易出现的问题是把站内路由也拦截掉。
YesPlayMusic 使用 hash 路由后,#/explore 这类站内地址在浏览器里也是完整 HTTP URL。如果只判断 http,就会导致路由点击失效。
最终判断条件改为"跨域 http/https 才拦截",同时主进程对本地同源 URL 做保护,不再打开新窗口。
6.7 运行链路确认顺序
YesPlayMusic 在鸿蒙端的运行链路可以按这个顺序确认:
dist/index.html是否生成。resources/app/dist是否同步成功。resources/app/main.js是否生成。resources/app/node_modules是否包含运行时依赖。- 本地网易云 API 是否启动。
- Express 本地服务是否监听
127.0.0.1:27232。 - BrowserWindow 是否加载首页。
- 顶部导航、搜索、头像菜单是否可点击。
- 歌曲是否能播放、失败是否能恢复。
- HAP 图标和窗口按钮是否符合鸿蒙端表现。
这个顺序能把问题从 HAP、主进程、前端资源、本地服务、播放器行为逐层拆开。
七、构建与运行方式
同步资源:
bash
cd ~/XM/yesplaymusic-ohos
npm run ohos:sync
构建 HAP:
bash
cd ~/XM/yesplaymusic-ohos
env NODE_OPTIONS=--openssl-legacy-provider OHOS_YESPLAYMUSIC_FORCE_REBUILD=1 npm run ohos:build
HAP 输出目录:
text
ohos_hap/electron/build/default/outputs/default/
当前生成的 HAP:
text
electron-default-unsigned.hap
安装和启动可以使用:
bash
hdc shell aa force-stop io.yesplaymusic.ohos
hdc uninstall io.yesplaymusic.ohos
hdc shell mkdir -p data/local/tmp/yesplaymusic-ohos
hdc file send ohos_hap/electron/build/default/outputs/default/electron-default-unsigned.hap data/local/tmp/yesplaymusic-ohos
hdc shell bm install -p data/local/tmp/yesplaymusic-ohos
hdc shell aa start -a EntryAbility -b io.yesplaymusic.ohos
安装成功后可以看到:
text
install bundle successfully.
start ability successfully.

八、适配成果
yesplaymusic-ohos 已经完成:
- OpenHarmony Electron HAP 工程接入
- Vue 前端产物构建与同步
- 本地 Express 服务承载前端页面
- 本地网易云 API 服务启动
/api同源代理- Electron IPC 安全封装
- 播放器音源获取和失败恢复优化
- 首页、发现、音乐库、搜索主路径适配
- 设置页鸿蒙端能力裁剪
- Last.fm 授权窗口处理
- 网易云外部链接处理
- 鸿蒙原生窗口按钮适配
- YesPlayMusic 原图标同步到 AppScope
- 命令行构建 HAP
暂时不强行迁移的能力包括:
- 托盘图标
- MPRIS
- Discord Rich Presence
- 全局快捷键
- Linux 自定义标题栏
- UnblockNeteaseMusic 桌面端完整能力
这部分能力并不是完全不能做,而是当前更适合先从鸿蒙端隐藏或禁用,避免用户看到无效选项。
九、功能验证结果
这次适配按播放器实际使用路径做了验证。
第一组是应用启动和页面:
- 应用可以安装并启动
- 首页能正常显示
- 发现页面能切换
- 音乐库入口正常
- 搜索框可以输入并跳转搜索页
- 头像菜单可以打开设置、登录或 GitHub 入口
第二组是音乐数据和播放:
- 首页推荐数据可加载
- 搜索结果可显示
- 歌曲点击后底部播放器更新
- 播放、暂停、切歌、进度显示正常
- 不可播放歌曲会提示并跳过
第三组是鸿蒙端交互:
- 原生最大化、最小化、关闭按钮可见
- 顶部导航不再被拖拽区域吞掉点击
- 外部链接不会误伤站内路由
- Last.fm 授权能通过新窗口打开
- 设置页不再展示明显不适合鸿蒙端的桌面选项
第四组是构建和图标:
npm run ohos:build可以生成 HAP- HAP 可通过
hdc安装 - 应用图标替换为 YesPlayMusic 原图标
- 启动图标和小图标同步到鸿蒙资源目录
十、结语
yesplaymusic-ohos 的适配重点,是把一个依赖 Electron 主进程和本地音乐 API 的桌面播放器,迁移到鸿蒙 Electron runtime 中稳定运行。
这次实践中最重要的不是简单把页面塞进 HAP,而是把几条链路打通:
- HAP 工程承载应用资源
- Electron 主进程启动本地服务
- Vue 前端通过同源
/api获取音乐数据 - 播放器能处理成功和失败状态
- 桌面端能力在鸿蒙端有明确边界
- 原生窗口按钮和应用图标接入鸿蒙工程
从结果看,YesPlayMusic 已经完成鸿蒙端的基础可用适配。它保留了播放器的核心体验,也没有强行展示不适合鸿蒙端的桌面能力。
这类迁移项目的经验是:先把运行链路做稳,再逐步补平台能力。对音乐播放器来说,真正重要的是用户能打开、能搜索、能播放、能切换、能退出,而不是一开始就把桌面端所有边角能力全部搬过去。