项目: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 (base64: string) => {// 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.value, 0, 0, 240, 240);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;// #endif4.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 自定义配置全攻略》。
六、失败路径对比
plus.nativeObj.Bitmap | plus.gallery.save(...) | |||
getFileSystemManager + _doc/ | fs.writeFile({filePath:'_doc/xxx'}) | 1300002 | ||
fs.writeFile({filePath:'xxx.png'}) | 1300002 | |||
unifile://cache/ | fs.writeFile({filePath:'unifile://cache/xxx'}) | 1300013 | ||
uni.writeFile | uni.writeFile({...}) | |||
import { fn } from '@/uni_modules/...' | uni-uts-v1 | |||
| 7 | Canvas 绕行 | drawImage → canvasToTempFilePath → saveImageToPhotosAlbum | ✅ 成功 |
七、核心教训
鸿蒙 ≠ Android。
plus.*全家桶在鸿蒙上完全不可用,需要为鸿蒙端单独写一套保存逻辑。uni.getFileSystemManager().writeFile在鸿蒙上有路径翻译 bug。无论怎么换路径格式(_doc/、unifile://、纯文件名),底层桥接层都无法正确翻译到鸿蒙沙箱路径。这是 HBuilderX 的已知问题。不要和文件系统较劲,用渲染引擎的能力。Canvas 的
toTempFilePath由系统内部生成临时文件,完美绕过 JSVM 的文件系统桥接层。UTS 插件理论上是最优解,但实际落地时依赖链太重(需要
@dcloudio/uni-uts-v1特定版本),不如 Canvas 方案轻量。权限声明走
harmony-configs/。manifest.json的app-harmony.distribute字段不被构建系统处理,模块权限必须在module.json5中声明。
本文记录了 uni-app 鸿蒙 App 中「保存 Base64 图片到相册」这一看似简单的需求背后隐藏的 7 个坑。核心发现是 getFileSystemManager 的 JSVM 桥接层存在路径翻译缺陷,最终通过 Canvas 渲染引擎的导出能力绕过了整个文件系统问题。
夜雨聆风