把 JasperReports 搬上鸿蒙 PC:一条「纯 JS 引擎 + JSVM」的移植路线,以及它如何走到应用市场上架

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

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

适配开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_jasperreports

本文记录一个完整的开源移植项目:把世界上最流行的开源报表引擎 JasperReports 的报表能力模型与设计器交互形态移植到 HarmonyOS PC,执行层内嵌同属开源生态的 pdf-lib + fontkit 真实源码,最终以「墨表(from JasperReports)」的名字通过华为应用市场审核上架。全文以技术为主线:为什么这么选型、五个深坑怎么定位怎么填、真机怎么验收、上架要准备什么。

关于页:技术栈与开源归属

一、痛点:鸿蒙桌面缺一块「报表拼图」

鸿蒙 PC 生态起来之后,办公场景的移植需求是肉眼可见的:文档、表格、演示、思维导图都有人做了,但有一个品类长期空着——报表设计与 PDF 导出。

在企业办公世界里,报表不是「打印一张表格」这么简单。它有一套成熟的工程范式:定义模板(标题带、列头带、明细带、合计带、页脚带的分带结构)→ 填充数据 → 渲染输出像素级可控的 PDF。这套范式的鼻祖就是 JasperReports——Java 编写、LGPL-3.0 协议、GitHub 上数万 star 的开源报表引擎,Jaspersoft Studio 则是它的官方桌面设计器。

而鸿蒙 PC 上,这个环节是零。一个想在鸿蒙桌面开发票、出对账单、打销售明细的用户,没有任何原生工具可用。

我们决定填这个坑。但摆在面前的问题是:JasperReports 本体是 Java 生态的,把 JVM 塞进 HAP 是一条已知极其沉重的路。所以这个项目真正的技术命题是——

如何用一条轻量、自包含、可上架的路线,把「报表五带模型 + 中文字体嵌入 + PDF 生成」完整搬到鸿蒙 PC?
应用市场上架页

二、作品定义:墨表(from JasperReports)

先说结果。最终交付的应用叫墨表,应用市场显示名「墨表(from JasperReports)」,一款运行在 HarmonyOS PC 上的本地报表设计与 PDF 导出工具:

  • 界面上直接编辑报表标题、副标题、页脚文本、字段名、数据明细,字段与行均可增删,末列数值实时求和;
  • 点「生成 PDF」,引擎按 JasperReports 经典版式渲染:灰底列头、斑马纹明细、金额右对齐、标题双线分隔、末尾自动合计、超页自动分页且新页重绘列头、每页自动页码;
  • 点「另存为…」走系统保存对话框导出,文件管理器可见、双击可开。

设计器主界面

几个关键指标先摆出来:

项内容
上游Jaspersoft/jasperreports(LGPL-3.0),能力对齐其报表模型与 Jaspersoft Studio 设计器形态
执行引擎pdf-lib 1.17.1 + @pdf-lib/fontkit 1.1.1(MIT,纯 JavaScript,上游源码全量参与编译运行)
运行载体OpenHarmony NDK JSVM-API(libjsvm.so),进程内标准 JS 引擎
界面层ArkTS Stage 模型,2in1 / 平板
包体约 26MB(含 22MB 中文字体),零外部二进制、零外部进程
验收真机全链路:设计 → 填充 → 生成 → 导出 → WPS 打开

三、技术选型:为什么不嵌 JVM,也不走外挂进程

跨平台移植 GUI 应用,执行层怎么落地无非几条路。我们把候选路线摆在一起比较:

路线代表包体增量主要风险
内嵌 JVM打包 JRE 运行时100MB+SELinux noexec、JNI 适配、HNP 打包链路复杂
外挂进程带一个可执行文件 + exec中鸿蒙沙箱禁止 exec 外部二进制,基本走不通
混合渲染Web 组件加载前端资产中内存占用、桌面体验打折
纯 JS 引擎 + JSVMpdf-lib 全量进进程内 JSVM~2.3MB需要自己解决「无事件循环」问题

pdf-lib 是纯 JavaScript 实现的 PDF 生成库,核心数据结构全部基于 Uint8Array,没有 Node 原生依赖;fontkit 负责字体解析与子集化,是中文字体嵌入的必需组件。两者加起来恰好构成一个完整报表导出引擎的执行层——而且是真实开源上游的源码,不是重写、不是裁剪。

而 OpenHarmony NDK 提供的 JSVM-API(libjsvm.so)是一个进程内的完整标准 JS 引擎(ark_runtime)。ArkTS 是严格类型子集,没法直接 import 动态 CommonJS 产物,但 C++ 侧可以通过 JSVM-API 执行任意脚本、注入 native 函数、交换 ArrayBuffer。

于是整体路线定了:esbuild 把 pdf-lib + fontkit 全量打包成单文件 IIFE 随 HAP 分发,C++ NAPI 桥宿主一个进程内 JSVM,业务 glue 脚本驱动引擎出 PDF,ArkTS 画原生设计器界面。

这条路线最大的收益是自包含:不带任何运行时依赖、不 exec 任何进程、不受 hnp/filesDir noexec 的 SELinux 约束——那些内嵌 JDK/CPython 路线必趟的坑,这条路线天然不存在。2.3MB 的引擎 bundle 换来一个真实开源引擎,性价比极高。

四、总体架构

在这里插入图片描述

数据流一句话概括:ArkTS 设计器 →(Worker 消息传路径与 JSON)→ Worker 线程 →(NAPI)→ C++ 读文件 → JSVM eval/调用 → pdf-lib 生成 PDF 字节 → 逐层返回 → 沙箱落盘 + 系统对话框导出。

引擎打包有两个硬性约束,值得单独说:

  1. 全量 bundle,不 external 任何依赖。esbuild 配置里 globalName: PDFLib、IIFE 格式,同时注入 Buffer/process polyfill(fontkit 需要)。之所以用 IIFE 而不是 ESM,是因为 JSVM 的 OH_JSVM_RunScript 执行的是脚本,export 语句会直接报语法错误。
  2. 打包完先过沙箱自测。仓库里有一个 sandbox-test-iife.mjs,用 Node 的 vm 模块把 process、setTimeout 等全局剥掉,模拟 JSVM 的贫瘠环境跑一遍,确认能产出合法 PDF 才允许进 rawfile。这一步把「环境假设」问题提前暴露在开发机而不是真机上。

为什么要在打包层下这么大功夫?因为嵌入式 JSVM 和浏览器、Node 的运行环境差异是结构性的:浏览器有事件循环、有完整的 Web API、有 ESM 模块系统,JSVM 一样都没有。打包层是唯一能把这三层差异一次性抹平的地方——bundle 格式选 IIFE 解决模块系统差异,polyfill 解决 Buffer/process 差异,剩下的「没有事件循环」问题留给运行时(后文坑 1)。分层解决,每一层的问题都不外溢,这是整个移植工程能保持可控的关键。

另一个工程决策是资产落盘策略。rawfile 资产在 HAP 里是只读的,而 C++ 桥需要用 ifstream 按路径读取,所以首次启动时 ArkTS 会把三个资产(引擎 bundle 约 2.3MB、glue 脚本、中文字体 22MB)从 rawfile 拷贝到应用沙箱的 filesDir/engine/ 目录。这个目录在应用卸载时自动回收,不产生任何用户可见的存储负担;每次启动强制重落盘则保证了升级后引擎版本与应用版本严格一致,杜绝「新代码配旧引擎」的诡异问题。

五、移植实录:五个深坑

这条路线的难点,全部集中在**「让一个为浏览器/Node 设计的异步 JS 引擎,在一个没有事件循环的嵌入式 JSVM 里同步跑完」**。以下每一坑都在真机上复现、定位、修复并回归验证过。

坑 1:JSVM 没有事件循环,Promise 链永远不 settle

pdf-lib 的 PDFDocument.create()、embedFont()、save() 全部返回 Promise。第一版 glue 把 Promise 直接返回给 C++,C++ 侧当 Uint8Array 读——永远失败。后来在 native 侧加微任务泵,也只解了一半。

最终方案是 glue 侧的 syncAwait:把 promise 的 resolve/reject 写进共享状态,然后在一个 JS 循环里反复调用 native 注入的 __pumpMicrotasks()(内部是 OH_JSVM_PerformMicrotaskCheckpoint + OH_JSVM_PumpMessageLoop),直到 settle。心跳探针实验证明这个 API 在 JS 执行栈内可以驱动新入队的微任务——这是整个方案成立的实验基础:

function syncAwait(p, label) {
  var settled = false, value = null, failure = null;
  p.then(function (v) { settled = true; value = v; },
         function (e) { settled = true; failure = e; });
  // 心跳探针:自己入队一个微任务,验证泵是否真的在执行"新"微任务
  var hbFired = false, spins = 0;
  Promise.resolve().then(function () { hbFired = true; });
  while (!settled) {
    __pumpMicrotasks();          // native: checkpoint + pumpMessageLoop
    if (++spins > 3000000) throw new Error('promise never settled');
  }
  if (failure) throw failure;
  return value;
}

native 侧的泵实现很薄:

JSVM_Value PumpMicrotasksCb(JSVM_Env env, JSVM_CallbackInfo info) {
  if (g_vm) {
    OH_JSVM_PerformMicrotaskCheckpoint(g_vm);
    bool hasMore = false;
    OH_JSVM_PumpMessageLoop(g_vm, &hasMore);
    if (hasMore) OH_JSVM_PerformMicrotaskCheckpoint(g_vm);
  }
  ...
}

create/embedFont 两步由此跑通。

坑 2:worker postMessage 序列化限制,23MB 字体过不去

引擎资产最初计划从 UI 线程经 worker 消息传给引擎线程,真机直接报 An exception occurred during serialization——大 ArrayBuffer / 长字符串过不了鸿蒙 Worker 的序列化层。

修复原则:大资源一律走文件路径。 ArkTS 把 rawfile 三个资产(引擎 bundle、glue、中文字体)落盘到 filesDir/engine/,消息里只传三个路径字符串 + spec JSON,C++ 桥自己用 ifstream 读文件。这里还顺带修了一个缓存陷阱:filesDir 里已存在的旧文件曾被跳过重写,导致新 glue 永远不生效——改为每次启动强制重落盘。这个坑的教训是:消息通道里只传引用(路径),不传内容。

坑 3:native 回调结构体的生命周期(UAF 崩溃)

glue 脚本里第一句 console.log 直接把进程打崩。addr2line 定位到 OH_JSVM_RunScript 内部,根因让人后背发凉:注册 console 时,JSVM_CallbackStruct 声明成了栈变量——函数返回后,JSVM 引擎在真正调用 console.log 时解引用悬空指针。

void RegisterConsole(JSVM_Env env) {
  ...
  // MUST be static: JSVM_CreateFunction may retain the callback pointer and
  // dereference it long after this function has returned.
  // A stack instance here caused a use-after-free crash on the first
  // console.log inside the glue script.
  static JSVM_CallbackStruct cbStruct = {ConsoleLogCb, nullptr};
  for (const char* m : {"log", "warn", "error", "info", "debug"}) { ... }
}

修复规则由此固化:所有注入 JSVM 的 native 回调结构体一律 static 生命周期。同时把三处 JSVM 字符串提取统一成容量安全的 helper——先查长度,再按 len+1 分配、resize(copied) 收尾,杜绝越界写。

坑 4:fontkit 内嵌 Node stream 的调度链断掉,save 永久卡死

这是最深的坑。create/embedFont 都通了,唯独 pdf.save() 三百万次泵都不 settle,且 setTimeout 探针计数为 0——定时器根本没被调过。

逐层解剖 bundle 源码后才找到根因:fontkit 的 subset.encodeStream() 用的是 Node 风格流(.on("data")/.on("end")),其可读端调度(resume_/flow/endReadableNT)依赖内嵌 browserify process.nextTick 队列的持续 drain,而这个 drain 在 JSVM 里几轮之后就停了——事件循环不存在,流的调度链自然断掉。

修复思路:不再修事件循环,改外科手术。 monkey-patch CustomFontSubsetEmbedder.serializeFont,手动同步完成原本由延迟回调做的事:

EmbedderCtor.prototype.serializeFont = function () {
  var subset = this.subset;
  return new Promise(function (resolve, reject) {
    try {
      var s = subset.encodeStream();
      s.on('error', function () {});   // 吞掉迟到调度抛在已结束流上的错
      subset.encode(s);                // 原本延迟回调做的事,现在同步做
      s.end();
      var parts = [], chunk;
      while ((chunk = s.read()) !== null) parts.push(chunk);  // 原本 resume_/flow 做的事
      // ... 拼接为 Uint8Array resolve
    } catch (e) { reject(e); }
  });
};

同时给 pdf.save() 传 objectsPerTick: Infinity, useObjectStreams: false——前者关掉 pdf-lib 的协作让步(它假设有定时器可让出),后者绕开流压缩路径。修复后 save 从「永久卡死」变成 95ms。

这个坑的方法论价值在于:当底层调度模型不可修复时,在正确的抽象层做等价替换。我们没有去模拟一个完整 Node 事件循环(不可能也不必要),而是在 pdf-lib 的 embedder 层找到那一个异步点,把它同步化。

坑 5:UI 线程被同步 NAPI 阻塞,触发 APP_FREEZE

引擎跑通后,22MB 字体的 embedFont 在 UI 线程同步执行约 1 秒,系统直接报应用冻结(APP_FREEZE,鸿蒙版 ANR)。

修复:全部引擎调用迁入专用持久 Worker 线程(@ohos.worker 的 ThreadWorker),UI 线程只收发消息。迁移时踩到两个 API 更名:worker.workerInstance 在 API 23 里已更名为 worker.workerPort;ThreadWorkerMessageEvents 类型改为 MessageEvents——以 SDK 的 d.ts 为准,不要信旧示例。

迁移后的时序:点「生成 PDF」→ UI 线程把 spec JSON 发给 Worker → Worker 调 NAPI → JSVM 内全链路同步跑完(字体子集化约 1 秒占大头)→ PDF 字节原路返回 → UI 线程落盘。全程 UI 不卡,生成约 1.2 秒。

这里有一个线程模型上的取舍值得展开:为什么是持久 Worker(应用生命周期内创建一次、常驻)而不是按需创建?因为 JSVM 实例的初始化和 2.3MB 引擎 bundle 的 eval 加起来约 230ms,如果每次生成都重建,用户感知会从「1.2 秒出结果」劣化成「1.5 秒起步还伴随时延抖动」;而持久 Worker 意味着引擎在应用启动后就完成热身,后续每次生成都是纯增量成本。代价是Worker 与 UI 线程之间的消息协议要自己维护——但我们把消息内容收敛成「路径 + JSON」之后(坑 2 的结论),这个协议薄到几乎不需要维护,取舍自然是划算的。

性能账也算得清楚:生成 1.2 秒里,embedFont(22MB 字体解析 + 子集化)约 1 秒,pdf.save() 修复后仅 95ms,绘制层可忽略。也就是说瓶颈完全在字体侧,这直接指明了优化方向——换子集化后的小字体(3~5MB)后,首启 embedFont 会降到百毫秒级,整条生成链路有望进入 300ms 以内,这是后续迭代性价比最高的一刀。

六、界面层:Jaspersoft Studio 信息架构的 ArkTS 原生实现

跨平台 GUI 移植的通行做法是:界面层随目标平台原生技术栈落地,执行层保持上游真实实现。界面按 Jaspersoft Studio 的信息架构组织:

  • 顶部工具栏:logo + 引擎状态 + 生成 / 另存 / 关于;
  • 左侧 JRXML Bands 结构面板:Title、Page Header、Column Header、Detail、Summary、Page Footer 六带,实时反映设计数据(几行明细、几个字段,一眼可见);
  • 中部设计区:三张带编辑卡片(报表标题组、字段定义、数据明细),字段可增删、行可增删、末列数值实时合计;
  • 底部控制台:引擎日志直出,[engine] 前缀的每一步(落盘、JSVM 初始化、导出字节数)在界面上可见,排查问题时不用连电脑抓日志。

五带结构与编辑区

原生实现的细节收益是实打实的:字段增删时列宽自动重排、合计即时联动、大屏 2in1 上的多栏布局、以及与系统字体渲染一致的中文字形。这些用 Web 壳做,每一项都要额外调。

七、PDF 版式:把「JasperReports 观感」做进绘制层

引擎层的五带绘制 glue(约 400 行纯 JS)完整实现了经典版式:灰底列头带、偶数行斑马纹、末列(金额)按 font.widthOfTextAtSize 精确右对齐、标题带下方粗细双线、Summary 带强调色合计、Page Footer 带逐页页码。分页逻辑内置:明细行超出页高自动换页,新页自动重绘列头带。

排版上还有一处容易被忽略的细节:drawText 的 y 是文字基线,字形向上伸展、向下留降部。20pt 标题与 11pt 副标题若只按基线间距排布,副标题顶部会顶进标题的降部里。所以标题、副标题、双线之间按各自字号加固定留白逐级下移——这类像素级排版问题,恰恰是「用成熟引擎而不是自己拼字符串」的价值所在。

导出版式

中文字体链路是另一个重点:22MB CJK TTF 经 fontkit 子集化后嵌入 PDF,产物是合法的 %PDF-1.7,24~28KB——中文字体子集化嵌入保证了在任何设备上打开都不缺字、不乱码。

八、真机验收:导出链路逐环截图

导出走的是系统保存对话框(DocumentViewPicker),生成成功后文件直接落到用户选择的位置:

系统保存对话框导出

导出的 PDF 在 WPS Office 里打开验证:标题、副标题、灰底列头、斑马纹、右对齐金额、合计行、页码全部到位,中文渲染无缺字:

WPS 打开导出的 PDF

真机验收清单(全部实测口径,非看代码推断):

  • HAP 可安装,桌面图标与显示名正确;
  • 启动约 2 秒就绪,无黑屏白屏;冷启动多次复验一致,无 native 崩溃;
  • JSVM 内完成 2.3MB 引擎 bundle eval(约 230ms)+ glue 注册;
  • 字段增删、数据行编辑、合计实时联动;
  • 生成中文 PDF(%PDF-1.7,五带样式齐全,含 22MB 字体子集化约 1 秒,Worker 线程不冻结 UI);
  • 系统对话框导出,文件管理器可见、双击可开。

九、上架应用市场:移植之外的第二场仗

技术做完只是半场。上架华为应用市场是另一套完全不同的工程,这里记录实际过程与要点。

应用命名。 「墨表」是我们的应用名,但纯两字名在应用市场重名冲突概率高,审核会要求提供命名依据。最终显示名定为「墨表(from JasperReports)」,既保住了品牌短名,又在命名层面明确了对上游开源项目的归属致敬——这也是后文开源合规的一部分。

素材三件套。 应用市场要求图标、截图、介绍三者严格一致。我们做了三张版式统一的宣传图(引擎来路 / 组织报表 / 一键导出),与设计器实拍截图配套提交。其中「引擎来路」那张直接把技术栈与开源协议写进画面:pdf-lib 1.17.1 + @pdf-lib/fontkit 1.1.1,JasperReports 遵循 LGPL-3.0,NAPI 直接绑定 JSVM、无外部进程代理——用得上什么就写什么,不上锁、不隐藏。

关于页:技术栈与开源归属

权限最小化。 这是审核重点。本应用的权限表是精简后的最小集:读写用户文件走 picker(用户授权制),无网络、无位置、无通讯录。每一个权限都要在审核备注里说清用途,说不清的权限直接删。

隐私合规。 本地工具类应用的优势是可以做到「零数据出设备」:报表数据全部本地处理,无任何上报。隐私声明按实际行为写——不采集、不共享、本地存储——如实陈述反而是最容易过审的写法。

提审材料清单。 实际走下来,一次完整的提审要准备这些东西:

  • 已签名的 release HAP(发布证书 + 发布 profile,与调试签名是两套材料,不能混用);
  • 应用图标 216×216、三至五张应用截图、一张功能介绍图;
  • 应用名称、一句话简介、详细介绍(详细介绍里如实写明「基于开源组件 pdf-lib/fontkit 构建」);
  • 隐私政策:需要一个可公开访问的 URL,内容与权限表严格对应;
  • 版权证明/开源声明:个人开发者账号上传自研声明即可,但关于页与介绍中的开源标注要与实际一致;
  • 权限用途说明:权限表里每一项都要有对应的业务理由。

审核反馈闭环。 首次提审被提到的问题集中在命名依据与功能说明上;按审核意见补充了开源上游说明与应用介绍后过审。经验是:审核备注里把「这是什么、基于什么开源项目、为什么这样命名、权限为什么这么少」一次讲透,能显著减少打回轮次。

还有一条容易被忽视的实操经验:关于页本身就是审核材料。本应用的关于页把技术栈、开源协议、执行方式逐项列出(如下图),审核人员点开应用即可交叉验证介绍里每一句声明——这种「自证透明」的设计,比在审核备注里写一百句话都有效。

十、开源合规:用什么,标什么

上架开源移植应用,合规清单必须在提审前过一遍:

组件协议合规动作
JasperReports(能力模型对齐对象)LGPL-3.0应用名与关于页明确归属上游;未打包其 Java 代码,不受 LGPL 传染约束
pdf-libMIT关于页标注版本与协议;保留上游版权声明
@pdf-lib/fontkitMIT同上
内置中文字体必须 OFL这是唯一的高危项,见下文

关于页把这套归属完整标注(技术栈、协议、执行方式一目了然),既是法律义务,也是对开源生态的诚实交代:

关于页把这套归属完整标注(技术栈、协议、执行方式一目了然),既是法律义务,也是对开源生态的诚实交代。特别要提醒同行的是字体版权:rawfile 里的中文字体如果用了 Arial Unicode 这类版权受限字体,上架即是侵权。正确做法是换成 OFL 协议的开源中文字体(如思源黑体按常用字子集化),体积还能从 22MB 降到 3~5MB,首启 embedFont 耗时也会从 1 秒降到百毫秒级——合规与性能在这里是同一件事。

十一、排障速查表

把真机调试阶段沉淀的排障表留给后来者:

症状原因处理
安装报 9568320部署了未签名包DevEco Signing Configs 自动签名后重编;确认 products[].signingConfig 指向 signingConfigs[].name
改了 glue 不生效filesDir/engine 旧缓存每次启动强制重落盘;老版本需卸载重装
点击生成后界面卡死数秒引擎跑在 UI 线程确认用的是 Worker 线程版本(hilog 里引擎日志线程号 ≠ 主线程号)
JS 侧 promise 永不 settleJSVM 无事件循环glue 用 syncAwait + native __pumpMicrotasks 泵
pdf.save() 三百万泵不返回fontkit 流调度依赖 nextTick drainmonkey-patch serializeFont 同步化;objectsPerTick: Infinity
glue 首句 console.log 即崩回调结构体栈变量 UAF所有 JSVM_CallbackStruct 用 static
postMessage 报序列化异常大 ArrayBuffer/长字符串消息只传路径与 JSON,大资源走文件

十二、能力边界与自检

按移植能力分级如实交代(全部真机实测口径):

  • T0 基础能力:100% —— 构建/签名/安装/启动/引擎初始化/中文字体子集化/中文 PDF 生成全部通过;
  • T1 日常核心能力:100% —— 五带编辑、字段动态增删、合计实时联动、五带样式 PDF、自动分页、系统导出全部通过;
  • T2 增强能力:约 20% —— 多页长报表可用;JRXML 模板导入导出、外部数据源(JDBC/CSV)、图表元素、子报表/分组带、Excel/HTML 多格式导出为后续迭代项。

这些「未适配」不是缺陷,是主动分期:JRXML 解析器、JFreeChart 生态、exceljs 多格式导出,都可以沿当前同构路线(纯 JS writer 进 JSVM)迭代,架构上没有预留死角。

十三、QA:几个高频问题

Q1:既然是 JasperReports 移植,为什么里面跑的是 pdf-lib 而不是 JasperReports 本体?

JasperReports 本体是 Java 生态的,内嵌 JVM 意味着 100MB+ 运行时和一整套 SELinux/HNP 的坑。本项目移植的是它的核心资产——五带报表模型与设计器交互形态;执行层选了同为开源、纯 JS 的 pdf-lib + fontkit。对「设计报表 → 出 PDF」这条用户链路而言,两者交付物一致,成本差一个数量级。

Q2:JSVM 里跑 JS,和直接用 ArkWeb 加载网页有什么区别?

ArkWeb 是完整浏览器内核,重(内存、启动耗时),且引擎逻辑藏在 Web 层里,和原生 UI 隔着一层桥。JSVM 是进程内的纯 JS 引擎,2.3MB、230ms 完成初始化,C++ 可以直接交换 ArrayBuffer、注入 native 函数——引擎是应用的一个「库」,不是一个「网页」。

Q3:为什么生成一份 PDF 要 1.2 秒?慢在哪?

95% 的时间花在 22MB 中文字体的解析与子集化上(embedFont),绘制和 pdf.save()(修复后 95ms)几乎可以忽略。换 OFL 子集化小字体(3~5MB)后,这条链路有望进入 300ms 以内。

Q4:我的报表数据会被上传吗?

不会。设计、填充、渲染、导出全部在本地完成,应用无网络权限,唯一走系统 API 的是导出时的文件保存对话框(picker 用户授权制)。

Q5:为什么不支持 JRXML 模板导入?

JRXML 解析器属 T2 分期项,当前引擎层未覆盖。架构上没有死角——后续沿「纯 JS 解析器进 JSVM」的同构路线迭代即可,与现有 glue 完全兼容。

十四、写在最后:这条路线的普适性

回头看,这个项目验证的不只是一个应用的移植,而是一条路线的可行性:「纯 JS 开源引擎 + esbuild IIFE 全量打包 + 进程内 JSVM + Worker 线程隔离 + ArkTS 原生 UI」。

它把跨平台移植的执行层成本压到了极低——2.3MB 引擎、26MB 全包、零外部依赖——同时保住了两个最容易丢的东西:真实开源上游的引擎行为,和原生平台的界面体验。凡是执行层存在纯 JS 实现的开源软件(PDF 之外,Excel 有 exceljs、ZIP 有 fflate、Markdown 有 marked……),这条路线都可以直接复用,连五个坑的填法都是现成的。

对鸿蒙桌面生态而言,缺的从来不是某一款应用,而是把开源世界的存量软件搬过来的低成本通路。希望这篇记录能成为其中一块铺路石。

最后用三句话收束全文的方法论:

  1. 选型先算总账:包体、依赖、SELinux 约束、上架可行性,四项一起算,「最省事的路线」往往是「最贵的那条」;
  2. 异步引擎进同步环境,问题必然出现在调度层:先做实验确认泵的能力边界(心跳探针),再决定是在调度层修复还是在业务层做等价替换(坑 4 的外科手术);
  3. 上架是移植的一部分,不是移植的后续:开源标注、权限最小化、命名依据,这些从写第一行代码起就要进入设计,而不是提审前一夜再补。

本文所有截图均为 HarmonyOS PC 真机实拍;文中代码节选与仓库源码一致。项目基于的开源组件:JasperReports(LGPL-3.0,能力模型对齐)、pdf-lib(MIT)、@pdf-lib/fontkit(MIT)。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐