把 Mockoon 搬上鸿蒙 PC:开源 API 模拟工具的完整移植与上架实战
把 Mockoon 搬上鸿蒙 PC:开源 API 模拟工具的完整移植与上架实战
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_Mockoon
本文记录一次完整的开源桌面软件移植实战:将知名开源 API 模拟工具 Mockoon(Electron 43 + Angular 22)完整移植到 HarmonyOS PC 平台,以 HAP 形式承载上架应用市场,并顺利通过应用市场审核的全过程。文章覆盖技术选型、构建链路改造、签名配置、上架材料准备,以及一次真实的应用市场审核被拒(应用内弹窗名称与上架名不一致)的定位与修复全过程,附踩坑清单与可复现命令。

一、为什么选择 Mockoon
做后端联调的同学都体会过这种痛苦:前端页面写好了,后端接口还没就绪;或者第三方服务限流、收费、不稳定,测试环境里想模拟一个超时、一个 500 错误都费劲。API Mock 就是解决这类问题的——在本地快速搭建一个假的 REST API 服务,返回你定义好的状态码、响应头和响应体。

在众多 Mock 方案里,我最终选定了 Mockoon,理由有三个:
第一,它是真正的桌面软件,不是网页工具。 不需要把接口数据上传到任何云服务,纯本地运行,这对有数据安全顾虑的团队和个人都很关键。
第二,它是开源的(MIT 协议),并且代码质量很高。 GitHub 上持续维护,架构清晰:主进程、渲染进程、公共库分层明确,这意味着移植过程中遇到问题可以读源码解决,而不是黑盒猜测。
第三,它的功能密度足够支撑一个"工具类"上架应用。 可视化创建路由、CRUD 数据桶、动态响应模板、Faker.js 随机数据生成、请求代理转发、TLS、请求日志——这些能力让应用在应用市场上是有真实使用价值的作品,而不是一个空壳 Demo。
Mockoon 官网对它的定位非常直白:“the easiest and quickest way to design and run mock APIs. No remote deployment, no account required, free and open-source.”

二、认识上游:技术栈与架构
动手之前必须先把上游摸透。Mockoon 当时的目标版本是 9.8.0,整个 monorepo 结构大致是:
mockoon-src/
├── packages/
│ ├── commons/ # 公共类型与工具(纯 TS 库)
│ ├── commons-server/ # Mock 服务端核心(基于 Express 封装)
│ ├── app/ # 桌面应用(本次移植主体)
│ │ ├── src/main/ # Electron 主进程(TypeScript + esbuild 打包)
│ │ ├── src/renderer/ # Angular 22 渲染进程
│ │ └── src/shared/ # 主/渲染进程共享类型
│ ├── cli/ # 命令行版(本次不涉及)
│ └── desktop/ # Electron 打包器(electron-builder,本次不涉及)
└── package.json # npm workspaces
几个对移植决策影响很大的技术事实:
- 渲染进程是 Angular 22,构建产物是静态文件(index.html + JS bundle + CSS),这决定了渲染层可以原样搬迁,不需要任何重写;
- 主进程用 esbuild 打包成单文件
app.js,Electron 本身被声明为 external,这是后面构建策略改造的核心位置; - 数据存储走本地 JSON 文件(环境配置、设置项都是 JSON 落盘),没有 SQLite 之类的原生依赖,省掉了 Node 原生模块交叉编译这个大坑;
- 服务端核心 commons-server 是纯 JS,起 HTTP 服务用的是 Node 的 http/express 生态,在鸿蒙侧只要 Node 运行时可用,这部分就是零成本。
先看一下原版在桌面上的样子,这是一个典型的三栏布局 Mock 工具:左侧环境列表、中间路由列表、右侧路由详情(请求方法、路径、状态码、响应体编辑器),顶部一排页签切换路由/数据/请求头/回调/日志/代理/设置。

在动手前,值得把它的核心能力逐个盘点一遍,因为这些能力决定了移植后的应用"值不值得上架":
可视化路由定义。 每条路由由请求方法(GET/POST/PUT/PATCH/DELETE 等)+ 路径 + 响应规则组成,路径支持 :param 参数和通配符匹配。所有配置都是表单化操作,不写一行代码就能起一个 API。
CRUD 数据桶。 这是它最"聪明"的功能:定义一个数据桶(Data bucket),一条路由配置自动展开成 GET/POST/PUT/DELETE 全套接口,数据持久化在本地 JSON 里,相当于白送一个带增删改查的假数据库。上图里的 users 路由就是这种模式。
动态响应模板。 响应体里可以嵌模板语法,用 Faker.js 生成随机姓名、邮箱、地址、UUID,用 {{body 'xxx'}} 回显请求参数,实现"每次请求返回不一样"的拟真效果。服务端核心 commons-server 里内置了完整的模板解析器。
请求代理与录制。 可以把本地 Mock 服务配置成反向代理指向真实后端,未命中 Mock 规则的请求透传给真实服务并录制响应,实现"渐进式 Mock"——联调初期全透传,逐步把稳定下来的接口替换成 Mock 规则。
请求日志。 每个环境有独立的请求日志页签,记录每次命中的路由、请求头、请求体、响应状态与耗时,是排查"为什么我的 Mock 没生效"的第一现场。
这些能力全部跑在主进程的 Node 运行时上,这正是路线 A(Electron 全量承载)能保真的根本原因:只要 Node 跑起来了,服务端核心一行不改。
三、移植路线选型:三条路的取舍
拿到一个 Electron 应用,往鸿蒙 PC 上搬,摆在面前的路有三条:
路线 A:Electron 全量承载(定制运行时)。 鸿蒙生态里有社区维护的 Electron 定制运行时(libelectron.so + libadapter.so + libffmpeg.so,通过 web_engine HAR 提供 WebWindowNode),可以把 Electron 应用原封不动跑起来。优点是改动量最小、UI 与功能 100% 保真;缺点是包体大(运行时 .so 本身就 170MB+),且依赖运行时与新系统版本的兼容性。
路线 B:ArkUI 原生重写。 用 ArkTS 把界面重写一遍,逻辑层保留 JS/TS 部分。理论上包体最小、体验最"原生",但对 Mockoon 这种功能密度极高的工具(几十个界面组件、Monaco 编辑器、树形控件、弹窗系统),重写工作量是以月计的,且很难保证行为一致。
路线 C:ArkWeb 加载 Web 版。 类似把 renderer 部分塞进 Web 组件,主进程逻辑另想办法。问题是 Mockoon 的核心价值恰恰在主进程(起本地 HTTP 服务、文件读写、系统对话框),Web 化之后这些能力全要重新桥接,改造量反而最大。
我选择了路线 A。决策依据:
- 我此前的移植实践已经验证过这条链路的可行性——定制 Electron 运行时在 HarmonyOS PC(API 24/26)上可以稳定拉起多进程(主进程 + GPU + NetworkService),关键是要用与新系统匹配的新版底座(Electron 37+ 底座,旧底座在新系统上会因 NAPI 注册失败直接闪退);
- Mockoon 的三个适配障碍(依赖打包、入口扩展名、资源路径)都是构建期问题,改配置就能解决,不涉及运行时逻辑改写;
- 作为上架作品,"运行的是真正的 Mockoon"本身就是说服力——功能完整性远比包体大小重要,工具类应用用户对安装包体积的容忍度也高。
把三条路线的关键维度摆在一起看更清楚:
| 维度 | A:Electron 全量承载 | B:ArkUI 原生重写 | C:ArkWeb 混合 |
|---|---|---|---|
| 改动量 | 构建期配置级 | 界面层全部重写 | 主进程能力全部重桥接 |
| 功能保真 | 100% | 取决于重写投入 | 渲染层保真、主进程重写 |
| 包体 | 200 MB 级 | 最小 | 中等 |
| 上手周期 | 天级 | 月级 | 周级 |
| 后续跟版 | 同步上游成本低 | 每次升级手工对齐 | 升级需重做桥接 |
| 适合对象 | 功能完整的成熟工具 | 界面简单的轻工具 | 以网页为主的工具 |
选型时还有两个隐性成本容易被低估:一是维护成本——上游 Mockoon 迭代很快,路线 A 下同步一个新版本基本等于"重新构建 + 重新同步产物",路线 B 下每次上游升级都要人工对齐几十处界面差异;二是验证成本——重写版本和原版的行为差异需要逐功能回归,而承载版本天然与原版一致,回归范围只有运行时兼容性一层。
当然,路线 A 不是无脑最优:如果目标应用重度依赖 Node 原生模块(SQLite、native 加密库等),交叉编译 aarch64 的 .node 文件会是另一场硬仗;如果目标应用是单窗口轻界面,路线 B 的原生体验回报则更高。选型的本质是让改动量与保真需求匹配,而不是套用统一答案。
定下路线后,整个移植的本质就变成了两件事:把 Electron 应用改造成能在鸿蒙沙箱里自包含运行的形态,以及把它装进鸿蒙的 HAP 包壳里并签好名。
四、工程结构设计
最终工程目录结构如下:
ohos_Mockoon/
├── mockoon-src/ # 上游源码 monorepo(构建前端产物用)
│ ├── packages/app/ # 改造后的应用包
│ └── package-lock.json
└── ohos_hap/ # DevEco HAP 工程
├── AppScope/app.json5 # bundleName: com.mockoon.ohos
├── build-profile.json5 # 签名与产品配置
├── oh-package.json5
├── electron/ # entry 模块(宿主)
│ ├── libs/arm64-v8a/ # libelectron/libadapter/libffmpeg 预编译 so
│ └── src/main/ets/ # Ability + 页面(拉起 WebWindowNode)
└── web_engine/ # HAR:Electron 适配框架 + 应用资源
└── src/main/resources/resfile/resources/app/
├── main.cjs # esbuild 打包的 Mockoon 主进程(6.1 MB)
├── preload.cjs # contextBridge 桥
├── package.json # main: main.cjs, dependencies: {}
└── renderer/ # Angular 构建产物(3.1 MB)
两个设计要点:
双模块分工。 electron 模块是 HAP 的 entry,负责 Ability 生命周期和通过 import { WebWindowNode } from 'web_engine' 拉起 Web 窗口节点;web_engine 是 HAR 包,携带适配框架和全部应用资源。应用资源放在 resfile/resources/app/ 下,这是鸿蒙应用沙箱内随包分发的标准位置。
产物与源码分离。 mockoon-src 只负责产出四个东西:app.js(主进程)、preload.js(桥)、renderer/(Angular 静态产物)。构建完用脚本同步进 web_engine 的 resfile,HAP 工程本身不包含任何 TS/JS 源码。这样上游升级时只需要重新构建同步,HAP 壳完全不用动。
五、四个关键适配点
路线定了,真正动手时面对的是四个具体的坑。这四个点是整个移植里最核心的技术内容。
5.1 esbuild 打包策略:external 改 bundle
上游的 esbuild 配置里,packages 选项是 'external'——所有 npm 依赖不打进产物,运行时从 node_modules 解析。这在常规桌面分发里没问题,因为 electron-builder 会把 node_modules 一起打进安装包。但鸿蒙 HAP 内没有 node_modules,产物必须自包含。
所以第一刀砍在 packages/app/src/main/esbuild.config.js:
// packages/app/src/main/esbuild.config.js
export const buildConfig: BuildOptions = {
entryPoints: ['src/main/main.ts'],
bundle: true,
platform: 'node',
- packages: 'external',
+ packages: 'bundle',
external: ['electron'], // 仅 electron 保持 external,由鸿蒙运行时提供
outfile: 'dist/app.js'
};
external: ['electron'] 必须保留——electron 模块在鸿蒙侧由 libadapter 运行时注入,打包进去反而找不到。其余所有依赖(Express、RxJS 等几十个包)全部打进单文件。改完之后 app.js 从几百 KB 涨到 6.1 MB,这是预期内的。
5.2 入口文件扩展名:preload.js → preload.cjs
鸿蒙侧的应用 package.json 把 main 指向 main.cjs。之所以用 .cjs 后缀,是为了强制 CommonJS 解析语义(Electron 主进程传统上是 CJS),避免被运行时按 ESM 处理时 require 报错。preload 同理:
// packages/app/src/main/libs/main-window.ts
const mainWindow = new BrowserWindow({
width: mainWindowState.width,
height: mainWindowState.height,
title: '月桥接口',
backgroundColor: '#252830',
- preload: pathJoin(__dirname, 'preload.js'),
+ preload: pathJoin(__dirname, 'preload.cjs'),
icon: pathJoin(__dirname, 'renderer/assets/icon.png'),
webPreferences: { ... }
});
对应的部署脚本里也要做同名映射:dist/app.js → main.cjs,dist/preload.js → preload.cjs。顺手在 main-window.ts 里改了窗口标题(这里是应用内窗口标题的源头之一)和图标路径。
5.3 渲染进程产物部署路径
Electron 主进程里加载渲染页面的代码通常是 loadFile('renderer/index.html') 这类相对路径。鸿蒙侧 resfile 布局里应用根就是 resfile/resources/app/,所以 Angular 构建产物要部署到 renderer/ 子目录,与主进程 loadFile 的路径约定严格对齐:
APP_DIR=ohos_hap/web_engine/src/main/resources/resfile/resources/app
SRC_DIR=mockoon-src/packages/app/dist
cp $SRC_DIR/app.js $APP_DIR/main.cjs
cp $SRC_DIR/preload.js $APP_DIR/preload.cjs
cp $SRC_DIR/renderer/index.html $APP_DIR/renderer/
cp $SRC_DIR/renderer/main-*.js $APP_DIR/renderer/
cp $SRC_DIR/renderer/polyfills-*.js $APP_DIR/renderer/
cp $SRC_DIR/renderer/styles-*.css $APP_DIR/renderer/
cp -r $SRC_DIR/renderer/assets $APP_DIR/renderer/
注意 main-*.js 是带内容 hash 的文件名(比如 main-CP2B434R.js),每次重新构建 hash 都会变。同步脚本必须先删旧的 main-*.js 再拷新的,否则旧 bundle 残留在包里,既浪费体积,更危险的是旧代码还在——我后来修复审核问题时就特意验证了这一点。
完整的同步脚本(PowerShell 版,含旧产物清理)建议固定下来放在工程根目录,每次构建后一键执行:
$APP = 'ohos_hap\web_engine\src\main\resources\resfile\resources\app'
$SRC = 'mockoon-src\packages\app\dist'
Copy-Item "$SRC\app.js" "$APP\main.cjs" -Force
Copy-Item "$SRC\preload.js" "$APP\preload.cjs" -Force
Remove-Item "$APP\renderer\main-*.js" -Force # 先删旧 hash bundle
Copy-Item "$SRC\renderer\index.html" "$APP\renderer\" -Force
Copy-Item "$SRC\renderer\main-*.js" "$APP\renderer\" -Force
Copy-Item "$SRC\renderer\polyfills-*.js" "$APP\renderer\" -Force
Copy-Item "$SRC\renderer\styles-*.css" "$APP\renderer\" -Force
Copy-Item "$SRC\renderer\assets\*" "$APP\renderer\assets\" -Recurse -Force
一个部署后的体积参考:主进程 main.cjs 6.1 MB,渲染产物(main bundle + polyfills + styles + assets)约 3.1 MB,加上 Angular 的懒加载分片,应用自身资源总共不到 10 MB。对比动辄数百 MB 的运行时 so,应用层是"轻"的——这也说明包体优化未来唯一的抓手在运行时裁剪,与应用逻辑无关。
5.4 资源路径相对化
这是四个点里最隐蔽的一个。Angular 构建的 index.html 里,<base href="."> 配合资源引用,在常规桌面环境从文件协议加载没问题,但在鸿蒙的 WebWindowNode 里,绝对路径(/assets/xxx)会落到沙箱根而不是应用目录,直接 404 白屏。
改造两处:
<!-- packages/app/src/renderer/index.html -->
- <base href="." />
+ <base href="./" />
同时把 angular.json 里的资源部署配置改为相对引用,确保构建出来的 HTML 里全部是 ./main-xxx.js、./styles-xxx.css 这种形态。改完可以用一个快速自检:构建产物 index.html 里 grep 一遍,不允许出现以 / 开头的资源引用。
六、构建流水线:从源码到 HAP
四个适配点改完,完整的构建流水线如下(Windows 环境实测通过):
# ① 构建公共库(commons + commons-server)
cd mockoon-src
npm install --legacy-peer-deps
npm run build:libs
# ② 构建应用(Angular 渲染进程 + esbuild 主进程)
npm run build:prod:all --workspace=@mockoon/app
# ③ 部署产物到 HAP 资源目录(见 5.3 的脚本)
# ④ 构建 HAP
cd ../ohos_hap
ohpm install # 生成 electron/oh_modules/web_engine 软链,不可省略
hvigorw assembleHap --mode module -p product=default
流水线里有几个环境细节值得单独说:
Node 版本卡点。 Angular CLI 22 对 Node 有硬性下限(≥ 22.22.3),低版本构建会直接报引擎不匹配。本机用 Node v24.x 通过。
Windows 上跑上游构建脚本。 上游 clean-dist 脚本写的是 rm -rf ./dist,Windows 的 cmd 里没有 rm,npm run build:prod:all 第一步就挂。两种解法:要么给系统装POSIX 工具,要么绕开聚合脚本手动分步执行:
Remove-Item -Recurse -Force .\dist
npx ng build --configuration=production
node ./src/main/esbuild.config.js --mode production
hvigor 的环境变量。 命令行构建 HAP 需要 JAVA_HOME 指向 DevEco 自带 jbr(PackageHap 任务要起 java 进程),DEVECO_SDK_HOME 指向 DevEco SDK 目录,并把 jbr\bin 前置到 PATH:
$env:JAVA_HOME='C:\Program Files\Huawei\DevEco Studio\jbr'
$env:DEVECO_SDK_HOME='C:\Program Files\Huawei\DevEco Studio\sdk'
$env:PATH="C:\Program Files\Huawei\DevEco Studio\jbr\bin;$env:PATH"
hvigorw.bat --mode module -p module=electron@default -p product=default assembleHap --no-daemon
ohpm install 是隐性前置。 electron 模块通过 import { WebWindowNode } from 'web_engine' 引用本地 HAR,这个依赖解析靠 ohpm install 在 electron/oh_modules/ 下生成的软链。漏掉这一步会收获二十多个 ArkTS:10905204 编译错误,报错信息完全不指向根因,属于"报错在东、病灶在西"的典型。
全链路第一次跑通后,产出签名 HAP 约 205 MB,其中 libelectron.so 占 169 MB,应用自身产物(main.cjs + renderer)不到 10 MB。
七、签名配置:三个经典报错
签名是鸿蒙上架的第一道关卡,这次把三个高频报错全踩了一遍。
00303074(SignHap 失败)。 profile 或证书路径失效、密码不匹配导致。处理方式是在 DevEco Studio 的 Signing Configs 里走自动签名,重新生成本 bundleName 的调试材料。
9568320(no signature file)。 最迷惑的一个:签名步骤显示成功,但装到设备上时报"没有签名文件"。根因是 build-profile.json5 里 products[].signingConfig 是空字符串——signingConfigs 数组里定义了签名配置,但 product 没有引用它,构建全程跳过签名,产出的是 unsigned 包。修复就一行:
"products": [{
"name": "default",
"signingConfig": "default", // 必须与 signingConfigs[].name 一致
...
}]
9568322(not trusted app source)。 用了 release profile 的包走 hdc 直接安装时出现——正式签名的包只能走应用市场通道分发安装,真机日常调试要用 debug 签名的包。这是签名体系的设计而非 bug,但要提前知道,否则会误判成签名坏了。
另外一个容易忽略的约束:签名 profile 与 bundleName 是绑定的。换包名必须重新生成 profile,拿 A 应用的 profile 签 B 应用的包必然失败。上架用的 release 证书、调试用的 debug 证书建议从第一天就分开管理。
八、上架准备:应用命名与商店素材
技术链路通了之后进入上架环节。这一环节的头号决策是应用命名。
直接以"Mockoon"这个名字上架是不合适的:一是名称归属,Mockoon 是上游项目的名称,移植分发版本需要一个自己的产品名;二是应用市场要求应用名称在商店、安装后的桌面图标、应用内展示三处保持一致,这个一致性要求后面还会再出现一次(就是审核被拒的那次)。
最终定名"月桥接口"——"月桥"作为产品名有辨识度,"接口"点明工具属性。应用内窗口标题、about 信息统一用"月桥接口(From Mockoo)"的形态,既保持一致又注明技术来源,尊重上游。
商店素材方面,工具类应用的核心是把"能用它干什么"在三张图内讲清楚。我做了两张特性图:
第一张主打开箱即用的 CORS 配置——这是前端联调场景的刚需,新建环境自动预置 Content-Type 和完整的 CORS 跨域请求头,浏览器直接可调:

第二张主打隐私与本地运行——所有数据仅保存在本机、无需注册登录,这是相对云端 Mock 平台的差异化卖点:

一句话简介按 15 字限制打磨:“鸿蒙本地一键模拟接口调试工具”。应用描述里把功能密度讲全:可视化创建模拟 API、多环境管理、动态响应模板、随机数据生成、请求代理转发、请求日志查看。
商店素材的制作有几个实操层面的规范值得记录:
截图与宣传图分开做。 纯截图(真实界面 + 真实数据)负责"可信",宣传图(提炼卖点 + 大字标语 + 特性图标)负责"好懂",两类图按顺序排布,前一张讲清楚是什么,后几张讲清楚好在哪。
宣传图内的界面必须与实际版本一致。 图里嵌的应用窗口截图要用最终版本的界面,窗口标题、汉化文案、菜单结构都要和用户装到手里的一致,否则图文不符本身就会引发审核或用户投诉。
文字信息控制在三行以内。 工具类应用的商店图在手机上会被缩略浏览,每张图传递一个卖点就够:第一张"CORS 开箱即用",第二张"隐私可控/本地运行",各自配一句副标题说明,不贪多。
隐私声明与权限说明提前写。 工具类应用常被要求补充说明"为什么需要这些权限"和"数据存在哪里",这个应用的答案是现成的:全部数据本地沙箱内 JSON 存储,无敏感权限申请,材料里直接如实陈述即可。
上架页在应用市场审核通过后的最终形态如下,分类"工具",安装后桌面上就是"月桥接口":

九、审核被拒实战:欢迎弹窗的定位与修复
上架材料提交后收到了应用市场的审核意见,原文是:
应用内引导弹窗模块展示的应用名称与在 AppGallery Connect 上提交的应用名称不一致,不符合相关法律法规要求。
这个审核意见非常精确地命中了我们前面在"命名一致性"里埋的雷:首次启动时,应用会弹一个欢迎引导弹窗,弹窗标题赫然写着"欢迎使用 Mockoon!“,正文里还有两处 Mockoon 字样和三个指向 mockoon.com 的外链。商店里提交的名字是"月桥接口”,用户装上打开第一眼看到的却是另一个名字——这确实不合规,该改。
9.1 定位弹窗的触发链路
这个弹窗是上游 Mockoon 的欢迎引导组件。顺着源码往下挖,完整触发链路是三层:
第一层:弹窗组件本体 welcome-modal.component.html,标题模板:
<h4 class="modal-title">欢迎使用 Mockoon{{ isWeb ? ' Web' : '' }}!</h4>
第二层:注册 ui.service.ts 里以 'welcome' 为名注册到模态框注册表:
welcome: {
component: WelcomeModalComponent,
options: commonConfigs.medium
},
第三层:触发条件 settings.service.ts 的 loadSettings():
public loadSettings(): Observable<any> {
return this.storageService.loadSettings().pipe(
tap((settings: Settings) => {
// ... 更新日志弹窗逻辑 ...
if (!settings.welcomeShown) {
this.uiService.openModal('welcome'); // ← 首次启动在这里弹出
}
})
);
}
welcomeShown 是一个设置项,默认 false,用户点掉弹窗后写回 true。所以每个新装用户首次启动必弹。
9.2 修复方案:为什么不是简单改个默认值
修这个问题有三种思路,权衡之后选了第三种:
思路一:把弹窗文案里的 Mockoon 全改成月桥接口。 能过审,但正文里"由数十位贡献者共同构建的免费开源项目""支持我们"这些上游社区的表述对月桥接口的用户来说是错位的,而且弹窗本身对用户价值不大——这只是个介绍页。
思路二:把 SettingsDefault.welcomeShown 默认值从 false 改成 true。 一行改动,弹窗不弹了。但这一刀会误伤——往下追一层就发现了:
// environments.service.ts
if (!settings.environments.length && !settings.welcomeShown) {
// First load: create one blank local environment
const defaultEnvironment = { ...BuildEnvironment({...}), name: '新环境' };
// ... 写入存储并加载
}
首次启动时自动创建一个空白环境的逻辑,复用了 welcomeShown === false 作为"这是新用户"的判据。如果默认值改成 true,新用户第一次打开应用会是一个空列表、无环境的裸界面,交互起点直接坏掉。
思路三(采用):只摘除弹窗触发,保留状态语义。 在 settings.service.ts 里删除 openModal('welcome') 调用,welcomeShown 保持默认 false 不动:
public loadSettings(): Observable<any> {
return this.storageService.loadSettings().pipe(
tap((settings: Settings) => {
const validatedSchema = SettingsSchema.validate(settings);
this.updateSettings(validatedSchema.value);
settings = validatedSchema.value;
if (semverGt(Config.appVersion, settings.lastChangelog)) {
this.uiService.openModal('changelog');
this.updateSettings({ lastChangelog: Config.appVersion });
}
// 欢迎引导弹窗已移除(应用上架名称与上游名不一致,按审核要求不再展示)。
// welcomeShown 保持默认 false:environments.service 依赖它做首次启动建环境。
})
);
}
这个方案的副作用分析:welcomeShown 永远停在 false,唯一的消费方除了首启建环境,还有 web 版的登录弹窗判断(if (!user && settings.welcomeShown)——本地版没有云登录,不受影响);以及"用户删光所有环境后重启会再建一个空环境",这恰好符合"给用户一个可用起点"的产品意图,可以接受。
顺带检查了另一个首启弹窗——更新日志弹窗(changelog modal),它的标题是"更新日志 vX.Y.Z",不带 Mockoon 字样,无需处理。
9.3 整改提交与复核要点
修复出包后重新提审,复核阶段建议主动做两件事:
第一,在提审备注里写明整改内容。 简明告知审核方:已移除首启欢迎弹窗,应用内所有用户可见名称已与 AGC 提交名称统一。审核人员复核时能直奔整改点,缩短复核周期。
第二,自己先把"首次启动全路径"过一遍。 审核方测的是真实体验路径:全新安装 → 首次启动 → 逐个出现的弹窗和页面。开发者自己用"卸载重装 + 清数据"的方式走一遍同样的路径,把每一个弹窗的标题、正文、按钮文案都截图核对,比等审核意见来发现问题快得多。这次的问题如果提审前做过这个自查,一轮审核周期就省下来了。
另外提醒一个容易连坐的点:商店截图与应用截图也要同步核对。如果宣传图里嵌的是旧版界面截图,而弹窗已经改了,图内图外不一致同样可能引起审核质疑。商店素材图是重新制作的(图内窗口标题用的是"月桥接口(From Mockoo)"的统一形态),所以这次没有连带问题。
9.4 重建、部署与自验证
源码改完走一遍完整流水线:
# 渲染进程重建(Windows 下绕开 rm,手动清理)
Remove-Item -Recurse -Force .\dist
npx ng build --configuration=production
# → main-CP2B434R.js(新内容 hash)
# 主进程重建
node ./src/main/esbuild.config.js --mode production
# → dist/app.js 6.1mb, dist/preload.js 1.2kb
# 同步到 resfile(先删旧 hash bundle 防残留)
Remove-Item "$APP\renderer\main-*.js" -Force
Copy-Item "$SRC\renderer\main-*.js" "$APP\renderer\" -Force
# ... 其余产物同步
# HAP 重新构建签名
hvigorw.bat --mode module -p module=electron@default -p product=default assembleHap --no-daemon
# → BUILD SUCCESSFUL, electron-default-signed.hap (206 MB)
部署完成后做了一个产物级自验证——直接在打包进 HAP 的最终 bundle 上确认触发逻辑已经消失,而不是只看源码:
grep 检索目标:openModal("welcome") / welcomeShown
检索文件:resfile/.../renderer/main-CP2B434R.js
结果:0 命中 ✓
这一步很有必要:如果同步脚本漏删旧 bundle 或者拷错了目录,源码对了但包是旧的,装到设备上弹窗照弹。产物级验证能当场暴露这类"构建链路和部署链路不一致"的问题。
9.5 复盘:审核意见其实是产品体检
回过头看,这次被拒的修复成本极低(删 3 行代码 + 重构建),但它暴露的问题是普遍性的:移植/套壳类应用最容易翻车的点不在技术,在"应用身份"的一致性。商店名、桌面名、窗口标题、应用内弹窗、关于页——所有用户能看见名字的地方必须是同一个名字。建议每个做移植上架的开发者在提审前自己过一遍这个清单,尤其是首次启动路径上的每一个弹窗。
十、真机效果验证
重新出包后卸载重装,在鸿蒙 PC 真机上做了完整的启动路径验证:欢迎弹窗不再出现,应用直接进入主界面。
首次进入是一个干净的空环境状态——左侧环境列表里已经自动创建了"新环境",路由页签显示"未定义路由",等待用户创建第一条 Mock 路由:

新建环境自动预置了四条请求头,Content-Type 为 application/json,外加完整的三段 CORS 跨域请求头(Allow-Origin/Allow-Methods/Allow-Headers),右下角还有"添加 CORS 请求头"的快捷入口——这就是商店图里"开箱即用的 CORS 配置"的真机实证,前端页面跨域调用本地 Mock 服务不需要任何手工配置:

设置弹窗功能完整可用:界面选项、环境变量前缀(MOCKOON_)、环境日志容量、ISO 时间格式开关、Faker.js 数据生成的区域与随机种子、启动行为等全部可调,汉化完整:

从进程角度看,Electron 定制运行时在鸿蒙上稳定维持主进程 + GPU 进程 + NetworkService 的多进程结构,长时间挂机无崩溃、无闪退,本地 HTTP 服务的生命周期跟随环境启停按钮控制。
10.1 五分钟上手:从空环境到第一条 Mock 接口
光看界面截图还不够,这里给一个完整的上手流程,也是验证移植质量的功能用例:
第一步,启动环境。 打开应用,左侧已经有了自动创建的"新环境",本地端口默认从 3000 起。点击顶部的绿色启动按钮,环境状态变为运行中,本地 HTTP 服务开始监听回环地址。
第二步,创建路由。 在"路由"页签点"添加路由",请求方法选 GET,路径填 /api/hello,状态码保持 200。响应体里填入一段带模板的内容:
{
"message": "你好,月桥接口",
"requestId": "{{faker 'string.uuid'}}",
"timestamp": "{{now 'iso'}}"
}
第三步,发起请求验证。 在同一台机器的终端里用 curl 打一下:
curl http://127.0.0.1:3000/api/hello
# {"message":"你好,月桥接口","requestId":"3f8a...","timestamp":"2026-10-30T..."}
每次请求 requestId 都不同——说明 Faker.js 模板引擎在鸿蒙侧的 Node 运行时里完整工作。
第四步,看日志。 切到"日志"页签,刚才这次请求的方法、路径、请求头、响应耗时全部在列。联调时"我的请求到底发出去什么样"在这里一目了然。
第五步,跨域验证。 在浏览器控制台里用 fetch('http://127.0.0.1:3000/api/hello') 请求任意本地页面,请求直接成功——因为环境预置的 CORS 请求头已经放行了跨域,这正是前端联调场景里最省心的一环。
这五个步骤全部在真机上跑通,覆盖了路由引擎、模板引擎、HTTP 服务、日志、CORS 五个核心子系统,可以作为移植质量的基线用例。
十一、安全与隐私设计
工具类应用过审,隐私合规是必答题,这个应用在这块有天然优势,但值得把设计显式讲出来:
数据全本地。 环境配置、路由定义、请求日志全部以 JSON 文件形式保存在应用沙箱内的本地存储,不经过任何云端。没有账号体系,没有遥测上报。
服务只听本地回环。 Mock 服务默认绑定 127.0.0.1,仅本机可访问,不对外网暴露端口——这也是它作为开发工具的合理安全边界。
权限最小化。 HAP 的权限声明只保留运行时必需项,不申请位置、通讯录、存储等与功能无关的敏感权限;应用内唯一的外部访问是更新日志弹窗对 GitHub release notes 的拉取,且失败时静默降级为"更新日志加载失败"提示,不影响功能。
上架名与内部标识的合规一致性。 bundleName、签名 profile、AGC 应用、商店名、应用内展示五处统一在"月桥接口 / com.mockoon.ohos"体系下,这正是本次审核意见的整改落点。

十二、踩坑清单汇总
把全过程的坑浓缩成一张表,后来者可以按图索骥:
| # | 坑 | 现象 | 根因 | 解法 |
|---|---|---|---|---|
| 1 | node_modules 缺失 | 运行时找不到模块 | HAP 内没有 node_modules | esbuild packages: 'bundle' 全量打包 |
| 2 | preload 加载失败 | 渲染进程无桥 | ESM/CJS 语义 | 改 .cjs 后缀强制 CJS |
| 3 | 渲染页白屏 | 资源 404 | 绝对路径落沙箱根 | base href="./" + 相对引用 |
| 4 | rm 不识别 | 构建第一步挂 | 上游脚本面向 POSIX | Windows 手动分步构建 |
| 5 | Angular 构建引擎报错 | node 引擎不匹配 | Node 版本过低 | Node ≥ 22.22.3 |
| 6 | ArkTS:10905204 | 20+ 编译错误 | oh_modules 软链缺失 | ohpm install |
| 7 | spawn java ENOENT | PackageHap 失败 | java 不在 PATH | jbr 前置 PATH + JAVA_HOME |
| 8 | SignHap 00303074 | 签名失败 | 材料/密码失效 | DevEco 自动签名重生成 |
| 9 | 9568320 无签名文件 | 装包报未签名 | product 未引用 signingConfig | "signingConfig": "default" |
| 10 | 9568322 不受信来源 | hdc 装正式包被拒 | release 包不走 hdc | 真机调试用 debug 包 |
| 11 | 旧 bundle 残留 | 改动不生效 | hash 文件名不同的旧 js 未删 | 同步脚本先删 main-*.js |
| 12 | 审核被拒 | 弹窗名与上架名不一致 | 欢迎弹窗写死上游名 | 摘除触发逻辑,保留建环境判据 |
十三、复现指南
如果想把这套流程完整走一遍,最小复现路径如下:
- 准备源码:克隆上游 Mockoon 仓库并检出目标版本,按第五章的四个适配点修改(合计约 10 行 diff);
- 准备运行时:获取鸿蒙 Electron 定制底座(libelectron/libadapter/libffmpeg 三件套 + web_engine HAR),按第四章结构放入
electron/libs/arm64-v8a与web_engine; - 构建产物:Node 22+ 环境下
npm install --legacy-peer-deps && npm run build:libs,再按 5.3 的同步脚本部署; - 组装 HAP:
ohpm install后用 hvigorw 构建,签名材料用 DevEco 自动签名生成; - 真机验证:按 10.1 的五步用例过一遍核心功能;
- 上架:AGC 创建应用 → 提交材料 → 按第九章的命名一致性清单自查后提审。
整个过程里真正需要"理解上游"的只有四个适配点,其余都是工程操作,这也是本文把它写成教程的原因:方法可复制。
十四、常见问题 QA
这一章把移植过程中最容易被问到的问题集中回答一遍,按"选型判断 / 构建排错 / 签名上架 / 运行使用"四类组织。
14.1 选型与可行性
Q1:Electron 承载方案包体动辄 200 MB,值得吗?
要看目标应用和使用场景。对开发工具类应用,用户的使用周期长、安装频次低,包体换来的功能 100% 保真是划算的;对一个只查一次天气的小工具,显然不划算。另外要区分"值不值"和"能不能优化":运行时 .so 占了绝大部分体积,真正由应用层带来的不到 10 MB,若未来需要瘦身,抓手在运行时裁剪(去掉用不到的编解码模块、精简 ffmpeg),而不是精简业务代码。
Q2:这套方法能套用到别的 Electron 应用上吗?判断标准是什么?
可以看三个条件:一是有无 Node 原生模块依赖(.node 文件),没有最好,有则要做 aarch64 交叉编译,成本陡增;二是渲染进程的产物形态,Angular/React/Vue 构建出的静态资源都能原样承载,纯靠 ESM 动态 import 的服务端渲染模式要额外处理;三是数据层是否依赖外部二进制(SQLite、Redis、FFmpeg 可执行文件等),每多一个外部依赖就多一层移植。满足前两条的 Electron 应用,基本都能复用本文的四个适配点。
Q3:上游 Mockoon 出新版本了,跟进成本高吗?
低。因为所有自定义改动都压在构建配置和资源路径上,运行时逻辑一行没动,跟版的上限操作就是:拉上游新代码 → 检查四个适配点对应的配置文件在 diff 里是否被改过 → 重新构建 → 同步产物 → 出包回归。真正需要注意的是"上游是否引入了新的原生依赖",每跟进一个版本先看一遍 package.json 的 dependencies 变化,就能提前规避大多数问题。
14.2 构建与排错
Q4:渲染进程白屏,第一步应该查什么?
三步定位:先确认 index.html 里还有没有以 / 开头的资源引用(相对路径没改干净是首因);再确认 main bundle 的文件名与部署目录里的文件是否同 hash(旧 bundle 没删干净或同步漏拷);最后确认主进程 loadFile 的路径与产物实际落盘路径一致。三处都正常再看运行时报错——打开渲染进程的控制台,看是 404 还是 JS 运行时异常,两者的排查方向完全不同。
Q5:为什么源码改了,打包出来的行为没变?
九成是三个原因之一:旧产物残留(hash 文件名的 bundle 没删)、同步目标写错(改了 web_engine 但 HAP 其实从 electron 模块的 intermediates 取资源,或反之)、构建没实际执行(聚合脚本在第一步 rm 就失败了,后面两步压根没跑,用的还是 dist 里的老产物)。排查手段是"产物级验证":不看源码,直接在最终打包进 HAP 的那个 bundle 上 grep 关键字,能立刻区分"没改到"和"没部署到"。
Q6:Windows 上构建上游项目总报 'rm' is not recognized 怎么办?
上游脚本默认 POSIX 环境。两种解法:给系统补 POSIX 工具(Git Bash、MSYS2 的 coreutils 加入 PATH),或者绕开聚合脚本、手动分步执行它的每一条命令。后者更可控,代价是要自己知道聚合脚本里到底串了哪几步(可以从 package.json 的 scripts 里读出来)。
Q7:ArkTS:10905204 报了一屏,是不是代码写错了?
绝大多数情况下不是。这个错误指向模块解析失败,典型根因是本地 HAR 依赖的软链缺失——import ... from 'web_engine' 依赖 ohpm install 生成的 oh_modules/web_engine 链接。执行 ohpm install 后全量重编即可。它的迷惑性在于报错位置全部落在业务代码上,而病灶在依赖图上。
14.3 签名与上架
Q8:换了 bundleName,原来的签名材料还能用吗?
不能。签名 profile 与 bundleName 绑定,换包名必须重新生成 profile 和证书。建议每个应用的签名材料单独管理、目录隔离,避免复制粘贴 A 项目的材料给 B 项目用时只改了路径没改 profile 内容——这类错误报出来的往往是 00303074 这类通用签名失败,很难一眼看出是"串了项目"。
Q9:正式签名包为什么用 hdc 装不上?
这是分发通道的设计约束,不是签名坏了:release 签名的包只能通过应用市场通道安装,因为它默认被归类为"正式分发渠道"的应用;日常真机调试用 debug 签名的包走 hdc 安装。常见的误判是"签名工具坏了"或"包有问题",实际换 debug 包立刻就通。
Q10:应用名称到底要在哪些地方保持一致?
至少五处:AGC 上创建的应用名称、商店展示名称、安装后桌面图标下的名称、应用内窗口标题、以及所有首次启动路径上的弹窗文案。本文的审核意见就出在第五处——商店名提交了"月桥接口",但欢迎弹窗标题写死了上游名字。建议在提审前用"卸载重装 + 首次启动全路径截图"的方式自查一遍,成本十分钟,收益是省掉一轮审核周期。
14.4 运行与使用
Q11:移除欢迎弹窗之后,用户会不会缺少上手引导?
不会。欢迎弹窗的内容本质是一个"感谢安装 + 文档链接"的介绍页,不是操作引导;真正的功能入口(新建环境、添加路由、启动服务)在主界面上一目了然。移除它的代价接近于零,而换来的是首页启动路径更干净。真正影响上手体验的是"首次打开有没有一个可用的起点",这个由自动创建空环境来保证,不受本次修改影响。
Q12:Mock 服务能被同一局域网的其他机器访问吗?
出于安全边界考虑,默认只监听本地回环地址(127.0.0.1),设计为仅本机的开发工具。这也符合隐私合规要求:不对外暴露端口,就没有未授权访问的风险。如果确有内网联调需求,应当在明确的知情前提下修改监听地址,而不是作为默认行为。
Q13:数据存在哪里?卸载会不会丢?
环境配置、路由定义、请求日志全部以 JSON 文件形式存在应用沙箱内,随应用卸载一并清除。需要长期保存或团队共享环境配置时,建议通过导出功能把环境文件纳入版本管理——Mockoon 的环境 JSON 是可读文本,天然适合放进 Git 做 diff 和 review。
Q14:只想 Mock 一部分接口、其他走真实后端,能做到吗?
可以,用代理模式:把环境配置成指向真实后端的反向代理,未命中 Mock 规则的请求会透传给真实服务,同时还能录制响应。这让"渐进式 Mock"成为现实——联调初期全透传、随进度逐条把稳定的接口改成本地 Mock 规则,最终把后端依赖彻底摘掉。这是 Mockoon 相对简单 Mock 工具最具生产力的特性之一。
十五、总结
这次移植从动手到上架,验证了一条对整个开源桌面软件生态都成立的路:Electron 应用 → 构建期自包含改造 → 鸿蒙 HAP 壳 → 应用市场。四个适配点(bundle 打包、.cjs 入口、renderer 部署、相对路径)全部是构建期问题,意味着同一套方法可以低成本复用到其他 Electron 应用上。
几点方法论层面的收获:
- 选型先看数据层。没有原生模块依赖(SQLite 等)的 Electron 应用,移植成本会低一个数量级,选品时就要把这个因素称进去;
- 改动守恒。所有运行时逻辑一行业务代码都没改,全部改动集中在构建配置和资源路径——移植做得好不好,就看能不能把改动压在构建期;
- 产物级验证。改完源码要在最终打包产物上验证,构建链路和部署链路的断裂是"明明改了却没生效"的第一大来源;
- 应用身份一致性清单。商店名/桌面名/窗口标题/弹窗/关于页五处一致,提审前自查一遍,能省一轮审核周期。
开源软件与鸿蒙 PC 生态的结合,本质上是给成熟的桌面工具找新的运行土壤。Mockoon 这样的开发者工具能以"月桥接口"的形态在应用市场上被搜索、被安装、被使用,这条路对更多开源项目都是敞开的。对开发者个人而言,一次这样的完整实战能同时串起构建工程、签名体系、应用市场合规三块知识——它们平时分散在不同文档里,只有真正走完一遍,才会知道每一块在整条链路上的位置和分量。希望这篇复盘能帮后来者少走弯路,把更多开源好工具带上鸿蒙 PC,也期待这些被移植过来的开发者工具能在新的土壤上真正跑起来、被用起来、长出新价值。
本文涉及的工具版本:Mockoon 9.8.0(上游)、Electron 43(上游栈)/ 37 系定制底座(鸿蒙运行时)、Angular 22、HarmonyOS SDK 6.x、DevEco Studio 命令行构建。
更多推荐



所有评论(0)