【HarmonyOS ArkTS】 Axios 图片上传

766 阅读9分钟

基于 ArkTS 实现图片上传

本技术文档基于项目中的文件,详细说明图片上传流程的实现逻辑。 适合 HarmonyOS 开发初学者理解如何在 ArkTS 中完成图片选择、沙箱拷贝与上传服务器的操作。


一、整体流程步骤

  1. 用户点击调用手机图库选择图片后获取URI
  2. 将图片(压缩写入/复制)到应用沙箱目录
  3. 使用 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

    1. EntryAbilityonCreate生命周期钩子函数中:
    onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
      ...
       // 通过应用状态管理对上下文进行状态创建设置
       AppStorage.setOrCreate('context', this.context)
    }
    
    1. 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

    1. EntryAbilityonCreate生命周期钩子函数中【如果上一部已经创建可忽略】:
    onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
      ...
       // 通过应用状态管理对上下文进行状态创建设置
       AppStorage.setOrCreate('context', this.context)
    }
    
    1. 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
axiosHTTP 客户端,用于发送请求
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 中图片上传的核心流程与关键技术点。开发者可根据此文档快速理解图片上传逻辑,并根据需求进行扩展与优化。