鸿蒙 PC Markdown 编辑器 Bridge 协议:原生外壳与 Web 内核的状态同步

OhMarkdown把系统文件、标签和窗口放在 ArkUI,把 CodeMirror和 Markdown渲染放在 ArkWeb。两端不能共享内存,只能通过协议传递状态。若协议没有明确方向、类型、频率和身份,连续输入会造成性能问题,异步回调还可能写错标签。

本文拆解双向 Bridge的接口、节流、命令快照、参数编码和失败降级。代码位于 https://gitcode.com/VON-/codex_md_oh,对应提交 3a9146e

两条方向各有职责

Web到原生使用 JavaScript Proxy,适合事件:

.javaScriptProxy({
  object: this.editorBridge,
  name: 'ohMarkdownBridge',
  methodList: [
    'onReady',
    'onState',
    'onChange',
    'onSnapshot',
    'onCommand'
  ],
  controller: this.editorController
})

原生到 Web使用 runJavaScript,适合命令:设置文档、激活会话、切换模式、查找、跳转、主题和导出。高频滚动留在 Web内部,不跨 Bridge。

代理对象只暴露白名单

class EditorBridge {
  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);
  }
}

Web不能访问任意 ArkTS方法。接口名与参数固定,业务回调通过构造函数注入。methodList是运行时能力面,TypeScript声明是 Web编译期契约,两者都要同步审查。

Ready 是初始化屏障

ArkWeb页面加载期间,原生不能假设 OhMarkdownEditor已存在。onReady后:

private onEditorReady(): void {
  this.editorReady = true;
  this.setEditorDocument(this.documentContent);
  this.setEditorMode();
  this.setEditorSyncScroll();
  this.setEditorTheme();
  if (!this.recoveryChecked) {
    this.recoveryChecked = true;
    this.checkStartupRecords();
  }
}

原生重放权威状态。页面重载后不依赖旧 Web内存。runEditorScript在 editorReady=false时返回,调用失败会把 ready重置,避免继续向失效页面发命令。

状态与正文分开传

普通输入只向原生发送字数和 dirty:

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;
  }
}

每键传全文会导致文档越大输入越慢。全文只在恢复快照、保存命令和标签切换主动捕获时传递。协议按数据成本拆分,而不是一个 onEverythingChanged

输入通知有长度相关防抖

const debounceMilliseconds =
  documentLength > 1024 * 1024 ? 600 : 160;
window.clearTimeout(bridgeTimer);
bridgeTimer = window.setTimeout(
  flushToNative,
  debounceMilliseconds
);

普通文档160毫秒更新状态栏,大于一兆延长到600毫秒。防抖只传轻状态,恢复另用1.5秒节流全文。两类计时器不能混用:状态栏允许停顿后更新,恢复必须在持续输入中周期落盘。

命令携带一致快照

requestCommand: (command) => {
  flushToNative();
  if (command === 'save') {
    flushRecoverySnapshot();
    pendingSaveDocument = editor.state.doc;
  }
  const content = command === 'save'
    ? editor.state.sliceDoc()
    : '';
  window.ohMarkdownBridge?.onCommand(
    command,
    content
  );
}

Ctrl+S先刷新 dirty,固定保存基线,再发送当前正文。Open、Find、New不需要全文。命令和内容在同一回调中,避免原生收到命令后再异步查询导致版本变化。

恢复快照有 revision

onSnapshot(content, revision)让原生识别版本并保存格式。原生限制五兆、非负整数,不能信任 Web输入。写入队列合并中间版本,避免多个回调并行写沙箱。

标签切换清除旧会话 Bridge和恢复定时器,防止甲的延迟事件在乙激活后到达。长期更强协议应让每个回调显式携带 sessionId,减少对“当前活动会话”的隐含依赖。

原生到 Web 参数结构化编码

this.runEditorScript(
  `window.OhMarkdownEditor?.activateSession(` +
  `${JSON.stringify(session.id)}, ` +
  `${JSON.stringify(session.content)}, ` +
  `${JSON.stringify(session.dirty)})`
);

正文、查询和文件名可能包含引号、反斜杠与换行,必须 JSON.stringify。手工单引号拼接既会语法错误,也有脚本注入风险。布尔和数字也统一生成合法字面量。

接口只调用 window.OhMarkdownEditor白名单,不把任意脚本暴露给用户输入。

字符串返回要 JSON 解码

runJavaScript的字符串结果可能是 JSON字符串表示:

private decodeJavaScriptString(
  result: string
): string {
  try {
    return JSON.parse(result) as string;
  } catch (_) {
    return result;
  }
}

活动正文和导出 HTML都经过解码,否则外层引号、\n和转义会写入文件。数字搜索结果用 Number.parseInt,布尔打印结果与 'true'明确比较,不把所有返回值混成一种解析。

Web API 是窄能力集合

OhMarkdownEditor?: {
  setSessionDocument(...): void;
  activateSession(...): void;
  closeSession(sessionId: string): void;
  getDocument(): string;
  setMode(mode: ViewMode): void;
  find(...): number;
  replaceAll(...): number;
  jumpToOffset(offset: number): boolean;
  setTheme(theme: 'light' | 'dark'): void;
  exportHtml(title: string): string;
}

原生不直接查询 DOM类名或操作 CodeMirror内部字段。Web实现可以升级,只要保持协议。主题和同步滚动只传意图,不传 CSS或具体 scrollTop。

失败降级

捕获正文失败时保留原生最近 documentContent,不覆盖为空。runEditorScript失败将 editorReady置 false,等待页面重新 ready重放。搜索失败只更新状态栏;保存失败保留 dirty与恢复记录。

错误日志不能包含正文和 URI。协议可观测应记录方法、耗时、长度、revision和错误类型,不记录用户数据。

安全配置缩小 Web 能力

.javaScriptAccess(true)
.domStorageAccess(false)
.onlineImageAccess(false)
.fileAccess(false)
.geolocationAccess(false)
.zoomAccess(false)

JavaScript是 CodeMirror必需,其他不需要能力关闭。Web资源打成离线单 HTML,不依赖网络。Bridge最小化与 ArkWeb权限收缩共同构成边界,不能只靠 CSP。

鸿蒙 PC 实际协同

下图中原生文件面板、标签和状态栏与 Web编辑区同屏。标签切换、dirty、字数和模式都通过协议同步,但用户不应感知运行时边界。

在这里插入图片描述

测试应覆盖多行字符串、空值、emoji、大正文、页面重载、命令重复、旧定时器和接口不存在。协议测试比单一 UI截图更能发现序列化错误。

版本演进

当前双方随 HAP一起发布,未显式交换协议版本。随着接口增长,可在 onReady携带版本和能力列表;原生只调用双方共同能力。不兼容时显示编辑器资源错误,而不是静默返回空值。

消息对象比位置参数更易扩展,例如 { sessionId, revision, dirty, wordCount }。但对象通过 Proxy的支持与序列化成本需在 HarmonyOS环境验证。当前小接口保持直接类型,避免无必要抽象。

性能边界

全文 Bridge会产生字符串序列化和内存副本。五兆以上禁用周期快照,标签切换仍需捕获全文。未来增量同步可传 transaction changes,原生按 revision应用;丢包时请求完整快照校正。

高频滚动、光标移动和预览 DOM绝不跨 Bridge。只有产品状态和持久化需要的数据穿越边界,才能保持输入流畅。

当前边界

回调未显式携带 sessionId;没有协议版本协商;字符串解码运行时类型检查可加强;大文档切换仍传全文;ArkWeb重载只恢复原生已捕获正文,不恢复完整 EditorState历史。

结语

OhMarkdown Bridge把事件和命令分向设计:Web主动报告 ready、轻状态、恢复快照和命令;原生发送文档、会话、模式、搜索与主题。轻状态防抖、全文受限、参数 JSON编码、返回按类型解析、失败保留旧值。

混合鸿蒙 PC编辑器的可靠性取决于这份协议是否像文件格式一样被认真对待。边界清楚,ArkUI和 ArkWeb才能各自发挥优势而不互相泄漏复杂度。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐