
鸿蒙 PC Markdown 编辑器内核在 ArkWeb 中离线运行 CodeMirror 6本文讨论 CodeMirror 6 在 ArkWeb 中的状态模型、离线打包、文档生命周期和大文件降级不把 Web 页面当作简单的 UI 容器。完整示例代码https://gitcode.com/VON-/codex_md_oh。为什么是 ArkWeb 与 CodeMirror鸿蒙 PC 编辑器的核心难点不是显示一个多行文本框而是在中文 IME、长文本、撤销重做、选区和语法高亮之间保持正确性。CodeMirror 6 已经解决了大量文本编辑基础问题ArkUI 则保留鸿蒙 PC 的原生窗口、文件服务和系统交互。这种混合架构的一个前提是编辑内核必须可以完全离线加载不能在启动时访问 CDN也不能把用户的 Markdown 上传到服务器。只保留一个可写文本事实源编辑器的真实初始化代码位于web-editor/src/main.tsfunctioncreateEditorState(content:string):EditorState{constuseLargeDocumentSetupcontent.lengthLARGE_DOCUMENT_CHARACTER_THRESHOLD;constlineSeparatorcontent.includes(\r\n)?\r\n:\n;returnEditorState.create({doc:content,extensions:[useLargeDocumentSetup?minimalSetup:basicSetup,useLargeDocumentSetup?[]:markdown({base:markdownLanguage}),EditorState.lineSeparator.of(lineSeparator),placeholder(Start writing Markdown...),useLargeDocumentSetup?[]:EditorView.lineWrapping,EditorView.updateListener.of((update){if(!update.docChanged){return;}documentRevision1;pendingDirtyforcedDirty||!update.state.doc.eq(baselineDocument);updateLargeDocumentMode(update.state.doc.length);if(currentModesource){previewDirtytrue;}else{renderPreview(update.state.sliceDoc());}notifyNative(update.state.doc.length);scheduleRecoverySnapshot();}),EditorView.theme({:{height:100%},.cm-scroller:{overflow:auto},.cm-content:{minHeight:100%}})]});}代码来源web-editor/src/main.tsCodeMirror 的EditorState.doc是编辑期唯一可写文本。Source、Split 和 Preview 只是视图模式预览由当前文本派生不反向改写 Markdown。这个约束能避免切换模式时的源码格式化、空行丢失或不可逆转换。将依赖内联进 HAPVite 的输出目录直接指向 HarmonyOS 的rawfile资源并使用vite-plugin-singlefile将 JavaScript 和 CSS 内联import{defineConfig}fromvite;import{viteSingleFile}fromvite-plugin-singlefile;exportdefaultdefineConfig({base:./,plugins:[viteSingleFile()],build:{outDir:../entry/src/main/resources/rawfile/editor,emptyOutDir:true,sourcemap:false,target:es2020,chunkSizeWarningLimit:800}});代码来源web-editor/vite.config.ts生产 HTML 不带外部script src或样式链接ArkWeb 只需加载$rawfile(editor/index.html)。这同时简化了资源路径、跨源策略和离线验证。鸿蒙 PC 模拟器输入截图下图为内联 CodeMirror 编辑器在鸿蒙 PC / 2in1 模拟器 ArkWeb 中输入 Markdown 的实际画面。验证结论当前纵切已经证明CodeMirror、Markdown 语法扩展、markdown-it 和 DOMPurify 可以在不访问外网的前提下随 HAP 加载文档可输入模式可切换撤销栈属于当前文档。真实鸿蒙 PC 上的物理输入法和长时间稳定性仍应作为发布前必测项。选择编辑内核时真正要比较什么桌面 Markdown 编辑器并不是给文本加一个光标那么简单。编辑内核需要同时处理多行选区、双向文本、组合输入、撤销分组、滚动虚拟化、语法树更新、快捷键和无障碍。纯 ArkUI 文本控件与 ArkWeb 编辑器的比较不能只看首屏大小还要看这些能力需要自行实现多少、后续能否持续维护。OhMarkdown 选择 CodeMirror 6主要基于四点状态和视图分离文档变化通过事务表达便于管理撤销、选区和扩展。扩展可以按场景组合大文档时能够关闭 Markdown 解析与换行而不用替换整套编辑器。浏览器对中文 IME、键盘事件和文本选择已经有成熟基础适合先验证鸿蒙 ArkWeb 兼容性。依赖可以打入 HAP运行时不需要访问在线编辑服务。代价也很明确ArkWeb 会增加进程和内存开销原生与 JavaScript 之间需要设计 Bridge焦点和系统能力跨越两个运行环境。初始技术验证不是证明这个方案没有代价而是证明这些代价可测、可控并且没有出现必须更换内核的阻断问题。EditorState 是编辑期唯一事实源CodeMirror 6 的重要特性是不可变EditorState。一次输入不会原地修改字符串而是创建描述变化的事务EditorView应用事务后得到新状态。撤销栈、选区和插件状态都跟随这条事务链。项目把EditorState.doc设为唯一可写文本来源解决了混合编辑器常见的“双模型漂移”ArkTS 不在每次输入后保存一份同步全文只接收轻量状态和必要快照。预览从sliceDoc()得到当前 Markdown再渲染和净化。保存由用户显式命令取得当下完整文本。切换 Source、Split、Preview 不把 HTML 转回 Markdown。如果同时让 ArkTS 字符串和 CodeMirror 文档接受编辑Bridge 延迟就会造成覆盖原生侧可能用旧全文刷新 WebCodeMirror 的新输入因此丢失。单一事实源把普通编辑留在一个运行时只有明确的生命周期事件才跨边界。初始化顺序决定首屏是否可靠ArkWeb 页面加载、JavaScript 初始化、Bridge 注入和 ArkTS 文档读取不是同时完成的。编辑器必须处理两种顺序Web 先就绪但文档尚未读取或者文档已经准备好但 Web 还不能接收命令。实际实现通过onReady通知原生侧ArkTS 在确认控制器可执行脚本后再设置文档。新文档加载不是往 DOM 中写文本而是创建新的 EditorState 并替换当前状态。这样可以清理上一份文档的撤销历史避免用户打开 B 文件后按撤销却看到 A 文件内容。页面启动还应有明确失败状态。单 HTML 找不到、脚本初始化异常、Bridge 未注入或编辑器超时都不能只留下空白区域。桌面工具后续需要把这些失败转换为可见错误和诊断信息同时保留新建、重新加载或关闭文档的操作入口。为什么预览不能参与编辑所见即所得编辑器通常把富文本结构当作主模型Markdown 与 DOM 之间需要双向转换。OhMarkdown 的产品优先级是源码无损因此预览只读。用户写下的空行、转义、列表缩进、引用风格和链接定义不应因为看了一次预览就被重新排版。只读预览还有安全收益净化后的 DOM 不需要把点击、输入等变化同步回源码Markdown 中的原始 HTML被禁用危险节点经过 DOMPurify预览链接在当前阶段也不会直接发起导航。编辑面和渲染面职责分离后保存永远读取 Markdown 文本而不是读取可能被浏览器规范化过的innerHTML。大文档模式依赖扩展可组合性超过 5MiB 字符阈值后项目使用minimalSetup关闭 Markdown 语言扩展、行换行和实时预览。这样做不是因为 CodeMirror 无法保存更大的字符串而是因为多个看似独立的能力会共同放大成本语法树需要维护节点软换行增加布局计算预览会生成第二份 DOM字数统计需要扫描全文频繁 Bridge 快照还会复制字符串。CodeMirror 的扩展组合让降级可以在创建文档状态时决定而不是在各处增加零散的if。大文档仍然保留基本编辑、选区、撤销、滚动和保存工作台则禁用 Split/Preview 并显示状态。产品层应把这称为“保护模式”而不是让功能悄悄失效。阈值本身不是永恒常量。后续可以根据鸿蒙 PC 真机 P95、内存和文档结构调整也可以拆成多个级别先关闭实时预览再关闭语法高亮最后限制恢复快照。但任何调整都要有固定语料和 Release 数据不能只凭主观流畅度。离线包仍然需要供应链管理把依赖内联进 HAP 解决的是运行时网络依赖不等于依赖来源天然安全。CodeMirror、markdown-it、DOMPurify 和构建工具仍来自 npm需要锁文件、升级审查和自动化回归。尤其 DOMPurify 属于安全边界升级时应单独验证危险标签、属性、URL 和导出结果。单文件构建还会把源码压缩成较大的 HTML。项目应保留可审查的 TypeScript 源码和依赖清单不能只把生成的index.html当作源代码。生产产物关闭 sourcemap 可以减少发布信息但调试阶段仍需要通过浏览器测试和日志定位源模块。离线验证至少包含断开网络后启动应用、打开已有文档、输入、切换模式、预览、保存和再次启动。只检查首屏出现并不足以证明所有依赖都已内联因为某些语法主题、图片或导出路径可能在使用时才尝试加载外部资源。生命周期与内存回收ArkWeb 不是一个永久存在且永不重载的黑盒。窗口关闭、系统回收、页面异常和未来多标签都会触发生命周期问题。编辑器需要区分三种状态磁盘已保存基线、当前 CodeMirror 文档、用于异常恢复的最近快照。普通重绘不应销毁 EditorView。真正替换文档时要创建新状态并重置文档相关基线关闭页面时要释放监听器和计时器应用进入后台前则应尽量刷新恢复快照。多标签方案不能简单创建无限多个 ArkWeb每个标签都保持完整语法树和预览 DOM 会快速放大内存需要基于真机数据选择活动视图复用或会话冻结策略。如何验证内核正确而不只验证“能输入”内核回归需要覆盖状态语义输入中文后组合文本只提交一次字数和脏状态正确。连续输入可撤销重做恢复相同内容。保存后继续编辑再撤销回保存基线时修改标记清除。打开另一份文档后撤销不会返回前一份文档。CRLF 文档经过编辑后sliceDoc()仍遵循配置的行分隔符。Source、Split、Preview 展示同一份内容切换模式不改变源码。恶意 Markdown 不在预览 DOM 中生成可执行脚本。5MiB 以上文档进入降级模式基本输入和保存仍可用。生产 HTML 不引用外部脚本和样式资源。浏览器自动化适合验证 CodeMirror 与渲染逻辑鸿蒙模拟器负责 ArkWeb 集成真机负责中文输入法、物理键盘、触控板和完整进程组内存。三个层级互补不能用 Chromium 页面通过来推断所有 ArkWeb 行为。混合内核的边界清单ArkUI 管系统窗口、用户授权 URI、文件读写、打印和原生状态。CodeMirror 管当前文本、光标、选区、撤销栈和编辑扩展。Markdown 预览只从源码派生任何时候都不反向覆盖源码。Bridge 使用白名单方法普通按键不传输完整文档。Web 资源随 HAP 离线分发ArkWeb 不获得任意文件或网络能力。大文档通过关闭昂贵扩展降级不更换数据模型。文档替换显式清理撤销历史布局变化不重建编辑器。性能结论基于 Release HAP 的完整进程组而不是只看 Web 页面帧率。CodeMirror 在这里不是一块嵌入式网页装饰而是受原生外壳约束的专业文本内核。把事实源、权限、生命周期和性能边界写清后ArkUI 与 ArkWeb 的组合才具备长期扩展为鸿蒙 PC 桌面编辑器的条件。事务模型如何承载编辑语义CodeMirror 的修改通过 transaction 表达事务中包含文本 changes、选区、效果和注解。这个模型允许多个变化作为一次用户操作提交例如自动补全同时插入文本并移动光标。撤销管理依据事务边界分组而不是盲目记录每一个 DOM 输入事件。编辑器扩展功能时应尽量通过事务与 StateEffect 表达不直接修改内部 DOM。直接操作.cm-content可能短暂改变显示却不会更新 EditorState保存、撤销和预览仍看到旧内容。任何程序化替换文档、格式化或批量编辑都要走dispatch并明确是否进入撤销历史。保存基线也可以用状态比较而不是维护第二份可写字符串。当前文档与基线相等时 dirty 为 false即使用户经历多次输入和撤销。未来大型文档若完整比较成本过高可以在事务层维护 revision 与已保存 revision但必须处理撤销回基线的语义不能仅用“发生过变化”永久标脏。扩展配置应按成本和生命周期分组CodeMirror 6 的扩展很多全部塞进一个数组虽然能运行却难以在主题、语言和大文档模式之间动态调整。可以按职责分组基础编辑与键位、Markdown 语言、视觉主题、变更监听、恢复与 Bridge、辅助功能。每组都有明确成本和关闭条件。需要动态切换的设置适合使用 Compartment例如主题、只读状态或行换行替换 compartment 不必重建文档和撤销历史。文档行分隔符与初始语言结构更适合在创建状态时确定。大文件从完整配置切到最小配置时应确认哪些扩展可重配、哪些需要创建新状态并保留文本与选区。扩展顺序也会影响快捷键优先级和事件处理。新增插件后必须回归中文组合输入、系统快捷键和保存命令避免某个 keymap 在 composition 中提前消费按键。预览调度应与可见性一致实时预览的直接实现是在每次docChanged后同步运行 markdown-it、DOMPurify 并替换innerHTML。文档变大或用户连续输入时这会在关键输入路径创建大量 HTML 和 DOM。当前 Source 模式只把previewDirty设为 true进入 Split/Preview 时再更新可见预览才需要实时刷新。进一步优化可以使用短延迟合并多次事务、在浏览器空闲阶段渲染并用 revision 丢弃过期结果。无论异步到什么程度预览必须清楚对应哪份正文旧渲染不能晚到后覆盖新内容。超大文档直接禁用预览比渲染截断版本更安全。截断预览容易让用户误以为文档只有显示部分导出时又可能处理完整内容造成峰值。若未来提供部分预览必须显式标注范围并保证它从不参与保存。搜索、替换和大纲如何保持单一事实源搜索与替换应直接使用 CodeMirror 文档和事务。搜索结果保存位置时要考虑前方文本修改导致偏移最好使用状态字段或映射变化而不是长期缓存绝对字符索引。替换全部是一次还是多次撤销需要明确用户语义。大纲从 Markdown 语法树或受控解析结果派生只保存标题文本、层级和位置。点击大纲通过 EditorView 定位选区并滚动不重新生成正文。大文档模式关闭完整语法高亮时大纲可以延迟、降级为后台扫描或暂时不可用不能为了侧栏立即可见而在每次输入重新解析 10MiB 文档。这些功能都遵循同一原则读取 EditorState输出视图或事务不创建另一份可以独立编辑的内容模型。文档切换必须清理哪些状态替换文档不仅是设置新字符串。上一文档的撤销历史、搜索选择、语法树、预览 DOM、保存基线、恢复计时器和 Bridge revision 都可能残留。新状态创建后应重新计算格式、字数与大文件模式预览清空或按新文档渲染。异步任务需要会话标识。假如 A 文档预览正在渲染用户已打开 BA 的结果完成后不能写入 B 的预览A 的恢复快照也不能覆盖 B 的记录。会话 ID 与 revision 组合可以拒绝迟到结果。多标签会放大这个问题。冻结非活动标签时要保存文本、选区、滚动、撤销历史或可接受的压缩状态销毁 EditorView 前清理 DOM 监听和定时器。简单把页面节点隐藏并无限保留会话内存会随标签数线性增长。ArkWeb 页面异常后的重建策略ArkWeb 可能因为进程回收或页面错误重新加载。原生侧不能假定 Web 内存永远存在应通过就绪握手识别新实例。重建时优先选择当前内存/恢复快照还是磁盘基线需要结合是否 dirty 和最近 revision。如果 ArkTS 没有最新全文只保存了节流快照重载窗口内的少量输入可能丢失。降低窗口可以主动在页面隐藏或系统生命周期事件前刷新彻底解决大文档问题则需要增量会话日志。任何情况下不要在新页面就绪后无条件加载磁盘版本覆盖已存在恢复草稿。页面异常次数也应有限制。连续初始化失败时显示可诊断的错误界面并停止自动重载避免循环消耗资源。错误记录包含页面版本和阶段不包含 Markdown 正文。编辑器主题与内容样式要隔离CodeMirror 主题控制光标、选区、行号和编辑表面Markdown 预览样式控制标题、列表、代码块和表格。两者不应使用宽泛选择器互相污染。应用外壳的 ArkUI 主题又是第三层需要通过明确颜色与状态同步而不是让 Web 读取系统任意设置。深色模式切换适合通过受控命令和 CodeMirror compartment 更新不重建文档。主题必须检查选区对比度、IME 组合下划线、搜索高亮和只读状态。字体变化会影响行高、滚动和大文档布局不能只做静态颜色替换。用户自定义 CSS 若未来支持应限制在预览沙箱或经过解析不能让任意样式覆盖编辑器与原生 Bridge 相关页面结构。可访问性与浏览器能力边界CodeMirror 对屏幕阅读器和键盘提供基础支持但嵌入 ArkWeb 后还要验证原生焦点能进入、离开和返回编辑器。页面不能为了拦截快捷键阻止所有默认事件Tab、方向键和组合输入要遵循编辑语义。编辑器的可访问标签、当前行信息和选择状态需要在鸿蒙辅助功能中实测。大量文本下屏幕阅读器可能要求不同的虚拟化策略这类平台差异不能只根据桌面 Chrome 结论推断。剪贴板、拖放、拼写检查和上下文菜单也属于浏览器能力。开放前逐项确定数据边界和用户价值不因为 ArkWeb 默认提供就全部启用。尤其粘贴 HTML 应转换为纯文本或明确 Markdown 规则不能把活动 HTML 直接进入预览。CodeMirror 升级的回归重点编辑内核升级通常包含解析、视图、输入和状态包的联动版本。应保持官方兼容组合避免多个codemirror/state实例导致类型相同但运行时身份不同。锁文件中出现重复核心包时要调查。升级回归至少包含中文 IME、emoji、撤销分组、多选区、超长行、CRLF、文档切换、大文件降级、ArkWeb 重载和内存。若升级改善某项性能也要确认没有改变 Markdown 源码、键位或保存快照。依赖发布说明提供方向最终结论必须来自目标 ArkWeb。浏览器 API 支持、selection 行为和 composition 事件在不同内核版本可能有差异Release HAP 真机验证不可省略。内核故障时必须优先保住源码语法解析、主题或预览扩展异常时理想降级是回到最小纯文本编辑而不是让整个页面空白。扩展初始化可以按核心与可选能力分层基础 EditorState、输入和保存通道属于核心Markdown 高亮、预览和统计属于可降级层。捕获异常后记录扩展类别保留当前正文与恢复快照并允许用户另存。页面若无法继续编辑原生侧至少应检测超时或重载读取最近有效恢复记录。不要用自动刷新无限循环覆盖仍在内存中的新输入。故障设计与正常功能同样属于编辑内核边界因为用户最需要保护数据的时刻往往正是扩展失效时。评估替换内核的客观条件架构决定不是永久承诺。若目标鸿蒙 PC 上中文输入长期无法正确、进程组内存远超目标且无可行优化、ArkWeb 生命周期导致不可接受的数据丢失或关键无障碍能力无法满足就应重新评估纯原生或其他内核。评估必须使用相同语料、功能和设备比较总成本不能只比较空页面内存。只要现有方案仍能通过明确门槛继续优化比频繁重写更有工程价值。