HarmonyOS 6商城开发学习:网络图片解析后缀并保存相册——http下载+Content-Type取扩展名+showAssetsCreationDialog实战
在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/jpeg、image/png、image/webp、image/gif等由服务端正确返回(CDN 图片均会带)。从 Content-Type取 MIME 子类型(如 jpeg→jpg、png→png)作为 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)
}
}
四、避坑指南
|
问题 |
原因 |
修复 |
|---|---|---|
|
扩展名变 |
URL |
不解析URL取扩展名,用 |
|
|
用户点取消或沙箱URI格式错 |
沙箱URI须 |
|
保存后相册看不到 |
未调 |
确保 |
|
WebP 图某些老设备不显示 |
设备相册不支持 webp |
业务层可按 |
|
http 请求报权限拒绝 |
|
确认 |
五、总结:网络图片存相册 SOP
-
module.json5声明ohos.permission.INTERNET(仅网络权限,无存储权限) -
http.request()GET 图片 → 读响应头Content-Type→image/*子类型映射为扩展名(jpeg→jpg) -
写沙箱文件
filesDir + 'name.ext' -
showAssetsCreationDialog([sandboxUri], [{fileNameExtension:ext}]) → 用户确认 →copyFile沙箱fd → 相册URI fd -
成功提示 / 失败 Catch 处理
核心法则:HarmonyOS 6 中网络图片存相册 = "HTTP头解析 MIME→沙箱下载→showAssetsCreationDialog弹窗授权→copyFile落盘",禁止靠URL尾缀猜扩展名。
©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任。
更多推荐



所有评论(0)