鸿蒙开发基础 - 如何实现图片多张上传以及工具类封装

1,340 阅读5分钟

1. 前言

在开发的过程中,不可避免的会遇到需要上传图片资源的情况。比如用户的头像上传,用户反馈的图片内容上传等等。
如何实现图片上传呢?当我去翻看官文文档的时候,看到文档上一坨一坨的没有注释的代码,我整个人都不好了!

54747f41c96692e91025c3d7e7dffc27.jpg

所以,这里在简化官网的案例后,对图片上传业务做一个比较详细的说明。
那么就来一步一步看看如何实现上传图片这个功能的吧~并且在最后还提供一个封装好可以通用的图片上传工具类
为了方便演示,这里用到的上传接口为图床接口:点击查看 API接口文档

2. 图片上传的步骤

首先,我们需要知道,在鸿蒙的系统中,应用是没有办法直接操作系统相册内的内容的。那么,我们需要怎么样才能实现上传图片的功能呢?
其实可以换个思路,既然我没办法去操作系统相册里的资源,那我可以不可以把图片资源复制到我自己应用内的缓存目录,然后再进行操作呢?
答案当然是可以的,同时,这也是鸿蒙开发中所推荐的方式
在上传图片的功能实现中,一共有3个核心步骤:

  • 如何选择图片(使用picker模块)
  • 如何拷贝图片到缓存目录(使用fileIo模块)
  • 如何实现图片上传(使用request模块)

注意,使用图片上传需要对网络权限进行配置!!!如何配置网络权限,可以点击我的另一篇文章查看
-> (鸿蒙开发基础 - Http请求数据接口动态渲染首页)

3f1f842250e31f67dccc3dc23710655e.jpg

2.1 如何选择图片?

这里需要使用 picker 模块来选择一张图片,下面是官网对picker模块的介绍 传送门

@ohos.file.picker (选择器)

选择器(Picker)是一个封装PhotoViewPicker、DocumentViewPicker、AudioViewPicker等系统应用选择与保存能力的模块。应用可以自行选择使用哪种picker实现文件选择和文件保存的功能。

PhotoViewPicker

图库选择器对象,用来支撑选择图片/视频和保存图片/视频等用户场景。在使用前,需要先创建PhotoViewPicker实例。

系统能力:SystemCapability.FileManagement.UserFileService

示例:

let photoPicker = new picker.PhotoViewPicker();

这里不一句一句解释官方文档的代码啦,我们直接上简化后的代码
这段代码主要内容就是拉起一个图片选择器,并且在用户选择完图片之后,返回图片的地址

// 进行模块导入
import { picker } from '@kit.CoreFileKit'
import { promptAction } from '@kit.ArkUI';

// 选择图片,返回选择图片的地址
async pickerAvatar() {
  // 实例化选择参数的对象
  const options = new picker.PhotoSelectOptions()
  // 设定需要选择的文件类型, 这里只选择图片
  options.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE
  // 设定选择的数量, 这里只需要选择一张
  options.maxSelectNumber = 1

  // 实例化选择器, 然后选入设定好的对象
  const pickerView = new picker.PhotoViewPicker
  // 调用选择器, 并且接受返回的结果
  const ImgResults = await pickerView.select(options)
  
  return ImgResults.photoUris
}

执行,并打印ImgResults效果图:

recording.gif

2.2 如何拷贝图片到缓存目录

这里需要使用到fileIo文件操作模块进行图片的拷贝,下面是官网对fileIo模块的介绍 传送门

@ohos.file.fs (文件管理)

该模块为基础文件操作API,提供基础文件操作能力,包括文件基本管理、文件目录管理、文件信息统计、文件流式读写等常用功能。

fs.copyFileSync

copyFileSync(src: string|number, dest: string|number, mode?: number): void

以同步方法复制文件。

这里直接上代码,说明一下这段代码的作用:
使用fileIo模块把图片复制到应用的缓存目录,然后返回一个图片上传需要用到的参数对象

// 导入模块
import { fileIo } from '@kit.CoreFileKit'

// 拿到地址, 拷贝到应用的缓存目录. 然后上传图片
// 注意, 这里处理单张图片路径! name的参数为后端规定的图片name格式!!! 常见为:img/image/file等...
fileCopies(ImagePath: string, name: string): request.File {
  // 使用文件管理模块打开,拿到源文件
  const file = fileIo.openSync(ImagePath, fileIo.OpenMode.READ_ONLY)

  // 设置一下需要拷贝到缓存目录的图片名字
  const filename = `${Date.now()}_${Math.floor(Math.random() * 1000)}`
  // 动态取到文件名的格式后缀, 一般是jpg/png格式
  const extension = ImagePath.slice(ImagePath.lastIndexOf(".") + 1)

  // 把路径和文件名组合
  const copyThePath = `${getContext().cacheDir}/${filename}.${extension}`
  // 打印一下准备复制的路径做确认
  console.log(copyThePath)
  // 拿到源文件的唯一表示,和准备复制的路径,把相册中的图片复制到应用自身的沙箱中
  fileIo.copyFileSync(file.fd, copyThePath)

  // 需要用到的上传参数对象
  return {
    filename: `${filename}.${extension}`,
    type: extension,
    name: name,
    uri: `internal://cache/${filename}.${extension}`
  }
}

点击这里,查看return出来的request.File对象的参数说明

如何查看图片是否复制到了应用的缓存目录呢?
点击Dev Eco Studio 左上角的 视图 -> 工具窗口 -> Device File Browser

image.png

按照这个路径一路展开:/data/app/el2/100/base/com.chiqingsan.harmonymodule/haps/entry/cache

image.png

在`cache`文件夹下面的图片就是我们复制过来的啦,双击图片即可进行预览。如果你的`cache`文件夹下面没有图片,可以尝试鼠标右键后对文件夹进行刷新

image.png

到这里,我们终于完成了图片上传的所有准备工作!下面,让我们开始对图片进行上传!

bbb6fc1ef79bebe7429980c1cf3bddf6.jpg

2.3 如何实现图片上传

这里需要使用到request对图片文件进行批量上传,下面是官网对request模块的介绍 传送门

request.uploadFile

uploadFile(context: BaseContext, config: UploadConfig): Promise

上传,异步方法,使用promise形式返回结果。

需要权限:ohos.permission.INTERNET

系统能力: SystemCapability.MiscServices.Upload

这里主要是拿到上面的参数条件,使用request模块对图片进行上传

// 导入模块
import { request } from '@kit.BasicServicesKit';
import { http } from '@kit.NetworkKit';

// 上传图片
async uploadAnImage(url: string, fileArr: request.File[]) {

  const files: Array<request.File> = fileArr

  // 开始准备上传图片,需要两个参数,一个是上下文对象,一个是上传的配置
  const res = await request.uploadFile(getContext(), {
    url: url,
    method: http.RequestMethod.POST,
    header: {
      //multipart/form-data为媒体内容上传的格式
      "Content-Type": 'multipart/form-data',
      // "Authorization": `Bearer ${"token"}`  看需求,是否需要携带token
    },
    files,
    // data参数必填
    data: []
  })

  // 这里是监听上传的进度
  res.on("progress", async (progress, totalSize) => {
    console.info("当前上传进度为:", `${progress / totalSize * 100}%`)
    // 如果上传大小等于总大小,则说明上传完成
    if (progress === totalSize) {
      promptAction.showToast({ message: "图片上传成功" })
    }
  })

  // 这里是监听上传响应的标头
  res.on("headerReceive", (header) => {
    // 打印标头信息, 这里包含上传的图片访问地址
    console.log("FileUpload:", JSON.stringify(header))
  })
}

上传图片演示: recording.gif

3. 图片上传工具类封装

当然,如果我们每次上传图片都要把这么多代码写一次,那也太不优雅了。毫无疑问,这样繁琐的代码,把它封装起来是最好的选择!

图片上传工具类封装:

import { fileIo, picker } from '@kit.CoreFileKit'
import { request } from '@kit.BasicServicesKit';
import { http } from '@kit.NetworkKit';
import { promptAction } from '@kit.ArkUI';

// 图片上传的基地址,按需求更改
const Url_Base = "https://www.imgtp.com/api"

class ImageUploadModule {
  private Url_Base: string

  constructor(Url_Base: string) {
    this.Url_Base = Url_Base
  }

  // 选择图片,返回选择图片的地址
  async pickerAvatar(selectNum: number = 1) {
    // 规范选择的合法数量
    if (selectNum < 1) {
      selectNum = 1
    } else if (selectNum > 9) {
      selectNum = 9
    }
    // 实例化选择参数的对象
    const options = new picker.PhotoSelectOptions()
    // 设定需要选择的文件类型, 这里只选择图片
    options.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE
    // 设定选择的数量, 这里只需要选择一张
    options.maxSelectNumber = selectNum

    // 实例化选择器, 然后选入设定好的对象
    const pickerView = new picker.PhotoViewPicker
    // 调用选择器, 并且接受返回的结果
    const ImgResults = await pickerView.select(options)

    // 判断是否选择了图片,如果取消了图片选择,则使用Promise抛出异常
    if (ImgResults.photoUris.length > 0) {
      return Promise.resolve(ImgResults.photoUris)
    } else {
      // console.warn("取消了图片选择")
      return Promise.reject("用户取消了图片选择")
    }
  }

  // 拿到地址, 拷贝到应用的缓存目录. 然后上传图片
  // 注意, 这里处理单张图片路径! name的参数为后端规定的图片name格式!!! 常见为:img/image/file等...
  fileCopies(ImagePath: string, name: string): request.File {
    // 使用文件管理模块打开,拿到源文件
    const file = fileIo.openSync(ImagePath, fileIo.OpenMode.READ_ONLY)

    // 设置一下需要拷贝到缓存目录的图片名字
    const filename = `${Date.now()}_${Math.floor(Math.random() * 1000)}`
    // 动态取到文件名的格式后缀, 一般是jpg/png格式
    const extension = ImagePath.slice(ImagePath.lastIndexOf(".") + 1)

    // 把路径和文件名组合
    const copyThePath = `${getContext().cacheDir}/${filename}.${extension}`
    // 打印一下准备复制的路径做确认
    console.log(copyThePath)
    // 拿到源文件的唯一表示,和准备复制的路径,把相册中的图片复制到应用自身的沙箱中
    fileIo.copyFileSync(file.fd, copyThePath)

    return {
      filename: `${filename}.${extension}`,
      type: extension,
      name: name,
      uri: `internal://cache/${filename}.${extension}`
    }
  }

  // 上传图片
  async uploadAnImage(url: string, fileArr: request.File[]) {

    const files: Array<request.File> = fileArr

    console.log("FileUpload:", JSON.stringify("开始准备文件上传"))
    // 开始准备上传图片,需要两个参数,一个是上下文对象,一个是上传的配置
    const res = await request.uploadFile(getContext(), {
      url: this.Url_Base + url,
      method: http.RequestMethod.POST,
      header: {
        "Content-Type": 'multipart/form-data',
        // "Authorization": `Bearer ${"token"}`  看需求,是否需要携带token
      },
      files,
      data: []
    })

    // 这里是监听上传的进度
    res.on("progress", async (progress, totalSize) => {
      console.info("当前上传进度为:", `${progress / totalSize * 100}%`)
      // 如果上传大小等于总大小,则说明上传完成
      if (progress === totalSize) {
        promptAction.showToast({ message: "图片上传成功" })
      }
    })

    // 这里是监听上传响应的标头
    res.on("headerReceive", (header) => {
      // 打印标头信息, 这里包含上传的图片访问地址
      console.log("FileUpload:", JSON.stringify(header))
    })
  }

  // 对上传图片的全部步骤完成函数封装
  async imageUpload(url: string, name: string, selectNum = 1) {
    try {
      // 拉起图片选择器, 如果用户取消选择则进入catch,代码不会往下执行
      // 默认选择图片为一张
      const photoUris = await this.pickerAvatar(selectNum)
      // 拿到拷贝到应用沙箱的文件配置对象列表
      const files = photoUris.map(v => this.fileCopies(v, name))
      // 开始准备上传图片
      this.uploadAnImage(url, files)
    } catch (e) {
      // 打印错误
      console.log("ImgUpload:", JSON.stringify(e))
    }
  }
}

// 实例化图片上传实例,然后导出
export const imageUploadModule = new ImageUploadModule(Url_Base)

页面使用参考:

import { imageUploadModule } from '../common/utils/ImgUpload';

@Entry
@Component
struct FileUpload {
  build() {
    Column() {
      Button("上传图片")
        .onClick(async () => {
          imageUploadModule.imageUpload("/upload", "image", 3)
        })
    }.width("100%").height("100%").justifyContent(FlexAlign.Center)
  }
}

多张图片上传演示:

recording.gif