当前位置: 首页 > news >正文

鸿蒙 PC Markdown 编辑器 ArkUI 界面分层:从超大工作台到可维护组件边界

鸿蒙 PC Markdown 编辑器 ArkUI 界面分层:从超大工作台到可维护组件边界

鸿蒙 PC 上的 Markdown 编辑器并不是把一个手机页面放大。桌面窗口里同时存在活动栏、文件树、全文搜索、大纲、版本历史、设置、十二文档标签、编辑器模式、状态栏、拖放、右键菜单、自由窗口和可调侧栏。如果所有 ArkUI 展示与系统编排都持续写进同一个组件,功能表面上仍能运行,但修改成本会快速上升:搜索面板的一处复选框布局可能需要穿过文件保存、恢复、分享和多窗口代码才能定位;标签右键菜单的局部状态也会与文档事实状态混在一起。

这篇文章完整说明一套适合鸿蒙 PC Markdown 编辑器的渐进式界面分层方案。它不引入新的状态管理框架,不把现有工程推倒重写,也不改变 Markdown 文档、文件服务和 ArkWeb Bridge 的行为。核心目标是让代码所有权与用户看见的桌面区域一致,并用编译、结构门禁、自动化和模拟器交互证明拆分没有把“能运行”换成“看起来更整齐”。

项目仓库地址:https://gitcode.com/VON-/codex_md_oh

本次实现提交:9f3eab0

问题不只是文件行数

重构前,工作台入口WorkspaceShell.ets达到 5011 行。单看行数并不能证明设计错误,有些复杂状态机确实需要较长代码;真正的问题是同一文件包含了不同变化原因:

  • 窗口级文档会话、未保存状态和操作状态;
  • Core File Kit 打开、读取、保存与指纹检测;
  • 崩溃恢复、版本历史和窗口会话恢复;
  • ArkWeb JavaScriptProxy 白名单 Bridge;
  • 活动栏、标签栏、搜索、文件树、大纲、历史和设置展示;
  • 侧栏拖动悬停、标签右键弹层等短生命周期交互状态;
  • 导出 HTML、打印 PDF、PNG 与系统分享命令;
  • 多窗口、外部修改冲突和关闭确认编排。

这些代码的运行时关系并不相同。文档正文、活动标签和侧栏宽度是需要跨重绘保持、部分需要持久化的窗口事实;鼠标是否正悬停在分隔线上,只属于一个可视控件当前几百毫秒的交互。把两者都提升到同一个容器,看似统一,实际会模糊状态所有权。

所以本次目标不是机械地把 5011 行切成若干个 300 行文件,而是回答三个问题:谁持有事实状态,谁只负责展示,谁连接外部边界。

分层后的目录语义

最终界面目录形成五个清晰区域:

shared/ui/ ├── WorkspaceShell.ets ├── bridge/ │ └── EditorBridge.ets ├── model/ │ └── WorkspaceViewModels.ets ├── components/ │ ├── EditorSurface.ets │ ├── ExternalConflictBanner.ets │ ├── WorkspaceActivityRail.ets │ ├── WorkspaceControls.ets │ ├── WorkspaceDocumentBar.ets │ ├── WorkspaceSidebarResizeHandle.ets │ └── WorkspaceStatusBar.ets └── panels/ ├── WorkspaceFilesPanel.ets ├── WorkspaceHistoryPanel.ets ├── WorkspaceOutlinePanel.ets ├── WorkspaceSearchPanel.ets └── WorkspaceSettingsPanel.ets

WorkspaceShell仍然重要,但职责收缩为容器和应用协调器。它持有每个窗口的文档会话、当前面板、视图模式、操作状态和侧栏宽度,调用services/完成文件、搜索、版本历史与设置能力,再把可渲染数据和动作回调交给子组件。

panels对应桌面活动栏可以切换的完整区域。文件、搜索、大纲、历史、设置都具有独立标题、控件组合、列表和空状态,因此它们是稳定的业务面板边界。

components对应可以独立理解的工作台区域。文档标签加顶部工具栏是一个组件;活动栏、状态栏、冲突横幅、侧栏分隔线各是一个组件。按钮、分段按钮和搜索选项等重复视觉原语集中到WorkspaceControls.ets,但没有为每个按钮创建单独文件。

bridge明确表示跨 ArkUI 与 ArkWeb 的边界适配器。EditorBridge不是视图模型,它只把 JavaScriptProxy 白名单调用转发给容器,因此从原来的混合位置移入 Bridge 层。

model只保存 Shell 与面板共同使用的 UI 类型。例如搜索面板模式需要同时被父容器的命令逻辑和子面板的按钮选择使用:

exportenumSearchPanelMode{DOCUMENT='document',WORKSPACE='workspace',QUICK_OPEN='quick-open'}

它不包含文件 I/O,也不试图复制业务 service。这个约束很关键,否则“分层”很容易退化为把相同业务规则在 UI 目录再写一次。

容器持有事实,面板接收数据与意图

搜索面板是最能体现边界的一部分。它包含当前文档查找、工作区搜索、快速打开、大小写、整词、正则、替换和结果列表。直接让面板调用搜索服务会减少几行回调,但会带来两个问题:面板开始拥有任务取消与代际状态;多窗口下又要判断当前会话属于哪个 Shell。

因此面板只声明输入与动作:

@Componentexportstruct WorkspaceSearchPanel{@PropsidebarWidth:number;@Propexpanded:boolean;@Propmode:SearchPanelMode;@PropmatchCount:number;@PropactiveQuery:string;@PropreplaceQuery:string;@Propresults:Array<WorkspaceSearchResult>;onModeChange:(mode:SearchPanelMode)=>void=()=>{};onQueryChange:(value:string)=>void=()=>{};onSubmit:()=>void=()=>{};onCancel:()=>void=()=>{};onReplaceCurrent:()=>void=()=>{};onReplaceAll:()=>void=()=>{};onOpenResult:(result:WorkspaceSearchResult)=>void=()=>{};}

Shell 的装配代码则显式表达每个动作最终去哪:

WorkspaceSearchPanel({sidebarWidth:this.sidebarWidth,expanded:this.usesExpandedToolbar(),mode:this.searchPanelMode,matchCount:this.searchMatchCount,activeQuery:this.getActiveSearchQuery(),replaceQuery:this.replaceQuery,results:this.workspaceSearchResults,onModeChange:(mode:SearchPanelMode)=>this.setSearchPanelMode(mode),onQueryChange:(value:string)=>this.updateActiveSearchQuery(value),onSubmit:()=>this.submitSearchInput(),onCancel:()=>this.cancelWorkspaceSearch(),onReplaceCurrent:()=>this.replaceCurrentMatch(),onReplaceAll:()=>this.replaceAllMatches(),onOpenResult:(result:WorkspaceSearchResult)=>this.openWorkspaceSearchResult(result)})

这种写法的代价是装配参数较多,但它比隐式共享状态更适合 PC 编辑器。读代码时无需搜索事件总线,也不会出现另一个窗口的搜索结果误写当前窗口。类型变化会在 ArkTS 编译阶段直接暴露,而不是运行到某个冷门面板才发现。

局部交互状态应该留在组件内部

不是所有状态都必须由 Shell 管理。标签右键菜单的可见性、被点击标签索引和会话 ID,只服务于WorkspaceDocumentBar。侧栏拖动时的悬停、激活和起始宽度,只服务于WorkspaceSidebarResizeHandle。这些状态既不需要写入窗口会话,也不应该触发恢复记录。

侧栏组件把短生命周期状态留在内部,把最终宽度仍交给父容器:

@Componentexportstruct WorkspaceSidebarResizeHandle{@PropsidebarWidth:number;@ProphandleWidth:number;@Stateprivatehovered:boolean=false;@Stateprivateactive:boolean=false;privatestartWidth:number=0;onWidthChange:(width:number)=>void=()=>{};onCommit:()=>void=()=>{};onKeyAction:(event:KeyEvent)=>boolean=()=>false;}

拖动更新只上报候选宽度,Shell 继续执行 PC 窗口预算限制:

WorkspaceSidebarResizeHandle({sidebarWidth:this.sidebarWidth,handleWidth:SIDEBAR_RESIZE_HANDLE_WIDTH,onWidthChange:(width:number)=>{this.sidebarWidth=this.clampSidebarWidth(width);},onCommit:()=>this.persistSidebarWidth(),onKeyAction:(event:KeyEvent):boolean=>this.handleSidebarResizeKey(event)})

这样既避免了组件越权写 Preferences,也保留了拖动的连续反馈。鼠标离开或组件卸载时,组件自己恢复系统指针;方向键微调和持久化继续复用容器的既有规则。

文档标签栏为何是一个复合组件

标签栏不是一排普通文本。它需要显示活动状态、未保存标记、关闭按钮、右键菜单、最多十二标签的水平滚动;同一条顶部区域还承载打开、保存、源码、即时、分栏、预览、同步滚动、新窗口和侧栏命令,并接受原生文件拖放。

如果只把单个标签抽成组件,而工具栏仍留在 Shell,标签右键弹层、水平滚动和拖放边界仍然散落。最终选择WorkspaceDocumentBar作为复合区域,是因为这些控件共享同一条稳定桌面空间和相同的响应式策略。

模式按钮仍复用通用控件,并保留大文档保护:

WorkspaceModeButton({label:$r('app.string.preview_mode'),mode:'preview',selectedMode:this.selectedMode,expanded:this.expanded,isEnabled:!this.largeDocumentMode,action:this.onModeChange})

这里没有把“为什么大文档禁用预览”的规则复制到组件。组件只收到largeDocumentMode并据此控制按钮可用性,真正的大文档阈值和编辑器配置仍由既有性能服务与 Shell 负责。

Bridge 是边界适配器,不是第二份状态

ArkWeb 编辑器通过白名单对象向 ArkUI 上报 ready、字数、脏状态、快照、图片导入、图片读取和命令。拆分时最危险的做法,是让每个面板各自暴露一个 Bridge,或在 Bridge 中保存一份当前文档。

本次只移动EditorBridge的文件所有权,不改变协议:

exportclassEditorBridge{privatereadonlysnapshotHandler:(content:string,revision:number)=>void;privatereadonlycommandHandler:(command:string,content:string)=>void;onSnapshot(content:string,revision:number):void{this.snapshotHandler(content,revision);}onCommand(command:string,content:string):void{this.commandHandler(command,content);}}

它仍然只是函数转发器。CodeMirror 的 EditorState 是编辑期正文事实来源,ArkUI 的 DocumentSession 是窗口会话事实,Bridge 不缓存正文,也不提供任意方法调用。这使目录更清楚,但没有增加新的同步问题。

防止半年后又长回一个文件

一次重构并不能保证长期边界。新需求赶时间时,最容易发生的事情是“先把面板写回 Shell,后面再拆”。所以工程增加了结构门禁scripts/verify-ui-layering.sh

门禁做三类检查:

shell_lines="$(wc-l<"$SHELL_FILE"|tr-d' ')"if["$shell_lines"-gt4000];thenecho"UI 分层检查失败:WorkspaceShell.ets 超过 4000 行上限。">&2exit1ficomponent_count="$(awk'/^@Component$/ { count += 1 } END { print count + 0 }'"$SHELL_FILE")"if["$component_count"-ne1];thenecho"WorkspaceShell.ets 只应声明一个容器组件。">&2exit1fi

它还检查 bridge、model、components 和 panels 的关键文件是否存在,并接入统一的verify-local.sh。4000 行不是代码质量分数,而是一个回退报警器:当前 Shell 为 3551 行,预留了合理装配空间,但不允许整块展示 UI 无声回流。如果未来新增的确是复杂应用协调逻辑,应该先复核 ADR,而不是为了绿灯随意调高数字。

真实鸿蒙 PC 模拟器验证

下面的图片来自 HarmonyOS 6.1.1 API 24 MateBook Pro 2in1 模拟器,使用最终 Debug HAP 运行后由snapshot_display直接采集。截图中搜索面板已经作为独立WorkspaceSearchPanel渲染;侧栏由拖动条从较窄宽度扩大后,三个搜索选项恢复为单行,标签、工具栏、预览和状态栏同时保持完整。

设备验证不是只看截图。实际执行路径包括:

  1. 启动最终 Debug HAP,确认 ArkWeb Bridge 完成加载并显示已有 Markdown 预览。
  2. 从文件面板切换到大纲面板,确认标题数量与跳转行正常显示。
  3. 切换到搜索面板,确认当前文档、工作区、快速打开三个模式和查找替换控件完整。
  4. 拖动侧栏分隔线约 176 像素,确认父容器收到宽度变化、编辑区同步缩放、搜索选项响应式切换。
  5. 运行模拟器 ohosTest,确认文件、恢复、窗口会话、版本历史、搜索、链接和设置服务没有因 UI 移动回退。

测试结果与第一次失败的意义

最终验证结果如下:

  • UI 结构门禁通过:WorkspaceShell.ets3551 行,只包含一个@Component容器声明;
  • CommonMark 0.31.2 conforming 配置652/652
  • GFM 冻结扩展语料 30 例通过;
  • Web Playwright66/66
  • Debug HAP 构建通过;
  • ArkTSUnitTestBuild通过;
  • ohosTest HAP 构建通过;
  • MateBook Pro 2in1 模拟器 ohosTest16/16,Failure 0、Error 0,总耗时 2267 ms;
  • 模拟器搜索面板切换和侧栏拖动通过。

验证过程中出现过一次真实失败:EditorBridgemodel移到bridge后,Shell 的导入已经修改,但EditorSurface.ets仍指向旧路径。Web 的 66 项测试全部通过,随后 ArkTS 编译报出无法解析../model/EditorBridge。这正好说明混合工程不能只跑 Web 测试。修正组件导入后重新执行完整统一验证,Web、Debug HAP 和 UnitTestBuild 才全部通过。

这个过程没有被从报告中删除,因为工程证据的价值不只是最后一行绿色输出。它证明结构调整需要覆盖依赖图的不同编译单元,也证明统一脚本应该把 Web、结构检查和 ArkTS 构建串在一起。

为什么没有继续拆成更多 Controller

分层后 Shell 仍有 3551 行,主要是保存、恢复、外部冲突、搜索任务、导出、分享、多窗口和窗口关闭编排。继续拆当然可能让数字更小,但这会跨越多个已经稳定的系统能力链路。

本次采用 Level 2、D2:只拆当前用户明确指出的 UI 所有权问题,保持 service 契约和窗口状态模型不变。没有引入全局 store、事件总线、依赖注入或新的 Controller 框架。后续只有在某条业务编排出现第二个真实调用方、难以单元测试或再次逼近结构门禁时,才按纵切提取独立控制器。

这种克制对鸿蒙 PC 文档软件尤其重要。文件写入、崩溃恢复和多窗口冲突不是适合为了目录美观而大范围搬动的代码。展示层先变清楚,系统能力继续稳定,是更可验证的顺序。

可以复用的判断方法

为其他 ArkUI 桌面应用拆分超大页面时,可以用四个问题判断代码应该去哪:

  • 这个状态是否需要跨重绘、重启或窗口恢复?需要时由容器或 service 持有;只描述悬停和弹层时由组件持有。
  • 这个区域是否对应用户能够命名的完整桌面区域?如果是,它适合作为 panel 或 composite component。
  • 这个代码是否接触文件、网络、系统窗口或持久化?如果是,它不应藏在纯展示面板内部。
  • 这个抽象是否减少当前真实的心智负担?如果只是把十行代码搬到新文件且没有稳定边界,就不值得创建。

最终可维护性并不来自文件数量,而来自稳定的依赖方向:服务提供能力,容器组织状态与命令,面板和组件展示数据并上报意图,Bridge 只连接受限边界。对于优先适配鸿蒙 PC 的 Markdown 编辑器,这种层次能够让后续安全、隐私、发布和更多桌面功能继续推进,同时避免工作台再次退化成一个任何修改都牵动全局的超大文件。

http://www.jsqmd.com/news/1265120/

相关文章:

  • Windows虚拟门禁系统部署全攻略
  • 3步搞定百度网盘限速:开源解析工具实战指南
  • 泰州出发西藏跟团游怎么选?这份本地地接社的纯玩攻略请收好| 附:旅行社电话 - 西藏康泰旅行社
  • Ubuntu 22.04源码编译安装ROOT v6.32.00指南
  • 基于YOLOv5的道路坑洼检测技术实践
  • C/C++指针深度解析:从内存模型到智能指针实战
  • 智能薪酬计算系统:AI与微服务在财务数字化转型中的应用
  • 开源AI解决方案:IOC架构与图像搜索实践
  • Windows C++开发环境配置指南:Visual Studio与VSCode+MinGW双路径详解
  • AI图像生成技术常见问题与解决方案
  • 企业级办公AI Agent系统开发实战与架构解析
  • GPT-5.4 生成的单元测试你敢直接 commit?覆盖率85%背后的四重陷阱与Mock实战
  • 深度学习注意力机制原理与工程实践详解
  • 大模型如何从博学到善言:三步提升对话效果
  • (2026最新)新余漏水检测维修一站式上门服务-本地专业防水补漏公司TOP5推荐:暗管漏水检测精准定位 - 安佳防水
  • 【本地大模型搭建终极指南】:20年AI架构师亲授7步零基础部署私有LLM,错过再等一年!
  • 吴恩达Agentic AI实战:智能体开发核心技术解析
  • 深入解析TI CC13xx/CC26xx AUX传感器控制器GPIO与事件寄存器配置
  • 技术人员税务规划指南:从马斯克案例到实战策略
  • Windows终端美化:WSL+Zsh打造macOS级体验
  • 深度学习注意力机制:原理、实现与优化技巧
  • 第二十三章 WSaiOS 感知学习与自适应进化机制实现
  • Oracle数据库ORA-01017错误全面解析与解决方案
  • 基于深度学习的水果成熟度检测系统设计与实现
  • AI如何重构创意工作流:从工具应用到思维升级
  • HINDSIGHT记忆架构:AI长期记忆管理的突破性解决方案
  • Windows平台宽字符与UTF-8编码转换技术详解
  • 智能体AI核心技术解析与2026年应用预测
  • 通义千问音视频智能处理全链路解析(工业级部署避坑手册)
  • 基于PySpark和LSTM的美食推荐系统设计与实现