《HarmonyOS NEXT 文件上传(HTTP/OSS)企业级实践》
第一章 企业级文件上传架构设计
1.1 为什么需要重新设计文件上传系统?
很多 HarmonyOS NEXT 初学者第一次实现上传,大概率会这样写:
Button('上传文件') .onClick(async()=>{ let result = await picker.select() http.request( "https://xxx.com/upload", { method:http.RequestMethod.POST, extraData:result } ) })看起来:
简单。
直接。
能跑。
但是进入企业项目以后,这种写法马上暴露问题。
真实业务场景
假设开发一个商城 App:
用户发布商品:
需要上传:
商品主图 | |- 图片1 8MB |- 图片2 6MB |- 图片3 10MB 商品视频 | |- 500MB如果直接上传:
选择文件 ↓ HTTP POST ↓ 服务器接收 ↓ 保存会出现:
问题一:大文件上传失败
例如:
视频:
500MB用户上传到:
80%突然:
网络断开传统方式:
重新开始。
用户体验:
灾难。
问题二:图片体积过大
手机拍照:
4000 x 3000 8MB但是服务器真正需要:
1080 x 1080 200KB如果不处理:
每天大量浪费:
- 流量
- CDN成本
- 存储空间
问题三:OSS 密钥泄露
错误设计:
客户端:
const accessKey = "xxxxxxxx" const secret = "xxxxxxxx"然后:
APP | | OSS这是严重安全问题。
因为:
APK 可以反编译。
攻击者拿到:
AccessKey SecretKey直接:
删除文件 上传病毒 下载全部数据
问题四:上传逻辑散落
很多项目:
页面1:
uploadAvatar()页面2:
uploadVideo()页面3:
uploadFile()最后:
三个上传逻辑:
三套代码。
维护困难。
所以企业项目需要:
统一上传架构。
1.2 企业级上传整体架构
一个成熟上传系统:
应该如下:
用户操作 | ↓ UploadManager | ---------------------------- | | | ↓ ↓ ↓ HTTP上传 OSS上传 MinIO上传 | | ---------------------------- 文件处理层 | ↓ 文件选择 Uri解析 文件读取 图片压缩 MD5计算 分片 重试 进度
1.3 分层设计
企业项目推荐:
第一层:UI层
负责:
- 选择文件
- 展示进度
- 展示结果
例如:
UploadPage.ets不要:
在页面里面写:
HTTP。
OSS。
第二层:业务层
UploadManager
职责:
- 创建上传任务
- 管理状态
- 调度上传
例如:
UploadManager.ets
第三层:上传实现层
不同上传方式:
实现统一接口。
例如:
Uploader | | |------ HttpUploader | |------ OssUploader | |------ CosUploader
第四层:文件处理层
包括:
FileUtil CompressUtil HashUtil ChunkUtil
最终:
项目结构:
entry ├── pages │ └── UploadPage.ets ├── common │ ├── upload │ │ ├── UploadManager.ets │ │ ├── UploadTask.ets │ │ ├── UploadFile.ets │ │ ├── Uploader.ets │ │ ├── HttpUploader.ets │ │ ├── OssUploader.ets │ │ ├── ChunkUploader.ets │ │ ├── CompressUtil.ets │ │ └── Md5Util.ets
1.4 上传生命周期设计
一个文件上传:
实际上是一个状态机。
流程:
WAIT | ↓ SELECT | ↓ READ | ↓ COMPRESS | ↓ HASH | ↓ CHECK | ↓ UPLOAD | ↓ SUCCESS失败:
UPLOAD | ↓ ERROR | ↓ RETRY
定义状态:
创建:
UploadState.ets代码:
export enum UploadState { /** * 等待上传 */ WAIT, /** * 文件读取 */ READING, /** * 图片压缩 */ COMPRESS, /** * 计算文件hash */ HASH, /** * 上传中 */ UPLOADING, /** * 上传成功 */ SUCCESS, /** * 上传失败 */ ERROR, /** * 用户取消 */ CANCEL }
为什么需要这么多状态?
因为 UI 需要反馈。
例如:
状态:
COMPRESS显示:
正在优化图片...状态:
HASH显示:
正在检查文件...状态:
UPLOADING显示:
上传中 65%
1.5 定义上传文件模型
创建:
UploadFile.ets代码:
export class UploadFile { /** * 文件Uri */ uri:string="" /** * 文件名称 */ name:string="" /** * 文件大小 */ size:number=0 /** * MIME类型 */ mimeType:string="" /** * MD5值 */ md5:string="" /** * 文件类型 */ type:string="" }
为什么不用:
string例如:
upload(uri:string)后面一定会扩展:
需要:
文件大小 文件名称 md5 mime 分片信息所以提前设计模型。
1.6 上传任务模型
一个文件:
对应一个 Task。
创建:
UploadTask.ets代码:
import { UploadState } from './UploadState' import { UploadFile } from './UploadFile' export class UploadTask { /** * 任务ID */ id:string="" /** * 上传文件 */ file:UploadFile /** * 当前状态 */ state: UploadState= UploadState.WAIT /** * 上传进度 */ progress:number=0 constructor( file:UploadFile ){ this.file=file } }
例如:
上传头像:
Task: id: 10001 file: avatar.png state: UPLOADING progress: 45
1.7 定义上传接口
核心设计。
所有上传方式:
必须实现:
Uploader创建:
Uploader.ets代码:
import { UploadTask } from './UploadTask' export interface Uploader { upload( task:UploadTask ):Promise<string> cancel( taskId:string ):void }
以后:
HTTP:
class HttpUploader implements UploaderOSS:
class OssUploader implements Uploader页面完全无感。
1.8 UploadManager核心实现
创建:
UploadManager.ets代码:
import { UploadTask } from './UploadTask' import { UploadFile } from './UploadFile' import { Uploader } from './Uploader' export class UploadManager { private uploader:Uploader constructor( uploader:Uploader ){ this.uploader=uploader } async upload( file:UploadFile ):Promise<string>{ let task = new UploadTask(file) return await this.uploader.upload(task) } }
业务调用:
let manager = new UploadManager( new HttpUploader() ) let url = await manager.upload(file)
此时:
业务层不知道:
上传到了哪里。
可能:
服务器 OSS MinIO COS都可以。
第二章 HarmonyOS NEXT 文件选择体系详解
在上一章我们完成了企业级上传框架设计。
但是上传之前,还有一个核心问题:
文件从哪里来?
在 HarmonyOS NEXT 中,文件来源和 Android、iOS 有明显区别。
传统开发思维:
用户选择文件
↓
得到文件路径
↓
File对象
↓
上传
但是 HarmonyOS NEXT:
用户选择文件
↓
Picker
↓
Uri
↓
打开Uri
↓
读取数据流
↓
上传
核心:
HarmonyOS 不鼓励应用直接访问用户真实文件路径,而是通过 Uri 进行安全访问。
2.1 HarmonyOS 文件访问模型
HarmonyOS NEXT 文件系统主要分:
应用沙箱文件
↓
公共文件
↓
媒体文件
↓
用户授权文件
2.1.1 应用沙箱
每个应用拥有独立空间。
例如:
/data/storage/el2/base
里面:
files
cache
database
preferences
例如:
用户下载头像:
/data/storage/el2/base/files/avatar.png
特点:
- APP自己管理
- 不需要用户授权
- 其他APP无法访问
2.1.2 公共文件
例如:
Download
Documents
Pictures
Videos
这些属于系统公共空间。
访问:
需要用户授权。
2.1.3 媒体资源
例如:
相册:
照片
视频
音频
HarmonyOS NEXT:
推荐:
photoAccessHelper
而不是直接扫描目录。
2.2 为什么返回 Uri?
例如:
用户选择一张图片。
Android早期:
返回:
/storage/emulated/0/DCIM/a.jpg
HarmonyOS:
返回:
file://media/Photo/1/xxx
为什么?
因为:
安全
如果APP直接获得:
/storage/xxx
APP可以:
- 遍历用户文件
- 偷读取照片
- 获取隐私
Uri机制:
类似:
临时访问许可证
用户选择:
授权一个文件。
APP只能访问:
这个文件。
2.3 文件选择方式
HarmonyOS NEXT 常见:
| 场景 | API |
|---|---|
| 图片视频 | PhotoViewPicker |
| 普通文件 | DocumentViewPicker |
| 保存文件 | DocumentSavePicker |
| 拍照 | Camera |
| 录音 | AudioCapturer |
2.4 PhotoViewPicker 图片选择实战
企业最常见:
上传头像。
流程:
点击按钮
↓
打开相册
↓
用户选择图片
↓
返回Uri
↓
读取图片
↓
压缩
↓
上传
2.4.1 导入模块
import {
photoAccessHelper
} from '@kit.MediaLibraryKit'
2.4.2 创建选择器
代码:
let photoPicker =
new photoAccessHelper.PhotoViewPicker()
2.4.3 调用选择
let result =
await photoPicker.select({
MIMEType:
photoAccessHelper.PhotoViewMIMETypes
.IMAGE_TYPE,
maxSelectNumber:1
})
返回:
PhotoSelectResult
结构:
{
"photoUris":[
"file://media/Photo/xxx"
]
}
完整封装
企业项目:
不要页面直接调用。
创建:
FilePickerManager.ets
代码:
import {
photoAccessHelper
}
from '@kit.MediaLibraryKit'
export class FilePickerManager {
static async pickImage()
:Promise<string>{
let picker =
new photoAccessHelper
.PhotoViewPicker()
let result =
await picker.select({
MIMEType:
photoAccessHelper
.PhotoViewMIMETypes
.IMAGE_TYPE,
maxSelectNumber:1
})
if(
result.photoUris.length>0
){
return result.photoUris[0]
}
return ""
}
}
页面:
let uri =
await FilePickerManager.pickImage()
页面完全不知道 Picker 细节。
2.5 DocumentViewPicker 文件选择
除了图片:
企业更多是:
PDF
Excel
Word
ZIP
TXT
例如:
OA审批上传合同。
2.5.1 引入
import {
picker
}
from '@kit.CoreFileKit'
2.5.2 创建
let documentPicker =
new picker.DocumentViewPicker()
2.5.3 选择文件
let result =
await documentPicker.select()
返回:
uri
例如:
file://docs/contract.pdf
封装:
export class DocumentPickerManager {
static async pickFile(){
let picker =
new picker.DocumentViewPicker()
let result =
await picker.select()
return result[0]
}
}
2.6 统一文件选择接口
现在:
图片:
PhotoViewPicker
文件:
DocumentViewPicker
如果业务层判断:
if(image){
PhotoPicker
}else{
DocumentPicker
}
以后维护很痛苦。
所以统一:
FilePicker
|
|
-----------------
ImagePicker
DocumentPicker
VideoPicker
设计:
export interface Picker {
pick()
:
Promise<string>
}
图片实现:
export class ImagePicker
implements Picker{
async pick(){
return await
FilePickerManager.pickImage()
}
}
文件实现:
export class FilePicker
implements Picker{
async pick(){
return await
DocumentPickerManager.pickFile()
}
}
业务:
let picker =
new ImagePicker()
let uri =
await picker.pick()
2.7 Uri 转换文件信息
拿到:
file://xxx
还不能上传。
需要:
Uri
↓
文件大小
↓
文件名称
↓
MIME
创建:
FileInfoUtil.ets
获取文件大小
使用:
import fileIo
from '@ohos.file.fs'
打开:
let fd =
fileIo.openSync(uri)
获取:
let stat =
fileIo.statSync(fd)
得到:
stat.size
封装:
export class FileInfoUtil {
static getSize(
uri:string
):number{
let stat =
fileIo.statSync(uri)
return stat.size
}
}
2.8 Uri读取文件流
上传最终需要:
ArrayBuffer
或者
Stream
读取:
let file =
fileIo.openSync(uri)
let buffer =
new ArrayBuffer(size)
读取:
fileIo.readSync(
file.fd,
buffer
)
流程:
Uri
↓
open
↓
read
↓
ArrayBuffer
↓
HTTP
↓
服务器
2.9 大文件不要一次读取
错误:
let buffer =
readAll(file)
例如:
视频:
2GB
直接:
OOM。
正确:
流式读取。
文件
↓
1MB
↓
上传
↓
下一块
↓
上传
这就是:
后面章节的:
分片上传
2.10 创建企业统一 FileService
最终:
所有文件处理:
统一入口。
目录:
common/file
FileService.ets
代码:
export class FileService {
static async getFile(
uri:string
):Promise<UploadFile>{
let file =
new UploadFile()
file.uri=uri
file.name=
this.getName(uri)
file.size=
this.getSize(uri)
return file
}
}
以后:
所有上传:
统一:
let file =
await FileService.getFile(uri)
await uploadManager.upload(file)
2.11 企业项目中的文件选择完整流程
最终:
页面
|
↓
FilePickerManager
|
↓
Uri
|
↓
FileService
|
↓
UploadFile
|
↓
UploadManager
|
↓
Uploader
|
↓
OSS/HTTP
第三章 HarmonyOS NEXT Uri、File、ArrayBuffer 深度解析
3.1 HarmonyOS 文件数据流模型
传统 Web:
<input type=file>
|
↓
File对象
|
↓
Blob
|
↓
ArrayBuffer
|
↓
上传
Android:
Uri
|
ContentResolver
|
InputStream
|
byte[]
|
上传
HarmonyOS NEXT:
Uri
|
fileIo
|
FileDescriptor
|
ArrayBuffer
|
HTTP Request
|
服务器
核心:
HarmonyOS 文件上传,本质:
把 Uri 指向的数据转换成二进制流,然后通过 HTTP 发送。
3.2 Uri 到底是什么?
例如:
PhotoPicker 返回:
file://media/Photo/1/IMG_001.jpg
这个字符串:
不是文件内容。
只是一个:
"文件引用地址"
类似:
数据库:
user_id=1001
它不是用户数据。
只是定位信息。
所以:
不能:
http.request({
extraData:uri
})
因为上传的是:
file://media/xxx.jpg
而不是:
图片二进制
3.3 获取文件描述符 FD
HarmonyOS:
通过:
fileIo.open()
打开文件。
引入:
import {
fileIo
}
from '@kit.CoreFileKit'
代码:
let file =
fileIo.openSync(
uri,
fileIo.OpenMode.READ_ONLY
)
返回:
File
包含:
{
fd:number
}
例如:
console.log(file.fd)
输出:
23
这个:
23
就是系统文件描述符。
3.4 获取文件大小
为什么需要大小?
因为读取:
需要知道:
读取多少字节
代码:
let stat =
fileIo.statSync(uri)
返回:
{
size:2048000,
mode:0,
mtime:xxx
}
获取:
let size =
stat.size
单位:
byte。
例如:
2048000 byte
≈2MB
封装:
export class FileUtil {
static getSize(
uri:string
):number{
let stat =
fileIo.statSync(uri)
return stat.size
}
}
3.5 ArrayBuffer 是什么?
HarmonyOS 二进制处理核心。
简单理解:
字符串:
hello
对应:
ASCII:
68 65 6C 6C 6F
计算机真正处理:
是:
byte数组。
ArrayBuffer:
就是:
连续内存区域。
例如:
let buffer =
new ArrayBuffer(5)
内存:
+----+----+----+----+----+
|00 |00 |00 |00 |00 |
+----+----+----+----+----+
文件:
avatar.jpg
读取:
变成:
FF D8 FF E0 ...
存储:
ArrayBuffer。
3.6 读取整个文件
简单实现:
export function readFile(
uri:string
):ArrayBuffer{
let stat =
fileIo.statSync(uri)
let size =
stat.size
let buffer =
new ArrayBuffer(size)
let fd =
fileIo.openSync(
uri,
fileIo.OpenMode.READ_ONLY
)
fileIo.readSync(
fd.fd,
buffer
)
fileIo.closeSync(
fd
)
return buffer
}
调用:
let buffer =
readFile(uri)
得到:
ArrayBuffer
但是!
这个方法:
只适合:
小文件。
例如:
头像:
200KB
可以。
3.7 为什么大文件不能 readAll?
假设:
视频:
2GB
执行:
new ArrayBuffer(
2*1024*1024*1024
)
手机内存:
可能:
8GB
但是:
系统还需要:
- ArkUI
- JS Runtime
- 图片缓存
结果:
容易:
OOM
企业方案:
流式读取。
3.8 分块读取文件
例如:
视频:
500MB
切:
500MB
↓
1MB
1MB
1MB
...
500次
代码:
const CHUNK_SIZE =
1024 * 1024
1MB。
读取:
function readChunk(
fd:number,
position:number
){
let buffer =
new ArrayBuffer(
CHUNK_SIZE
)
fileIo.readSync(
fd,
buffer,
{
offset:position
}
)
return buffer
}
上传:
chunk1
↓
chunk2
↓
chunk3
↓
chunk4
这就是:
后面:
OSS Multipart Upload
基础。
3.9 ArrayBuffer 与 Uint8Array
很多 API:
需要:
Uint8Array。
例如:
MD5:
hash.update(
Uint8Array
)
转换:
let uint8 =
new Uint8Array(
buffer
)
关系:
ArrayBuffer
|
|
↓
Uint8Array
区别:
ArrayBuffer
负责:
存储内存。
Uint8Array
负责:
操作每一个 byte。
例如:
修改:
uint8[0]=255
3.10 文件转 Base64
一些接口:
例如:
AI识别:
要求:
{
image:"base64..."
}
转换:
流程:
Uri
↓
ArrayBuffer
↓
Uint8Array
↓
Base64
代码:
function bufferToBase64(
buffer:ArrayBuffer
){
let bytes =
new Uint8Array(buffer)
let binary=""
for(
let i=0;
i<bytes.length;
i++
){
binary +=
String.fromCharCode(
bytes[i]
)
}
return encode(
binary
)
}
但是:
注意!
Base64 会膨胀:
约:
文件大小
×
1.33
例如:
图片:
3MB
Base64:
4MB
所以:
普通上传:
不要 Base64。
推荐:
multipart/form-data。
3.11 Multipart/form-data 原理
浏览器上传文件:
其实:
也是:
multipart。
格式:
POST /upload
Content-Type:
multipart/form-data;
boundary=xxx
------xxx
Content-Disposition:
form-data;
name="file";
filename="a.png"
(binary)
------xxx--
一个请求:
包含:
多个字段。
例如:
userId
token
file
企业后台:
SpringBoot:
@PostMapping("/upload")
public String upload(
MultipartFile file
){
}
接收:
就是这里。
3.12 HarmonyOS HTTP上传结构
HarmonyOS:
import http
from '@ohos.net.http'
创建:
let request =
http.createHttp()
请求:
request.request(
url,
{
method:
http.RequestMethod.POST,
header:{
},
extraData:data
}
)
但是:
data是什么?
这里就是:
关键。
普通 JSON:
{
name:"test"
}
不能传文件。
文件:
必须:
二进制。
第四章 HarmonyOS NEXT HTTP Multipart 文件上传完整实现
4.1 为什么企业上传优先选择 Multipart?
文件上传常见方案:
| 方案 | 适用场景 | 推荐程度 |
|---|---|---|
| Base64 JSON | 小图片、AI接口 | ⭐⭐ |
| FormData Multipart | 普通文件上传 | ⭐⭐⭐⭐⭐ |
| OSS直传 | 大文件、视频 | ⭐⭐⭐⭐⭐ |
| 分片上传 | 超大文件 | ⭐⭐⭐⭐⭐ |
企业后台最常见:
客户端
|
|
multipart/form-data
|
|
业务服务器
|
|
OSS
或者:
客户端
|
|
OSS直传
|
|
业务服务器保存URL
4.2 Multipart 请求结构
一个文件上传请求:
POST /api/file/upload HTTP/1.1
Host: api.xxx.com
Content-Type:
multipart/form-data;
boundary=HarmonyBoundary001
--HarmonyBoundary001
Content-Disposition:
form-data;
name="userId"
10001
--HarmonyBoundary001
Content-Disposition:
form-data;
name="file";
filename="avatar.png"
Content-Type:image/png
<二进制文件>
--HarmonyBoundary001--
这里有三个关键:
1. boundary
分隔符。
例如:
----HarmonyBoundary001
用于告诉服务器:
哪里开始字段。
哪里结束字段。
2. 文件 Header
必须:
Content-Disposition
告诉服务器:
字段名称。
文件名称。
3. 二进制内容
不能:
file://xxx.jpg
必须:
FF D8 FF E0 ...
4.3 HarmonyOS HTTP模块
引入:
import {
http
} from '@kit.NetworkKit'
创建请求:
let httpRequest =
http.createHttp()
基础请求:
let response =
await httpRequest.request(
url,
{
method:
http.RequestMethod.POST,
header:{
},
extraData:data
}
)
4.4 创建 MultipartBuilder
企业项目不要每次拼字符串。
封装:
目录:
upload
└── MultipartBuilder.ets
代码:
export class MultipartBuilder {
private boundary:string
private chunks:ArrayBuffer[]=[]
constructor(){
this.boundary =
"----HarmonyBoundary"
+
Date.now()
}
getBoundary(){
return this.boundary
}
}
boundary示例:
----HarmonyBoundary172233333
保证唯一。
4.5 添加普通参数
例如:
上传接口需要:
userId
token
folder
添加:
addField()
实现:
addField(
name:string,
value:string
){
let text =
`--${this.boundary}
Content-Disposition:
form-data;
name="${name}"
${value}
`
}
最终:
userId
↓
10001
4.6 添加文件
核心方法:
addFile()
参数:
addFile(
fieldName:string,
fileName:string,
mimeType:string,
data:ArrayBuffer
)
实现:
addFile(
fieldName:string,
fileName:string,
mime:string,
buffer:ArrayBuffer
){
let header =
`--${this.boundary}
Content-Disposition:
form-data;
name="${fieldName}";
filename="${fileName}"
Content-Type:${mime}
`
}
然后拼接:
header
+
binary
4.7 ArrayBuffer拼接
Multipart:
实际上:
多个二进制块组合。
例如:
Header
+
图片
+
End
所以需要:
ArrayBuffer concat。
工具:
export class BufferUtil {
static concat(
buffers:ArrayBuffer[]
):ArrayBuffer{
let total=0
buffers.forEach(
b=>{
total+=b.byteLength
}
)
let result =
new Uint8Array(total)
let offset=0
buffers.forEach(
b=>{
let arr =
new Uint8Array(b)
result.set(
arr,
offset
)
offset+=arr.length
})
return result.buffer
}
}
4.8 完整 MultipartBuilder
最终:
export class MultipartBuilder {
private boundary:string
private buffers:ArrayBuffer[]=[]
constructor(){
this.boundary=
"----HarmonyBoundary"
+
Date.now()
}
addField(
name:string,
value:string
){
let content=
`--${this.boundary}
Content-Disposition:
form-data;
name="${name}"
${value}
`
this.buffers.push(
TextEncoderUtil.encode(content)
)
}
addFile(
name:string,
filename:string,
mime:string,
buffer:ArrayBuffer
){
let header=
`--${this.boundary}
Content-Disposition:
form-data;
name="${name}";
filename="${filename}"
Content-Type:${mime}
`
this.buffers.push(
encode(header)
)
this.buffers.push(
buffer
)
}
build(){
let end=
`
--${this.boundary}--
`
this.buffers.push(
encode(end)
)
return BufferUtil.concat(
this.buffers
)
}
}
4.9 HttpUploader实现
创建:
HttpUploader.ets
代码:
export class HttpUploader
implements Uploader {
async upload(
task:UploadTask
):Promise<string>{
let builder =
new MultipartBuilder()
builder.addField(
"userId",
"10001"
)
let buffer =
await FileUtil.read(
task.file.uri
)
builder.addFile(
"file",
task.file.name,
task.file.mimeType,
buffer
)
let body =
builder.build()
let request =
http.createHttp()
let response =
await request.request(
"https://api.xxx.com/upload",
{
method:
http.RequestMethod.POST,
header:{
"Content-Type":
"multipart/form-data;
boundary="
+
builder.getBoundary()
},
extraData:body
}
)
return response.result
}
}
4.10 上传进度监听
普通:
request()
无法满足。
企业使用:
uploadTask
监听:
bytesSent
totalBytes
计算:
progress=
bytesSent / totalBytes *100
例如:
task.progress=
65
ArkUI:
Progress({
value:task.progress
})
4.11 Token认证
企业接口:
必须:
Authorization
例如:
Authorization:
Bearer eyJxxxx
代码:
header:{
"Authorization":
"Bearer "
+
token
}
不要:
把 token 写死。
推荐:
TokenManager
↓
Preferences
↓
自动刷新
4.12 文件类型校验
客户端:
第一层校验。
例如:
只允许图片:
const allow=[
"image/png",
"image/jpeg",
"image/webp"
]
判断:
if(
!allow.includes(
file.mimeType
)
){
throw Error(
"文件类型错误"
)
}
但是注意:
客户端校验不安全。
服务器必须再次校验。
4.13 文件大小限制
例如:
头像:
最大:
5MB
视频:
最大:
500MB
代码:
if(
file.size >
5*1024*1024
){
throw Error(
"文件过大"
)
}
4.14 HTTP上传架构优化
普通:
页面
↓
http.request
生产:
页面
↓
UploadManager
↓
HttpUploader
↓
HttpClient
↓
Server
4.15 HTTP上传存在的问题
Multipart解决了:
普通文件上传。
但是:
大文件:
例如:
1GB视频
依然存在问题:
- 内存占用
new ArrayBuffer(1GB)
- 网络中断
重新上传
- 无法暂停
- 无法续传
第五章 HarmonyOS NEXT 大文件分片上传架构
在企业应用中,普通 Multipart 上传只能解决:
几 KB
几 MB
几十 MB
级别文件。
但是:
真实业务经常出现:
视频:
800MB
直播录像:
5GB
企业资料:
2GB
如果仍然:
整个文件
↓
一次HTTP请求
↓
服务器
会出现严重问题。
5.1 为什么需要分片上传?
假设:
视频:
1GB
网络:
10MB/s
理论:
100秒
但是现实:
- 用户移动网络波动
- WiFi切换
- APP进入后台
- 系统杀进程
例如:
上传:
已经完成:
850MB / 1024MB
突然:
网络断开
普通上传:
重新开始
损失:
850MB流量。
分片上传:
变成:
文件
↓
Chunk1
Chunk2
Chunk3
Chunk4
...
Chunk1024
每个:
1MB
上传:
Chunk1 √
Chunk2 √
Chunk3 √
Chunk4 ×
Chunk5 √
失败:
只重新:
Chunk4
5.2 分片上传整体流程
企业级流程:
客户端
|
|
计算文件信息
|
|
请求上传初始化
|
|
服务器返回 UploadId
|
|
文件切片
|
|
并发上传 Chunk
|
|
记录每个Chunk状态
|
|
全部成功
|
|
通知服务器合并
|
|
返回文件URL
5.3 文件切片原理
假设:
文件:
100MB
设置:
Chunk大小:
5MB
切:
100 / 5
=20片
结果:
chunk0
0MB - 5MB
chunk1
5MB -10MB
chunk2
10MB -15MB
...
chunk19
95MB-100MB
5.4 Chunk模型设计
创建:
ChunkInfo.ets
代码:
export class ChunkInfo {
/**
* 分片编号
*/
index:number=0
/**
* 开始位置
*/
offset:number=0
/**
* 分片大小
*/
size:number=0
/**
* 上传状态
*/
uploaded:boolean=false
/**
* ETag
*/
etag:string=""
}
例如:
第10片:
index:
10
offset:
52428800
size:
1048576
uploaded:
true
5.5 文件切片工具
创建:
ChunkUtil.ets
定义:
const CHUNK_SIZE =
1024 * 1024
1MB。
生成分片:
export class ChunkUtil {
static createChunks(
fileSize:number
):ChunkInfo[]{
let chunks:ChunkInfo[]=[]
let count =
Math.ceil(
fileSize / CHUNK_SIZE
)
for(
let i=0;
i<count;
i++
){
let chunk =
new ChunkInfo()
chunk.index=i
chunk.offset=
i*CHUNK_SIZE
chunk.size=
Math.min(
CHUNK_SIZE,
fileSize -
chunk.offset
)
chunks.push(chunk)
}
return chunks
}
}
例如:
文件:
10MB
输出:
[
{
index:0,
offset:0,
size:1MB
},
{
index:1,
offset:1MB,
size:1MB
}
...
]
5.6 为什么 Chunk 不建议太小?
例如:
100MB文件。
方案1:
1000片
每片100KB
问题:
HTTP请求:
1000次。
消耗:
- TCP连接
- Header
- TLS握手
方案2:
20片
每片5MB
效率更高。
企业经验:
| 文件类型 | Chunk大小 |
|---|---|
| 图片 | 不用分片 |
| 100MB以内 | 1MB |
| 视频 | 5MB~10MB |
| 超大文件 | 10MB~50MB |
5.7 UploadId设计
为什么需要 UploadId?
因为:
服务器必须知道:
这些 Chunk:
属于哪个文件。
例如:
上传视频:
video.mp4
初始化:
客户端:
POST /upload/init
发送:
{
"name":"video.mp4",
"size":102400000,
"md5":"xxxx"
}
服务器返回:
{
"uploadId":
"abc123"
}
后续:
每个分片:
带:
uploadId
例如:
POST /upload/chunk
uploadId=abc123
chunkIndex=1
5.8 分片上传接口设计
推荐三个接口。
1. 初始化上传
请求:
POST
/upload/init
参数:
{
"name":"test.mp4",
"size":10000000,
"md5":"xxxxx"
}
返回:
{
"uploadId":"10001"
}
2. 上传分片
请求:
POST
/upload/chunk
参数:
uploadId
chunkIndex
chunkData
返回:
{
"chunkIndex":1,
"etag":"xxxx"
}
3. 合并文件
请求:
POST
/upload/merge
参数:
{
"uploadId":"10001",
"chunks":[
{
"index":0,
"etag":"xxx"
}
]
}
5.9 HarmonyOS读取指定分片
关键:
不要:
readAll()
而是:
offset
+
length
代码:
export function readChunk(
uri:string,
offset:number,
size:number
):ArrayBuffer{
let fd =
fileIo.openSync(
uri,
fileIo.OpenMode.READ_ONLY
)
let buffer =
new ArrayBuffer(size)
fileIo.readSync(
fd.fd,
buffer,
{
offset:offset
}
)
fileIo.closeSync(fd)
return buffer
}
例如:
读取:
第5片
位置:
5MB
大小:
1MB
返回:
ArrayBuffer(1MB)
5.10 分片上传器设计
创建:
ChunkUploader.ets
结构:
export class ChunkUploader {
async upload(
task:UploadTask
){
//1 创建分片
let chunks =
ChunkUtil.createChunks(
task.file.size
)
//2 获取UploadId
let uploadId =
await this.init(task)
//3 上传分片
for(
let chunk of chunks
){
await this.uploadChunk(
uploadId,
chunk
)
}
//4 合并
return await
this.merge(uploadId)
}
}
5.11 并发上传优化
上面的代码:
串行:
chunk1
↓
chunk2
↓
chunk3
速度慢。
企业:
控制并发。
例如:
同时:
3个chunk
效果:
chunk1 上传
chunk2 上传
chunk3 上传
完成后:
chunk4加入
设计:
TaskQueue
例如:
const MAX_UPLOAD=3
队列:
while(queue.length){
let tasks =
queue.splice(
0,
MAX_UPLOAD
)
await Promise.all(
tasks.map(
item=>upload(item)
)
)
}
5.12 分片状态保存
断点续传核心:
保存:
哪些已经完成。
例如:
本地:
preferences
{
uploadId:
"abc123",
chunks:
[
0,
1,
2,
5
]
}
重新打开APP:
查询:
chunk 0 已完成
chunk 1 已完成
chunk 2 已完成
chunk 3 未完成
继续:
chunk3
第六章 HarmonyOS NEXT 断点续传完整实现
大文件上传最重要的能力:
不是上传。
而是:
上传失败以后,能够继续上传。
企业 App:
- 云盘
- 视频平台
- IM
- 企业网盘
- OA系统
几乎都必须支持:
断点续传。
6.1 什么是断点续传?
普通上传:
文件
↓
HTTP
↓
服务器
失败:
重新开始
断点续传:
文件
↓
切片
↓
chunk0 √
chunk1 √
chunk2 √
chunk3 ×
网络恢复
继续:
chunk3
核心思想:
保存:
已经上传成功的部分
6.2 断点续传整体流程
完整流程:
第一次上传
选择文件
|
计算文件MD5
|
请求初始化
|
服务器返回 uploadId
|
创建chunk
|
上传chunk
|
保存chunk状态
|
完成
异常退出:
重新打开APP
选择同一个文件
|
计算MD5
|
查询本地上传记录
|
恢复uploadId
|
查询服务器已完成chunk
|
继续上传剩余chunk
6.3 为什么需要文件MD5?
问题:
如何判断:
是不是同一个文件?
例如:
用户上传:
video.mp4
上传50%。
第二天:
又选择:
video.mp4
系统需要知道:
是不是同一个。
不能靠:
文件名
因为:
两个文件可能:
test.mp4
内容不同
使用:
MD5:
文件内容指纹
例如:
文件A:
video.mp4
MD5:
a8237xxx
文件B:
video.mp4
MD5:
b9288xxx
不同。
6.4 HarmonyOS 文件MD5计算
引入:
import {
cryptoFramework
}
from '@kit.CryptoArchitectureKit'
但是:
大文件不能:
一次读取。
错误:
let buffer =
readAll(file)
例如:
2GB:
直接爆内存。
正确:
流式计算。
流程:
文件
↓
chunk1
↓
MD5.update()
↓
chunk2
↓
MD5.update()
↓
最终digest
6.5 HashUtil封装
创建:
HashUtil.ets
代码:
export class HashUtil {
static async md5(
uri:string
):Promise<string>{
let md5 =
cryptoFramework
.createMd(
"MD5"
)
let file =
fileIo.openSync(
uri
)
let bufferSize =
1024*1024
while(true){
let buffer =
new ArrayBuffer(
bufferSize
)
let length =
fileIo.readSync(
file.fd,
buffer
)
if(length<=0){
break
}
await md5.update(
{
data:
new Uint8Array(buffer,0,length)
}
)
}
let result =
await md5.digest()
return this.hex(
result.data
)
}
static hex(
data:ArrayBuffer
){
let arr =
new Uint8Array(data)
return Array.from(
arr
)
.map(
b=>
b.toString(16)
.padStart(2,'0')
)
.join('')
}
}
得到:
MD5:
e10adc3949ba59abbe56e057f20f883e
6.6 上传记录模型设计
需要保存:
文件信息
uploadId
已完成chunk
创建时间
状态
创建:
UploadRecord.ets
代码:
export class UploadRecord {
fileMd5:string=""
uploadId:string=""
fileName:string=""
fileSize:number=0
uploadedChunks:number[]=[]
createTime:number=0
}
例如:
{
"fileMd5":
"abc123",
"uploadId":
"upload001",
"uploadedChunks":[
0,
1,
2,
3
]
}
6.7 本地保存上传状态
HarmonyOS:
推荐:
Preferences。
导入:
import {
preferences
}
from '@kit.ArkData'
创建:
let store =
await preferences.getPreferences(
context,
{
name:"upload"
}
)
保存:
await store.put(
fileMd5,
JSON.stringify(record)
)
await store.flush()
读取:
let data =
await store.get(
fileMd5,
''
)
6.8 ResumeManager设计
创建:
ResumeManager.ets
职责:
- 保存任务
- 查询任务
- 删除任务
- 恢复任务
代码:
export class ResumeManager {
async save(
record:UploadRecord
){
let store =
await this.getStore()
await store.put(
record.fileMd5,
JSON.stringify(record)
)
await store.flush()
}
async get(
md5:string
){
let store =
await this.getStore()
let value =
await store.get(
md5,
''
)
if(value){
return JSON.parse(
value as string
)
}
return null
}
}
6.9 上传过程中保存状态
上传一个chunk:
async uploadChunk(
chunk:ChunkInfo
){
await request(chunk)
record.uploadedChunks
.push(
chunk.index
)
await resumeManager.save(
record
)
}
关键:
不是全部完成才保存。
而是:
每成功一个:
保存一次。
6.10 APP异常退出恢复
场景:
上传:
100个chunk
完成80个
APP:
被系统杀死
重新打开:
流程:
启动APP
↓
读取上传记录
↓
找到未完成任务
↓
展示:
继续上传?
↓
恢复
查询:
let records =
await resumeManager.list()
展示:
视频上传
80%
[继续]
6.11 网络变化监听
移动端:
网络经常变化:
WiFi
↓
4G
↓
无网络
上传应该暂停。
HarmonyOS:
监听网络状态。
import {
connection
}
from '@kit.NetworkKit'
获取:
let net =
connection.createNetConnection()
监听:
net.on(
'netAvailable',
()=>{
resume()
}
)
无网络:
pause()
6.12 上传暂停设计
用户点击:
暂停
不是取消。
区别:
暂停:
保存状态。
取消:
删除状态。
Task:
增加:
paused:boolean=false
上传循环:
while(chunks){
if(task.paused){
break
}
upload()
}
6.13 上传取消设计
取消:
删除服务器临时文件
删除本地记录
释放资源
接口:
cancelUpload(
uploadId
)
服务器:
delete uploadId
6.14 秒传设计
大型云盘:
为什么上传几秒完成?
因为:
服务器已经存在。
流程:
计算MD5
↓
服务器查询
↓
存在
↓
直接返回URL
接口:
POST
/upload/check
参数:
{
"md5":"abc123",
"size":100000
}
返回:
存在:
{
exists:true,
url:"https://xxx.com/a.mp4"
}
不存在:
{
exists:false,
uploadId:"xxx"
}
6.15 断点续传核心代码整合
最终:
async upload(
file:UploadFile
){
let md5 =
await HashUtil.md5(
file.uri
)
let exists =
await api.check(
md5
)
if(exists){
return exists.url
}
let uploadId =
await api.init(
file
)
let chunks =
ChunkUtil.createChunks(
file.size
)
for(
let chunk of chunks
){
if(
record.uploadedChunks
.includes(
chunk.index
)
){
continue
}
let data =
FileUtil.readChunk(
file.uri,
chunk.offset,
chunk.size
)
await api.uploadChunk(
uploadId,
chunk.index,
data
)
record.uploadedChunks
.push(
chunk.index
)
await resume.save(
record
)
}
return api.merge(
uploadId
)
}
6.16 企业级断点续传优化点
生产环境还需要:
1. 上传速度限制
避免:
上传占满网络
增加:
speedLimit
2. 后台上传
APP进入后台:
继续。
使用:
BackgroundTask
3. 上传任务恢复
APP启动:
自动扫描:
未完成任务
4. 服务端校验
客户端:
不能相信。
服务器:
校验:
- MD5
- Size
- Chunk数量
- ETag
第七章 HarmonyOS NEXT 图片上传优化实战
图片上传是移动端最常见的文件上传场景。
但是:
图片上传 ≠ 选择图片 + 上传。
企业级图片上传流程:
用户选择图片
↓
读取Uri
↓
解析图片信息
↓
获取EXIF
↓
旋转纠正
↓
尺寸检测
↓
压缩
↓
格式转换
↓
生成缩略图
↓
计算MD5
↓
上传OSS
↓
返回URL
7.1 为什么图片一定要压缩?
手机照片越来越大。
例如:
华为旗舰手机:
RAW照片
20MB+
普通拍照:
4000 x 3000
8MB
但是业务展示:
可能只需要:
1080 x 1080
300KB
如果不压缩:
100万用户:
每天上传:
100万 × 8MB
存储:
8TB/天
CDN:
成本巨大。
7.2 图片处理架构
企业图片Pipeline:
Uri
|
↓
ImageSource
|
↓
PixelMap
|
----------------
| |
↓ ↓
Resize Compress
|
↓
JPEG
|
↓
Upload
7.3 HarmonyOS 图片核心对象
HarmonyOS 图片处理:
核心:
ImageSource
PixelMap
ImagePacker
ImageSource
作用:
读取图片源。
例如:
jpg
png
webp
PixelMap
作用:
图片内存对象。
可以:
- 缩放
- 裁剪
- 旋转
- 修改像素
ImagePacker
作用:
编码。
例如:
PixelMap:
↓
JPEG
7.4 创建图片解析工具
目录:
common/image
ImageUtil.ets
导入:
import {
image
}
from '@kit.ImageKit'
7.5 Uri读取ImageSource
代码:
export class ImageUtil {
static async createSource(
uri:string
){
let source =
image.createImageSource(
uri
)
return source
}
}
得到:
ImageSource
7.6 获取图片宽高
为什么需要?
例如:
用户上传:
6000x4000
需要判断:
是否压缩。
代码:
let info =
await source.getImageInfo()
console.log(
info.size.width
)
console.log(
info.size.height
)
返回:
{
width:6000,
height:4000
}
7.7 创建 PixelMap
ImageSource:
不是图片。
需要转换。
代码:
let pixelMap =
await source.createPixelMap()
现在:
ImageSource
↓
PixelMap
PixelMap:
可以操作:
像素
尺寸
方向
颜色
7.8 图片尺寸压缩
目标:
最大宽:
1080
例如:
原图:
4000x3000
比例:
4:3
计算:
宽:
1080
高:
810
缩放参数:
let options =
{
size:{
width:1080,
height:810
}
}
创建:
let pixelMap =
await source.createPixelMap(
options
)
7.9 自动计算缩放比例
不要写死。
工具:
export function calculateSize(
width:number,
height:number
){
const MAX=1080
if(
width<=MAX &&
height<=MAX
){
return {
width,
height
}
}
let scale =
MAX /
Math.max(
width,
height
)
return {
width:
Math.floor(
width*scale
),
height:
Math.floor(
height*scale
)
}
}
例如:
输入:
4000x3000
输出:
1080x810
输入:
800x600
输出:
800x600
无需压缩。
7.10 图片质量压缩
尺寸压缩:
减少:
像素数量。
质量压缩:
减少:
编码质量。
例如:
JPEG:
100%
↓
80%
↓
60%
企业推荐:
头像:
80%
商品图片:
85%
聊天图片:
70%
7.11 ImagePacker编码
PixelMap:
不能直接上传。
需要:
编码。
流程:
PixelMap
↓
ImagePacker
↓
ArrayBuffer
↓
HTTP/OSS
代码:
let packer =
image.createImagePacker()
配置:
let options = {
format:"image/jpeg",
quality:80
}
编码:
let buffer =
await packer.packToData(
pixelMap,
options
)
得到:
ArrayBuffer
可以上传:
upload(buffer)
7.12 图片压缩完整封装
创建:
CompressUtil.ets
代码:
export class CompressUtil {
static async compressImage(
uri:string
):Promise<ArrayBuffer>{
//1 创建source
let source =
image.createImageSource(
uri
)
//2 获取尺寸
let info =
await source.getImageInfo()
let size =
calculateSize(
info.size.width,
info.size.height
)
//3 创建PixelMap
let pixelMap =
await source.createPixelMap(
{
size
}
)
//4 编码
let packer =
image.createImagePacker()
let buffer =
await packer.packToData(
pixelMap,
{
format:
"image/jpeg",
quality:80
}
)
return buffer
}
}
7.13 上传前Pipeline设计
不要:
选择图片
↓
上传
应该:
UploadManager
|
↓
FileProcessor
|
--------------------
| | |
↓ ↓ ↓
压缩 MD5 校验
|
↓
Uploader
定义:
interface Processor{
process(
file:UploadFile
)
:Promise<UploadFile>
}
图片处理:
class ImageProcessor
implements Processor{
async process(
file
){
let buffer =
await CompressUtil
.compressImage(
file.uri
)
file.buffer=buffer
return file
}
}
7.14 EXIF方向问题
很多图片:
看起来正常。
但是:
上传后:
旋转90度。
原因:
手机照片:
实际像素:
横向保存
但是:
EXIF:
记录:
应该旋转
例如:
照片:
width:
3000
height:
4000
但是:
orientation:
rotate90
上传服务器:
如果忽略:
图片倒置。
处理:
流程:
读取EXIF
↓
orientation
↓
PixelMap.rotate
↓
重新编码
7.15 图片裁剪
常见:
头像。
需求:
正方形
中心裁剪
例如:
原图:
4000x3000
裁剪:
3000x3000
算法:
计算:
left:
(width-height)/2
然后:
PixelMap:
裁剪。
7.16 缩略图生成
列表:
不要加载原图。
例如:
聊天列表:
100张图片
每张:
5MB。
内存:
爆炸。
生成:
thumbnail:
原图
5MB
↓
缩略图
30KB
上传两个:
originalUrl
thumbnailUrl
7.17 微信朋友圈图片方案
类似:
微信:
选择9张。
流程:
9张图片
↓
并行压缩
↓
生成缩略图
↓
上传缩略图
↓
上传原图
↓
返回URL
并发控制:
MAX=3
避免:
一次压缩9张导致:
内存暴涨。
7.18 图片上传最终架构
PhotoViewPicker
|
↓
UploadFile
|
↓
ImageProcessor
|
↓
PixelMap
|
↓
ImagePacker
|
↓
ArrayBuffer
|
↓
OSS Multipart
|
↓
URL
7.19 企业图片上传优化清单
生产环境建议:
| 优化 | 作用 |
|---|---|
| 尺寸压缩 | 降低体积 |
| 质量压缩 | 降低存储 |
| EXIF处理 | 避免旋转 |
| 缩略图 | 提升列表性能 |
| MD5 | 秒传 |
| OSS CDN | 加速 |
| WebP | 减少流量 |
| 分片 | 大图支持 |
8.1 OSS直传核心思想
APP:
不保存:
AccessKey
SecretKey
因为:
一旦泄露:
攻击者可以:
- 上传垃圾文件
- 删除文件
- 下载私密数据
正确:
服务器生成:
STS临时Token
返回:
{
"accessKeyId":"xxx",
"securityToken":"xxx",
"expiration":"xxx",
"endpoint":"oss.xxx.com"
}
APP:
拿这个Token:
上传。
8.2 OSS上传安全模型
完整链路:
用户
|
↓
HarmonyOS APP
|
↓
请求上传权限
|
↓
业务服务器
|
↓
STS服务
|
↓
返回临时凭证
|
↓
APP上传OSS
|
↓
OSS
核心原则:
永远不要把永久密钥放客户端。
8.3 STS临时授权流程
流程:
第一步
APP:
请求:
POST /upload/token
参数:
{
"fileType":"image",
"size":1024000
}
第二步
业务服务器:
调用:
STS AssumeRole
生成:
{
"AccessKeyId":
"TMP.xxx",
"AccessKeySecret":
"xxx",
"SecurityToken":
"xxx"
}
第三步
返回APP:
{
"endpoint":
"https://oss-cn.xxx.com",
"bucket":
"my-bucket",
"objectKey":
"user/avatar/a.png",
"token":
"xxx"
}
8.4 OSS文件路径设计
不要:
/avatar.png
生产环境:
需要目录规划。
推荐:
bucket
|
├── user
| └── avatar
| └── 10001
| └── xxx.jpg
├── video
| └── 2026
| └── 07
└── document
规则:
业务类型
+
用户ID
+
日期
+
随机ID
例如:
user/avatar/10001/20260731/a8sd91.jpg
优势:
- 防止文件覆盖
- 查询方便
- CDN缓存友好
8.5 HarmonyOS OSS上传模块设计
目录:
upload
|
├── OssUploader.ets
├── OssTokenManager.ets
├── OssRequest.ets
└── UploadManager.ets
架构:
UploadManager
|
↓
OssUploader
|
↓
OssRequest
|
↓
OSS
8.6 OssConfig模型
创建:
export class OssConfig {
endpoint:string=""
bucket:string=""
objectKey:string=""
accessKeyId:string=""
accessKeySecret:string=""
securityToken:string=""
}
服务器返回:
转换:
let config =
new OssConfig()
config.endpoint =
data.endpoint
config.bucket =
data.bucket
8.7 OSS HTTP上传原理
OSS本质:
也是HTTP。
请求:
PUT
/bucket/object
Header:
Content-Type:image/jpeg
x-oss-security-token:xxx
Authorization:xxx
Body:
图片二进制
所以:
HarmonyOS:
最终还是:
ArrayBuffer
↓
HTTP PUT
↓
OSS
8.8 OSS签名机制
OSS需要:
证明:
"这个请求是合法用户发起"
所以:
需要签名。
公式:
Signature =
HMAC-SHA1(
AccessKeySecret,
StringToSign
)
StringToSign:
例如:
PUT
image/jpeg
Fri,31 Jul 2026
/xxx/avatar.png
生成:
Authorization
8.9 HarmonyOS OSS Signature实现
创建:
OssSigner.ets
伪代码:
export class OssSigner {
static sign(
secret:string,
content:string
){
let hmac =
crypto.hmacSha1(
secret,
content
)
return Base64.encode(
hmac
)
}
}
最终:
Header:
{
Authorization:
"OSS "
+
accessKeyId
+
":"
+
signature
}
8.10 OSS上传实现
核心:
async upload(
file:UploadFile,
config:OssConfig
){
let request =
http.createHttp()
let response =
await request.request(
config.endpoint
+
"/"
+
config.objectKey,
{
method:
http.RequestMethod.PUT,
header:{
"Content-Type":
file.mimeType,
"x-oss-security-token":
config.securityToken
},
extraData:
file.buffer
}
)
return response
}
8.11 OSS大文件Multipart Upload
普通:
PUT:
适合:
几十MB
大文件:
使用:
Multipart Upload
流程:
初始化
↓
UploadId
↓
上传Part
↓
保存ETag
↓
Complete
8.12 OSS Multipart流程
1 初始化
请求:
POST
?uploads
返回:
<UploadId>
xxxx
</UploadId>
2 上传Part
每片:
PartNumber=1
Data=chunk
返回:
ETag
3 合并
提交:
<Part>
<PartNumber>1</PartNumber>
<ETag>xxx</ETag>
</Part>
OSS:
自动合并。
8.13 HarmonyOS OSS分片上传架构
File
|
↓
ChunkManager
|
↓
PartUploader
|
↓
OSS UploadPart
|
↓
CompleteMultipart
8.14 OSS上传进度
计算:
progress =
uploadedSize /
totalSize
例如:
uploaded:
50MB
total:
100MB
结果:
50%
UI:
Progress({
value:
progress*100
})
8.15 OSS失败重试机制
移动端:
必须重试。
例如:
网络失败:
Part3
失败
不要全部重新上传。
策略:
第一次失败
等待1秒
第二次失败
等待3秒
第三次失败
等待10秒
指数退避:
delay =
2^retry * 1000
8.16 OSS上传状态管理
保存:
{
uploadId:
"xxx",
parts:[
{
number:1,
etag:"xxx"
}
]
}
存储:
HarmonyOS:
Preferences
或者:
RelationalStore数据库
8.17 企业完整上传架构
最终:
APP
|
↓
UploadManager
|
----------------------
| |
HTTPUploader OssUploader
| |
业务服务器 OSS
支持:
- 普通上传
- OSS直传
- 分片上传
- 断点续传
- 秒传
- 进度监听
- 暂停恢复
第九章 HarmonyOS NEXT 文件上传企业级 UploadManager 源码设计
在大型 HarmonyOS NEXT 应用中,文件上传不能散落在页面代码中。
错误方式:
Button(){
onClick(async()=>{
let result =
await http.request(
url
)
})
}
问题:
- 页面耦合网络
- 无法暂停
- 无法恢复
- 无法统一管理
- 无法统计上传状态
企业级设计:
UI层
|
↓
UploadManager
|
-------------------------
| |
HttpUploader OssUploader
| |
HTTP服务器 OSS
9.1 UploadManager职责
UploadManager 是上传中心。
负责:
任务管理
创建上传任务
删除任务
查询任务
生命周期
开始
暂停
继续
取消
完成
失败
调度
同时上传几个文件
哪个优先
哪个等待
状态通知
例如:
页面:
上传中 65%
来自:
UploadManager
9.2 上传任务模型设计
创建:
model
└── UploadTask.ets
代码:
export class UploadTask {
id:string=""
file:UploadFile|null=null
status:
UploadStatus=
UploadStatus.WAITING
progress:number=0
uploadedSize:number=0
totalSize:number=0
retry:number=0
createTime:number=0
}
9.3 上传状态机设计
上传不是简单:
true
false
需要状态。
定义:
export enum UploadStatus {
WAITING="waiting",
UPLOADING="uploading",
PAUSED="paused",
SUCCESS="success",
FAILED="failed",
CANCELLED="cancelled"
}
状态变化:
WAITING
|
↓
UPLOADING
|
----------------
| |
SUCCESS FAILED
暂停:
UPLOADING
|
↓
PAUSED
9.4 UploadTask状态流转
禁止:
非法状态。
例如:
SUCCESS
↓
UPLOADING
不允许。
设计:
class TaskStateMachine{
transition(
task,
next
){
if(
!this.allow(
task.status,
next
)
){
throw Error(
"invalid state"
)
}
task.status=next
}
}
9.5 UploadManager单例设计
整个APP:
一个上传中心。
export class UploadManager {
private static instance:
private tasks:
Map<string,UploadTask>
=new Map()
static getInstance(){
if(!this.instance){
this.instance =
new UploadManager()
}
return this.instance
}
}
使用:
let manager =
UploadManager
.getInstance()
9.6 创建上传任务
方法:
createTask()
代码:
createTask(
file:UploadFile
){
let task =
new UploadTask()
task.id =
uuid()
task.file=file
task.totalSize=
file.size
task.status=
UploadStatus.WAITING
this.tasks.set(
task.id,
task
)
return task
}
返回:
{
id:"task001",
status:"waiting",
progress:0
}
9.7 开始上传
调用:
manager.start(
task.id
)
实现:
async start(
id:string
){
let task =
this.tasks.get(id)
if(!task){
return
}
task.status=
UploadStatus.UPLOADING
await this.execute(
task
)
}
9.8 上传执行器
根据文件类型:
选择:
Uploader。
接口:
export interface Uploader {
upload(
task:UploadTask
)
:
Promise<string>
pause(
task:UploadTask
)
cancel(
task:UploadTask
)
}
实现:
Uploader
|
----------------------------
| |
HttpUploader OssUploader
9.9 UploadManager调用Uploader
代码:
async execute(
task:UploadTask
){
let uploader =
this.getUploader(
task
)
try{
let url =
await uploader.upload(
task
)
task.status=
UploadStatus.SUCCESS
task.progress=100
task.url=url
this.notify(task)
}catch(e){
task.status=
UploadStatus.FAILED
}
}
9.10 上传队列设计
企业:
不能:
10个视频同时上传。
原因:
- 占满带宽
- CPU升高
- 手机发热
设置:
MAX_RUNNING=3
队列:
任务1 上传
任务2 上传
任务3 上传
任务4 等待
任务5 等待
9.11 TaskQueue实现
export class UploadQueue {
waiting:
UploadTask[]=[]
running:number=0
max:number=3
async add(
task
){
this.waiting.push(task)
this.schedule()
}
schedule(){
while(
this.running<this.max
&&
this.waiting.length
){
let task =
this.waiting.shift()
this.running++
this.start(task)
}
}
}
9.12 上传暂停实现
用户:
点击:
暂停
调用:
manager.pause(
taskId
)
代码:
pause(id:string){
let task =
this.tasks.get(id)
task.status=
UploadStatus.PAUSED
this.uploader.pause(
task
)
}
9.13 上传恢复
继续:
resume(taskId)
流程:
PAUSED
|
↓
读取上传记录
|
↓
找到未完成Chunk
|
↓
继续上传
9.14 上传取消
取消:
不是暂停。
区别:
| 操作 | 状态 | 保留 |
|---|---|---|
| 暂停 | PAUSED | 保留 |
| 取消 | CANCELLED | 删除 |
代码:
cancel(id){
let task =
this.tasks.get(id)
task.status=
UploadStatus.CANCELLED
this.uploader.cancel(
task
)
this.tasks.delete(id)
}
9.15 上传事件监听
页面需要:
实时刷新。
例如:
progress:
60%
设计:
EventEmitter。
事件:
upload:start
upload:progress
upload:success
upload:error
upload:cancel
代码:
class UploadEmitter{
listeners=[]
emit(
event,
data
){
this.listeners
.forEach(
fn=>fn(
event,
data
)
)
}
}
9.16 ArkUI绑定上传进度
页面:
@State progress:number=0
监听:
UploadManager
.onProgress(
(task)=>{
this.progress=
task.progress
}
)
UI:
Progress({
value:this.progress
})
效果:
上传中
████████░░
80%
9.17 上传失败自动重试
失败:
不要立即结束。
策略:
第1次失败
↓
1秒
第2次失败
↓
3秒
第3次失败
↓
10秒
代码:
async retry(task){
while(
task.retry<3
){
try{
await upload()
break
}catch(e){
task.retry++
await sleep(
1000*
task.retry
)
}
}
}
9.18 企业级目录结构
推荐:
common
└── upload
├── UploadManager.ets
├── UploadTask.ets
├── UploadQueue.ets
├── UploadStatus.ets
├── HttpUploader.ets
├── OssUploader.ets
├── ResumeManager.ets
├── ChunkUploader.ets
└── UploadEvent.ets
9.19 完整上传链路
最终:
用户选择文件
↓
FilePicker
↓
FileService
↓
UploadManager
↓
TaskQueue
↓
Uploader
↓
HTTP / OSS
↓
Progress Event
↓
ArkUI刷新
↓
完成
第十章 HarmonyOS NEXT 文件上传后台服务与生命周期管理
移动端上传和桌面端最大的区别:
用户不会一直停留在上传页面。
真实场景:
用户上传视频:
开始上传
↓
返回桌面
↓
锁屏
↓
切换APP
↓
重新打开
上传任务仍然应该:
- 保持状态
- 控制资源
- 恢复执行
10.1 为什么需要后台上传?
普通页面上传:
Page
↓
http.request()
↓
上传
问题:
当页面销毁:
Page disappear
↓
对象释放
↓
请求可能终止
例如:
用户:
点击上传视频。
上传:
60%
然后:
按 Home 键。
如果没有后台能力:
上传失败
企业需要:
UI层
↓
上传服务
↓
后台任务
↓
网络请求
10.2 HarmonyOS应用生命周期
ArkUI页面生命周期:
aboutToAppear()
aboutToDisappear()
但是:
它只负责:
页面。
应用生命周期:
id="m1q0rs"
Ability
|
↓
onForeground()
|
↓
onBackground()
关键:
后台切换:
onBackground()
10.3 上传服务独立化
错误:
Page
└── upload()
正确:
Page
|
↓
UploadService
|
↓
UploadManager
|
↓
Uploader
页面:
只负责:
开始
暂停
查看状态
上传:
独立运行。
10.4 UploadService设计
目录:
id="gq3s8x"
upload
└── service
└── UploadService.ets
代码:
export class UploadService {
private manager:
UploadManager
constructor(){
this.manager =
UploadManager
.getInstance()
}
start(task){
return this.manager
.start(task.id)
}
pause(taskId){
this.manager
.pause(taskId)
}
resume(taskId){
this.manager
.resume(taskId)
}
}
页面:
let service =
new UploadService()
service.start(task)
页面销毁:
上传仍然存在。
10.5 后台任务机制
HarmonyOS:
提供:
BackgroundTask。
作用:
告诉系统:
当前APP:
有重要后台任务。
典型:
- 文件上传
- 文件下载
- 音频播放
- 导航
流程:
上传开始
↓
申请后台任务
↓
执行上传
↓
完成
↓
释放后台任务
10.6 后台任务生命周期
开始:
requestBackgroundRunning()
结束:
cancelBackgroundRunning()
例如:
class UploadBackground {
async start(){
await requestBackgroundRunning()
}
async stop(){
await cancelBackgroundRunning()
}
}
10.7 上传状态持久化
后台上传:
必须保存状态。
不能:
let task={
progress:50
}
因为:
内存会丢。
必须:
UploadTask
↓
Preferences
↓
RelationalStore
保存:
{
"id":"001",
"file":"video.mp4",
"progress":50,
"status":"uploading",
"uploadId":"abc"
}
10.8 APP被系统杀死怎么办?
移动端:
系统可能:
低内存
↓
杀死APP
恢复流程:
APP重新启动:
Ability启动
↓
读取UploadRecord
↓
查询服务器状态
↓
恢复任务
代码:
async restore(){
let tasks =
await resumeManager
.getAll()
for(
let task of tasks
){
if(
task.status==="uploading"
){
manager.resume(
task.id
)
}
}
}
10.9 网络状态管理
上传不能:
一直发送。
需要监听:
网络变化
状态:
WiFi
4G
5G
无网络
设计:
enum NetworkType{
WIFI,
CELLULAR,
NONE
}
10.10 WiFi优先上传策略
企业常见:
用户上传大视频。
规则:
图片
↓
任何网络
视频
↓
WiFi优先
例如:
if(
file.size >
500*1024*1024
){
if(
network!==WIFI
){
pause()
}
}
10.11 网络恢复自动继续
断网:
upload
↓
pause
恢复:
network available
↓
resume
监听:
network.onChange(
()=>{
if(
available
){
manager.resumeAll()
}
}
)
10.12 电量策略
视频上传:
非常耗电。
策略:
电量 <20%
↓
暂停大文件上传
模型:
if(
battery.level<20
){
pauseLargeUpload()
}
10.13 上传优先级设计
企业APP:
同时:
头像:
200KB
视频:
2GB
不能:
视频阻塞头像。
增加:
priority。
enum Priority{
HIGH,
NORMAL,
LOW
}
任务:
task.priority=
Priority.HIGH
队列排序:
HIGH
↓
NORMAL
↓
LOW
10.14 上传调度器 Scheduler
设计:
UploadScheduler
|
↓
TaskQueue
|
↓
Worker
Worker数量:
例如:
MAX_WORKER=3
执行:
while(true){
task =
queue.next()
await worker.run(task)
}
10.15 上传Worker设计
代码:
class UploadWorker {
async execute(
task:UploadTask
){
try{
await uploader
.upload(task)
task.status=
SUCCESS
}catch(e){
task.status=
FAILED
}
}
}
10.16 后台上传通知
长时间上传:
需要用户知道。
例如:
系统通知:
正在上传视频
65%
通知内容:
文件:
course.mp4
进度:
65%
10.17 企业上传服务架构
最终:
ArkUI
|
↓
UploadService
|
↓
UploadScheduler
|
----------------
| |
Worker1 Worker2
| |
OSSUploader HttpUploader
|
↓
OSS
10.18 完整生命周期
用户:
选择文件:
PhotoPicker
↓
创建:
UploadTask
↓
加入:
Queue
↓
调度:
Worker
↓
上传:
OSS
↓
后台:
继续执行
↓
完成:
通知UI
10.19 生产环境检查列表
上传稳定性
✅ 断点续传
✅ 分片上传
✅ 自动重试
✅ 网络恢复
✅ 后台任务
性能
✅ 限制并发
✅ 图片压缩
✅ 流式读取
✅ 内存控制
安全
✅ STS临时授权
✅ HTTPS
✅ 文件类型校验
✅ 大小限制
用户体验
✅ 进度显示
✅ 暂停
✅ 恢复
✅ 取消
✅ 秒传
第十一章 HarmonyOS NEXT 文件上传安全体系设计
文件上传看似只是:
选择文件
↓
上传服务器
但是在企业系统中:
上传入口往往是攻击入口。
例如:
用户上传:
头像
合同
视频
附件
攻击者可能上传:
恶意脚本
病毒文件
超大文件
伪造格式文件
非法内容
所以企业上传系统必须建立:
客户端 + 服务端 + 存储 + CDN 全链路安全体系。
11.1 文件上传主要安全风险
风险1:文件类型伪造
例如:
前端限制:
只允许 jpg
攻击者:
修改文件名:
virus.exe
↓
photo.jpg
如果服务器只判断:
file.name.endsWith(".jpg")
直接绕过。
风险2:超大文件攻击
攻击者上传:
50GB文件
导致:
- 带宽耗尽
- 存储爆炸
- 服务异常
风险3:恶意文件上传
例如:
上传:
木马
脚本
病毒
然后:
通过URL访问。
风险4:Token泄露
错误:
APP内:
AccessKey
SecretKey
攻击者反编译:
直接获取。
风险5:文件URL泄露
例如:
OSS:
https://bucket.xxx.com/user/a.png
被公开访问。
11.2 企业安全上传架构
推荐:
APP
|
↓
上传安全模块
|
-------------------
| |
Token校验 文件预处理
| |
↓ ↓
上传网关
|
↓
文件检测服务
|
↓
OSS
|
↓
CDN
11.3 客户端安全校验
客户端校验:
目的:
提升体验。
不能作为最终安全。
客户端检查:
文件大小
例如:
头像:
max=5MB
代码:
if(
file.size >
5*1024*1024
){
throw Error(
"文件过大"
)
}
文件扩展名
例如:
.jpg
.png
.webp
但是:
只是第一层。
11.4 MIME Type校验
文件:
包含:
Content-Type
例如:
图片:
image/jpeg
客户端:
if(
!mime.startsWith(
"image/"
)
){
reject()
}
但是:
MIME也可以伪造。
11.5 文件Magic Number检测
真正判断文件类型:
看文件头。
例如:
JPEG:
FF D8 FF
PNG:
89 50 4E 47
PDF:
25 50 44 46
流程:
文件
↓
读取前16字节
↓
Magic Number
↓
判断真实类型
例如:
let header =
buffer.slice(
0,
16
)
服务器:
必须做。
11.6 文件名称安全处理
危险:
用户上传:
../../test.exe
服务器:
不要直接使用:
原文件名
生成:
UUID
+
扩展名
例如:
8f82a.jpg
推荐:
objectKey
=
业务目录
+
用户ID
+
UUID
11.7 OSS STS安全设计
客户端:
获得:
临时权限。
但是:
权限必须最小化。
错误:
{
Action:[
"*"
]
}
正确:
{
Action:[
"oss:PutObject"
],
Resource:[
"user/avatar/*"
]
}
只允许:
上传头像。
不能:
删除文件。
11.8 STS有效时间
不要:
24小时
推荐:
5分钟
15分钟
例如:
{
expiration:
"900"
}
上传完成:
立即失效。
11.9 上传接口防重放
攻击:
抓包:
上传请求
重复发送。
方案:
增加:
nonce。
例如:
请求:
{
timestamp:1720000000,
nonce:"abc123"
}
服务器:
检查:
是否已经使用
11.10 HTTPS传输
上传必须:
HTTPS
禁止:
HTTP
原因:
文件包含:
- 身份信息
- 合同
- 图片
- 视频
11.11 文件病毒扫描
企业:
上传流程:
增加:
AV扫描。
架构:
APP
↓
OSS临时目录
↓
病毒扫描
↓
通过
↓
正式目录
状态:
pending
↓
scanning
↓
safe
↓
available
11.12 图片内容审核
例如:
用户头像。
需要:
检测:
- 色情
- 暴恐
- 涉政
- 违规内容
流程:
上传图片
↓
AI审核
↓
通过
↓
展示
11.13 文件访问权限设计
错误:
public-read
所有人访问。
企业:
推荐:
private。
访问:
生成:
Signed URL
例如:
有效:
10分钟
URL:
https://xxx.com/file?a=signature
11.14 防盗链设计
CDN:
检查:
Referer。
或者:
Token。
例如:
请求:
url
+
expire
+
sign
服务器:
验证:
sign是否正确
11.15 上传日志审计
企业必须记录:
谁
什么时候
上传什么
大小
IP
结果
数据库:
upload_log
id
user_id
file_name
file_size
status
created_time
11.16 防止恶意占用存储
限制:
用户配额。
例如:
普通用户:
5GB
VIP:
100GB
上传前:
检查:
当前使用量
+
文件大小
<=
额度
11.17 企业上传安全流程
完整流程:
用户选择文件
↓
客户端基础校验
↓
获取STS
↓
上传临时空间
↓
服务端检测
↓
病毒扫描
↓
内容审核
↓
移动正式目录
↓
生成访问URL
↓
记录日志
第十二章 HarmonyOS NEXT 文件上传性能优化与压测体系
文件上传系统从 Demo 到企业级,最大的区别:
不是:
“能不能上传”。
而是:
在百万用户、高并发、大文件环境下,依然稳定上传。
企业级上传系统需要解决:
- 上传速度
- 内存占用
- CPU消耗
- 网络波动
- 并发压力
- 服务容量
- 存储成本
12.1 上传性能模型
上传速度由多个因素决定:
实际上传速度
=
min(
客户端读取速度,
网络速度,
HTTP速度,
服务器接收速度,
OSS写入速度
)
例如:
手机:
读取:
200MB/s
网络:
20MB/s
那么:
最大:
20MB/s
所以:
优化不是单点优化。
需要全链路。
12.2 文件读取性能优化
错误:
一次读取整个文件:
let buffer =
readFile(
path
)
问题:
100MB:
还能接受。
2GB:
直接:
内存爆炸
企业方案:
流式读取。
File
↓
1MB
↓
上传
↓
释放
↓
继续读取
12.3 Chunk大小设计
分片大小:
影响:
- 上传速度
- 请求数量
- 内存
太小:
例如:
64KB
问题:
请求太多。
100GB:
需要:
百万请求。
太大:
例如:
1GB
问题:
失败重传成本高。
推荐:
| 文件大小 | Chunk |
|---|---|
| <100MB | 不用分片 |
| 100MB-1GB | 5MB |
| 1GB-10GB | 10MB-50MB |
| 10GB以上 | 50MB+ |
12.4 动态Chunk策略
不要固定。
根据文件大小:
计算。
function getChunkSize(
size:number
){
if(size<100*1024*1024){
return size
}
if(size<1024*1024*1024){
return 5*1024*1024
}
return 20*1024*1024
}
优势:
小文件:
快速完成。
大文件:
稳定。
12.5 上传并发优化
单线程:
chunk1
↓
chunk2
↓
chunk3
速度:
慢。
多线程:
chunk1 --->
chunk2 --->
chunk3 --->
但是:
不是越多越好。
例如:
10个并发:
可能:
- 手机发热
- 网络拥堵
- 电量下降
移动端推荐:
WiFi:
3~5并发
4G:
2~3并发
12.6 并发控制器设计
创建:
ConcurrencyLimiter.ets
代码:
export class ConcurrencyLimiter {
max:number=3
running:number=0
queue:Function[]=[]
async execute(
task:Function
){
if(
this.running>=this.max
){
await new Promise(
resolve=>{
this.queue.push(
resolve
)
}
)
}
this.running++
try{
return await task()
}finally{
this.running--
let next =
this.queue.shift()
if(next){
next()
}
}
}
}
效果:
始终:
最多3个上传
12.7 HTTP连接优化
频繁创建:
http.createHttp()
不好。
原因:
每次:
- DNS
- TCP连接
- TLS握手
优化:
连接复用。
HttpClient
长期存在
减少:
连接建立成本
12.8 Keep-Alive优化
HTTP:
开启:
Connection:
keep-alive
效果:
多个Chunk:
复用连接。
12.9 上传内存优化
图片:
最大问题。
错误:
10张图片
↓
全部解码
↓
上传
内存:
可能:
500MB。
正确:
流水线。
图片1
↓
压缩
↓
上传
释放
图片2
类似:
生产流水线。
12.10 PixelMap释放
HarmonyOS图片对象:
占用Native内存。
完成:
需要释放。
例如:
pixelMap.release()
否则:
大量图片:
可能:
OOM。
12.11 上传队列优化
普通队列:
先进先出:
视频
↓
头像
↓
文本
问题:
头像等待。
优先队列:
HIGH
头像
NORMAL
图片
LOW
视频
12.12 上传速度预测
显示:
剩余:
2分钟
需要:
计算。
公式:
speed
=
uploadedBytes
/
time
remaining
=
remainBytes
/
speed
例如:
已经:
100MB
耗时:
10秒。
速度:
10MB/s
剩余:
900MB。
预计:
90秒。
12.13 失败重试优化
不要:
所有错误都重试。
分类:
网络错误
重试:
YES
权限错误
例如:
403。
不要:
无限重试
文件错误
例如:
损坏。
直接失败。
错误分类:
enum ErrorType{
NETWORK,
AUTH,
FILE,
SERVER
}
12.14 上传压测指标
企业关注:
成功率
公式:
成功上传次数
/
总上传次数
目标:
99.9%
平均速度
例如:
平均:
8MB/s
P95耗时
例如:
90%:
5秒完成。
95%:
10秒完成。
失败率
目标:
<0.1%
12.15 百万用户上传架构
大型应用:
架构:
用户
|
↓
API Gateway
|
-----------------
| |
Token服务 上传服务
| |
--------OSS------
|
CDN加速
12.16 上传服务水平扩展
不要:
单服务器。
应该:
Upload Server 1
Upload Server 2
Upload Server 3
无状态:
服务器:
不保存上传状态。
状态:
放:
- Redis
- DB
- OSS
12.17 Redis管理上传状态
例如:
upload:task:001
{
status:"uploading",
progress:70
}
优势:
多个服务器共享。
12.18 监控体系设计
企业必须监控:
上传数量
QPS
上传速度
MB/s
错误
401
403
500
OSS状态
请求成功率
12.19 链路追踪
一次上传:
生成:
traceId。
例如:
upload-20260731-001
贯穿:
APP
↓
API
↓
STS
↓
OSS
出现问题:
快速定位。
12.20 企业上传最佳实践总结
架构:
UploadManager
↓
Scheduler
↓
Worker
↓
ChunkUploader
↓
OSS
第十三章 HarmonyOS NEXT 文件上传综合实战:打造类似微信/网盘上传框架
13.1 企业上传SDK整体设计
最终效果:
业务页面:
只需要:
UploadManager.upload({
file,
type:"image"
})
即可。
内部:
自动完成:
选择文件
↓
任务创建
↓
文件检测
↓
图片处理
↓
计算Hash
↓
判断秒传
↓
获取STS
↓
上传OSS
↓
断点保存
↓
完成回调
13.2 SDK整体架构
推荐目录:
UploadSDK
├── core
│ ├── UploadManager.ets
│ ├── UploadTask.ets
│ ├── UploadQueue.ets
│ ├── UploadScheduler.ets
│
├── uploader
│ ├── BaseUploader.ets
│ ├── HttpUploader.ets
│ ├── OssUploader.ets
│ ├── MultipartUploader.ets
│
├── processor
│ ├── ImageProcessor.ets
│ ├── VideoProcessor.ets
│ ├── FileProcessor.ets
│
├── storage
│ ├── ResumeStore.ets
│ ├── UploadDatabase.ets
│
├── security
│ ├── TokenManager.ets
│ ├── FileValidator.ets
│
└── utils
├── HashUtil.ets
├── ChunkUtil.ets
13.3 核心接口设计
企业SDK:
必须面向接口。
不要:
new OssUploader()
写死。
定义:
export interface IUploader {
upload(
task:UploadTask
)
:
Promise<UploadResult>
pause(
taskId:string
)
resume(
taskId:string
)
cancel(
taskId:string
)
}
未来支持:
OSS
↓
腾讯COS
↓
AWS S3
↓
自建服务器
只需要:
增加实现。
13.4 UploadTask完整设计
任务:
不是简单对象。
包含:
生命周期。
export class UploadTask {
id:string
file:UploadFile
status:UploadStatus
progress:number=0
speed:number=0
remainingTime:number=0
hash:string
uploadId:string
chunks:Chunk[]
createdAt:number
updatedAt:number
}
例如:
运行时:
{
"id":"task1001",
"status":"uploading",
"progress":65,
"speed":5242880,
"remainingTime":120
}
13.5 UploadManager核心实现
单例:
export class UploadManager {
private static instance:
UploadManager
private uploader:IUploader
private scheduler:
UploadScheduler
static getInstance(){
if(!this.instance){
this.instance=
new UploadManager()
}
return this.instance
}
}
13.6 初始化SDK
App启动:
初始化:
UploadManager.init({
oss:true,
endpoint:
"https://oss.xxx.com"
})
内部:
创建:
TokenManager
ResumeStore
Scheduler
Uploader
13.7 上传入口设计
业务调用:
let task =
UploadManager
.upload({
uri:
"file://xxx.jpg",
type:
"image"
})
内部流程:
async upload(options){
let task =
TaskFactory.create(options)
queue.add(task)
return task.id
}
13.8 TaskFactory任务工厂
为什么需要?
不同文件:
不同策略。
例如:
图片:
压缩
↓
上传
视频:
切片
↓
上传
代码:
class TaskFactory {
create(
options
){
let task=
new UploadTask()
task.id=
uuid()
task.file=
options.file
return task
}
}
13.9 Processor处理链设计
类似:
Java:
责任链。
流程:
File
↓
Validator
↓
Compress
↓
Encrypt
↓
Upload
定义:
interface Processor{
next?:Processor
process(
task
)
}
组合:
validator
.next(
compress
)
.next(
encrypt
)
13.10 图片处理Processor
class ImageProcessor{
async process(
task
){
if(
task.file.type
==="image"
){
task.file.buffer=
await compress(
task.file
)
}
return task
}
}
13.11 视频上传Processor
视频:
通常:
不压缩原文件。
主要:
生成:
封面
缩略图
流程:
video.mp4
↓
extractFrame()
↓
cover.jpg
↓
upload
13.12 ChunkManager设计
大文件核心。
创建:
class ChunkManager{
create(
size:number
){
let chunks=[]
let chunkSize=
10*1024*1024
let count=
Math.ceil(
size/chunkSize
)
for(
let i=0;i<count;i++
){
chunks.push({
index:i,
offset:i*chunkSize,
size:chunkSize
})
}
return chunks
}
}
例如:
文件:
1GB
分片:
100个chunk
13.13 MultipartUploader设计
负责:
OSS分片。
流程:
init
↓
uploadPart
↓
complete
代码结构:
class MultipartUploader
implements IUploader{
async upload(task){
let uploadId=
await init(task)
for(
let chunk of task.chunks
){
await uploadPart(
uploadId,
chunk
)
}
return complete(
uploadId
)
}
}
13.14 断点恢复机制
核心:
保存:
{
taskId:"001",
uploadId:"xxx",
completed:[
1,
2,
3
]
}
恢复:
let record =
resumeStore.get(
task.id
)
continueUpload(
record
)
13.15 上传事件系统
SDK:
提供:
task.on(
"progress",
callback
)
事件:
created
waiting
uploading
progress
paused
success
failed
cancelled
实现:
class UploadEvent {
emit(
name,
data
){
}
on(
name,
callback
){
}
}
13.16 ArkUI上传组件封装
业务:
不想写:
监听逻辑。
封装:
UploadProgressView
使用:
UploadProgressView({
taskId:"001"
})
显示:
视频上传中
███████░░░
70%
速度:
8MB/s
剩余:
30秒
13.17 多文件上传
例如:
朋友圈:
9张。
调用:
UploadManager.uploadBatch([
file1,
file2,
file3
])
返回:
[
{
id:"1"
},
{
id:"2"
}
]
内部:
加入队列。
13.18 上传优先级
定义:
enum Priority{
URGENT=10,
HIGH=5,
NORMAL=1
}
例如:
头像:
priority=10
视频:
priority=1
13.19 SDK最终调用体验
业务页面:
const task =
await uploadManager.upload({
file,
priority:
"HIGH"
})
task.onProgress(
(progress)=>{
this.progress=
progress
}
)
业务层:
完全不知道:
- OSS
- HTTP
- Chunk
- Token
- Resume
13.20 本章架构总结
最终企业上传SDK:
UploadManager
|
UploadScheduler
|
UploadTask Queue
|
----------------------------
| |
ImageProcessor ChunkProcessor
| |
--------Uploader-----------
|
----------------------
| |
HttpUploader OssUploader
|
OSS
第十四章 HarmonyOS NEXT 上传SDK数据库持久化设计与断点续传引擎
企业级上传系统最大的区别:
不是上传过程。
而是:
上传任务能够跨越页面、进程、设备状态持续存在。
例如:
用户上传一个:
video.mp4
大小:
5GB
上传到:
70%
此时:
- APP被杀死
- 手机重启
- 网络断开
- 页面关闭
再次打开:
仍然可以:
继续70%
这需要:
上传任务数据库。
14.1 为什么不能只使用Preferences?
前面简单场景:
Preferences:
可以。
例如:
{
"uploadId":"xxx",
"progress":50
}
但是企业环境:
任务数量:
可能:
1000+
同时存在:
- 视频上传
- 图片上传
- 文档上传
Preferences问题:
1. 查询困难
例如:
查询:
所有正在上传任务
需要:
遍历。
2. 数据结构复杂
一个任务:
包含:
任务信息
分片信息
失败记录
重试次数
速度统计
3. 并发安全
多个上传Worker:
同时更新状态。
企业方案:
使用:
RelationalStore
14.2 数据库设计
设计三张核心表:
upload_task
upload_chunk
upload_log
关系:
upload_task
1
|
|
N
upload_chunk
14.3 upload_task任务表
字段:
| 字段 | 说明 |
|---|---|
| id | 任务ID |
| file_name | 文件名 |
| file_uri | 文件地址 |
| file_size | 文件大小 |
| file_md5 | 文件Hash |
| upload_id | OSS UploadId |
| status | 状态 |
| progress | 进度 |
| create_time | 创建时间 |
SQL:
CREATE TABLE upload_task(
id TEXT PRIMARY KEY,
file_name TEXT,
file_uri TEXT,
file_size INTEGER,
file_md5 TEXT,
upload_id TEXT,
status TEXT,
progress INTEGER,
create_time INTEGER
)
14.4 upload_chunk分片表
一个大文件:
对应多个chunk。
例如:
video.mp4
chunk0
chunk1
chunk2
chunk3
表:
CREATE TABLE upload_chunk(
id INTEGER PRIMARY KEY,
task_id TEXT,
chunk_index INTEGER,
offset INTEGER,
size INTEGER,
etag TEXT,
status TEXT
)
数据:
task_id:
001
chunk_index:
0
status:
success
14.5 upload_log上传日志表
用于:
问题排查。
例如:
为什么失败?
表:
CREATE TABLE upload_log(
id INTEGER PRIMARY KEY,
task_id TEXT,
event TEXT,
message TEXT,
time INTEGER
)
记录:
task001
chunk3 upload failed
network timeout
14.6 HarmonyOS RelationalStore初始化
导入:
import {
relationalStore
}
from '@kit.ArkData'
创建数据库:
let config = {
name:
"upload.db",
securityLevel:
relationalStore.SecurityLevel.S1
}
let store =
await relationalStore.getRdbStore(
context,
config
)
14.7 DatabaseManager封装
创建:
database
|
DatabaseManager.ets
代码:
export class DatabaseManager {
private store:
relationalStore.RdbStore
async init(context){
this.store =
await relationalStore.getRdbStore(
context,
{
name:"upload.db",
version:1
}
)
}
}
14.8 保存上传任务
方法:
saveTask()
代码:
async saveTask(
task:UploadTask
){
let value = {
"id":
task.id,
"file_name":
task.file.name,
"file_size":
task.file.size,
"status":
task.status,
"progress":
task.progress
}
await this.store.insert(
"upload_task",
value
)
}
14.9 查询未完成任务
APP启动:
执行:
SELECT *
FROM upload_task
WHERE status != 'success'
代码:
async getUnfinished(){
let result =
await this.store.querySql(
`
SELECT *
FROM upload_task
WHERE status != ?
`,
[
"success"
]
)
return result
}
返回:
[
{
"id":"001",
"status":"uploading",
"progress":70
}
]
14.10 Chunk状态恢复
例如:
文件:
10个分片。
数据库:
chunk0 success
chunk1 success
chunk2 success
chunk3 uploading
chunk4 waiting
恢复:
跳过:
chunk0
chunk1
chunk2
继续:
chunk3
chunk4
查询:
SELECT *
FROM upload_chunk
WHERE
task_id=?
AND
status!='success'
14.11 上传状态机持久化
状态:
enum UploadStatus{
WAITING,
UPLOADING,
PAUSED,
SUCCESS,
FAILED
}
每次变化:
保存:
task.status=
UploadStatus.UPLOADING
await db.update(task)
避免:
内存状态丢失。
14.12 崩溃恢复流程
APP启动:
Ability启动
↓
DatabaseManager.init()
↓
查询upload_task
↓
过滤未完成任务
↓
恢复UploadTask
↓
重新加入Scheduler
↓
继续上传
代码:
async restoreTasks(){
let tasks=
await database
.getUnfinished()
for(
let task of tasks
){
scheduler.add(task)
}
}
14.13 Chunk上传状态更新
上传成功:
不要:
全部完成再保存。
应该:
每个Chunk:
立即更新。
流程:
上传chunk5
↓
OSS返回ETag
↓
更新数据库
↓
继续chunk6
代码:
async completeChunk(
chunk
){
chunk.status=
"success"
await db.updateChunk(
chunk
)
}
14.14 防止重复上传
场景:
两个页面:
同时上传同一个文件。
需要:
任务去重。
根据:
fileMd5
查询:
SELECT *
FROM upload_task
WHERE file_md5=?
存在:
直接返回:
已有任务。
14.15 秒传查询流程
上传前:
计算MD5
↓
查询本地数据库
↓
查询服务器
↓
存在
↓
直接完成
状态:
task.status=
SUCCESS
14.16 数据库事务
批量更新:
例如:
100个chunk。
不要:
一个个提交。
使用事务:
await store.beginTransaction()
try{
updateChunk()
updateTask()
await store.commit()
}catch(e){
await store.rollBack()
}
14.17 数据清理策略
数据库不能无限增长。
规则:
成功任务:
保存:
7天。
失败任务:
保存:
30天。
定时清理:
DELETE FROM upload_task
WHERE
create_time < xxx
14.18 企业级恢复能力
最终支持:
| 场景 | 恢复 |
|---|---|
| 页面关闭 | ✅ |
| APP后台 | ✅ |
| APP重启 | ✅ |
| 手机重启 | ✅ |
| 网络断开 | ✅ |
| 系统杀进程 | ✅ |
14.19 UploadDatabase最终结构
UploadDatabase
|
|
-----------------------
| | |
Task Chunk Log
| |
| |
任务状态 分片状态
14.20 完整恢复架构
APP启动
|
↓
UploadDatabase
|
↓
查询未完成Task
|
↓
恢复Chunk状态
|
↓
Scheduler重新调度
|
↓
Worker继续上传
第十五章 HarmonyOS NEXT UploadScheduler 并发调度算法实现
企业上传系统中,真正复杂的部分不是 HTTP 请求。
而是:
如何让几十、几百个上传任务,在有限网络、CPU、电量条件下稳定运行。
例如:
用户一次选择:
20张图片
3个视频
5个PDF
如果全部同时上传:
结果:
网络拥堵
手机发热
耗电增加
APP卡顿
所以需要:
UploadScheduler 上传调度器。
15.1 Scheduler职责
UploadScheduler负责:
任务排队
等待上传任务
↓
队列
↓
执行
并发控制
例如:
同时:
最多3个上传
优先级调度
例如:
头像:
优先。
视频:
后台。
状态管理
控制:
waiting
uploading
paused
success
failed
15.2 Scheduler整体结构
UploadScheduler
|
---------------------------
| |
PriorityQueue WorkerPool
| |
UploadTask UploadWorker
15.3 Worker模型
Worker:
就是上传执行单元。
例如:
配置:
MAX_WORKER = 3
代表:
同时:
Worker1
Worker2
Worker3
任务:
Task1
Task2
Task3
执行:
Worker1 ---> Task1
Worker2 ---> Task2
Worker3 ---> Task3
15.4 Worker类设计
创建:
worker
|
UploadWorker.ets
代码:
export class UploadWorker {
id:number
busy:boolean=false
constructor(
id:number
){
this.id=id
}
async execute(
task:UploadTask
){
this.busy=true
try{
await task.uploader.upload(
task
)
}catch(e){
console.error(e)
}
finally{
this.busy=false
}
}
}
15.5 WorkerPool设计
管理多个Worker。
export class WorkerPool {
workers:
UploadWorker[]=[]
constructor(
count:number
){
for(
let i=0;i<count;i++
){
this.workers.push(
new UploadWorker(i)
)
}
}
}
初始化:
new WorkerPool(3)
产生:
Worker0
Worker1
Worker2
15.6 获取空闲Worker
方法:
getIdleWorker()
代码:
getIdleWorker(){
return this.workers.find(
worker=>
!worker.busy
)
}
结果:
Worker1 空闲
↓
分配任务
15.7 普通FIFO队列
最简单:
class Queue{
tasks:UploadTask[]=[]
push(task){
this.tasks.push(task)
}
pop(){
return this.tasks.shift()
}
}
问题:
没有优先级。
例如:
队列:
视频10GB
头像100KB
结果:
头像等待。
体验差。
15.8 优先级队列设计
定义:
enum TaskPriority{
HIGH=3,
NORMAL=2,
LOW=1
}
任务:
task.priority=
TaskPriority.HIGH
排序:
tasks.sort(
(a,b)=>
b.priority-a.priority
)
结果:
HIGH
↓
NORMAL
↓
LOW
15.9 PriorityQueue实现
export class PriorityQueue {
private tasks:
UploadTask[]=[]
push(
task:UploadTask
){
this.tasks.push(task)
this.sort()
}
sort(){
this.tasks.sort(
(a,b)=>
b.priority-
a.priority
)
}
pop(){
return this.tasks.shift()
}
}
15.10 Scheduler核心代码
export class UploadScheduler {
queue:
PriorityQueue
pool:
WorkerPool
constructor(){
this.queue=
new PriorityQueue()
this.pool=
new WorkerPool(3)
}
add(
task
){
this.queue.push(task)
this.schedule()
}
schedule(){
let worker=
this.pool.getIdleWorker()
while(
worker
&&
this.queue.length()>0
){
let task=
this.queue.pop()
worker.execute(task)
worker=
this.pool.getIdleWorker()
}
}
}
15.11 并发限制算法
例如:
最大:
3
当前:
Worker1 busy
Worker2 busy
Worker3 busy
新任务:
进入:
waiting queue
状态:
WAITING
↓
UPLOAD
15.12 动态并发调整
固定3个:
不一定最佳。
根据网络:
动态调整。
WiFi:
maxWorker=5
4G:
maxWorker=2
弱网:
maxWorker=1
15.13 网络感知调度
监听:
NetworkChange
例如:
onNetworkChange(type){
if(type==="wifi"){
setWorker(5)
}
if(type==="cellular"){
setWorker(2)
}
}
15.14 大文件公平调度
问题:
一个:
10GB视频。
占满:
所有Worker。
解决:
时间片。
例如:
每次:
上传:
10个chunk
释放Worker。
类似:
CPU调度。
15.15 Chunk级调度
不是:
任务级:
video
↓
上传完成
而是:
video chunk1
image chunk1
pdf chunk1
video chunk2
优势:
公平。
15.16 ChunkScheduler设计
Task
|
↓
ChunkQueue
|
↓
Worker
队列:
chunk1
chunk2
chunk3
Worker:
执行:
一个chunk
15.17 暂停任务调度
用户点击:
暂停。
任务:
UPLOADING
↓
PAUSED
Scheduler:
删除:
running queue
代码:
pause(taskId){
let task=
findTask(taskId)
task.status=
PAUSED
queue.remove(task)
}
15.18 取消任务
取消:
需要:
停止:
- 网络请求
- chunk上传
- 重试
设计:
CancelToken。
class CancelToken{
cancelled=false
cancel(){
this.cancelled=true
}
}
上传:
if(token.cancelled){
throw Error(
"cancel"
)
}
15.19 失败任务重新调度
失败:
进入:
RetryQueue。
流程:
FAILED
↓
等待
↓
retry
↓
UPLOAD
例如:
retryCount < 3
15.20 Scheduler完整流程
用户上传
↓
创建Task
↓
PriorityQueue
↓
Scheduler
↓
Worker空闲?
|
|
是
↓
Worker执行
↓
上传Chunk
↓
更新数据库
↓
完成
15.21 企业级调度策略
最终:
UploadScheduler
├── PriorityQueue
├── WorkerPool
├── NetworkManager
├── RetryManager
├── ChunkScheduler
└── TaskStore
15.22 生产参数建议
图片上传
并发:
5
chunk:
1MB
视频上传
并发:
2-3
chunk:
10-20MB
文档上传
并发:
3
chunk:
5MB
第十六章 HarmonyOS NEXT UploadWorker线程模型与大文件分片并行上传
企业级文件上传中,大文件是最复杂的场景。
例如:
用户上传:
video.mp4
大小:
20GB
要求:
- 上传不中断
- 支持暂停
- 支持恢复
- 支持后台
- 支持弱网
- 支持失败重传
- 不占满内存
核心方案:
文件切片 + Worker池 + Chunk状态管理 + 并行上传。
16.1 大文件上传整体流程
完整流程:
File
↓
FileInfo
↓
ChunkSplitter
↓
Chunk Queue
↓
Worker Pool
↓
Upload Part
↓
OSS
↓
Complete Multipart
↓
Merge File
16.2 UploadWorker生命周期
一个Worker不是简单函数。
它拥有完整生命周期:
CREATE
↓
IDLE
↓
RUNNING
↓
PAUSED
↓
FAILED
↓
DESTROY
状态:
export enum WorkerStatus{
IDLE="idle",
RUNNING="running",
PAUSED="paused",
FAILED="failed"
}
16.3 Worker核心模型
export class UploadWorker {
workerId:number
status:
WorkerStatus
currentChunk:
UploadChunk|null
constructor(
id:number
){
this.workerId=id
this.status=
WorkerStatus.IDLE
}
}
16.4 Worker执行任务
Worker获取Chunk:
async run(
chunk:UploadChunk
){
this.status=
WorkerStatus.RUNNING
this.currentChunk=
chunk
try{
await this.uploadChunk(
chunk
)
chunk.status=
"success"
}catch(e){
chunk.status=
"failed"
}
finally{
this.status=
WorkerStatus.IDLE
}
}
16.5 文件分片算法
大文件:
不能一次读取。
例如:
文件:
1GB
分片:
10MB
产生:
100个Chunk
计算:
chunkCount =
Math.ceil(
fileSize /
chunkSize
)
例如:
fileSize:
1073741824
chunkSize:
10485760
chunk:
103
16.6 Chunk模型设计
export class UploadChunk{
index:number
offset:number
size:number
status:string
etag:string
}
示例:
{
"index":5,
"offset":52428800,
"size":10485760,
"status":"uploading"
}
16.7 ChunkSplitter实现
export class ChunkSplitter{
split(
size:number,
chunkSize:number
){
let chunks=[]
let count=
Math.ceil(
size/chunkSize
)
for(
let i=0;
i<count;
i++
){
chunks.push({
index:i,
offset:
i*chunkSize,
size:
Math.min(
chunkSize,
size-i*chunkSize
)
})
}
return chunks
}
}
16.8 分片读取文件
不要:
readAll()
正确:
偏移读取。
例如:
读取:
offset:
50MB
length:
10MB
伪代码:
async readChunk(
file,
chunk
){
return await file.read({
offset:
chunk.offset,
length:
chunk.size
})
}
16.9 Chunk上传流程
单个Chunk:
读取
↓
计算Hash
↓
生成请求
↓
上传
↓
获取ETag
↓
保存状态
代码:
async uploadChunk(
chunk
){
let data=
await readChunk(chunk)
let etag=
await oss.uploadPart(
data
)
chunk.etag=
etag
await database.updateChunk(
chunk
)
}
16.10 OSS Multipart Upload完整流程
OSS:
不是直接:
PUT文件
而是:
第一步
初始化:
POST ?uploads
返回:
{
uploadId:"xxx"
}
第二步
上传Part:
PUT
?partNumber=1
&uploadId=xxx
返回:
ETag
第三步
完成:
POST
?uploadId=xxx
提交:
[
{
partNumber:1,
etag:"xxx"
}
]
16.11 MultipartUploader实现
export class MultipartUploader{
async upload(task){
let uploadId=
await this.initMultipart()
let chunks=
task.chunks
for(
let chunk of chunks
){
await this.uploadPart(
uploadId,
chunk
)
}
return await this.complete(
uploadId
)
}
}
16.12 并行Chunk上传
串行:
chunk1
↓
chunk2
↓
chunk3
速度:
慢。
并行:
Worker1
chunk1
Worker2
chunk2
Worker3
chunk3
速度:
提升。
16.13 ChunkScheduler
class ChunkScheduler{
queue:
UploadChunk[]
workers:
UploadWorker[]
start(){
while(
hasIdleWorker()
&&
queue.length
){
let worker=
getWorker()
let chunk=
queue.shift()
worker.run(chunk)
}
}
}
16.14 并行数量控制
不要无限。
例如:
const MAX_CHUNK_WORKER=4
原因:
手机:
CPU:
有限。
网络:
有限。
推荐:
| 网络 | 并发 |
|---|---|
| WiFi | 4-6 |
| 5G | 3-5 |
| 4G | 2-3 |
| 弱网 | 1 |
16.15 上传速度限制
某些场景:
需要限速。
例如:
后台上传:
不能影响用户。
设计:
BandwidthLimiter。
算法:
Token Bucket。
桶:
容量:
10MB
每秒:
增加:
1MB
上传:
消耗。
16.16 BandwidthLimiter实现
class BandwidthLimiter{
capacity:number
tokens:number
consume(size:number){
if(
this.tokens>=size
){
this.tokens-=size
return true
}
return false
}
}
16.17 内存池设计
问题:
多个Chunk同时读取。
例如:
5个Worker。
每个:
20MB。
内存:
100MB
设计:
BufferPool。
class BufferPool{
buffers:ArrayBuffer[]
get(size){
return new ArrayBuffer(size)
}
release(buffer){
this.buffers.push(buffer)
}
}
16.18 Chunk失败恢复
例如:
100个Chunk。
完成:
99%
最后一个失败。
不要重新上传。
只重试:
chunk99
数据库:
记录:
{
"chunk":99,
"status":"failed"
}
16.19 Worker异常处理
异常:
分类。
enum UploadError{
NETWORK,
TIMEOUT,
AUTH,
SPACE,
UNKNOWN
}
处理:
网络:
重试。
权限:
刷新Token。
空间:
失败。
16.20 Token过期处理
大文件:
上传几个小时。
STS:
可能:
15分钟过期。
流程:
上传
↓
401
↓
TokenManager刷新
↓
继续上传
代码:
if(error.code===401){
await tokenManager.refresh()
retry()
}
16.21 大文件上传完整链路
20GB文件
↓
ChunkSplitter
↓
1000 Chunk
↓
ChunkQueue
↓
Worker Pool
↓
OSS UploadPart
↓
保存ETag
↓
CompleteMultipart
↓
生成URL
↓
任务成功
16.22 企业级大文件参数
视频:
Chunk:
20MB
Worker:
4
Retry:
3次
压缩包:
Chunk:
50MB
Worker:
3
移动网络:
Chunk:
5MB
Worker:
2
第十七章 HarmonyOS NEXT 上传SDK ArkUI组件封装与业务层接入
企业项目中,上传能力最终一定要暴露给业务页面。
但是业务开发不应该关心:
- OSS签名
- HTTP请求
- Chunk切片
- Worker调度
- 断点恢复
- Token刷新
业务页面只需要:
选择文件
↓
调用上传
↓
显示状态
↓
处理结果
因此需要封装:
Upload UI Component 层。
17.1 上传组件整体设计
最终:
id="n8h4m2"
业务页面
|
↓
Upload Components
|
↓
Upload SDK
|
↓
OSS / HTTP
组件层:
components
├── UploadButton.ets
├── UploadProgress.ets
├── UploadList.ets
├── UploadItem.ets
└── UploadPreview.ets
17.2 UploadButton组件设计
作用:
提供统一上传入口。
业务:
UploadButton({
type:"image",
onSuccess:(url)=>{}
})
内部:
负责:
- 打开文件选择
- 创建任务
- 开始上传
17.3 UploadButton实现
@Component
export struct UploadButton {
@State uploading:boolean=false
type:string="file"
onSuccess?:
(url:string)=>void
build(){
Button("选择文件")
.onClick(()=>{
this.selectFile()
})
}
async selectFile(){
let file =
await FilePicker.pick()
let task=
UploadManager.upload({
file:file,
type:this.type
})
task.onSuccess(
(url)=>{
this.onSuccess?.(
url
)
}
)
}
}
17.4 页面使用
业务页面:
UploadButton({
type:"image",
onSuccess:(url)=>{
console.log(url)
}
})
页面无需:
OSS
HTTP
Token
Chunk
17.5 UploadProgress进度组件
上传:
最常见需求:
显示:
正在上传
████████░░
80%
组件:
UploadProgress.ets
输入:
taskId
代码:
@Component
export struct UploadProgress {
taskId:string
@State progress:number=0
aboutToAppear(){
UploadManager
.observe(
this.taskId,
(data)=>{
this.progress=
data.progress
}
)
}
build(){
Progress({
value:this.progress
})
}
}
17.6 UploadItem上传列表
类似:
微信聊天:
多个文件。
显示:
id="m7x9v3"
photo1.jpg
上传中 60%
video.mp4
等待
doc.pdf
完成
数据:
@State tasks:
UploadTask[]=[]
页面:
ForEach(
this.tasks,
task=>{
UploadItem({
task
})
}
)
17.7 UploadItem状态显示
根据状态:
switch(task.status){
case "waiting":
显示等待
break;
case "uploading":
显示进度
break;
case "success":
显示完成
break;
case "failed":
显示重试按钮
}
17.8 ArkUI状态绑定
核心:
UploadTask变化:
触发:
UI刷新。
错误:
普通对象:
let task={
progress:20
}
变化:
ArkUI不知道。
正确:
使用:
@Observed
17.9 Observable UploadTask
定义:
@Observed
export class UploadTask {
progress:number=0
status:string="waiting"
}
组件:
@ObjectLink
task:UploadTask
变化:
task.progress=80
UI:
自动更新。
17.10 上传列表响应式设计
数据流:
id="x4p8k2"
UploadManager
|
↓
Observable Store
|
↓
ArkUI
类似:
Redux。
17.11 UploadStore设计
创建:
store
└── UploadStore.ets
代码:
export class UploadStore {
tasks:
UploadTask[]=[]
add(task){
this.tasks.push(task)
}
update(task){
let old=
this.tasks.find(
x=>x.id===task.id
)
Object.assign(
old,
task
)
}
}
17.12 图片上传完整案例
场景:
用户修改头像。
流程:
id="f3k7m9"
点击头像
↓
PhotoPicker
↓
ImageProcessor
↓
UploadManager
↓
OSS
↓
更新头像URL
页面:
UploadButton({
type:"avatar",
onSuccess(url){
user.avatar=url
}
})
17.13 IM聊天图片上传
微信类似:
流程:
图片选择
↓
压缩
↓
上传
↓
发送消息
↓
服务器保存URL
消息:
{
type:"image",
url:"https://xxx/a.jpg"
}
注意:
聊天:
不能等待上传完成才显示。
采用:
临时消息。
状态:
sending
↓
uploaded
↓
sent
17.14 视频上传页面设计
直播/短视频:
选择视频
↓
生成封面
↓
压缩
↓
分片上传
↓
审核
↓
发布
UI:
显示:
视频.mp4
上传:
65%
速度:
8MB/s
剩余:
2分钟
17.15 上传暂停按钮
组件:
Button("暂停")
.onClick(()=>{
UploadManager
.pause(
taskId
)
})
恢复:
UploadManager
.resume(
taskId
)
17.16 上传失败重试UI
失败:
显示:
上传失败
[重新上传]
点击:
retry(){
UploadManager
.retry(
taskId
)
}
17.17 上传取消UI
取消:
UploadManager
.cancel(
taskId
)
同时:
清理:
- 网络请求
- Chunk记录
- 临时文件
17.18 企业商城上传案例
商品后台:
上传:
- 商品图片
- 商品视频
- 商品详情附件
流程:
商品编辑页
↓
UploadButton
↓
UploadTask
↓
OSS
↓
返回URL
↓
保存商品
商品数据:
{
name:"手机",
images:[
"https://oss/a.jpg"
]
}
17.19 企业IM上传案例
聊天:
支持:
- 图片
- 视频
- 文件
统一:
UploadManager.upload()
消息:
{
uploadTaskId:"001",
status:"uploading"
}
上传完成:
推送:
{
status:"success",
url:"xxx"
}
17.20 直播APP上传案例
主播上传:
- 视频
- 封面
- 回放
架构:
主播端
↓
UploadSDK
↓
OSS
↓
转码服务
↓
CDN
↓
用户观看
17.21 上传组件最佳实践
组件负责:
✅ 文件选择
✅ UI展示
✅ 用户交互
✅ 状态绑定
SDK负责:
✅ 网络
✅ OSS
✅ 分片
✅ 重试
✅ 恢复
业务负责:
✅ 业务数据保存
✅ 页面逻辑
17.22 最终业务调用效果
业务代码:
const task =
UploadManager.upload({
file,
category:"avatar"
})
UploadProgress({
taskId:
task.id
})
整个业务层:
只有:
几行代码。
第十八章 HarmonyOS NEXT 文件上传SDK完整企业项目落地架构
企业项目中,上传能力通常不是一个页面功能,而是一个基础能力平台。
例如一个大型 App:
包含:
商城模块
IM模块
直播模块
社区模块
后台管理模块
这些模块都会上传文件。
如果每个业务自己实现:
商城写一套
IM写一套
直播写一套
最终会产生:
- 重复开发
- Bug无法统一修复
- 上传行为不一致
- 运维困难
所以企业通常建设:
统一 Upload Platform。
18.1 企业上传平台架构
整体:
业务层
--------------------------------
商城 IM 直播 社区
|
↓
Upload SDK
|
-------------------------
| | |
Task管理 上传引擎 安全模块
|
-------------------------
| |
OSS HTTP Server
|
↓
存储系统
18.2 HarmonyOS工程模块设计
推荐:
entry
feature_shop
feature_im
feature_live
common
|
└── upload-sdk
其中:
upload-sdk:
独立HAR模块。
18.3 HAR封装上传SDK
HarmonyOS:
推荐:
HAR
(HarmonyOS Archive)
结构:
upload-sdk
├── src/main/ets
│
├── UploadManager.ets
├── OssUploader.ets
├── UploadTask.ets
└── index.ets
业务:
依赖:
{
"dependencies":{
"@company/upload-sdk":
"../upload-sdk"
}
}
18.4 SDK出口设计
不要暴露内部。
例如:
错误:
import {
OssUploader
}
应该:
import {
UploadManager
}
index.ets:
export {
UploadManager
}
from
"./core/UploadManager"
export {
UploadTask
}
from
"./model/UploadTask"
18.5 多业务统一配置
不同业务:
不同策略。
例如:
商城:
{
bucket:"shop",
maxSize:"20MB"
}
直播:
{
bucket:"video",
chunkSize:"20MB"
}
配置中心:
UploadConfig
18.6 UploadConfig设计
export class UploadConfig{
bucket:string
endpoint:string
maxSize:number
chunkSize:number
workerCount:number
}
初始化:
UploadManager.init({
chunkSize:
10*1024*1024,
workerCount:
4
})
18.7 多环境配置
企业:
通常:
三个环境。
开发环境
↓
测试环境
↓
生产环境
配置:
const config={
dev:{
endpoint:"dev-oss"
},
test:{
endpoint:"test-oss"
},
prod:{
endpoint:"prod-oss"
}
}
18.8 上传监控系统
生产环境:
必须知道:
每天:
多少上传?
失败多少?
平均速度?
数据:
upload_start
upload_progress
upload_success
upload_failed
18.9 上传埋点设计
创建:
UploadTracker.ets
事件:
tracker.report({
event:
"upload_success",
taskId,
fileSize,
duration
})
18.10 关键指标
成功率
公式:
success
/
total
目标:
99.9%
平均上传速度
例如:
8.5MB/s
P95耗时
95%用户:
多少时间完成。
失败原因分布
例如:
网络失败 60%
Token过期 20%
服务器异常 20%
18.11 上传日志模型
class UploadLog{
taskId:string
event:string
time:number
extra:any
}
保存:
本地
+
服务器
18.12 灰度发布策略
上传SDK升级:
不能全部上线。
流程:
内部员工
↓
1%用户
↓
10%用户
↓
50%用户
↓
100%
原因:
上传属于核心链路。
18.13 SDK版本管理
例如:
1.0.0
1.1.0
2.0.0
兼容:
旧业务:
继续运行。
18.14 API兼容设计
不要频繁修改:
旧:
upload(file)
新:
upload({
file,
options
})
保留:
旧方法。
18.15 上传异常中心
统一错误码:
enum UploadCode{
NETWORK_ERROR=1001,
TOKEN_EXPIRED=1002,
FILE_TOO_LARGE=1003,
NO_PERMISSION=1004,
SERVER_ERROR=1005
}
业务:
根据错误码处理。
18.16 上传安全中心
统一:
TokenManager
FileValidator
VirusScanner
PermissionChecker
所有业务:
共用。
18.17 上传缓存体系
减少重复上传。
三级缓存:
Memory
↓
Database
↓
OSS
例如:
用户重复上传:
同MD5:
直接返回。
18.18 秒传系统
流程:
计算MD5
↓
发送服务器
↓
查询文件表
↓
存在
↓
返回URL
文件表:
file_hash
file_url
file_size
create_time
18.19 企业文件生命周期管理
OSS:
设置:
临时文件
保存:
24小时
正式文件
保存:
长期
冷文件
转:
低频存储。
18.20 上传平台最终形态
完整:
Business
|
↓
Upload SDK
|
------------------------------------------------
| | | |
Task Scheduler Security Monitor
| | | |
------------------------------------------------
|
Storage Adapter
|
-------------------
| |
OSS HTTP
第十九章 HarmonyOS NEXT 文件上传源码级优化:内存、线程、性能极限调优
企业级上传系统进入生产环境以后,问题通常不是“上传失败”。
而是:
- 上传过程中 APP 卡顿
- 大文件导致 OOM
- 多任务导致内存暴涨
- 长时间上传速度下降
- 后台运行被系统限制
- 低端设备体验差
因此需要从:
- ArkTS运行模型
- 文件IO
- 内存管理
- 并发模型
- 网络层
进行深度优化。
19.1 HarmonyOS 文件上传内存模型
一次上传流程:
File
↓
读取Buffer
↓
转换ArrayBuffer
↓
HTTP Request
↓
OSS
如果:
直接读取:
let buffer =
readFileAll()
文件:
2GB
内存:
直接:
2GB
移动设备:
无法接受。
19.2 大文件禁止全量读取
错误:
const data =
file.read()
正确:
分段读取:
文件
0MB
|
10MB
|
20MB
|
30MB
一次:
只保持:
5MB~20MB
19.3 Buffer生命周期设计
Chunk上传流程:
申请Buffer
↓
读取文件
↓
上传
↓
释放Buffer
不要:
chunk1 buffer
chunk2 buffer
chunk3 buffer
全部保存
19.4 Buffer池优化
频繁:
new ArrayBuffer
会造成:
GC压力。
例如:
上传1000个chunk。
如果:
每次:
new ArrayBuffer(
10MB
)
产生:
大量垃圾对象。
设计:
BufferPool。
19.5 BufferPool实现
export class BufferPool {
private pool:
ArrayBuffer[]=[]
get(size:number){
let buffer=
this.pool.pop()
if(buffer){
return buffer
}
return new ArrayBuffer(size)
}
release(
buffer:ArrayBuffer
){
this.pool.push(buffer)
}
}
使用:
let buffer=
pool.get(
10*1024*1024
)
upload(buffer)
pool.release(buffer)
优势:
减少:
内存申请。
19.6 ArrayBuffer与Uint8Array优化
HarmonyOS文件操作:
大量使用:
ArrayBuffer
Uint8Array
关系:
ArrayBuffer
|
↓
Uint8Array
|
↓
二进制数据
转换:
let bytes =
new Uint8Array(buffer)
避免:
重复复制。
错误:
let newBuffer=
copy(buffer)
会:
增加内存。
19.7 零拷贝思想
传统:
文件
↓
Buffer1
↓
Buffer2
↓
HTTP
优化:
文件
↓
共享Buffer
↓
HTTP
减少:
复制。
19.8 图片上传内存优化
图片:
通常:
最大问题。
流程:
原图10MB
↓
解码PixelMap
↓
压缩
↓
重新编码
↓
上传
问题:
PixelMap可能:
几十MB。
处理:
完成后:
立即释放。
pixelMap.release()
19.9 图片压缩策略
不要:
固定压缩。
应该:
根据尺寸。
例如:
头像:
500KB
商品图:
2MB
朋友圈:
5MB
动态:
function getQuality(size){
if(size>10MB){
return 60
}
return 80
}
19.10 视频上传优化
视频:
特点:
- 大
- 时间长
- 不允许重复
优化:
1. 不读取完整视频
只读取:
chunk。
2. 提前获取Metadata
例如:
duration
width
height
codec
3. 异步生成封面
不要阻塞上传。
19.11 Worker线程模型优化
上传任务:
不要全部:
主线程执行。
主线程:
负责:
UI
事件
状态
Worker:
负责:
文件读取
Hash计算
网络上传
19.12 Worker通信模型
主线程:
发送任务:
worker.postMessage({
taskId:"001"
})
Worker:
返回:
postMessage({
progress:50
})
主线程:
更新:
ArkUI。
19.13 Hash计算优化
秒传:
需要:
MD5。
大文件:
不能:
一次计算。
错误:
md5(
wholeBuffer
)
正确:
流式Hash:
chunk1
↓
update
chunk2
↓
update
chunk3
↓
update
final
类似:
hash.update(buffer)
19.14 上传速度统计优化
不要:
每次进度:
立即刷新UI。
例如:
1秒:
100次。
会:
造成:
UI压力。
限制:
100ms刷新一次
代码:
if(
Date.now()-lastTime>100
){
notify()
}
19.15 网络请求优化
HTTP请求:
优化:
连接复用
开启:
keep-alive
超时控制
不要:
无限等待。
例如:
timeout:
30000
重试策略
指数退避:
1秒
↓
3秒
↓
10秒
19.16 弱网优化
移动网络:
经常:
- 延迟
- 丢包
- 切换网络
策略:
降低并发
WiFi:
5
弱网:
1
增大超时
例如:
普通:
30秒。
弱网:
60秒。
19.17 APP后台上传优化
后台:
系统关注:
资源。
需要:
控制:
CPU
网络
电量
例如:
锁屏:
降低:
worker数量。
19.18 电量感知
监听:
Battery Level
策略:
if(
battery<20
){
pauseLargeUpload()
}
19.19 温度控制
长时间视频上传:
设备发热。
策略:
检测:
thermal state
降低:
worker:
4
↓
1
19.20 大文件上传极限方案
例如:
100GB文件。
架构:
File
↓
Streaming Reader
↓
Chunk Generator
↓
Worker Pool
↓
Multipart Upload
↓
Checkpoint DB
↓
OSS
关键:
永远:
内存:
几十MB
不会:
跟文件大小增长。
19.21 性能参数推荐
普通图片
chunk:
1MB
worker:
3
高清视频
chunk:
20MB
worker:
4
超大文件
chunk:
50MB
worker:
3
19.22 性能优化检查表
内存
✅ 流式读取
✅ Buffer复用
✅ PixelMap释放
✅ 避免复制
CPU
✅ Hash异步
✅ 图片压缩控制
✅ Worker限制
网络
✅ HTTP复用
✅ 分片上传
✅ 动态并发
用户体验
✅ 后台上传
✅ 断点恢复
✅ 速度显示
✅ 暂停继续
19.23 生产级上传性能目标
大型APP:
目标:
上传成功率:
>99.9%
后台恢复:
>99%
内存增长:
<100MB
大文件:
10GB+
稳定上传。
第二十章 HarmonyOS NEXT 文件上传完整源码工程实战
本章开始进入完整工程实现阶段。
目标:
搭建一个可以直接用于企业项目的 HarmonyOS NEXT 上传框架。
最终能力:
id="upload_arch"
选择文件
↓
创建上传任务
↓
任务队列
↓
分片处理
↓
并发上传
↓
断点保存
↓
OSS存储
↓
返回文件地址
20.1 创建 UploadSDK 工程
推荐工程结构:
UploadSDK
├── entry
├── upload-core
│
├── upload-network
│
├── upload-storage
│
├── upload-ui
│
└── upload-example
模块职责:
| 模块 | 职责 |
|---|---|
| upload-core | 任务管理 |
| upload-network | HTTP/OSS |
| upload-storage | 数据库 |
| upload-ui | ArkUI组件 |
| example | 测试Demo |
20.2 upload-core核心模块
目录:
upload-core
└── ets
├── manager
│ UploadManager.ets
│
├── model
│ UploadTask.ets
│
├── scheduler
│ UploadScheduler.ets
│
└── worker
UploadWorker.ets
20.3 UploadTask任务模型
文件:
UploadTask.ets
代码:
export enum UploadStatus {
WAITING="waiting",
UPLOADING="uploading",
PAUSED="paused",
SUCCESS="success",
FAILED="failed"
}
@Observed
export class UploadTask {
id:string
fileName:string
fileSize:number
progress:number=0
status:
UploadStatus=
UploadStatus.WAITING
uploadId:string=""
speed:number=0
constructor(
id:string,
fileName:string,
size:number
){
this.id=id
this.fileName=fileName
this.fileSize=size
}
}
20.4 UploadManager核心入口
所有业务:
通过这里调用。
文件:
UploadManager.ets
代码:
export class UploadManager {
private static instance:
UploadManager
private scheduler:
UploadScheduler
private constructor(){
this.scheduler=
new UploadScheduler()
}
static getInstance(){
if(!this.instance){
this.instance=
new UploadManager()
}
return this.instance
}
upload(file){
let task=
new UploadTask(
Date.now()
.toString(),
file.name,
file.size
)
this.scheduler.add(task)
return task
}
}
20.5 业务调用方式
页面:
let task =
UploadManager
.getInstance()
.upload(file)
返回:
{
"id":"173456789",
"status":"waiting",
"progress":0
}
20.6 UploadScheduler实现
负责:
任务调度。
文件:
UploadScheduler.ets
代码:
export class UploadScheduler {
tasks:
UploadTask[]=[]
workers:
UploadWorker[]=[]
constructor(){
for(
let i=0;i<3;i++
){
this.workers.push(
new UploadWorker(i)
)
}
}
add(task:UploadTask){
this.tasks.push(task)
this.dispatch()
}
dispatch(){
let worker=
this.workers.find(
w=>!w.busy
)
if(
worker
&&
this.tasks.length>0
){
let task=
this.tasks.shift()
worker.execute(task)
}
}
}
20.7 UploadWorker实现
文件:
UploadWorker.ets
代码:
export class UploadWorker {
id:number
busy:boolean=false
constructor(
id:number
){
this.id=id
}
async execute(
task:UploadTask
){
this.busy=true
task.status=
UploadStatus.UPLOADING
try{
await UploadEngine.upload(
task
)
task.status=
UploadStatus.SUCCESS
}catch(e){
task.status=
UploadStatus.FAILED
}
this.busy=false
}
}
20.8 UploadEngine上传核心
目录:
engine
UploadEngine.ets
职责:
连接:
- 文件读取
- 分片
- 上传器
代码:
export class UploadEngine {
static async upload(
task:UploadTask
){
let chunks=
ChunkManager.create(
task.fileSize
)
for(
let chunk of chunks
){
await this.uploadChunk(
chunk,
task
)
}
}
}
20.9 Chunk模型
文件:
Chunk.ets
代码:
export class UploadChunk{
index:number
offset:number
size:number
status:string="waiting"
etag:string=""
}
20.10 ChunkManager
负责:
文件切片。
export class ChunkManager {
static create(
size:number
){
let result=[]
let chunkSize=
10*
1024*
1024
let count=
Math.ceil(
size/chunkSize
)
for(
let i=0;i<count;i++
){
result.push({
index:i,
offset:
i*chunkSize,
size:
Math.min(
chunkSize,
size-i*chunkSize
)
})
}
return result
}
}
20.11 OSS上传接口设计
不要绑定OSS。
定义:
interface StorageAdapter{
init(task)
uploadPart(
chunk
)
complete(
task
)
}
支持:
未来:
OSS
COS
S3
MinIO
20.12 OSS实现
文件:
OssAdapter.ets
代码:
export class OssAdapter
implements StorageAdapter{
async init(task){
return await Http.post(
"/multipart/init",
task
)
}
async uploadPart(chunk){
return await Http.upload(
chunk
)
}
async complete(task){
return await Http.post(
"/multipart/complete",
task
)
}
}
20.13 HTTP网络层封装
目录:
upload-network
HttpClient.ets
代码:
export class HttpClient {
async post(
url,
data
){
return await request.post({
url,
data
})
}
async upload(
url,
data
){
return await request.uploadFile({
url,
files:data
})
}
}
20.14 上传进度监听
增加:
EventEmitter。
export class UploadEvent {
private listeners=[]
on(callback){
this.listeners.push(callback)
}
emit(data){
this.listeners.forEach(
fn=>
fn(data)
)
}
}
任务:
绑定:
task.event.on(
(progress)=>{
console.log(progress)
}
)
20.15 ArkUI上传页面
示例:
图片上传。
@Entry
@Component
struct UploadPage {
@State progress:number=0
build(){
Column(){
Button("选择图片")
.onClick(()=>{
this.startUpload()
})
Progress({
value:this.progress
})
}
}
async startUpload(){
let file=
await Picker.select()
let task=
UploadManager
.getInstance()
.upload(file)
task.event.on(
data=>{
this.progress=
data.progress
}
)
}
}
20.16 完整调用链
UploadPage
↓
UploadManager
↓
UploadScheduler
↓
UploadWorker
↓
UploadEngine
↓
ChunkManager
↓
OssAdapter
↓
OSS
第二十一章 HarmonyOS NEXT 上传SDK生产级完善:断点续传数据库 + OSS Multipart完整实现
企业上传系统和普通上传最大的区别:
普通上传:
选择文件
↓
上传
↓
完成
生产级上传:
创建任务
↓
保存任务
↓
上传分片
↓
保存分片状态
↓
APP退出
↓
重新启动
↓
恢复任务
↓
继续上传
核心:
上传状态必须持久化。
21.1 上传任务持久化架构
整体:
UploadTask
|
↓
UploadDatabase
|
-------------------
| |
task表 chunk表
| |
恢复 分片恢复
21.2 RelationalStore数据库初始化
HarmonyOS NEXT:
使用:
@kit.ArkData
导入:
import {
relationalStore
}
from '@kit.ArkData';
创建数据库:
export class UploadDatabase {
private store:
relationalStore.RdbStore
async init(context){
let config:
relationalStore.StoreConfig={
name:
"upload.db",
securityLevel:
relationalStore.SecurityLevel.S1
}
this.store=
await relationalStore.getRdbStore(
context,
config
)
}
}
21.3 创建upload_task表
任务表:
保存:
上传整体状态。
字段:
id
file_name
file_path
file_size
file_hash
upload_id
status
progress
create_time
update_time
SQL:
CREATE TABLE upload_task(
id TEXT PRIMARY KEY,
file_name TEXT,
file_path TEXT,
file_size INTEGER,
file_hash TEXT,
upload_id TEXT,
status TEXT,
progress INTEGER,
create_time INTEGER,
update_time INTEGER
)
21.4 创建upload_chunk表
分片状态:
CREATE TABLE upload_chunk(
id INTEGER PRIMARY KEY,
task_id TEXT,
chunk_index INTEGER,
chunk_size INTEGER,
chunk_offset INTEGER,
etag TEXT,
status TEXT
)
例如:
任务:
video.mp4
分片:
0 success
1 success
2 uploading
3 waiting
21.5 UploadTask保存
新增任务:
async insertTask(
task:UploadTask
){
let values:
relationalStore.ValuesBucket={
"id":
task.id,
"file_name":
task.fileName,
"file_size":
task.fileSize,
"status":
task.status
}
await this.store.insert(
"upload_task",
values
)
}
21.6 保存Chunk
创建:
100个分片。
循环:
保存。
async insertChunks(
taskId,
chunks
){
for(
let chunk of chunks
){
await this.store.insert(
"upload_chunk",
{
"task_id":
taskId,
"chunk_index":
chunk.index,
"status":
"waiting"
}
)
}
}
21.7 更新上传进度
上传过程中:
task.progress=60
database.updateTask(
task
)
SQL:
UPDATE upload_task
SET progress=?
WHERE id=?
21.8 更新Chunk状态
某个分片成功:
chunk.status=
"success"
chunk.etag=
etag
保存:
updateChunk(chunk)
数据库:
chunk1
success
etag:
xxxx
21.9 APP启动恢复流程
APP重新打开:
Ability启动
↓
UploadDatabase初始化
↓
查询未完成任务
↓
恢复UploadTask
↓
读取Chunk状态
↓
继续上传
21.10 查询未完成任务
SQL:
SELECT *
FROM upload_task
WHERE status != 'success'
代码:
async getRunningTasks(){
return await this.store.querySql(
`
SELECT *
FROM upload_task
WHERE status != ?
`,
[
"success"
]
)
}
21.11 恢复Chunk
查询:
SELECT *
FROM upload_chunk
WHERE task_id=?
AND status!='success'
返回:
[
{
index:3,
status:"waiting"
},
{
index:4,
status:"waiting"
}
]
只上传:
未完成部分。
21.12 OSS Multipart完整流程
大文件:
不能:
一次PUT。
流程:
CreateMultipartUpload
↓
UploadPart
↓
ListParts
↓
CompleteMultipartUpload
21.13 初始化Multipart
请求:
POST
/object?uploads
返回:
{
uploadId:
"CAFE123456"
}
保存:
数据库。
task.uploadId=
response.uploadId
updateTask(task)
21.14 上传Part
请求:
PUT
/object
?partNumber=1
&uploadId=xxx
返回:
ETag:
"abc123"
保存:
chunk.etag=
etag
updateChunk(chunk)
21.15 Complete Multipart
所有分片完成:
收集:
[
{
partNumber:1,
etag:"xxx"
},
{
partNumber:2,
etag:"yyy"
}
]
发送:
POST
?uploadId=xxx
成功:
task.status=
SUCCESS
21.16 秒传系统设计
大厂:
基本都有。
核心:
文件Hash。
流程:
用户选择文件
↓
计算Hash
↓
服务器查询
↓
存在
↓
返回URL
↓
完成
21.17 文件Hash计算
不要:
一次读取。
错误:
md5(file)
正确:
流式计算:
chunk1
↓
hash.update
chunk2
↓
hash.update
chunk3
↓
final
伪代码:
let hash=
new MD5()
for(
chunk of chunks
){
hash.update(chunk)
}
let result=
hash.digest()
21.18 秒传接口设计
客户端:
POST /file/check
参数:
{
hash:"xxx",
size:102400
}
服务器返回:
存在:
{
exist:true,
url:"https://xxx"
}
不存在:
{
exist:false
}
21.19 断点续传完整流程
开始上传
↓
生成UploadId
↓
保存数据库
↓
上传Chunk
↓
保存ETag
↓
APP关闭
↓
再次打开
↓
读取UploadId
↓
读取完成Chunk
↓
继续上传
↓
Complete
21.20 崩溃恢复案例
上传:
10GB视频
进度:
80%
突然:
APP被系统杀死
重新打开:
数据库:
{
progress:80,
uploadId:"xxx",
completed:[
0,
1,
2,
...
80
]
}
继续:
81%
↓
100%
21.21 生产级恢复能力
支持:
| 场景 | 恢复 |
|---|---|
| 页面关闭 | ✅ |
| APP退出 | ✅ |
| 系统杀死 | ✅ |
| 网络断开 | ✅ |
| Token过期 | ✅ |
| 手机重启 | ✅ |
21.22 当前上传SDK能力
现在已经具备:
UploadManager
+
Scheduler
+
Worker
+
Chunk
+
Database
+
Multipart
+
Resume
+
Fast Upload
第二十二章 HarmonyOS NEXT 文件上传安全体系设计
企业文件上传系统中,上传链路属于高风险入口。
因为用户上传的数据:
可能包含:
- 图片
- 视频
- 文档
- 压缩包
- 配置文件
- 用户隐私数据
如果安全设计不足,会产生:
- 恶意文件上传
- 存储桶泄露
- Token泄露
- 文件覆盖攻击
- 木马传播
- 数据盗取
因此企业级上传系统必须建立:
客户端安全 + 网络安全 + 存储安全 + 服务端安全 四层体系。
22.1 文件上传安全整体架构
完整安全链路:
用户文件
↓
客户端校验
↓
安全Token
↓
HTTPS传输
↓
上传服务
↓
病毒检测
↓
OSS隔离存储
↓
审核发布
22.2 客户端安全职责
客户端主要负责:
第一道防线。
包括:
文件类型检测
文件大小限制
文件名称过滤
Hash计算
权限控制
Token管理
但是:
客户端不能作为最终安全依据。
原因:
客户端代码:
可以被:
- 反编译
- 修改
- Hook
最终安全:
必须依赖服务器。
22.3 文件类型校验
危险场景:
用户上传:
virus.exe
然后:
修改:
avatar.jpg
仅判断:
文件名:
不安全。
错误:
if(file.name.endsWith(".jpg")){
allow()
}
22.4 MIME类型检测
读取:
文件头。
例如:
JPEG:
FF D8 FF
PNG:
89 50 4E 47
称为:
Magic Number。
22.5 FileValidator设计
创建:
FileValidator.ets
代码:
export class FileValidator {
validate(file){
this.checkSize(file)
this.checkType(file)
this.checkName(file)
return true
}
}
22.6 文件大小限制
例如:
头像:
5MB
视频:
2GB
配置:
const limit={
image:
5*1024*1024,
video:
2*1024*1024*1024
}
检查:
if(
file.size>limit
){
throw Error(
"file too large"
)
}
22.7 文件名安全过滤
危险:
../../config.json
攻击:
路径穿越。
过滤:
function sanitizeName(
name
){
return name.replace(
/[^a-zA-Z0-9._-]/g,
""
)
}
22.8 禁止危险后缀
例如:
.exe
.sh
.bat
.apk
.js
黑名单:
const deny=[
".exe",
".sh",
".bat"
]
22.9 上传Token安全设计
客户端:
不能保存:
永久AccessKey。
错误:
AccessKey
SecretKey
写死APP
原因:
APK/HAP:
可以分析。
正确:
使用:
STS临时Token。
22.10 STS Token流程
APP
↓
业务服务器
↓
申请临时权限
↓
STS Token
↓
OSS上传
Token:
包含:
{
accessKey:"xxx",
expire:
3600,
policy:"upload-only"
}
22.11 Token生命周期
例如:
有效:
15分钟
过期:
自动刷新。
流程:
上传
↓
401
↓
刷新Token
↓
继续Chunk
22.12 OSS权限隔离
不要:
所有文件:
同一个Bucket。
推荐:
avatar-bucket
video-bucket
document-bucket
temp-bucket
权限:
不同。
22.13 临时目录隔离
上传:
不要直接:
正式目录。
流程:
upload/temp/
↓
审核
↓
move
↓
publish/
例如:
temp/video001.mp4
↓
media/video001.mp4
22.14 HTTPS安全
所有上传:
必须:
HTTPS
禁止:
http://
防止:
- 中间人攻击
- Token窃取
- 文件篡改
22.15 证书校验
高级场景:
Certificate Pinning。
客户端保存:
服务器证书指纹。
连接:
比较。
防止:
代理抓包。
22.16 上传签名机制
请求:
增加签名。
例如:
{
fileHash:"xxx",
timestamp:123456,
sign:"abcdef"
}
服务器:
验证:
hash
+
timestamp
+
secret
22.17 防止重放攻击
攻击:
重复发送:
同一个上传请求。
解决:
加入:
timestamp。
例如:
允许:
5分钟
超过:
拒绝。
22.18 文件病毒扫描
企业:
上传完成后:
不能立即开放。
流程:
Upload
↓
Scan
↓
Safe?
↓
Publish
状态:
enum FileStatus{
UPLOADING,
SCANNING,
SAFE,
BLOCKED
}
22.19 图片安全处理
图片可能:
包含:
EXIF信息。
例如:
GPS位置。
上传前:
清理:
EXIF
GPS
Camera Info
保护:
用户隐私。
22.20 视频安全处理
视频:
可能包含:
隐藏信息。
处理:
重新编码
↓
生成新文件
↓
发布
22.21 数据库安全
本地:
上传记录。
包含:
- 文件路径
- Token信息
- UploadId
不能明文保存。
敏感字段:
加密。
例如:
uploadId
token
22.22 本地安全存储
HarmonyOS:
使用:
安全存储。
例如:
Preferences
+
加密
不要:
localStorage
保存Token。
22.23 上传安全日志
记录:
用户
文件Hash
时间
结果
IP
用于:
审计。
22.24 企业安全架构
最终:
Client
|
FileValidator
|
HTTPS + Sign
|
Upload Gateway
|
-------------------
| |
Scanner OSS
| |
-------------------
|
File Service
第二十三章 HarmonyOS NEXT 文件上传云端架构设计:OSS、CDN、文件服务平台
企业应用中,文件上传完成并不代表结束。
真正完整的链路:
用户上传文件
↓
上传服务
↓
对象存储 OSS
↓
文件处理服务
↓
CDN分发
↓
用户访问
23.1 为什么需要文件服务平台
简单项目:
APP
↓
OSS
即可。
但是企业:
会出现:
- 文件权限复杂
- 多业务共享
- 图片需要压缩
- 视频需要转码
- 文件需要审核
- CDN需要加速
所以需要:
File Service 文件服务中心。
23.2 企业文件平台整体架构
HarmonyOS APP
|
↓
Upload Gateway
|
---------------------------
| |
File Service Upload Service
| |
-----------+-------------
|
OSS
|
----------------
| |
CDN Processing
23.3 Upload Gateway上传网关
作用:
统一入口。
所有上传:
经过:
api.company.com/upload
负责:
- 身份认证
- Token校验
- 限流
- 路由
请求:
POST /upload/create
参数:
{
fileName:
"test.jpg",
size:
102400,
hash:
"xxxx"
}
返回:
{
taskId:
"10001",
uploadToken:
"xxx"
}
23.4 文件服务中心设计
核心:
File Service。
管理:
文件元数据
权限
生命周期
访问地址
业务关系
数据库:
file_info
file_permission
file_relation
23.5 file_info文件表
设计:
CREATE TABLE file_info(
id BIGINT,
hash VARCHAR(64),
name VARCHAR(255),
size BIGINT,
type VARCHAR(50),
url TEXT,
status VARCHAR(20),
create_time DATETIME
)
数据:
{
"id":10001,
"name":"avatar.png",
"size":204800,
"status":"safe"
}
23.6 文件和业务关联
同一个文件:
可能:
多个业务使用。
例如:
商品图片:
商城使用。
社区帖子:
也使用。
不要复制文件。
设计:
file_relation。
CREATE TABLE file_relation(
file_id BIGINT,
biz_type VARCHAR(50),
biz_id VARCHAR(64)
)
例如:
file_id:
10001
biz:
product
id:
8888
23.7 秒传系统云端设计
客户端:
计算:
Hash。
服务器:
查询:
SELECT *
FROM file_info
WHERE hash=?
存在:
返回:
{
exist:true,
url:"xxx"
}
不存在:
创建上传任务。
23.8 OSS目录规划
不要:
全部放根目录。
推荐:
bucket
├── avatar
│
├── video
│
├── image
│
├── document
│
└── temp
23.9 文件命名策略
不要:
使用用户文件名。
例如:
test.jpg
可能:
覆盖。
推荐:
UUID。
例如:
20260731/
a8f923fd.jpg
规则:
业务
+
日期
+
UUID
23.10 CDN加速架构
用户访问:
不要:
直接OSS。
错误:
用户
↓
OSS
正确:
用户
↓
CDN节点
↓
OSS源站
优势:
- 降低延迟
- 减少OSS压力
- 节省成本
23.11 文件URL生成
三种方式。
公开URL
例如:
头像。
https://cdn.com/a.jpg
私有URL
需要签名。
例如:
https://cdn.com/a.jpg?token=xxx
临时URL
有效时间:
10分钟
23.12 图片处理服务
上传:
原图。
后台:
生成:
原图
↓
缩略图
↓
WebP
↓
水印
例如:
original.jpg
small.jpg
thumb.jpg
23.13 图片处理流程
Upload Success
↓
Image Service
↓
Resize
↓
Compress
↓
Store
↓
CDN
23.14 视频处理服务
视频:
上传完成:
进入任务队列。
流程:
video.mp4
↓
Transcoding
↓
480P
↓
720P
↓
1080P
↓
M3U8
↓
CDN
23.15 视频转码状态
enum VideoStatus{
UPLOADED,
PROCESSING,
SUCCESS,
FAILED
}
APP:
显示:
视频处理中...
23.16 文件审核流程
企业:
UGC场景。
例如:
社区。
流程:
上传
↓
审核
↓
通过
↓
发布
状态:
WAIT_SCAN
SAFE
BLOCK
23.17 文件权限系统
例如:
企业网盘。
权限:
owner
reader
writer
admin
表:
file_permission
file_id
user_id
role
23.18 文件删除策略
不要:
直接删除OSS。
采用:
软删除。
状态:
ACTIVE
↓
DELETED
↓
CLEAN
定时任务:
清理。
23.19 文件生命周期
完整:
上传
↓
存储
↓
访问
↓
低频
↓
归档
↓
删除
23.20 企业文件平台能力
最终:
File Platform
├── Upload
├── Storage
├── CDN
├── Image Service
├── Video Service
├── Permission
├── Audit
└── Monitor
23.21 HarmonyOS客户端最终接入
业务:
只关心:
UploadManager.upload({
file
})
返回:
{
fileId:"10001",
url:"https://cdn.xxx.com/a.jpg"
}
23.22 典型企业应用
电商
商品:
- 图片
- 视频
- 详情附件
IM
聊天:
- 图片
- 文件
- 视频
直播
主播:
- 视频
- 封面
- 回放
企业网盘
- 大文件
- 分片
- 权限
23.23 完整云端链路
HarmonyOS NEXT
|
↓
Upload SDK
|
↓
Upload Gateway
|
↓
File Service
|
↓
OSS
|
↓
Processing
|
↓
CDN
|
↓
用户访问
第二十四章 HarmonyOS NEXT 文件上传项目实战:商城商品图片上传系统
电商商城是文件上传最典型的业务场景。
商品中心通常需要:
- 商品主图
- 商品轮播图
- 商品详情图
- 商品视频
- 商品规格附件
一个商品可能:
id="u4m7qx"
商品A
图片:
10张
视频:
1个
详情附件:
多个
24.1 商品图片上传业务架构
完整流程:
id="p8n3mv"
商家后台
↓
选择商品图片
↓
图片预处理
↓
Upload SDK
↓
文件服务
↓
OSS
↓
图片处理
↓
CDN
↓
商品发布
24.2 商品图片数据模型
商品:
id="m7x2qw"
{
productId:"10001",
name:"手机",
images:[
{
fileId:"f001",
url:"https://cdn.xxx/1.jpg",
sort:1
},
{
fileId:"f002",
url:"https://cdn.xxx/2.jpg",
sort:2
}
]
}
24.3 商品图片表设计
数据库:
id="k9q5mw"
CREATE TABLE product_image(
id BIGINT,
product_id BIGINT,
file_id BIGINT,
url VARCHAR(500),
sort INT,
create_time DATETIME
)
24.4 商品图片上传流程
用户点击:
id="c5m8zx"
添加图片
执行:
id="x8q3mv"
PhotoPicker
↓
选择图片
↓
ImageProcessor
↓
UploadManager
↓
返回URL
↓
保存商品
24.5 HarmonyOS图片选择
使用:
PhotoViewPicker。
示例:
id="r7m2qx"
import {
photoAccessHelper
}
from '@kit.MediaLibraryKit';
创建:
id="h4v8mz"
let picker =
new photoAccessHelper.PhotoViewPicker()
选择:
id="n6x3qp"
let result =
await picker.select({
MIMEType:
'image/*'
})
返回:
id="b8m5qw"
{
uri:
"file://xxx.jpg"
}
24.6 图片上传前处理
不要直接上传原图。
流程:
id="z9q4mv"
原图片
↓
读取PixelMap
↓
压缩
↓
转换格式
↓
上传
24.7 商品图片压缩策略
不同用途:
不同质量。
例如:
主图
id="m3x8qp"
尺寸:
800x800
质量:
85%
详情图
id="q7v2mz"
尺寸:
1200px
质量:
80%
24.8 ImageProcessor设计
目录:
id="v5m9qx"
image
ImageProcessor.ets
代码:
id="w8q2mk"
export class ImageProcessor {
static async compress(
file
){
let pixelMap=
await ImageSource.create(
file
)
let result=
await ImagePacker.pack(
pixelMap,
{
quality:80
}
)
return result
}
}
24.9 多图片上传
商品:
一次:
10张图片。
不能:
串行。
错误:
id="f3x7mv"
图片1
↓
完成
图片2
↓
完成
图片3
↓
完成
耗时:
过长。
正确:
并发上传。
id="h8m4qx"
图片1
图片2
图片3
同时上传
24.10 商品多文件任务模型
创建:
UploadBatch。
id="r6q9mv"
class UploadBatch{
id:string
tasks:
UploadTask[]=[]
progress:number=0
}
24.11 批量上传管理
id="n4x7pz"
uploadImages(files){
let tasks=[]
files.forEach(file=>{
tasks.push(
UploadManager.upload(file)
)
})
return tasks
}
24.12 上传列表UI设计
页面:
id="x2m8qw"
商品图片
+ 添加图片
[图片1]
上传完成
[图片2]
上传中 60%
[图片3]
等待
ArkUI:
id="q8m3vz"
ForEach(
tasks,
task=>{
UploadItem({
task
})
}
)
24.13 商品图片排序
商城:
支持拖动排序。
例如:
id="w7m4qx"
图片A
图片B
图片C
拖动
图片C
图片A
图片B
保存:
sort字段。
24.14 上传完成回调
上传成功:
id="c9m5xp"
task.onSuccess=
(url)=>{
product.images.push({
url:url
})
}
最终:
提交商品。
24.15 商品草稿保存
企业后台:
不能要求:
全部上传完成才能保存。
支持:
草稿。
状态:
id="k3q8mv"
DRAFT
↓
UPLOADING
↓
READY
↓
PUBLISHED
24.16 上传失败处理
例如:
第5张失败。
不要:
全部失败。
显示:
id="m8x4pz"
图片1 √
图片2 √
图片3 √
图片4 √
图片5 ×
[重新上传]
24.17 商品视频上传
商品视频:
特点:
大。
使用:
Multipart。
流程:
id="v7m2qx"
video.mp4
↓
切片
↓
上传
↓
转码
↓
生成播放地址
24.18 商品视频封面生成
上传视频后:
服务器:
抽取第一帧。
生成:
id="p9x5mw"
video-cover.jpg
商品:
保存:
id="z6q2mv"
{
video:
"video.mp4",
cover:
"cover.jpg"
}
24.19 OSS目录设计
推荐:
id="h3m8qx"
product/
├── 2026/
│
├── 07/
│
└── productId/
|
├── image/
|
└── video/
例如:
id="r5x8mv"
product/2026/07/10001/image/a001.jpg
24.20 CDN访问
上传:
OSS地址:
id="w2q7mz"
oss.xxx.com/a.jpg
转换:
CDN:
id="n8m3qx"
cdn.xxx.com/a.jpg
用户访问:
CDN。
24.21 商品图片安全
上传:
检查:
id="x6q9mv"
文件类型
文件大小
图片内容
禁止:
伪装图片。
24.22 商品后台完整代码结构
id="m4x7qw"
product
├── ProductCreatePage.ets
├── ProductImagePicker.ets
├── ProductUploadList.ets
├── ProductVideoUpload.ets
└── ProductService.ets
24.23 ProductImagePicker组件
id="c7m2vx"
@Component
export struct ProductImagePicker {
tasks:
UploadTask[]=[]
build(){
Column(){
Button(
"添加图片"
)
ForEach(
this.tasks,
task=>{
UploadItem({
task
})
}
)
}
}
}
24.24 商品发布完整链路
最终:
id="u9m5qx"
商家选择图片
↓
图片压缩
↓
UploadSDK
↓
OSS
↓
图片服务
↓
CDN
↓
保存商品
↓
商品上线
第二十四章 HarmonyOS NEXT 文件上传项目实战:商城商品图片上传系统
电商商城是文件上传最典型的业务场景。
商品中心通常需要:
- 商品主图
- 商品轮播图
- 商品详情图
- 商品视频
- 商品规格附件
一个商品可能:
id="u4m7qx"
商品A
图片:
10张
视频:
1个
详情附件:
多个
24.1 商品图片上传业务架构
完整流程:
id="p8n3mv"
商家后台
↓
选择商品图片
↓
图片预处理
↓
Upload SDK
↓
文件服务
↓
OSS
↓
图片处理
↓
CDN
↓
商品发布
24.2 商品图片数据模型
商品:
id="m7x2qw"
{
productId:"10001",
name:"手机",
images:[
{
fileId:"f001",
url:"https://cdn.xxx/1.jpg",
sort:1
},
{
fileId:"f002",
url:"https://cdn.xxx/2.jpg",
sort:2
}
]
}
24.3 商品图片表设计
数据库:
id="k9q5mw"
CREATE TABLE product_image(
id BIGINT,
product_id BIGINT,
file_id BIGINT,
url VARCHAR(500),
sort INT,
create_time DATETIME
)
24.4 商品图片上传流程
用户点击:
id="c5m8zx"
添加图片
执行:
id="x8q3mv"
PhotoPicker
↓
选择图片
↓
ImageProcessor
↓
UploadManager
↓
返回URL
↓
保存商品
24.5 HarmonyOS图片选择
使用:
PhotoViewPicker。
示例:
id="r7m2qx"
import {
photoAccessHelper
}
from '@kit.MediaLibraryKit';
创建:
id="h4v8mz"
let picker =
new photoAccessHelper.PhotoViewPicker()
选择:
id="n6x3qp"
let result =
await picker.select({
MIMEType:
'image/*'
})
返回:
id="b8m5qw"
{
uri:
"file://xxx.jpg"
}
24.6 图片上传前处理
不要直接上传原图。
流程:
id="z9q4mv"
原图片
↓
读取PixelMap
↓
压缩
↓
转换格式
↓
上传
24.7 商品图片压缩策略
不同用途:
不同质量。
例如:
主图
id="m3x8qp"
尺寸:
800x800
质量:
85%
详情图
id="q7v2mz"
尺寸:
1200px
质量:
80%
24.8 ImageProcessor设计
目录:
id="v5m9qx"
image
ImageProcessor.ets
代码:
id="w8q2mk"
export class ImageProcessor {
static async compress(
file
){
let pixelMap=
await ImageSource.create(
file
)
let result=
await ImagePacker.pack(
pixelMap,
{
quality:80
}
)
return result
}
}
24.9 多图片上传
商品:
一次:
10张图片。
不能:
串行。
错误:
id="f3x7mv"
图片1
↓
完成
图片2
↓
完成
图片3
↓
完成
耗时:
过长。
正确:
并发上传。
id="h8m4qx"
图片1
图片2
图片3
同时上传
24.10 商品多文件任务模型
创建:
UploadBatch。
id="r6q9mv"
class UploadBatch{
id:string
tasks:
UploadTask[]=[]
progress:number=0
}
24.11 批量上传管理
id="n4x7pz"
uploadImages(files){
let tasks=[]
files.forEach(file=>{
tasks.push(
UploadManager.upload(file)
)
})
return tasks
}
24.12 上传列表UI设计
页面:
id="x2m8qw"
商品图片
+ 添加图片
[图片1]
上传完成
[图片2]
上传中 60%
[图片3]
等待
ArkUI:
id="q8m3vz"
ForEach(
tasks,
task=>{
UploadItem({
task
})
}
)
24.13 商品图片排序
商城:
支持拖动排序。
例如:
id="w7m4qx"
图片A
图片B
图片C
拖动
图片C
图片A
图片B
保存:
sort字段。
24.14 上传完成回调
上传成功:
id="c9m5xp"
task.onSuccess=
(url)=>{
product.images.push({
url:url
})
}
最终:
提交商品。
24.15 商品草稿保存
企业后台:
不能要求:
全部上传完成才能保存。
支持:
草稿。
状态:
id="k3q8mv"
DRAFT
↓
UPLOADING
↓
READY
↓
PUBLISHED
24.16 上传失败处理
例如:
第5张失败。
不要:
全部失败。
显示:
id="m8x4pz"
图片1 √
图片2 √
图片3 √
图片4 √
图片5 ×
[重新上传]
24.17 商品视频上传
商品视频:
特点:
大。
使用:
Multipart。
流程:
id="v7m2qx"
video.mp4
↓
切片
↓
上传
↓
转码
↓
生成播放地址
24.18 商品视频封面生成
上传视频后:
服务器:
抽取第一帧。
生成:
id="p9x5mw"
video-cover.jpg
商品:
保存:
id="z6q2mv"
{
video:
"video.mp4",
cover:
"cover.jpg"
}
24.19 OSS目录设计
推荐:
id="h3m8qx"
product/
├── 2026/
│
├── 07/
│
└── productId/
|
├── image/
|
└── video/
例如:
id="r5x8mv"
product/2026/07/10001/image/a001.jpg
24.20 CDN访问
上传:
OSS地址:
id="w2q7mz"
oss.xxx.com/a.jpg
转换:
CDN:
id="n8m3qx"
cdn.xxx.com/a.jpg
用户访问:
CDN。
24.21 商品图片安全
上传:
检查:
id="x6q9mv"
文件类型
文件大小
图片内容
禁止:
伪装图片。
24.22 商品后台完整代码结构
id="m4x7qw"
product
├── ProductCreatePage.ets
├── ProductImagePicker.ets
├── ProductUploadList.ets
├── ProductVideoUpload.ets
└── ProductService.ets
24.23 ProductImagePicker组件
id="c7m2vx"
@Component
export struct ProductImagePicker {
tasks:
UploadTask[]=[]
build(){
Column(){
Button(
"添加图片"
)
ForEach(
this.tasks,
task=>{
UploadItem({
task
})
}
)
}
}
}
24.24 商品发布完整链路
最终:
id="u9m5qx"
商家选择图片
↓
图片压缩
↓
UploadSDK
↓
OSS
↓
图片服务
↓
CDN
↓
保存商品
↓
商品上线
第二十五章 HarmonyOS NEXT 文件上传项目实战:IM即时通讯图片/文件上传系统
即时通讯(IM)是文件上传复杂度最高的业务之一。
因为 IM 上传不是简单:
选择文件
↓
上传
↓
发送
而是:
用户选择图片
↓
立即显示消息气泡
↓
后台上传文件
↓
上传成功
↓
发送正式消息
↓
对方收到
用户体验必须类似微信、Telegram、企业微信。
25.1 IM文件上传整体架构
完整链路:
用户A
|
↓
HarmonyOS APP
|
-------------------
| |
消息模块 上传模块
| |
↓ ↓
WebSocket Upload SDK
| |
↓ ↓
IM Server OSS
|
↓
用户B
25.2 IM上传核心特点
相比商城:
IM多了:
1. 即时性
用户点击发送:
马上看到消息。
2. 状态同步
需要:
发送中
上传中
发送成功
失败
3. 离线恢复
APP关闭:
重新打开:
继续发送。
25.3 消息状态设计
定义:
export enum MessageStatus{
CREATING,
UPLOADING,
UPLOADED,
SENDING,
SENT,
FAILED
}
状态流:
CREATING
↓
UPLOADING
↓
UPLOADED
↓
SENDING
↓
SENT
失败:
UPLOADING
↓
FAILED
25.4 IM消息数据模型
Message:
export class Message{
id:string
conversationId:string
type:string
content:string
fileId:string
status:
MessageStatus
createTime:number
}
图片消息:
{
type:"image",
fileId:"f10001",
url:"cdn.xxx.com/a.jpg"
}
25.5 图片发送流程
用户:
点击图片。
流程:
PhotoPicker
↓
生成本地消息
↓
显示聊天气泡
↓
压缩图片
↓
上传OSS
↓
更新消息
↓
发送服务器
25.6 为什么先显示消息
错误:
等待上传完成:
选择图片
↓
等待30秒
↓
显示消息
用户感觉:
卡顿。
正确:
乐观更新。
类似微信:
立即显示:
[图片]
上传中...
25.7 创建临时消息
选择图片后:
立即创建。
let message=
new Message()
message.id=
generateUUID()
message.status=
MessageStatus.UPLOADING
message.localPath=
file.uri
保存:
本地数据库。
25.8 IM本地消息数据库
表:
message。
CREATE TABLE message(
id TEXT PRIMARY KEY,
conversation_id TEXT,
type TEXT,
content TEXT,
file_id TEXT,
status TEXT,
create_time INTEGER
)
25.9 上传绑定消息
UploadTask:
增加:
messageId。
class UploadTask{
id:string
messageId:string
progress:number
}
关系:
Message
|
|
UploadTask
|
|
File
25.10 图片压缩
聊天图片:
不需要原图。
策略:
原图:
8MB
↓
压缩
↓
500KB
优势:
- 上传快
- 节省流量
- 降低服务器成本
25.11 IM图片处理
流程:
Image
↓
Resize
↓
Compress
↓
Remove EXIF
↓
Upload
25.12 Upload SDK调用
聊天模块:
不关心OSS。
代码:
let task=
UploadManager
.upload({
file,
type:"chat_image"
})
监听:
task.onProgress(
p=>{
message.progress=p
}
)
25.13 上传完成更新消息
成功:
task.onSuccess(
result=>{
message.fileId=
result.fileId
message.status=
MessageStatus.UPLOADED
sendMessage(message)
}
)
25.14 WebSocket发送消息
上传成功:
发送:
{
type:"image",
fileId:"10001",
url:"cdn.xxx/a.jpg"
}
服务器:
转发。
25.15 接收方处理
收到:
{
type:"image",
url:"xxx"
}
显示:
图片
↓
加载CDN
↓
展示
25.16 图片缓存系统
聊天大量图片:
必须缓存。
三级:
Memory Cache
↓
Disk Cache
↓
CDN
25.17 ImageCache设计
class ImageCache{
memory:
Map<string,PixelMap>
get(url){
return this.memory.get(url)
}
}
25.18 大文件发送
例如:
发送:
1GB视频。
不能:
普通上传。
采用:
Multipart。
流程:
视频
↓
Chunk
↓
Upload SDK
↓
OSS
↓
生成文件ID
↓
发送消息
25.19 文件消息模型
{
type:"file",
name:"demo.zip",
size:1024000000,
fileId:"f20001"
}
25.20 文件上传进度展示
聊天气泡:
demo.zip
██████░░░░
60%
状态:
绑定:
UploadTask。
25.21 断网恢复
场景:
上传:
80%。
网络断开。
保存:
{
uploadId:"xxx",
completedChunks:[
1,
2,
3
]
}
恢复:
继续上传。
25.22 APP重启恢复
启动:
查询:
SELECT *
FROM message
WHERE status!='SENT'
恢复:
未发送消息
↓
重新上传
↓
重新发送
25.23 失败重试
用户点击:
重新发送。
代码:
retryMessage(id){
let msg=
findMessage(id)
UploadManager
.resume(
msg.taskId
)
}
25.24 视频消息流程
短视频:
录制
↓
生成缩略图
↓
压缩
↓
上传
↓
转码
↓
发送消息
25.25 视频转码状态同步
服务器:
返回:
{
status:
"processing"
}
客户端:
显示:
视频处理中...
完成:
推送:
{
status:
"ready"
}
25.26 IM上传架构总结
完整:
Message
|
|
UploadTask
|
|
UploadSDK
|
|
OSS
|
|
FileService
|
|
WebSocket
|
|
Receiver
更多推荐



所有评论(0)