乐于分享
好东西不私藏

uni-app 鸿蒙 App 保存图片到相册:排坑全记录

uni-app 鸿蒙 App 保存图片到相册:排坑全记录

项目:Vue 3 + uni-app 3.0 App,编译目标 HarmonyOS App环境:HBuilderX 5.x + DevEco Studio 5.0 + 鸿蒙真机场景:将页面上的 Base64 二维码图片保存到系统相册


一、需求背景

项目有一个「数据同步」页面,生成绑定二维码后,用户需要将二维码保存到系统相册,然后在微信小程序中扫码同步数据。

在 Android/iOS 上,这段代码很简单:

JavaScript

// APP-PLUS 端(Android / iOS)constbitmap=newplus.nativeObj.Bitmap('qrcode-bitmap');bitmap.loadBase64Data(base64Str, () => {bitmap.save('_doc/qrcode.png', {}, () => {plus.gallery.save('_doc/qrcode.png', () => {uni.showToast({ title:'已保存到相册' });        });    });});

但到了一切都不一样的鸿蒙端,噩梦开始。


二、六次失败的尝试

尝试 1:直接用 plus.nativeObj.Bitmap

JavaScript

// 直接搬 Android 代码constbitmap=newplus.nativeObj.Bitmap(...);

结果:直接报错。plus 对象在鸿蒙上完全不可用。DCloud 官方文档明确说明:

由于性能原因,目前 uniapp 项目编译到 HarmonyOS 时,plus 对象不可用。

结论:此路不通。


尝试 2:getFileSystemManager().writeFile + _doc/ 路径

放弃 plus,改用 uni-app 的文件系统 API:

JavaScript

constbase64Data=base64Str.replace(/^data:image\/\w+;base64,/'');constfilePath='_doc/bind_qrcode.png';constfs=uni.getFileSystemManager();fs.writeFile({filePath,data:base64Data,encoding:'base64',success: () => {uni.saveImageToPhotosAlbum({ filePath, ... });    }});

结果:errCode: 1300002,提示路径不存在。

_doc/ 在 Android/iOS 的 plus.io 中能正确映射到应用沙箱,但鸿蒙的 getFileSystemManager 的路径翻译层有 bug,不认这个前缀。


尝试 3:去掉前缀,用纯文件名

JavaScript

constfilePath=`bind_qrcode.png`;   // 无任何前缀

结果:errCode: 1300002,同样的错误。纯文件名也不是有效路径。


尝试 4:unifile://cache/ 协议路径

查阅 uni-app 官方文档,发现 getFileSystemManager 的 filePath 参数被要求是「绝对路径,形如 unifile://cache/...」:

JavaScript

constfilePath=`unifile://cache/bind_qrcode.png`;

结果:errCode: 1300013(Permission denied),换了种方式拒绝。

unifile://cache/ 在理论上是正确的写法,但鸿蒙 JSVM 桥接层把它翻译到了一个没有写权限的目录。


尝试 5:全局 uni.writeFile

发现 getFileSystemManager().writeFile 和全局 uni.writeFile 是两个不同的 API——前者走 JSVM 直连 ArkTS(有路径翻译 bug),后者底层是 plus.io 桥接(理论上支持 _doc/):

JavaScript

uni.writeFile({filePath:'_doc/bind_qrcode.png',data:base64Data,encoding:'base64',success: () => { ... }});

结果:TypeError: uni.writeFile is not a function 鸿蒙端根本不存在这个全局 API。


尝试 6:UTS 原生插件

既然 JS 层面的 API 全都不行,那就写一个 UTS 插件,直接调用鸿蒙原生 API:

TypeScript

// utssdk/app-harmony/index.uts(伪代码)importfsfrom'@ohos.file.fs';importphotoAccessHelperfrom'@kit.MediaLibraryKit';exportconstsaveBase64ToAlbum=async (base64string=> {// 1. 解码 base64 → ArrayBuffer// 2. 写入 context.cacheDir 临时文件// 3. photoAccessHelper.showAssetsCreationDialog() 弹出保存弹窗// 4. 复制临时文件到授权 URI};

在 vite.config.js 中配置别名为 @/uni_modules/,导入即可。

结果:Cannot find module: @dcloudio/uni-uts-v1 UTS 插件依赖 @dcloudio/uni-uts-v1 这个编译运行时包,而项目中没有安装它。这个包带有特定版本号的后缀,说明是 DCloud 的内测/内部版,手动添加后还可能引入其他兼容性问题。

这条路能走通,但环境依赖太重,不适合轻量级的需求。


三、根因分析:为什么 getFileSystemManager 在鸿蒙上不工作

这是整个排坑过程中最重要的发现——不是路径格式的问题,而是架构层的硬伤

uni API 的执行路径

HTML

你的 JS 代码    ↓┌─────────────────────────────────────────┐│  uni.getFileSystemManager()             │  ← JS 层 API│       ↓                                  ││  【桥接层:JSVM → ArkTS 文件系统】      │  ← ❌ 这一层有 bug│       ↓                                  ││  @ohos.file.fs (鸿蒙原生文件 API)       │└─────────────────────────────────────────┘

uni-app 在鸿蒙上的架构是:Vue/JS 代码运行在一个嵌入的 JSVM(JavaScript 虚拟机) 中。getFileSystemManager().writeFile 是一个 JS API,需要经过一层桥接把调用翻译成 ArkTS 的文件操作。

问题就出在这层桥接上——路径翻译不完善:

你写的路径
桥接层的表现
错误码
_doc/xxx.png
不翻译,当纯文件名
1300002
(路径不存在)
xxx.png
(纯文件名)
不翻译
1300002
(路径不存在)
unifile://cache/xxx.png
翻译到无权限目录
1300013
(权限拒绝)

而 UTS 插件的原理就是绕过了这层桥接。UTS 代码编译后直接变成 ArkTS,不经过 JSVM 翻译,直接用 context.cacheDir(系统原生 API 返回的绝对路径)写文件。但由于 UTS 的编译依赖链太重,不适合这个场景。


四、最终方案:Canvas 绕行

所有直接操作文件的路径都被堵死了。换个思路——既然文件系统走不通,就用渲染引擎自带的导出能力。

页面上已经用 <image> 标签渲染了二维码,加一个隐藏的 <canvas>,把二维码画上去,再用 canvasToTempFilePath 导出临时文件路径,最后用 saveImageToPhotosAlbum 保存。

这个方案完全不碰文件系统 API,canvas 的临时路径由系统内部生成。

4.1 模板改动

在二维码图片旁边加一个隐藏 canvas:

JavaScript

<!-- 隐藏 canvas,仅用于鸿蒙端导出图片到相册 --><canvascanvas-id="qrcode-export-canvas"style="position: fixed; left: -9999px; top: -9999px; width: 240px; height: 240px;"></canvas>

4.2 保存逻辑

json5

// #ifdef APP-HARMONY// 鸿蒙端:所有 writeFile 写法都有路径翻译 bug// 绕过文件系统:把二维码画到 hidden canvas → canvasToTempFilePath → saveImageToPhotosAlbumconstctx=uni.createCanvasContext('qrcode-export-canvas');ctx.drawImage(qrImageBase64.value00240240);ctx.draw(false, () => {uni.canvasToTempFilePath({canvasId:'qrcode-export-canvas',width:240,height:240,success: (res) => {uni.saveImageToPhotosAlbum({filePath:res.tempFilePath,success: () => {uni.showToast({ title:'已保存到相册'icon:'success' });                },fail: (err) => {constmsg= (err&&err.errMsg||'';uni.showToast({title:msg.includes('auth'||msg.includes('permission')?'请允许相册权限后重试':'保存失败',icon:'none'                    });                }            });        },fail: (err) => {console.error('[BindQRCode] canvasToTempFilePath 失败:'err);uni.showToast({ title:'保存失败'icon:'none' });        }    });});return;// #endif

4.3 执行流程

JSON

二维码 Base64(页面已渲染的 <image>)    ↓ ctx.drawImage()Hidden Canvas(240×240,位于屏幕外)    ↓ ctx.draw(false, callback)Canvas 渲染完成    ↓ uni.canvasToTempFilePath()临时文件路径(系统内部生成,不经过 JSVM 文件系统桥接)    ↓ uni.saveImageToPhotosAlbum()系统相册

不需要任何文件写入、不需要路径翻译、不需要插件依赖。


五、权限配置

保存图片到相册需要在 harmony-configs/entry/src/main/module.json5 中声明权限:

text

{  "module": {    "requestPermissions": [      {        "name": "ohos.permission.WRITE_IMAGEVIDEO",        "usedScene": {          "abilities": ["EntryAbility"],          "when": "inuse"        }      },      {        "name": "ohos.permission.READ_IMAGEVIDEO",        "usedScene": {          "abilities": ["EntryAbility"],          "when": "inuse"        }      }    ]  }}

同时需要在 harmony-configs/entry/src/main/resources/base/element/string.json 中提供权限申请说明文案:

text

{"string": [    {"name""Reason_SaveImage","value""用于将二维码保存到系统相册"    }  ]}

权限声明是通过 harmony-configs/ 注入的,不能用 manifest.json 配置。详见《uni-app 鸿蒙 App 自定义配置全攻略》。


六、失败路径对比

序号
方案
写法
结果
原因
1
plus.nativeObj.Bitmapplus.gallery.save(...)
plus 不可用
鸿蒙不支持 plus API
2
getFileSystemManager + _doc/fs.writeFile({filePath:'_doc/xxx'})1300002
路径不翻译
3
无前缀文件名
fs.writeFile({filePath:'xxx.png'})1300002
路径不翻译
4
unifile://cache/fs.writeFile({filePath:'unifile://cache/xxx'})1300013
翻译到无权限目录
5
全局 uni.writeFile
uni.writeFile({...})
not a function
鸿蒙端不存在此 API
6
UTS 插件
import { fn } from '@/uni_modules/...'
缺少 uni-uts-v1
编译依赖链太重
7Canvas 绕行drawImage → canvasToTempFilePath → saveImageToPhotosAlbum✅ 成功
绕过文件系统

七、核心教训

  1. 鸿蒙 ≠ Androidplus.* 全家桶在鸿蒙上完全不可用,需要为鸿蒙端单独写一套保存逻辑。

  2. uni.getFileSystemManager().writeFile 在鸿蒙上有路径翻译 bug。无论怎么换路径格式(_doc/unifile://、纯文件名),底层桥接层都无法正确翻译到鸿蒙沙箱路径。这是 HBuilderX 的已知问题。

  3. 不要和文件系统较劲,用渲染引擎的能力。Canvas 的 toTempFilePath 由系统内部生成临时文件,完美绕过 JSVM 的文件系统桥接层。

  4. UTS 插件理论上是最优解,但实际落地时依赖链太重(需要 @dcloudio/uni-uts-v1 特定版本),不如 Canvas 方案轻量。

  5. 权限声明走 harmony-configs/manifest.json 的 app-harmony.distribute 字段不被构建系统处理,模块权限必须在 module.json5 中声明。


本文记录了 uni-app 鸿蒙 App 中「保存 Base64 图片到相册」这一看似简单的需求背后隐藏的 7 个坑。核心发现是 getFileSystemManager 的 JSVM 桥接层存在路径翻译缺陷,最终通过 Canvas 渲染引擎的导出能力绕过了整个文件系统问题。