鸿蒙 PC Markdown 编辑器文件拖放:文档会话与图片资源分流

桌面用户把文件拖进编辑器时,动作看起来完全相同,语义却可能相反。拖入 Markdown 文档通常表示“打开它”;拖入图片通常表示“把资源放到当前文档并插入链接”;拖入不支持文件应该明确拒绝;同时拖入文档和图片则不能悄悄打开一部分、复制另一部分。若所有文件都送进同一个“导入附件”函数,最常见的结果是把 .md 当图片读取失败,或在失败前已经修改了一半状态。

本文讨论 OhMarkdown 在 HarmonyOS PC / 2in1 上完成的原生 UDMF 拖放分流。代码来自 https://gitcode.com/VON-/codex_md_oh,对应提交 b11519c。图片 Copy、Move、Reference 已在此前模拟器真实文件拖放中通过;本轮新增 Markdown/TXT 多文档打开、混合批次整体拒绝和自由窗口入口,已经通过 ArkTS 编译与纯函数测试,最新系统文件管理器拖入仍等待设备恢复后验收。

拖放不是一种文件上传

Web 应用经常把 dataTransfer.files 统一上传服务器,但本地优先编辑器没有“上传”这个默认语义。用户文件继续留在原位置,Markdown 是文档事实来源;图片是否复制或移动由明确设置决定。HarmonyOS 的 UDMF 记录也可能提供 ImageFilegeneral.file-uri 或具体图片类型,不能只检查一个 MIME。

因此拖放处理分成四层:从 DragEvent 安全提取 URI;对 URI 做文档、图片或不支持分类;在整批层面决定接受或拒绝;最后分别进入文档会话服务或图片资源服务。分类层不读取内容,事务层才执行可能失败的 I/O。

这个顺序让失败容易解释。系统没有提供 URI时是“无法读取文件 URI”;扩展名不支持时是“只接受 Markdown 或受支持图片”;文档与图片混合时是“同一批次只能选择一种类型”;文档 UTF-8 解码失败则由 DocumentService 给出具体错误。用户不必从“图片导入失败”猜测为什么拖入 README 没有打开。

从 UDMF 提取 URI 要兼容多种记录

HarmonyOS 系统文件管理器可能把图片表示为 unifiedDataChannel.Image,普通文件表示为 File,也可能通过通用记录和值结构给出 URI。实现遍历记录,按具体类优先提取,再检查受限类型集合;最多保留十六个去重 URI,防止异常拖放一次制造无界任务。

private extractNativeDroppedFileUris(event: DragEvent): Array<string> {
  const sourceUris: Array<string> = [];
  const records = event.getData().getRecords();
  records.forEach((record: unifiedDataChannel.UnifiedRecord) => {
    let sourceUri: string = '';
    if (record instanceof unifiedDataChannel.Image) {
      sourceUri = record.imageUri;
    } else if (record instanceof unifiedDataChannel.File) {
      sourceUri = record.uri;
    } else {
      const recordType = record.getType();
      const value = record.getValue();
      if (typeof value === 'string' &&
        (recordType === 'general.file-uri' || recordType === 'general.file' ||
          recordType.startsWith('general.image'))) {
        sourceUri = value;
      }
    }
    if (sourceUri.length > 0 && !sourceUris.includes(sourceUri) && sourceUris.length < 16) {
      sourceUris.push(sourceUri);
    }
  });
  return sourceUris;
}

日志只记录类型集合,不记录文件正文或可恢复的敏感信息。真实 URI可能包含用户目录结构,正式报告只保存脱敏路径、文件名、大小和结果。提取异常被捕获后返回空集合,外层按可见失败处理,不让 DragEvent 异常终止整个工作台。

扩展名分类只是第一道门

Markdown 文档支持 .md.markdown.mdown.mkd.txt,与系统打开选择器一致。URI可能是本地路径,也可能是 file URI;文件名包含百分号编码和中文。DocumentService 复用已有 URI 解析取得末段,解码后再做大小写不敏感扩展判断:

export function isSupportedMarkdownDocumentUri(documentUri: string): boolean {
  return /\.(?:md|markdown|mdown|mkd|txt)$/i.test(getDocumentName(documentUri));
}

扩展名通过不代表内容可信。真正打开仍进入 readUtf8Document,执行 20 MiB 上限、UTF-8 fatal 解码、BOM 与换行检测、完整读取和文件指纹建立。分类只是避免错误路由,不能替代内容验证。

图片分类限制为 PNG、JPEG、GIF 和 WebP,与 AssetService 的 MIME 白名单一致。URI 查询和 fragment 不参与扩展判断。最终 AssetService 还会根据实际文件名推导 MIME、检查 10 MiB 大小、处理目标目录和命名冲突,所以 WorkspaceShell 的分类同样不是安全终点。

为什么混合批次要整体拒绝

假设用户一次拖入两个 Markdown 和一张图片。若先打开两个文档,再把图片插入“当前文档”,图片究竟属于拖放前的文档、最后打开的文档,还是鼠标落点对应的标签?任何默认答案都可能违背预期。若图片复制成功而第二个文档打开失败,用户还要判断哪些动作已经发生。

本轮采用确定规则:同一批次只能全是受支持文档,或全是受支持图片;任何不支持项或混合类型使整批在 I/O 前拒绝。DragResult 设置为失败,状态栏解释原因,文档会话和磁盘都不变化。

这不是永久限制,而是 Beta 阶段的安全语义。未来若要支持混合批次,需要先设计目标文档、执行顺序、部分失败回滚和结果摘要,不能在循环中顺手实现。对本地编辑器来说,少做一次模糊自动化通常比制造难以察觉的半成功更专业。

Markdown 拖放进入多文档会话

纯文档批次在操作锁内逐个调用 readUtf8Document,然后进入已有 applyOpenedDocument。这个函数会检查相同 URI 是否已打开:已打开时激活原标签,不创建重复会话;新文档在十二标签上限内增加会话;空白且未修改的 Untitled 标签可以被首个文档复用。

private async openNativeDroppedDocuments(sourceUris: Array<string>): Promise<boolean> {
  this.operationInProgress = true;
  try {
    for (const sourceUri of sourceUris) {
      const openedDocument = await readUtf8Document(sourceUri);
      await this.applyOpenedDocument(openedDocument);
    }
    this.operationStatus = sourceUris.length === 1 ?
      'Dropped document opened' : 'Dropped documents opened';
    return true;
  } catch (error) {
    this.operationStatus = `Document drop failed: ${error instanceof Error ? error.message : String(error)}`;
    return false;
  } finally {
    this.operationInProgress = false;
  }
}

每次切换前会捕获原活动标签的最新 CodeMirror 缓冲区,防止用户刚输入但 Bridge 状态尚未刷新时丢失。新文档保存基线、编码、换行、指纹和大文档模式都由标准打开链路建立。拖放只是绕过选择器的入口,不创造另一种“临时文档”。

当前多文档打开是顺序提交,不承诺整批回滚。如果第一份有效、第二份读取失败,第一份会保持打开,状态会指出失败。这与系统多选打开的常见语义一致,但设备报告必须记录具体成功数。混合类型在读取前整体拒绝,文件内容错误则属于执行期失败,两者边界不同。

图片拖放继续遵循资源事务

纯图片批次复用 G3-04 已验证的三模式:Copy 把源复制到文档资源目录;Move 在完整写入、fsync 和链接插入之后删除源;Reference 只允许受管理的 文档.assets 或共享 assets 内现有资源,不把任意外部绝对路径写入 Markdown。

工作台在导入前检查当前文档是否有可写父目录、是否存在外部冲突、是否已有其他文件操作。每张图片落盘成功后才调用 Web 的 insertNativeDroppedAsset 插入标准相对链接。会话 ID在整个导入期间保持一致,用户不能在中途切到另一标签让链接进入错误文档。

拖入 Markdown 不需要当前文档父目录,因为它只是打开文件;拖入图片需要资源目标。把这两个前置条件放在分流后非常重要。旧实现先检查父目录,会导致用户从文件管理器拖入 README 时收到“请先打开文档文件夹”,与实际任务无关。

DragResult 与后台异步工作的关系

系统拖放回调需要尽快设置 DragResult,真正读取文件则是异步工作。提取与分类同步完成后,应用即可判断是否接受。合法纯文档或纯图片设置 DRAG_SUCCESSFUL,再启动对应异步函数;非法和冲突状态设置 DRAG_FAILED

成功 DragResult 表示应用接受了任务,不表示磁盘操作最终成功。后续 UTF-8、权限、大小或写入错误会进入状态栏。设备测试要同时观察系统拖放反馈和最终应用状态,不能只看鼠标释放时的动画。

图片 Move 模式仍然设置 COPY 行为反馈,因为系统源删除由应用在事务完成后执行,而不是让系统在 DragEvent 返回时自动移动。这样应用能够保证目标落盘和链接插入先成功,再删除源;若直接声明系统 MOVE,可能失去对删除时序的控制。

原生标签栏为什么是拖放承载面

拖放绑定在 ArkUI 标签栏,而不是只绑定 Web 编辑区。系统文件管理器的 UDMF 数据由原生组件直接获得,不需要给 ArkWeb 开放文件访问或把绝对 URI发送进 JavaScript。Web 只收到已经落盘的相对图片链接,文档打开则由 ArkUI 会话更新。

标签栏横跨整个编辑工作区顶部,用户在源码、分栏或预览模式都能找到稳定目标。编辑区本身仍保留浏览器内图片粘贴与拖放,用于 Web File 对象;系统原生文件拖放优先走 ArkUI。两条路径最终汇入相同 AssetService,不产生不同命名规则。

应用内部证据

下图来自 HarmonyOS MateBook Pro 2in1 模拟器中的真实 OhMarkdown 设置侧栏,展示图片拖放的复制、移动和仅引用三种持久化模式。它记录了原生拖放资源语义已经进入应用,而不是浏览器演示页。

在这里插入图片描述

这张图属于已完成的图片拖放设备证据。提交 b11519c 新增的 Markdown 多文件打开和混合批次拒绝尚未在本轮模拟器截图,原因是 Mac 锁定且 hdc 无在线目标。文章不会把旧图描述成新文档拖放已通过;最新设备图将在 G3-09 闸门执行后补入本地资产目录。

自动化与构建验证

ArkTS 普通单元测试新增扩展名识别:中文 file URI 的 .md、大写 .MARKDOWN.txt 返回真,PNG 返回假。UnitTestBuild 让静态接口、URI解析和正则进入工程门禁。Debug HAP 编译确认 UDMF 类型判断、DragEvent、会话调用和响应式布局都符合 API 24。

完整 ./scripts/verify-local.sh 中 Web 回归 43/43 通过,说明拖放分流改动没有破坏 Web 图片粘贴、图片拖放、持久预览、多标签或导出。entry@ohosTest HAP 构建成功,但最新包尚未设备执行。

真正无法由无头浏览器证明的是系统文件管理器产生的记录类型、URI授权持续时间、多选顺序、DragResult 视觉反馈和物理鼠标落点。因此测试用例把单文档、多文档、重复文档、十二标签上限、图片三模式、混合批次、不支持文件和无权限文件逐项列为模拟器路径。

文件安全回归

拖放打开不会复制 Markdown,不改变源文件,不自动保存,也不把工作区之外的文件加入目录树。用户编辑后仍通过原 URI安全保存,外部修改指纹和三方冲突保护继续生效。重复拖入相同 URI只激活会话,避免两个标签同时编辑同一文件产生覆盖竞争。

图片 Copy/Move/Reference 继续遵循写入成功后插链接。Move 删除源前已确认目标 fsync 和编辑器插入;Reference 只允许受管理资源目录;失败不修改正文。新增文档分流没有放宽 AssetService 的路径、大小、MIME 或 Base64 限制。

不支持文件不会尝试以 UTF-8 打开,也不会作为图片复制。混合批次在任何 I/O 前拒绝,减少半成功。最多十六个 URI和十二文档会话上限共同限制一次拖放的资源占用。

大文件与性能

拖入文档使用 64 KiB 分块读取和 20 MiB 上限,UTF-8 decoder 使用 fatal 模式,不把无效字节替换成乱码后保存。超过 5 MiB 的正文进入大文档保护模式,关闭高成本预览但保留源码编辑。多文件顺序读取避免同时分配多个 20 MiB 缓冲。

分类只处理 URI字符串,不访问磁盘,DragEvent 可以快速返回。图片仍受 10 MiB 单文件与 16 个记录限制。设备性能测试应记录从释放鼠标到标签可输入的耗时、文件大小、文档数量和内存;仅记录系统拖放动画没有意义。

1000 文件工作区的性能与一次拖入最多十六个文档不同。前者由目录树惰性加载和 TaskPool 搜索负责,后者是用户明确选择的多文档打开。实现没有为拖放建立后台数据库或索引,保持 G3 的 Level 2 / D2 边界。

自由窗口中的拖放反馈

在 900 vp 以下,侧栏变为覆盖层,标签栏仍保持可见;用户可以先关闭侧栏再拖入文件。系统拖放目标宽度随窗口变化,但不会因为侧栏停靠压缩到低于核心编辑预算。低于 720 vp 时状态栏精简元数据,仍保留操作状态,用于显示“正在打开拖入文档”或具体失败。

窄窗口设备验收要检查:拖放高亮是否覆盖正确目标,文件释放后标签是否能滚动显示,多文档打开是否超出工具栏,错误状态是否被截断但可理解。这些视觉问题无法从 ArkTS 编译推断,必须由模拟器截图和操作记录收口。

可访问性替代路径

拖放天生依赖指针,不能成为唯一入口。单文件仍可通过 Ctrl+O、工具栏、文件树和命令面板打开;图片可以通过粘贴进入同一资源服务;工作区文件可通过快速打开搜索。拖放失败不会让键盘路径失效。

系统 DragResult 和状态栏提供反馈,错误不会只靠颜色。文件类型规则与选择器后缀一致,用户可以从错误中理解怎样修正。后续无障碍审查还需确认文件管理器与应用之间的拖放是否有系统辅助技术支持,但产品基础能力不依赖它。

竞争优势怎样量化

拖入 Markdown 打开和图片资源分流是专业编辑器的常见能力,优势要通过任务连续性证明。统一任务可以要求用户从文件管理器拖入三份 Markdown、切换其中一份、拖入图片、保存并重新打开,记录操作数、路径正确率、失败恢复和总耗时。OhMarkdown 的目标不是“接受更多格式”,而是保证文档不复制、图片链接可迁移、混合语义不含糊。

图片三模式已有模拟器 3 分证据,新的文档拖放仍处于 2 分:代码、构建、纯函数和安全边界完成,系统多文件记录和窗口行为待测。达到 3 分需要最新 HAP 在 MateBook Pro 2in1 模拟器完成全部拖放用例;达到 4 分还需真机与竞品同任务测量。

结语

文件拖放的难点不在监听一个事件,而在为不同文件建立正确语义。OhMarkdown 让 Markdown 进入文档会话,让图片进入受管理资源事务,让不支持和混合批次在副作用前失败。所有路径继续使用现有文件服务、标签上限、外部冲突和大文档保护,没有因追求“拖一下就行”而破坏本地文件事实来源。

提交 b11519c 已把分流实现推送仓库。下一步模拟器验收要真实从系统文件管理器拖入单个、多文件、重复 URI、图片和混合批次,并在 640 至 1280 vp 自由窗口观察反馈。完成这些设备证据后,才能把“编译可用的拖放”提升为“鸿蒙 PC 上可靠的桌面工作流”。

每条设备记录还要保存 UDMF 类型摘要、文件数量、成功打开数量、最终活动标签和失败文案。只有同时核对应用状态与磁盘文件,才能确认拖放没有留下部分复制、错误源删除或不可见会话。

真实文件管理器验收:鼠标框选不是文件拖放

本轮模拟器自动化先踩到一个很有代表性的坑。用虚拟鼠标从文件名拖向 OhMarkdown 时,文件管理器把动作解释为框选,最终选中三项,并没有生成 UDMF 文件记录。改用触控长按拖放后,系统才进入真正的跨窗口拖放协议。测试记录因此同时保留源文件选中状态、目标标签栏、应用状态和最终标签,不能只看指针移动轨迹。

单文件路径把 Untitled.md 拖入标签栏,应用打开新标签并显示 Dropped document opened。双文件路径先框选 Untitled.mdohmarkdown-1m.md,长按拖入后状态变为 Dropped documents opened,已有同 URI 标签被激活,1 MiB 文档创建新会话,未保存的 unsaved-g3 标签仍然存在。

图片回归切换到 Copy 模式后,从系统文件管理器拖入 JPEG。资源服务先写入 assets/ohmarkdown-g3-04.jpg,再在当前位置插入标准链接:

![ohmarkdown-g3-04](assets/ohmarkdown-g3-04.jpg)

Reference 模式下拖入工作区外图片会明确失败,不插入链接;切换 Copy 后同一来源成功。随后把 HTML 与 JPEG 组成混合批次,应用在任何文档会话或资源写入前整体拒绝,并显示 use only Markdown documents or only supported images in one drop。正文仍只有拒绝前的一个图片链接,这正是“失败不污染”的可观察证据。

在这里插入图片描述

文件拖放小阶段现在达到模拟器 3 分:单文档、多文档、1 MiB 文档、图片 Copy、Reference 安全失败和混合批次整体拒绝全部通过。G3-04 已有 Move 删除源与 Reference 不复制的磁盘证据继续有效。剩余缺口是真机文件管理器版本矩阵、十二标签上限批次、重复 URI 压力和竞品同任务计时。

Logo

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

更多推荐