鸿蒙 PC Markdown 编辑器通信架构:受限 ArkTS-JavaScript Bridge

鸿蒙 PC Markdown 编辑器通信架构:受限 ArkTS-JavaScript Bridge

本文从所有权、通信频率、信任边界和生命周期出发,设计 ArkUI 原生外壳与 ArkWeb 编辑内核之间的最小协议。完整示例代码:https://gitcode.com/VON-/codex_md_oh

Bridge 要解决的问题

OhMarkdown 的鸿蒙 PC 工作台由 ArkUI 管理,CodeMirror 编辑器运行在 ArkWeb 中。两侧的职责划分很清晰:

  • ArkTS 持有文件 URI、文件名、操作状态和鸿蒙系统能力。
  • JavaScript 持有 CodeMirror 文档、撤销栈、光标和预览状态。

Bridge 的目标不是让两侧任意互调,而是为必要的业务消息建立最小通道。

五个入站方法

ArkTS 侧的 Bridge 对象只定义了五类事件:

ts 复制代码
class EditorBridge {
  private readonly readyHandler: () => void;
  private readonly stateHandler: (wordCount: number) => void;
  private readonly changeHandler: (wordCount: number, dirty: boolean) => void;
  private readonly snapshotHandler: (content: string, revision: number) => void;
  private readonly commandHandler: (command: string, content: string) => void;

  onReady(): void {
    this.readyHandler();
  }

  onState(wordCount: number): void {
    this.stateHandler(wordCount);
  }

  onChange(wordCount: number, dirty: boolean): void {
    this.changeHandler(wordCount, dirty);
  }

  onSnapshot(content: string, revision: number): void {
    this.snapshotHandler(content, revision);
  }

  onCommand(command: string, content: string): void {
    this.commandHandler(command, content);
  }
}

代码来源:entry/src/main/ets/shared/ui/WorkspaceShell.ets

五个方法的含义分别是编辑器就绪、文档状态、编辑变更、节流恢复快照和显式命令。ArkWeb 注入时再次使用方法白名单:

ts 复制代码
Web({ src: $rawfile('editor/index.html'), controller: this.editorController })
  .javaScriptAccess(true)
  .domStorageAccess(false)
  .onlineImageAccess(false)
  .fileAccess(false)
  .geolocationAccess(false)
  .zoomAccess(false)
  .javaScriptProxy({
    object: this.editorBridge,
    name: 'ohMarkdownBridge',
    methodList: ['onReady', 'onState', 'onChange', 'onSnapshot', 'onCommand'],
    controller: this.editorController
  })

代码来源:entry/src/main/ets/shared/ui/WorkspaceShell.ets

即使 Web 内容出现异常,它也没有直接文件访问、定位、DOM 存储或在线图片权限。用户文件始终由 ArkTS 调用 Core File Kit 处理。

不在每次按键传输全文

JavaScript 侧会对高频变更通知做节流,普通状态消息只上报字数和是否已修改。完整正文只在用户明确保存,或小于 5MiB 的脏文档到达 1.5 秒恢复快照周期时传输;大文档不会周期传全文。

ts 复制代码
function flushToNative(content?: string): void {
  window.clearTimeout(bridgeTimer);
  if (pendingNativeChange) {
    const wordCount = largeDocumentMode ? -1 : countWords(content ?? editor.state.sliceDoc());
    window.ohMarkdownBridge?.onChange(wordCount, pendingDirty);
    pendingNativeChange = false;
  }
}

代码来源:web-editor/src/main.ts

这个设计避免大文档在每次键入时跨 Bridge 复制。对超过 1MiB 的文档,变更节流窗口还会从 160ms 延长到 600ms。

鸿蒙 PC 模拟器状态截图

下图显示文档保存后,Bridge 将文件状态和字数传递给 ArkUI 状态栏的实际效果。

边界与后续方向

当前 Bridge 已经能够支撑活动文档编辑、保存与小文档恢复,但还不是最终的大文档同步协议。正式版本需要增加协议版本、完整命令参数校验,并考虑增量恢复日志,以应对 ArkWeb 意外重载。

先按所有权划分通信方向

Bridge 设计的第一步不是罗列方法,而是确认数据由谁拥有。OhMarkdown 中,文件 URI 只能由原生侧持有,因为它来自系统授权并用于 Core File Kit;CodeMirror 文档和撤销栈只能由 Web 侧持有,因为每次输入都发生在 EditorState 事务中。两边共享的是少量业务事件,而不是彼此内部对象。

原生到 Web 的主要命令包括设置文档、切换模式、撤销、重做、请求保存或导出。Web 到原生的主要事件包括就绪、字数与脏状态、显式命令结果以及恢复快照。方向固定后,可以避免出现"ArkTS 直接读取 DOM""JavaScript 自行打开文件"这类越权捷径。

一个实用原则是:命令描述意图,事件描述已经发生的事实。原生发出"请求保存",Web 返回此刻的完整 Markdown;Web 发出"文档已变更",原生更新标签和状态栏。双方不共享可变对象,也不假定调用一定同步完成。

最小白名单本身就是安全措施

javaScriptProxymethodList 只公开明确方法,ArkWeb 同时关闭文件访问、在线图片、DOM Storage 和定位。即使 Markdown 预览出现漏洞,攻击面仍被限制:页面不能凭空遍历用户目录,也没有一个通用的 execute(command, args) 可以调用任意原生能力。

通用命令入口看起来扩展方便,却会把所有权限集中到字符串分发器。一旦命令名称或参数校验遗漏,新增原生能力可能被旧页面意外调用。更稳妥的方式是对高风险能力使用显式方法或严格枚举,并在原生侧再次判断当前状态、参数长度和用户动作来源。

Bridge 不能替代预览净化。方法白名单、CSP、DOMPurify 和 ArkWeb 权限收缩是不同层次:净化阻止恶意 DOM,CSP 限制页面加载和执行,权限配置限制 Web 能力,Bridge 白名单限制原生入口。任何一层都不应假定其他层永远不会失败。

为什么普通输入只传状态

假设用户打开 10MiB 文档并连续输入,每次按键都把全文从 JavaScript 传到 ArkTS,会产生至少三类成本:构造完整字符串、跨运行时复制、原生侧接收和处理。即使单次看似可接受,输入法候选、长按按键和批量粘贴都会放大频率。

当前变更消息只携带字数和 dirty。字数计算在大文档模式下还会返回降级标记,避免每次扫描全文。完整内容只在保存、导出或受节流的恢复快照等明确节点传输。这个策略让 Bridge 流量与用户命令相关,而不是与按键数量乘以文档大小相关。

节流不能简单理解为延迟所有事件。修改星号和保存按钮需要及时更新,恢复快照可以容忍约 1.5 秒窗口,保存内容必须立即取得最新状态。不同消息的时效性不同,应分别设计,而不是共用一个全局定时器。

就绪握手解决初始化竞态

ArkTS 创建 Web 组件后,页面脚本、CodeMirror 和 JavaScript 代理并不立即可用。若原生侧在页面就绪前执行 setDocument,命令可能丢失;若 Web 就绪后只加载默认空文档,用户刚选择的文件又可能被覆盖。

onReady 是最小握手:Web 完成初始化后通知原生,原生根据当前会话决定发送哪份文档。未来协议完善后,就绪消息还应包含协议版本和能力集合,例如是否支持恢复快照、CRLF 行分隔符或打印准备。原生侧据此决定兼容路径,而不是调用不存在的方法后才发现失败。

握手还要处理重复发生。ArkWeb 重载可能再次发送 onReady,原生不能把它永远视为第一次启动。合理行为是检查当前文档、未保存状态和最近快照,必要时重新注入当前会话,同时防止旧的异步结果覆盖新页面。

参数校验应靠近信任边界

Bridge 传入原生侧的任何值都应被视为外部输入。即使页面资源随 HAP 打包,也可能因为脚本缺陷、注入漏洞或版本漂移产生异常参数。恢复快照已经检查内容长度、revision 非负且为整数;命令名称应限制在枚举中;字数要限制范围;文档标题不能直接进入文件名或 HTML。

校验至少覆盖:

  • 类型是否符合协议,不能依赖隐式字符串转数字。
  • 字符串长度和文档大小是否超过当前能力上限。
  • 枚举值是否在支持集合中,未知命令默认拒绝。
  • revision 是否单调或至少不会让旧快照覆盖新快照。
  • 当前生命周期是否允许执行,例如页面未就绪时不能报告保存成功。
  • 高风险操作是否来自用户明确动作,而非预览内容自动触发。

错误参数不应导致应用崩溃,也不能悄悄按默认值执行。原生侧可以记录简洁的操作状态和诊断日志,但不要把完整文档写入日志。

全文命令需要明确的一致性点

保存时 Web 侧发送的内容必须代表用户看到的当前文档。若保存命令先异步等待很久再读取文本,中间的新输入可能被意外纳入或遗漏。项目在命令触发点从 editor.state.sliceDoc() 取得快照,再交给原生保存;原生保存期间可以显示忙状态,但编辑器是否继续允许输入需要产品语义明确。

一种稳妥语义是把发送时的 revision 与内容绑定。保存完成后,只有当前 revision 仍等于已保存 revision 才清除修改标记;如果用户在 I/O 期间继续输入,磁盘保存的是旧快照,标签仍应保持 Modified。未来多标签与慢速存储场景尤其需要这个约束。

同理,导出 HTML 和打印应使用命令触发时的文本快照,而不是稍后从另一个可变 DOM 读取。恢复快照则允许覆盖旧 revision,但不能让完成较晚的旧写入替换更新记录,原生侧需要串行化或按 revision 丢弃过期任务。

错误不能只停在某一侧

Bridge 跨越两个运行环境,错误也分为多类:JavaScript 方法不存在、runJavaScript 执行失败、参数被拒绝、文件 I/O 失败、页面重载或原生上下文不可用。只在控制台打印会让用户误以为命令成功。

工作台需要把用户可处理的结果转成操作状态,例如文件没有写入、恢复快照被拒绝、导出页面未就绪。诊断层则记录方法、阶段和错误类型,但不记录正文。命令完成后还要恢复按钮状态,避免一次异常让 Save 永久禁用。

对于保存这类数据操作,Bridge 成功只表示内容已送达原生侧,不表示磁盘持久化成功。只有 Core File Kit 完成写入、截断、同步以及必要的备份清理后,原生才能通知 Web 标记已保存。这个完成点必须由文件所有者决定。

协议版本如何演进

当前技术纵切的方法数量少,可以由 HAP 内 ArkTS 和 Web 资源同步升级。随着多标签、工作区和插件出现,协议会变复杂,建议引入显式版本:

ts 复制代码
interface BridgeCapabilities {
  protocolVersion: number;
  recoverySnapshot: boolean;
  documentFormat: boolean;
  printPreparation: boolean;
}

这段接口表示推荐的协议形态;落地时应继续使用 ArkTS/TypeScript 可共同表达的简单数字、布尔和字符串。版本变化要区分向后兼容新增和破坏性修改。未知字段可以忽略,未知命令必须拒绝;重大版本不兼容时应停止编辑并提示资源版本错误,而不是冒险保存。

生成资源与 ArkTS 一起进入 HAP,理论上不会长期错配,但增量构建、缓存和开发调试仍可能把旧 HTML 打进新包。就绪握手上报版本,可以把这种构建错误从随机功能异常变成明确诊断。

Bridge 自动化与设备验证

Web 测试可以注入模拟 ohMarkdownBridge,记录调用次数和参数,验证:输入只产生节流状态消息、显式保存才传全文、快照按间隔触发、大文档不发送周期全文、导出返回独立 HTML。原生单元测试适合验证参数校验和状态机。

设备侧仍要覆盖:

  1. ArkWeb 首次加载和重载后的就绪握手。
  2. 中文 IME 组合输入期间不会重复上报错误内容。
  3. 连续输入大文档时 Bridge 不造成明显卡顿或内存峰值。
  4. 保存失败后状态不会被误标为 Saved。
  5. 强制停止应用后,最近有效恢复快照能够重新加载。
  6. 系统文件选择器和打印界面返回后,Bridge 仍可继续工作。

测试报告应记录 HAP 哈希和设备,不能把桌面浏览器中的模拟 Bridge 当作 ArkWeb 集成完成的全部证据。

一份可执行的 Bridge 审查清单

  • 每个方法是否有单一业务语义,而不是通用原生执行器。
  • 数据所有权和通信方向是否清楚,是否存在两份可写全文。
  • 方法是否进入 methodList 白名单,ArkWeb 额外权限是否关闭。
  • 普通按键是否避免传输全文,大文档是否有进一步降级。
  • 初始化是否有握手,重复就绪和页面重载是否安全。
  • 所有入站参数是否做类型、长度、范围和状态校验。
  • 保存成功是否以磁盘持久化为准,而不是以 Bridge 调用返回为准。
  • 异步结果是否携带 revision,旧任务能否覆盖新状态。
  • 错误是否同时有用户状态和不含正文的诊断记录。
  • 协议升级是否有版本或能力协商,生产资源错配能否被发现。

受限 Bridge 的核心不是"ArkTS 能调用 JavaScript",而是让两个拥有不同职责的运行环境只交换必要事实。通信面越小,性能预算、安全审计和故障恢复越容易推理,这对需要长期处理本地文档的鸿蒙 PC 编辑器尤其重要。

命令状态机比零散回调更可靠

保存、导出和打印都不是一次函数调用,而是多个阶段:用户触发、请求 Web 快照、原生校验、系统能力执行、完成或失败、界面恢复。若每个按钮各自设置几个布尔值,重复点击和异常返回很容易留下冲突状态。

可以为长操作建立统一但有限的状态:idle、requesting、persisting、completed、failed。状态机控制哪些命令可并发、何时显示忙状态、失败后是否可重试。保存与导出仍有各自业务数据,不应把所有结果塞进一个字符串命令总线。

命令需要 requestId 与 document sessionId。JavaScript 返回的快照携带请求身份,原生侧只接受当前会话仍等待的结果;用户已切换文档或页面重载时,旧响应被丢弃。这样无需依赖"通常回调很快"的时间假设。

序列化格式要简单且有上限

ArkTS-JavaScript 代理适合传递数字、布尔和字符串。复杂嵌套对象在不同运行时的序列化规则、空值和异常类型可能不一致,协议应尽量扁平。文档格式可以用明确枚举字符串,revision 使用安全整数,正文长度在两侧都检查。

大型正文不宜包在多层 JSON 后再转义一次。当前显式保存直接传字符串,沙箱记录才进行 JSON 序列化。未来增量协议可以传 change ranges 与插入文本,但必须限制单条变化数量、总字符数和排序,原生侧验证范围不重叠且不越界。

错误也应使用稳定代码与可选短消息,例如 DOCUMENT_TOO_LARGESTALE_SESSIONPERSIST_FAILED。用户文案由原生资源决定,避免 Web 侧英文错误直接暴露,也方便本地化和统计。

防止 Bridge 重入与命令风暴

原生调用 JavaScript 后,页面可能同步回调原生;原生处理回调又立即执行脚本,形成难以推理的重入。高风险命令最好通过任务队列切到明确事件循环阶段,状态先提交再允许后续消息。

频率限制不只用于全文。损坏页面可能连续发送 onChange、快照或未知命令,造成主线程和日志压力。原生侧可以合并状态事件、限制快照最短间隔和连续错误日志,同时保持保存等用户命令不被普通状态消息饿死。

限流不能静默丢弃最后状态。合并时保留最新 dirty、wordCount 和 revision;超过安全上限则切换为可见错误,并阻止可能覆盖数据的命令。

焦点与输入法事件不应穿过业务协议

Bridge 不需要上报每个 keydown、composition update 或光标移动。把低层输入事件传到 ArkTS 会增加延迟,还可能让原生快捷键与 CodeMirror/IME 争夺按键。编辑内核应在 Web 内完成组合输入和常规编辑,只把稳定业务结果上报。

原生命令触发后需要恢复焦点时,可以发送明确的 focusEditor,但要避免在系统选择器或对话框仍打开时抢焦点。页面返回焦点成功不代表 IME 组合可以恢复,真机上需要检查候选窗和中英文状态。

未来状态栏若显示光标行列,可以低频上报选区摘要,而不是完整 selection ranges。多光标等复杂状态仍由 CodeMirror 拥有。

页面来源和代理注入要绑定

Bridge 只应注入应用自带的 rawfile 页面。若 ArkWeb发生导航,不能继续向任意网页暴露原生对象。当前链接点击被阻止且网络能力关闭,仍建议监听页面来源和加载错误,发现非预期 URL 时停止代理或重置页面。

本地页面完整性由 HAP 和签名保护,开发阶段还要防止旧生成资源与新 ArkTS 协议错配。就绪握手的协议版本能快速识别这种问题。正式产物不开放远程调试,也不从查询参数读取任意命令。

Bridge 测试替身应验证协议而非复制实现

Web 自动化中的 mock Bridge 只记录调用并返回必要结果,不应重写一套 ArkTS 业务逻辑。测试断言方法名、参数、频率、revision 和调用时机。原生侧则用可控的编辑器响应替身验证超时、旧 requestId、异常长度和重复完成。

契约测试可以保存一组协议样例,让 ArkTS 与 TypeScript 两侧都解析相同枚举和版本。破坏性变更时样例会明确失败,避免只改一侧。设备集成再证明真实 javaScriptProxy 的方法暴露、线程与生命周期符合假设。

超时用例很重要:页面永不返回保存快照时,按钮必须恢复并保留 dirty;原生持久化失败时,Web不能标记 saved;页面重载后旧响应不能完成新请求。

为多标签预留会话身份而非扩大方法面

多标签并不一定需要为每个标签复制一套 Bridge 方法。协议可以让当前活动会话携带 sessionId,原生会话管理器把事件路由到对应文档。若每个标签使用独立 ArkWeb,则每个代理实例仍绑定唯一会话,不能依赖全局 currentDocument

后台标签不应继续高频预览和状态上报。冻结时刷新恢复状态,恢复活动时重新握手或同步必要元数据。内存优化可能销毁非活动 Web 视图,此时会话基线和恢复记录必须足以重建,不让 Bridge 生命周期等同于文档生命周期。

工作区搜索、大纲索引等批处理能力不适合通过编辑 Bridge 传输大量结果,它们应有独立服务边界或在原生侧处理已授权文件。保持编辑协议小,才能避免功能规模增长后所有能力都耦合到 ArkWeb 页面。

协议可观测性要脱敏

调试时可以记录方法名、requestId、session 短标识、正文长度、revision、耗时和结果码,但不能记录正文、导出 HTML、用户 URI 或文件名。性能统计关注 Bridge 调用次数、平均载荷和 P95 耗时,足以发现全文风暴。

线上若需要诊断,采样策略默认关闭或匿名,用户可控。连续错误采用聚合计数,避免同一个页面异常刷满磁盘。日志版本与协议版本绑定,方便定位生成资源错配。

通过这些元数据,可以回答保存慢发生在 Web 取快照、跨 Bridge、文件写入还是系统选择器,而不触碰用户内容。

协议评审要检查最坏路径

正常消息通常很小且按顺序,真正暴露问题的是 20MiB 保存快照、页面重载、旧响应迟到、连续点击、原生上下文销毁和异常字符串。每个方法都应写出最大载荷、调用频率、超时、幂等性和失败后的文档状态。没有上限的"偶尔调用"在自动化或恶意页面下可能变成命令风暴。

评审还要跟踪一次用户动作经过哪些往返。保存若需要多次全文查询,协议应合并;状态栏若依赖同步脚本轮询,应改为节流事件。用时序图记录关键命令,能够直接发现重入、双重完成和提前清除脏状态。

Bridge 不承担业务存储

代理对象的生命周期依附页面或 Web 组件,不适合保存文档、文件权限和恢复队列。业务状态放在明确的原生会话与服务中,Bridge 只做适配和校验;页面销毁后可以创建新代理并重新握手。反过来,Web 侧也不把文件 URI、系统路径和保存凭证存入 DOM Storage。通信层保持无持久化,测试和安全审计都会更清楚。

相关推荐
chaoxiaomai1 小时前
电商套图批处理架构的性能分析——逐图生成与流水线模式的工程对比
架构
沸速存储1 小时前
内存技术的未来:DDR6、CAMM2与CXL将如何改变计算架构
科技·嵌入式硬件·架构·计算机外设·电脑
一缕清烟在人间2 小时前
HarmonyOS开发实战:小分享-TextEditPage文字编辑器——Header+TextArea+工具栏
后端·华为·harmonyos·鸿蒙
2501_918582372 小时前
HarmonyOS应用开发实战:小事记 - 应用包结构:HAP/HSP/HAR 的三层架构与 deliveryWithInstall 策略
华为·架构·harmonyos·鸿蒙
JouYY2 小时前
大模型底层学习(二)- 预训练(Pre-training)流程概览
架构·llm·agent
不肥嘟嘟右卫门2 小时前
鸿蒙原生ArkTS布局方式之Scroll+Column+Sticky粘性布局深度解析
华为·harmonyos
zzzll11113 小时前
Agent 开发的五种架构范式及选型思路
人工智能·架构·大模型·llm
宁&沉沦3 小时前
Chrome 扩展 Manifest 字段版本支持一览(全量)
前端·后端·编辑器
<小智>3 小时前
鸿蒙多功能工具箱开发实战(二十四)-单元测试与自动化测试
ui·华为·harmonyos