基于 ArkTS 实现图片上传
本技术文档基于项目中的文件,详细说明图片上传流程的实现逻辑。 适合 HarmonyOS 开发初学者理解如何在 ArkTS 中完成图片选择、沙箱拷贝与上传服务器的操作。
一、整体流程步骤
- 用户点击调用手机图库选择图片后获取URI
- 将图片(压缩写入/复制)到应用沙箱目录
- 使用 Axios 发送请求上传图片接收服务端返回结果并提示用户
二、关键代码模块封装
1. 用户点击调用手机图库选择图片后获取URI
import { photoAccessHelper } from '@kit.MediaLibraryKit';
/**
* 设置视图选择器,用于从相册中选择图片
* 通过配置相册访问参数并调用相册选择器,以获取用户选择的图片路径
* 主要涉及相册访问配置、相册实例创建、图片选择和路径获取等步骤
*
* @returns {Promise<Array<string>>}
* 返回一个Promise,解析为用户选择的图片的完整路径数组
*/
const setViewPicker = async () => {
// 配置图片参数实例
const opt = new photoAccessHelper.PhotoSelectOptions()
// 设置相册最大选择图片数量
opt.maxSelectNumber = 1
// 指定可选择类型 (image/* | video/* | image/movingPhoto)
opt.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
// 创建调用相册实例
const albumEx = new photoAccessHelper.PhotoViewPicker()
// 选择图片 返回promise 可通过 async/await 获取结果
const selectRes = await albumEx.select(opt)
// 获取图片完整路径数组
return selectRes.photoUris
}
- 功能说明:
- 创建
PhotoSelectOptions实例配置选择器参数。 - 设置最大选择数量为 1 张图片。
- MIME 类型限定为图片类型。
- 使用
PhotoViewPicker调起图库并返回选中图片的 URI 列表。
- 创建
2. 拷贝/压缩 图片到应用沙箱目录
-
fileToSandbox封装的工具函数中需要获取上下文this:- 在
EntryAbility中onCreate生命周期钩子函数中:
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { ... // 通过应用状态管理对上下文进行状态创建设置 AppStorage.setOrCreate('context', this.context) }- 在
fileToSandbox工具函数中先进行获取使用
const fileToSandbox = async (uri: string, fileSuffix: ImageFormat = '.jpg') => { ... // 获取应用状态管理的’context‘上下文 const ctx = AppStorage.get<Context>('context') ... // 直接传入使用 const targetFilePath = getContext(ctx).cacheDir + '/' + fileName_new } - 在
import { fileIo } from '@kit.CoreFileKit';
import { util } from '@kit.ArkTS';
type ImageFormat = '.jpg' | '.jpeg' | '.png' | '.gif' | '.webp' | '.bmp' | '.svg' | '.avif';
/**
* 将图片文件 拷贝/压缩 到沙箱环境
*
* 此函数用于将给定URI的文件复制到应用的沙箱目录中,在复制过程中可以保留原文件的格式
* 它首先生成一个唯一的文件名,以避免文件重复,然后打开源文件,并在沙箱目录中创建一个新文件,
* 最后将源文件内容复制到新文件中此函数支持常见的图片格式复制
*
* @param uri 文件的URI,指定要复制的文件路径
* @param fileSuffix 文件后缀名,决定了复制后的文件格式,默认为'.jpg',
* 可选格式包括 '.jpg' | '.jpeg' | '.png' | '.gif' | '.webp' | '.bmp' | '.svg' | '.avif';
* @returns 返回复制到沙箱后的文件名
*/
const fileToSandbox = async (uri: string, fileSuffix: ImageFormat = '.jpg') => {
// 获取应用状态管理的’context‘上下文
const ctx = AppStorage.get<Context>('context')
// 调用util工具模块生成唯一的文件名称
const fileName_new = util.generateRandomUUID() + fileSuffix
// 定义目标文件路径,存储拷贝后的文件(沙箱目录)
const targetFilePath = getContext(ctx).cacheDir + '/' + fileName_new
// 【只读模式】打开源文件
const sourceFile = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY)
// 【读写模式】创建目标文件
const targetFile = fileIo.openSync(targetFilePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
// 【复制】将打开的文件同步复制到新构建文件路径
fileIo.copyFileSync(sourceFile.fd, targetFile.fd)
//关闭打开的【源文件|目标文件】[文件必须关闭,可能会导致资源泄露/文件锁定]
fileIo.closeSync(sourceFile.fd)
fileIo.closeSync(targetFile.fd)
return fileName_new
}
- 功能说明:
- 使用
fileIo模块进行文件操作。 - 将图片从系统 URI 地址复制到应用私有缓存目录(沙箱)。
- 返回新生成的文件名以供后续上传使用。
- 注意关闭打开的文件描述符,防止资源泄露。
- 使用
⚠️ 可在此处添加图片压缩逻辑,如使用
image模块对原始图片进行压缩后再写入沙箱。
import { fileIo } from '@kit.CoreFileKit';
import { util } from '@kit.ArkTS';
import { image } from '@kit.ImageKit';
type ImageFormat = '.jpg' | '.jpeg' | '.png' | '.gif' | '.webp' | '.bmp' | '.svg' | '.avif';
/**
* 将指定 URI 的图片文件复制并压缩后保存到应用沙箱缓存目录中
*
* @param uri - 图片文件的原始路径 URI
* @param fileSuffix - 文件后缀名,表示图片格式,默认为 '.jpg'
* @returns 返回新生成的文件名(不含路径)
*/
fileToSandbox = async (uri: string, fileSuffix: ImageFormat = '.jpg') => {
// 获取应用状态管理的’context‘上下文
const ctx = AppStorage.get<Context>('context')
const fileName_new = util.generateRandomUUID() + fileSuffix
const targetFilePath = getContext(ctx).cacheDir + '/' + fileName_new
const sourceFile = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY)
const targetFile = fileIo.openSync(targetFilePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
// 【可对原始图片进行压缩处理】 使用 packing 压缩图片,二进制图片数据
// 通过文件描述符创建图像源,用于后续压缩处理;
const imageSource = image.createImageSource(sourceFile.fd)
// 创建图像打包器,用于将图像数据打包为特定格式
const imagePacker = image.createImagePacker()
// 使用 packToData 方法将图像压缩为 JPEG 格式,质量设置为 70%
const arrayBuffer = await imagePacker.packToData(imageSource, { format: 'image/jpeg', quality: 70 })
// 【将压缩后的图像数据写入目标文件
fileIo.writeSync(targetFile.fd, arrayBuffer)
//关闭打开的【源文件|目标文件】[文件必须关闭,可能会导致资源泄露/文件锁定]
fileIo.closeSync(sourceFile.fd)
fileIo.closeSync(targetFile.fd)
return fileName_new
}
- 功能说明:
- 使用
fileIo模块进行文件操作。 - 使用
image模块进行图片压缩。 - 将图片写入应用私有缓存目录(沙箱)。
- 返回新生成的文件名以供后续上传使用。
- 注意关闭打开的文件描述符,防止资源泄露。
- 使用
3. Axios发送请求上传图片接收服务端返回结果并提示用户
-
uploadFile封装的Axios接口请求中仍然需要获取上下文this:- 在
EntryAbility中onCreate生命周期钩子函数中【如果上一部已经创建可忽略】:
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { ... // 通过应用状态管理对上下文进行状态创建设置 AppStorage.setOrCreate('context', this.context) }- 在
uploadFileAxios接口请求中先进行上下文获取使用
const uploadFile = async (URL: string,uri: string) => { ... // 获取应用状态管理的’context‘上下文 const ctx = AppStorage.get<Context>('context') ... // 创建axios实例中直接进行使用 const response: AxiosResponse<IResponse> = await axios.post<IResponse, AxiosResponse<IResponse>, FormData>(URL, formData, { headers: { 'Content-Type': 'multipart/form-data' }, context: getContext(ctx) }) } - 在
import axios, { FormData, AxiosResponse } from '@ohos/axios';
/**
* 异步上传文件到服务器
*
* 将指定的文件上传到服务器它使用了axios库来发送HTTP请求,并传递文件数据
* 主要步骤包括:创建FormData对象以准备文件数据、配置axios请求参数、发送POST请求并处理响应
*
* @param URL 上传接口地址
* @param uri 文件的URI,用于指定需要上传的文件
* @returns 返回服务器的响应数据
*/
const uploadFile = async (URL: string,uri: string) => {
try {
// 获取应用状态管理的’context‘上下文
const ctx = AppStorage.get<Context>('context')
// 创建一个新的FormData对象,用于准备文件上传 这是axios上传图片需要的对象类型 FormData
const formData: FormData = new FormData()
// 添加一个名字叫img的数据 数据需要 `internal://cache/在沙箱目录的文件地址`
formData.append('img', 'internal://cache/' + uri)
// 创建axios实例
const response: AxiosResponse<IResponse> =
await axios.post<IResponse, AxiosResponse<IResponse>, FormData>(URL,
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
},
context: getContext(ctx)
})
return response.data
} catch (err) {
console.info('err:' + JSON.stringify(err));
}
}
- 功能说明:
- 构建
FormData对象用于上传。 - 使用
axios发送 POST 请求,指定multipart/form-data格式。 - 上传路径为
internal://cache/xxx.jpg,表示从沙箱缓存目录读取文件。 - 返回服务端响应数据,包含图片 URL 或错误信息。
- 构建
⚠️ Stage 模型:上传类型支持uri和ArrayBuffer
- 上传的内容为ArrayBuffer:
import axios, { FormData, AxiosResponse } from '@ohos/axios';
import fs from '@ohos.file.fs';
// 获取应用状态管理的’context‘上下文
const ctx = AppStorage.get<Context>('context')
let formData = new FormData()
// 获取当前应用的缓存目录路径
let cacheDir = getContext(ctx).cacheDir
try {
// 写入
let path = cacheDir + '/hello.txt';
// 以同步方式打开或创建一个文件 [CREATE: 如果文件不存在则创建; READ_WRITE: 以读写模式打开]
let file = fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE)
// 以同步方法将字符串 "hello, world" 同步写入文件
fs.writeSync(file.fd, "hello, world");
// 同步将缓冲区中的数据提交到磁盘,确保数据已写入
fs.fsyncSync(file.fd);
// 关闭文件句柄,释放资源
fs.closeSync(file.fd);
// 读取
// 以只读模式 (0o2) 打开文件
let file2 = fs.openSync(path, 0o2);
// lstatSync(): 获取文件元信息,如大小
let stat = fs.lstatSync(path);
// 创建一个 ArrayBuffer 用于存储文件内容
let buf2 = new ArrayBuffer(stat.size);
// 同步从文件中读取数据到 buf2 缓冲区
fs.readSync(file2.fd, buf2);
// 同步刷新缓冲区并关闭文件
fs.fsyncSync(file2.fd);
fs.closeSync(file2.fd);
// 将读取的内容添加到 FormData
// 将读取到的二进制数据作为字段 'file' 添加到 FormData 对象中
formData.append('file', buf2);
// 这在上传时可以告诉服务器这个文件的名称和类型
// formData.append('file', buf2, { filename: 'text.txt', type: 'text/plain'});
} catch (err) {
// 捕获可能发生的异常,例如文件权限问题、路径无效等
console.info('err:' + JSON.stringify(err));
}
- 上传的内容为uri:
import axios, { FormData, AxiosResponse } from '@ohos/axios';
// 获取应用状态管理的’context‘上下文
const ctx = AppStorage.get<Context>('context')
// internal协议类型 | "internal://cache/"为必填字段
formData.append('xxx', 'internal://cache/' + 'xxx.tet')
// 沙箱路径 | 获取当前应用的沙箱缓存目录路径
let cacheDir = getContext(ctx).cacheDir
formData.append('xxx', cacheDir + '/xxx.txt');
- uri支持
internal协议类型和沙箱路径:internal协议类型:"internal://cache/"为必填字段-
- 示例:
internal://cache/path/to/file.txt;
- 示例:
- 沙箱路径:
-
- 示例:
cacheDir + '/hello.txt'
- 示例:
三、Example:头像更换
// 头像点击事件
.onClick(async () => {
const uriLis = await this.setViewPicker()
const fileName = await this.copyFileToSandbox(uriLis[0])
const result = await this.uploadFile(fileName)
this.userInfo.avatar = result.data.url
promptAction.showToast({ message: result.message })
})
- 用户点击头像 → 打开图库 → 获取图片 URI → 拷贝至沙箱 → 上传至服务器 → 显示接口返回图片 → 结果提示。
四、相关依赖模块说明
| 模块 | 功能 |
|---|---|
photoAccessHelper | 提供访问系统图库的能力 |
fileIo | 提供文件读写能力 |
util | 提供工具方法,如生成唯一 UUID |
axios | HTTP 客户端,用于发送请求 |
FormData | 构建上传请求体 |
getContext(this) | 获取当前组件上下文,用于文件访问 |
五、常见问题及解决方案
1. 图片上传失败或返回 400 错误
-
可能原因:
- 文件路径不正确。
Content-Type未设置为multipart/form-data。- 后端接口字段名与
formData.append()的键名不一致。
-
解决建议:
- 确认
internal://cache/xxx.jpg文件存在。 - 查看服务端日志确认是否接收到请求。
- 使用抓包工具检查请求格式。
- 确认
2. 文件无法读取或复制失败
-
可能原因:
- 权限不足。
- 文件路径非法或不存在。
- 未关闭已打开的文件句柄。
-
解决建议:
- 检查文件权限和路径合法性。
- 添加异常处理逻辑(try/catch)。
- 确保每次打开文件后都调用
closeSync(fd)。
六、扩展建议
- ✅ 支持多图上传:修改
maxSelectNumber并循环处理多个文件。 - ✅ 增加图片预览:上传前展示本地图片。
- ✅ 添加加载动画:上传过程中显示 loading 提示。
- ✅ 图片压缩:使用
image.createImagePacker()压缩后再上传。 - ✅ 错误重试机制:网络失败时提供重试按钮。
七、总结
HarmonyOS 中图片上传的核心流程与关键技术点。开发者可根据此文档快速理解图片上传逻辑,并根据需求进行扩展与优化。