全部学科
Python全栈
python
NodeJS全栈
nodejs

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

📅 2026-07-23 uni-app HarmonyOS 鸿蒙 保存图片 相册 getFileSystemManager Canvas

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)
const bitmap = new plus.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 代码
const bitmap = new plus.nativeObj.Bitmap(...);

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

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

结论:此路不通。


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

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

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

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

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


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

JavaScript
const filePath = `bind_qrcode.png`;   // 无任何前缀

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


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

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

JavaScript
const filePath = `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(伪代码)
import fs from '@ohos.file.fs';
import photoAccessHelper from '@kit.MediaLibraryKit';

export const saveBase64ToAlbum = 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,仅用于鸿蒙端导出图片到相册 -->
<canvas
    canvas-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 → saveImageToPhotosAlbum
const ctx = 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) => {
                    const msg = (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 自定义配置全攻略》。


六、失败路径对比

序号方案写法结果原因
1plus.nativeObj.Bitmapplus.gallery.save(...)plus 不可用鸿蒙不支持 plus API
2getFileSystemManager + _doc/fs.writeFile({filePath:'_doc/xxx'})1300002路径不翻译
3无前缀文件名fs.writeFile({filePath:'xxx.png'})1300002路径不翻译
4unifile://cache/fs.writeFile({filePath:'unifile://cache/xxx'})1300013翻译到无权限目录
5全局 uni.writeFileuni.writeFile({...})not a function鸿蒙端不存在此 API
6UTS 插件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.jsonapp-harmony.distribute 字段不被构建系统处理,模块权限必须在 module.json5 中声明。


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

本文为开发实践记录,内容如有错误或不足,欢迎搜索微信公众号「卷王开发者」批评指正。共同进步,感谢阅读。

扫码体验小程序
加载中
想在手机上刷题学习?
使用微信卷王开发者小程序,打开首页顶部扫码功能识别二维码