HarmonyOS 文件选择上传实战:类型、大小、URI 与上传队列

文件上传看似只是选择一个文件,实际涉及文件类型、大小、URI 访问、进度、失败重试和隐私边界。用户上传头像、凭证、日志、PDF 时,应用不能直接相信选择结果,更不能把任意 URI 交给上传接口。文件上传治理要把选择、校验、排队、上传和结果反馈拆开。

请添加图片描述

本文会解决:如何封装 Picker 返回,如何限制类型和大小,如何让上传进度可见,失败后如何重试和清理。

1. 文件上传先定义边界

文件能力最容易失控:图片、视频、PDF、压缩包都可能被用户选中。不同业务应明确允许类型、数量、大小和用途。

请添加图片描述

场景 类型 大小 说明
头像 JPG/PNG 3MB 可裁剪压缩
售后凭证 JPG/PNG/PDF 10MB 保留证据
日志反馈 TXT/ZIP 20MB 需用户确认
普通表单 按业务限制 按业务限制 不默认放开

2. Picker 资料边界和工程目录

HarmonyOS 提供系统 Picker 和文件能力,应用需要对返回结果做二次校验。

资料入口 工程落点
Picker 选择器 图片和视频选择入口
文件管理能力 URI、文件读写边界
上传下载场景 网络上传任务和结果处理
entry/src/main/ets/common/upload/
  PickerFacade.ets
  FileBoundary.ets
  UploadQueue.ets
  UploadTaskRunner.ets
  UploadReporter.ets

3. PickerFacade 统一选择结果

Picker 返回的信息要归一化,页面不直接依赖底层返回结构。

export interface PickedFile {
  uri: string
  name: string
  mimeType: string
  sizeBytes: number
}

export class PickerFacade {
  normalize(uri: string, name: string, mimeType: string, sizeBytes: number): PickedFile {
    return {
      uri,
      name: name.trim().slice(0, 80),
      mimeType,
      sizeBytes
    }
  }
}

归一化后,后续边界校验和上传队列都使用同一份结构。

4. FileBoundary 校验类型、大小、数量

边界校验应该在进入上传队列之前完成。

export type UploadScene = 'avatar' | 'after_sale' | 'feedback_log'

export interface FileRule {
  types: string[]
  maxSize: number
  maxCount: number
}

export class FileBoundary {
  private readonly rules: Record<UploadScene, FileRule> = {
    avatar: { types: ['image/jpeg', 'image/png'], maxSize: 3 * 1024 * 1024, maxCount: 1 },
    after_sale: { types: ['image/jpeg', 'image/png', 'application/pdf'], maxSize: 10 * 1024 * 1024, maxCount: 6 },
    feedback_log: { types: ['text/plain', 'application/zip'], maxSize: 20 * 1024 * 1024, maxCount: 2 }
  }

  validate(scene: UploadScene, files: PickedFile[]): string[] {
    const rule = this.rules[scene]
    const errors: string[] = []
    if (files.length > rule.maxCount) errors.push('文件数量超出限制')
    for (const file of files) {
      if (!rule.types.includes(file.mimeType)) errors.push(`${file.name} 类型不支持`)
      if (file.sizeBytes <= 0 || file.sizeBytes > rule.maxSize) errors.push(`${file.name} 大小超出限制`)
    }
    return errors
  }
}

不要只靠文件后缀判断类型,后缀可以伪造。实际项目还要结合系统返回的 MIME 信息和服务端校验。

5. UploadQueue 管理上传任务

上传需要可见进度和可重试任务。

请添加图片描述

export type UploadState = 'WAITING' | 'UPLOADING' | 'SUCCESS' | 'FAILED'

export interface UploadTask {
  taskId: string
  file: PickedFile
  state: UploadState
  progress: number
  retryCount: number
}

export class UploadQueue {
  private readonly tasks: UploadTask[] = []

  add(file: PickedFile): UploadTask {
    const task: UploadTask = { taskId: `upload_${Date.now()}`, file, state: 'WAITING', progress: 0, retryCount: 0 }
    this.tasks.push(task)
    return task
  }

  list(): UploadTask[] {
    return this.tasks.map(item => ({ ...item }))
  }
}

队列能让页面展示多个文件上传进度,而不是只有一个全局 loading。

6. UploadTaskRunner 执行上传

上传执行器只处理单个任务,成功和失败都要更新状态。

export class UploadTaskRunner {
  async run(task: UploadTask): Promise<UploadTask> {
    task.state = 'UPLOADING'
    task.progress = 30
    try {
      // 实际项目中这里调用上传接口,并监听进度。
      task.progress = 100
      task.state = 'SUCCESS'
      return task
    } catch (_) {
      task.retryCount += 1
      task.state = 'FAILED'
      return task
    }
  }
}

上传失败不能丢任务,要让用户重试或删除。

7. UploadReporter 构建页面状态

export interface UploadViewState {
  title: string
  progressText: string
  canSubmit: boolean
}

export class UploadReporter {
  build(tasks: UploadTask[]): UploadViewState {
    const failed = tasks.filter(t => t.state === 'FAILED').length
    const success = tasks.filter(t => t.state === 'SUCCESS').length
    if (failed > 0) return { title: '部分文件上传失败', progressText: `${failed} 个文件需要重试`, canSubmit: false }
    if (success === tasks.length && tasks.length > 0) return { title: '文件已上传完成', progressText: '可以提交表单', canSubmit: true }
    return { title: '文件上传中', progressText: `${success}/${tasks.length} 已完成`, canSubmit: false }
  }
}

表单提交应等待必要文件上传完成,避免服务端收到缺失附件的工单。

8. URI 和隐私边界

文件 URI 不应长期暴露给业务页面。上传完成后保留服务端文件 ID,退出页面时清理临时任务。

export interface UploadedFileRef {
  fileId: string
  name: string
  mimeType: string
}

export function toFileRef(task: UploadTask, serverFileId: string): UploadedFileRef {
  if (task.state !== 'SUCCESS') {
    throw new Error('只有上传成功的任务才能生成文件引用')
  }
  return { fileId: serverFileId, name: task.file.name, mimeType: task.file.mimeType }
}

业务表单保存 fileId,不要保存本地 URI。

9. 文件上传验收动作

场景 操作 预期结果
头像上传 选择 JPG 校验通过并进入队列
类型错误 选择 EXE 阻止上传
超大文件 选择超过限制文件 提示大小限制
上传失败 模拟网络失败 任务保留,可重试
上传成功 提交表单 表单只保存服务端文件 ID
export function assertUploadTask(task: UploadTask): void {
  if (!task.taskId || !task.file.uri) throw new Error('上传任务缺少关键字段')
  if (task.progress < 0 || task.progress > 100) throw new Error('上传进度必须在 0 到 100')
}

10. 文件上传异常排查表

文件问题要从选择结果、边界校验、上传队列、服务端响应四层定位。

现象 优先查看 处理建议
文件选中后消失 Picker 返回 URI 统一归一化选择结果
上传接口报错 MIME 和大小 前端先校验,服务端再确认
多文件进度混乱 taskId 每个文件独立任务
表单缺附件 上传状态 成功后再允许提交
隐私解释困难 本地 URI 保存 表单只保存文件 ID

文件上传复现场景:给读者一组可执行核验

文件上传文章要验证类型、大小、URI、队列和失败重试。只调用选择器不能证明上传链路稳定。

核验维度 读者需要准备的证据
输入 页面入口、用户动作、关键参数
过程 日志、状态变化、异常分支
输出 UI 表现、回调结果、持久化结果
回归 同场景重复执行后的结果
interface UploadReplayCase {
  fileName: any
  mimeType: any
  size: any
  queueState: any
}

const replay84: UploadReplayCase = {
  fileName: 'sample',
  mimeType: 'sample',
  size: 'sample',
  queueState: 'sample',
}

function assertReplay84(item: UploadReplayCase): void {
  if (item.size <= 0) throw new Error('文件大小异常')
}

这组核验把文件属性和上传队列关联起来,读者可以用它定位类型、大小或队列状态异常。

上传队列回放表:把文章方法变成可复现动作

文件上传需要处理用户连续选择、网络中断和页面退出。建议构造大文件、错误类型、断网重试、取消上传四种路径,验证队列状态不会错乱。

回放动作 核验方式
大文件拒绝 准备输入、执行操作、记录结果、给出结论
错误类型拦截 准备输入、执行操作、记录结果、给出结论
断网进入重试 准备输入、执行操作、记录结果、给出结论
取消后清理队列 准备输入、执行操作、记录结果、给出结论

文件上传要把选择器和上传队列分开看。读者可以先用错误类型和超大文件验证前置拦截,再用断网和取消上传验证队列状态。上传失败后队列不能一直占用,同一个文件不能被重复提交,页面退出后也不能留下无法观察的后台上传任务。

文件能力的落地边界:不要把边界留给读者猜

文件选择和上传不要写成一个大函数。选择器负责拿到 URI,校验层负责类型和大小,队列层负责重试,业务层只关心最终文件 id。读者按这个边界拆分后,文件过大、类型错误、网络失败都会有清晰处理点。

落地项 处理要求
选择器不做业务判断 需要有明确输入、处理边界和失败兜底
校验层阻断非法文件 需要有明确输入、处理边界和失败兜底
队列层处理失败重试 需要有明确输入、处理边界和失败兜底
业务层只接收结果 需要有明确输入、处理边界和失败兜底

这类边界写清楚后,读者不需要猜哪些逻辑属于页面、哪些属于服务、哪些属于发布前验收。文章的价值也会从“讲了一个功能”变成“给了一套可迁移的工程判断”。

上传联调步骤:按真实路径走一遍

文件上传建议按队列视角联调。第一步选择合法小文件确认正常上传,第二步选择超大文件确认前置拦截,第三步选择不支持类型确认错误文案,第四步上传过程中断网确认进入重试,第五步取消上传确认队列清理。这样上传能力不只是能选文件,而是具备完整失败恢复。

这一步的意义是让读者拿到文章后可以直接复现,而不是只理解概念。技术文章如果能把“输入、动作、日志、结果、失败兜底”写完整,读者照着做时出错概率会低很多。

上传验收补充:补上容易漏掉的边界

补充一个真实验收例子:用户选择一个 80MB 的视频文件开始上传,上传到 40% 时切换网络,再点击取消。此时页面要能展示取消结果,队列中不能继续重试,临时文件也不能残留。这个场景比普通小图上传更能说明队列边界是否可靠。

文件上传还要关注页面退出后的状态。读者可以在上传进度达到一半时返回上一页,再重新进入上传页面,确认队列是否能恢复展示;如果业务要求退出即取消,也要确认临时文件和任务状态被清理。这个步骤能发现上传任务和页面生命周期耦合过深的问题。

这类补充不是为了增加篇幅,而是为了让读者在真实项目里少踩坑:正常路径一般最容易跑通,异常路径、退出路径和恢复路径才是质量差距所在。

11. 小结:上传前先做边界治理

文件上传的稳定性来自边界控制。Picker 只负责选择,FileBoundary 决定能不能上传,UploadQueue 负责进度和重试,业务表单只消费成功后的文件引用。这样文件能力既可用,也不会失控。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐