鸿蒙 PC Markdown 编辑器跨运行时通信:ArkWeb 返回值的 JSON 解码陷阱 鸿蒙 PC Markdown 编辑器跨运行时通信ArkWeb 返回值的 JSON 解码陷阱ArkUI 调用 ArkWeb JavaScript 时最容易被忽略的问题不是脚本能否执行而是返回值到底是什么格式。一个函数在浏览器控制台返回字符串不代表原生runJavaScriptPromise 得到的就是未经编码的原字符串。正文包含换行、引号或反斜杠时这个差异会直接污染多标签内容和 HTML 导出。本文基于鸿蒙 PC Markdown 编辑器 OhMarkdown 的真实缺陷与修复说明如何区分字符串、数字和布尔返回值为什么字符串结果要经过 JSON 解码为什么脚本参数也必须结构化编码以及如何把 Bridge 设计成可审查的窄接口。完整代码位于 https://gitcode.com/VON-/codex_md_oh本文对应提交3a9146e。问题为何会在多标签中暴露单文档编辑器大部分变化通过 JavaScript Proxy 主动回调原生层例如onChange、onSnapshot和onCommand。多标签切换前原生层需要立即捕获 CodeMirror 当前全文不能等待节流回调因此调用constresultawaitthis.editorController.runJavaScript(window.OhMarkdownEditor?.getDocument() ?? );Web API 的实现很直接window.OhMarkdownEditor{getDocument:()editor.state.sliceDoc()};最初代码把result直接当正文。只输入Session-B时问题不一定明显当正文是多行 Markdown原生层可能拿到带外层引号和转义符的 JSON 字符串表示。例如 Web 逻辑值是# 标题 正文 引用原生返回文本可能表达为# 标题\n\n正文 \引用\如果直接保存标签切换回来后编辑器会看到外层引号换行变成两个可见字符\和n引号前多出反斜杠。简单单行英文测试没有覆盖这些字符缺陷直到多标签与导出联调才出现。这类问题的本质是跨运行时序列化。ArkWeb 需要把 JavaScript 值转换为原生接口可以携带的字符串原生层不能假设结果等同于String(value)必须根据接口实际表示恢复逻辑值。解码函数只处理字符串结果OhMarkdown 增加一个小型边界函数privatedecodeJavaScriptString(result:string):string{try{returnJSON.parse(result)asstring;}catch(_){returnresult;}}正常 JSON 字符串通过JSON.parse去掉外层引号并还原转义。解析失败时返回原值兼容平台直接返回裸字符串的情况也避免一次格式差异让内容变为空。为什么不手工删除首尾引号并替换\n因为 JSON 字符串不只有换行转义还包含反斜杠、制表符、回车、Unicode 转义和嵌套引号。手工顺序稍有错误就会把用户真实输入的\\n误还原成换行。标准解析器已经定义完整语义应直接使用结构化 API。这个函数名明确带String不能用于所有返回值。若 JavaScript 返回数字2JSON.parse(2)得到 number但函数类型声明为 stringArkTS 的强制断言不会在运行时转换后续字符串操作可能出错。布尔值同理。调用方必须先决定期待的逻辑类型。更严格实现可以检查解析结果privatedecodeJavaScriptString(result:string):string{try{constdecoded:unknownJSON.parse(result);returntypeofdecodedstring?decoded:result;}catch(_){returnresult;}}当前代码使用类型断言满足既定 Web API 契约后续为了防御 Web 版本不一致可以加入运行时类型检查。跨运行时接口即使两端都由同一项目维护也应该把返回类型视为不可信边界。捕获活动文档的完整回退路径修复后的会话捕获如下privateasynccaptureActiveDocumentSession():Promisevoid{letcontentthis.documentContent;if(this.editorReady){try{constresultawaitthis.editorController.runJavaScript(window.OhMarkdownEditor?.getDocument() ?? );contentthis.decodeJavaScriptString(result);}catch(_){}}this.documentContentcontent;this.syncActiveDocumentSession(content);}函数先使用原生侧最近正文作为回退。只有编辑器 ready 并且脚本成功才用解码结果覆盖。ArkWeb 重载、页面销毁或脚本异常时不会把会话写成空字符串。catch 目前不显示错误因为会话切换仍可使用原生快照继续属于可降级路径。生产观测可以记录非敏感错误码但不能把正文写入日志。编辑器内容可能包含凭据、日记或公司文档调试跨运行时序列化时尤其要避免打印完整返回值。捕获完成后再同步活动会话并且调用顺序位于切换目标 sessionId 之前。若先切换身份再解码即使字符串内容正确也会写进错误会话。序列化正确性与状态机顺序必须同时成立。HTML 导出复用了同一修复HTML 导出由 Web 侧生成完整字符串因为渲染、净化和导出 CSS 都位于 Web 内核constexportedResultawaitthis.editorController.runJavaScript(window.OhMarkdownEditor?.exportHtml(${JSON.stringify(this.documentName)}) ?? );constexportedHtmlthis.decodeJavaScriptString(exportedResult);if(exportedHtml.length0){thrownewError(The editor did not produce HTML output.);}awaitwriteUtf8Document(saveUri,exportedHtml);完整 HTML 包含大量引号和换行是最容易暴露 JSON 编码差异的返回值。如果不解码导出文件的第一个字符可能是引号内部!doctype html前后带转义浏览器打开只显示一段 JSON 文本而不是网页。复用同一个解码函数避免会话捕获和导出各写一套字符串处理。边界函数虽小却属于跨运行时基础设施凡是runJavaScript期待字符串的调用都应经过它。导出前还检查空结果。解码成功不等于内容有效Web API 不存在、脚本返回 undefined 或页面版本不匹配时空值不能被写成“成功导出”的零字节文件。数字返回值不要走字符串解码搜索 API 返回匹配数量constresultawaitthis.editorController.runJavaScript((window.OhMarkdownEditor?.find(${JSON.stringify(this.searchQuery)},${JSON.stringify(this.searchCaseSensitive)},${JSON.stringify(this.searchWholeWord)},${JSON.stringify(this.searchRegularExpression)},${JSON.stringify(backwards)}) ?? 0));this.searchMatchCountNumber.parseInt(result);JavaScript 返回 number原生接口提供其文本表示例如2直接Number.parseInt。如果误用decodeJavaScriptString类型断言可能掩盖 number后续.length等调用会出错。数字解析还应检查Number.isNaN。当前 Web API与原生代码同版本返回契约稳定若未来 Web 资源可能独立更新应对非法结果回退为零并标记协议错误。parseInt(2.5)会得到 2也会宽松接受尾部字符匹配数契约更适合严格数字转换后检查整数。正则、整词和方向开关作为参数传入 Web返回值仍只有匹配数。不要为了“统一接口”把所有结果包成字符串因为丢失类型会把错误推迟到业务层。布尔返回值使用明确比较打印前Web API 返回预览是否准备完成constprintReadyawaitthis.editorController.runJavaScript(window.OhMarkdownEditor?.preparePrint() true);if(printReady!true){thrownewError(The editor preview is not ready for printing.);}脚本表达式本身强制得到 boolean原生侧比较文本true。没有写if (printReady)因为任何非空字符串在许多语言中都可能被视为真字符串false也不是 false。当预览不可用例如大文档模式API 返回 false原生层不会创建打印适配器。布尔协议保持最小不需要 JSON 字符串解码。更统一的 Bridge 层可以提供decodeBoolean和decodeNumber但只有调用点足够多、确实减少重复时才值得增加抽象。参数方向同样需要 JSON 编码返回值要解码传入 JavaScript 的字符串也要编码。OhMarkdown 不使用// 错误示例find(${this.searchQuery})查询包含单引号时脚本会语法错误包含反斜杠和换行时语义改变恶意文本甚至可能闭合字符串并执行额外表达式。正确方式是window.OhMarkdownEditor?.find(${JSON.stringify(this.searchQuery)},${JSON.stringify(this.searchCaseSensitive)})文档正文、sessionId、文件名、主题名和替换式都遵循同样规则。JSON.stringify生成合法 JavaScript 字面量避免手工转义遗漏。结构化编码并不意味着可以调用任意脚本。原生层只调用window.OhMarkdownEditor的白名单方法Web 页只向原生代理暴露onReady、onState、onChange、onSnapshot和onCommand。窄接口与正确编码共同降低边界风险。双向 Bridge 的职责不同OhMarkdown 有两条通信方向。Web 到原生使用javaScriptProxy.javaScriptProxy({object:this.editorBridge,name:ohMarkdownBridge,methodList:[onReady,onState,onChange,onSnapshot,onCommand],controller:this.editorController})这条方向适合事件通知和命令快照参数有明确类型。原生到 Web 使用runJavaScript调用受限 API适合设置文档、切换会话、查找、跳转和导出。如果所有通信都靠拼接脚本用户输入每次按键都跨边界性能和转义风险都会增大。如果所有通信都靠代理回调原生层又难以主动请求当前值。两种机制各自服务不同数据流关键是协议集中且类型清楚。Web TypeScript 声明列出完整接口OhMarkdownEditor?:{setSessionDocument(sessionId:string,content:string,recovered?:boolean):void;activateSession(sessionId:string,content:string,recovered?:boolean):void;getDocument():string;find(query:string,matchCase?:boolean,wholeWord?:boolean,regexp?:boolean,backwards?:boolean):number;exportHtml(title:string):string;}声明不能保证 ArkTS 运行时自动验证却能约束 Web 实现和 Playwright 测试。原生侧仍需为序列化结果做运行时处理。鸿蒙 PC 模拟器中的真实影响下图来自 MateBook Pro 2in1 模拟器。两个标签切换后分别保留正文说明活动文档从 ArkWeb 返回、解码并写入正确 session再恢复目标 EditorState 的链路已经工作。单凭截图不能验证引号和换行转义测试文档还需要包含多行中文、单双引号、反斜杠、制表符、CRLF、emoji、Markdown 代码围栏中的 JSON、末尾换行以及空字符串。切换标签后逐字节或逐字符串比较才能锁住解码行为。HTML 导出测试应断言文件以!doctype html开始而不是只检查文件存在。包含标题闭合片段、脚本标签和危险链接的文档可以同时覆盖字符串解码与安全净化。不要用 as string 代替验证ArkTS/TypeScript 的as string只影响编译器不改变运行时值。跨边界结果若实际是 number、null 或对象断言不会自动转换。边界代码应尽可能使用unknown思维先解析再检查类型再进入业务层。当前decodeJavaScriptString的回退兼容实际平台行为但还可以改进解析得到非字符串时抛出协议错误为正文设置最大长度导出 HTML检查 doctype记录协议版本在 Web ready 时交换能力列表。随着接口增长能力协商比依赖可选链返回空值更容易诊断版本不一致。另一个风险是大字符串复制。正文和完整 HTML 通过runJavaScript返回时会经历序列化与解码几兆文本可能产生多份内存副本。当前大文档模式禁用 HTML 导出和周期全文恢复降低压力多标签捕获仍需要关注大文档切换性能。未来可考虑增量 Bridge、共享文件写入或由 Web 只返回 revision再从原生已同步缓冲区取内容。测试应围绕协议而不是页面Web 单元路径可以验证getDocument()返回逻辑字符串却无法完全模拟 ArkWeb 原生接口如何包装结果。最终必须在鸿蒙模拟器调用真实runJavaScript记录经过类型检查后的结果特征同时避免打印用户正文。建议建立一个协议测试页面分别返回空字符串、普通字符串、多行字符串、引号、反斜杠、中文、emoji、数字零、负数、布尔值、null 和对象。ArkTS 侧对每项使用对应解码器断言。这样平台 SDK升级后可以快速发现返回格式变化而不必等到多标签内容损坏。协议测试还应覆盖 Web API 不存在、页面未 ready、调用中重载和超长返回。可恢复路径必须保留原生旧值不能用空结果覆盖。编辑器最重要的不是每次调用都成功而是通信失败时不丢缓冲区。结语ArkWebrunJavaScript的字符串返回值不是可以忽略的实现细节而是跨运行时协议的一部分。OhMarkdown 的修复使用标准 JSON 解析恢复正文和 HTML数字结果用数值解析布尔结果做明确文本比较反向参数统一用JSON.stringify所有调用限制在窄 API 中。这类缺陷通常躲过简单演示因为hello没有转义字符。只有把多行 Markdown、引号、反斜杠和导出 HTML 当成真实数据才能看到边界。鸿蒙 PC 编辑器要保证多标签和文件内容可靠必须把每一次跨 ArkUI 与 ArkWeb 的值传递当作正式协议而不是一次方便的脚本调用。

本月热点