在HarmonyOS 6购物比价或电商类应用中,商品详情页常提供"保存商品图到相册"功能。网络图片URL五花八样(https://cdn.xxx/path/to/img?id=123无后缀、或 .webp/.jpg/.png混杂),直接调用相册创建资产需明确 fileNameExtension。若凭 URL 尾缀猜测易出错,正确做法是 HTTP GET 读取响应头 Content-Type,解析 MIME 子类型作为扩展名,下载到沙箱后通过 showAssetsCreationDialog写入相册。本文将完整实现此流程。


一、现象:创建相册资产时不知扩展名填什么

1. 问题现场

// ❌ 直接取URL最后一段当扩展名,无后缀或参数拼接时全错
const ext = url.split('.').pop(); // "jpg?token=xxx" or undefined
photoAccessHelper.createAsset(..., { fileNameExtension: ext }) // 可能抛异常

商品图CDN常返回:

  • https://cdn.shop.com/oss/2024/goods/88521— 无后缀

  • https://img.xxx.cn/i/v2/abc.jpg?x-oss-process=style/webp— 后缀带参数

    直接用字符串拆分不可靠。

2. 根因揭秘与官方方案

HTTP 响应头中 Content-Type: image/jpegimage/pngimage/webpimage/gif等由服务端正确返回(CDN 图片均会带)。Content-Type取 MIME 子类型(如 jpegjpgpngpng)作为 fileNameExtension是最准确做法,再配合 showAssetsCreationDialog(用户授权弹窗)写入——无需动态申请存储权限。


二、网络图片下载与后缀解析工具类

// utils/NetImageSaver.ets
import { http } from '@kit.NetworkKit';
import { fileIo } from '@kit.CoreFileKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 下载网络图片到应用沙箱,返回沙箱URI与推断出的扩展名
 * @param ctx    UIAbilityContext(用于PAH)
 * @param url    网络图片URL
 * @returns      { sandboxUri, ext }
 */
export async function downloadNetImage(
  ctx: common.UIAbilityContext,
  url: string
): Promise<{ sandboxUri: string; ext: string }> {
  // ---- HTTP 下载 ----
  const resp = await http.createHttp().request(url, {
    method: http.RequestMethod.GET,
    connectTimeout: 30000,
    readTimeout: 30000
  });

  if (resp.responseCode !== http.ResponseCode.OK) {
    throw new Error(`HTTP ${resp.responseCode}`);
  }

  // ---- 解析 Content-Type 取扩展名 ----
  let contentType = (resp.header?.['content-type'] ?? 'image/jpeg') as string;
  contentType = contentType.split(';')[0].trim().toLowerCase(); // 去 charset
  let mimeSub = contentType.split('/')[1] ?? 'jpeg';
  // 常见映射:jpeg→jpg(相册接受 jpg/jpeg 均可,习惯用 jpg)
  let ext = mimeSub === 'jpeg' ? 'jpg' : mimeSub;

  // ---- 写入沙箱 ----
  const filesDir = ctx.filesDir;
  const fileName = `net_img_${Date.now()}.${ext}`;
  const fullPath = `${filesDir}/${fileName}`;

  const fd = fileIo.openSync(fullPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
  try {
    const buf: ArrayBuffer = resp.result as ArrayBuffer;
    await fileIo.write(fd, buf);
  } finally {
    fileIo.closeSync(fd);
  }

  return {
    sandboxUri: `file://${ctx.applicationInfo.bundleName}/data/storage/el2/base/haps/entry/files/${fileName}`,
    ext
  };
}

/**
 * 保存沙箱图片到相册(用户弹窗授权)
 * @param ctx         UIAbilityContext
 * @param sandboxUri  沙箱 file:// URI
 * @param ext         扩展名 jpg/png/webp …
 */
export async function saveToAlbum(
  ctx: common.UIAbilityContext,
  sandboxUri: string,
  ext: string
): Promise<void> {
  const ph = photoAccessHelper.getPhotoAccessHelper(ctx);
  const desUris = await ph.showAssetsCreationDialog(
    [sandboxUri],
    [{
      title: 'goods_image',
      fileNameExtension: ext,
      photoType: photoAccessHelper.PhotoType.IMAGE,
      subtype: photoAccessHelper.PhotoSubtype.DEFAULT
    }]
  );
  if (!desUris?.length) {
    throw new Error('用户取消保存或未授权');
  }

  // 复制沙箱文件 → 相册URI
  const srcFd = fileIo.openSync(sandboxUri, fileIo.OpenMode.READ_ONLY);
  const dstFd = fileIo.openSync(desUris[0], fileIo.OpenMode.WRITE_ONLY);
  try {
    await fileIo.copyFile(srcFd.fd, dstFd.fd);
  } finally {
    fileIo.closeSync(srcFd);
    fileIo.closeSync(dstFd);
  }
}

为什么用 showAssetsCreationDialog而非 createAsset+ 手动权限?

系统要求保存图片必须经用户确认,showAssetsCreationDialog弹系统保存弹窗,用户点"保存"即授权写入,应用无需 WRITE_MEDIA权限动态申请,符合最新管控规范。


三、商品详情页——"保存图片"按钮调用

// pages/GoodsDetailPage.ets
import { downloadNetImage, saveToAlbum } from '../utils/NetImageSaver';
import { common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct GoodsDetailPage {
  private ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;

  // 商品主图(示例URL,实际从接口来)
  private mainImgUrl = 'https://cdn.example.com/oss/goods/hero_watch_ultra';

  build() {
    Column({ space: 20 }) {
      Image(this.mainImgUrl)
        .width('100%')
        .height(320)
        .objectFit(ImageFit.Cover)
        .borderRadius(12)

      Row({ space: 16 }) {
        Button('保存图片到相册')
          .height(40)
          .backgroundColor('#FF5722')
          .borderRadius(20)
          .padding({ horizontal: 20 })
          .onClick(async () => {
            try {
              // 1. 下载 + 解析后缀
              const { sandboxUri, ext } = await downloadNetImage(this.ctx, this.mainImgUrl);
              // 2. 弹窗保存
              await saveToAlbum(this.ctx, sandboxUri, ext);
              promptAction.showToast({ message: '已保存到相册' });
            } catch (e) {
              const err = e as BusinessError;
              promptAction.showToast({ message: `保存失败: ${err.message ?? e}` });
              console.error(`save net img err: ${JSON.stringify(e)}`);
            }
          })

        Button('查看大图')
          .height(40)
          .backgroundColor('#1976D2')
          .borderRadius(20)
          .padding({ horizontal: 20 })
          .onClick(() => { /* 跳转预览页 */ })
      }
    }
    .padding(16)
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F6F8')
    .justifyContent(FlexAlign.Center)
  }
}

四、避坑指南

问题

原因

修复

扩展名变 jpg?token=xxx

URL split('.')未处理 queryString

不解析URL取扩展名,用 Content-Type子类型

showAssetsCreationDialog返回空数组

用户点取消或沙箱URI格式错

沙箱URI须 file://bundleName/data/storage/el2/base/haps/entry/files/xxx.ext严格匹配

保存后相册看不到

未调 copyFile或 fd 未 close

确保 openSync(src,RDONLY)+ openSync(dst,WRONLY)+ 双 close

WebP 图某些老设备不显示

设备相册不支持 webp

业务层可按 ext==='webp'提示或后端提供 jpg 备选

http 请求报权限拒绝

module.json5ohos.permission.INTERNET

确认 reqPermissions中声明 INTERNET


五、总结:网络图片存相册 SOP

  1. module.json5声明 ohos.permission.INTERNET(仅网络权限,无存储权限)

  2. http.request()GET 图片​ → 读响应头 Content-Typeimage/*子类型映射为扩展名(jpeg→jpg

  3. 写沙箱文件filesDir + 'name.ext'

  4. showAssetsCreationDialog([sandboxUri], [{fileNameExtension:ext}])​ → 用户确认 → copyFile沙箱fd → 相册URI fd

  5. 成功提示 / 失败 Catch 处理

核心法则:HarmonyOS 6 中网络图片存相册 = "HTTP头解析 MIME→沙箱下载→showAssetsCreationDialog弹窗授权→copyFile落盘",禁止靠URL尾缀猜扩展名。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐