HarmonyOS 文件选择上传实战:类型、大小、URI 与上传队列
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 负责进度和重试,业务表单只消费成功后的文件引用。这样文件能力既可用,也不会失控。
更多推荐




所有评论(0)