uni-app 里把接口文件流存到手机、再拉起系统分享
uni-app 里把接口文件流存到手机、再拉起系统分享
插件:lf-file-share 1.3.0
地址:https://ext.dcloud.net.cn/plugin?id=26490
业务场景:后端导出 Excel / PDF(二进制文件流),前端拿到 ArrayBuffer,用户保存到文件管理,或通过系统面板分享到微信、邮件等。H5、Android、iOS、鸿蒙实现不同。本文说明分流方式、关键代码和真机问题。
需求
两类交互:
- 保存:Android 写入公共
Download目录,可在系统「下载」中查看;不写入Android/data/包名私有目录(除非显式指定沙盒) - 分享:调起系统分享面板,将文件交给其他 App
两个主方法:
downloadOrShareArrayBuffer(buffer, filename, { appAction: 'save' | 'share', mime, ... })
downloadOrShareByUrl(url, { appAction, filename, mime, ... })
appAction 不传时默认 share。save 只写入本地并返回路径,不调起分享。
平台差异
| 环境 | 行为 |
|---|---|
| H5 | Blob 触发浏览器下载,无系统分享 |
| Android 10+ | 分区存储;公共目录使用 MediaStore / DownloadManager |
| Android 分享 | 私有路径其他 App 无法读取,必须使用 FileProvider |
| iOS | 写入 App 沙盒;分享使用 UIActivityViewController |
| 鸿蒙 | 不命中 APP-PLUS,不使用 plus API |
| nvue | 无 DOM,不能依赖 document 监听 plusready |
| 大文件 | 需处理 Base64 空串、禁止 JS 逐字节写 Java、写入后校验文件大小 |
处理流程:
ArrayBuffer / URL
│
├─ H5 ──────────── Blob 下载
├─ APP-PLUS ────── Android / iOS(plus)
├─ APP-HARMONY ─── 鸿蒙(getFileSystemManager)
└─ uni-app x ───── utssdk/
条件编译
鸿蒙命中 APP / APP-HARMONY,不命中 APP-PLUS。仅编写 APP-PLUS 分支时,鸿蒙端无实现。
// #ifdef H5
downloadFileForH5(buffer, name, mime)
// #endif
// #ifdef APP-HARMONY
return await downloadOrShareArrayBufferHarmony(...)
// #endif
// #ifdef APP-PLUS
await waitPlusReady()
// 写入公共目录或沙盒,再按 appAction 分享
// #endif
Android save 默认路径:
/storage/emulated/0/Download/{saveDirName}/文件名
写入沙盒时传 saveLocation: 'sandbox'。
接口导出表格
const res = await uni.request({
url: 'https://api.example.com/export/excel',
method: 'GET',
responseType: 'arraybuffer' // 必须设置,否则拿到的不是二进制
})
await downloadOrShareArrayBuffer(res.data, '销售报表.xlsx', {
appAction: 'save',
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
saveDirName: 'MyApp',
debug: true
})
无后端时使用内置 mock,生成可打开的最小 xlsx:
import {
mockFetchExcelExport,
downloadOrShareArrayBuffer
} from '@/uni_modules/lf-file-share'
const res = await mockFetchExcelExport({ filename: '销售报表.xlsx' })
await downloadOrShareArrayBuffer(res.data, res.filename, {
mime: res.mime,
appAction: 'share',
chooserTitle: '分享表格'
})
大文件 / 大图 mock:
await mockFetchLargeFile() // 默认 2.5MB
await mockFetchLargeImage() // 默认 2.6MB BMP
关键代码
ArrayBuffer 转 Base64
App 写文件需要 Base64。uni.arrayBufferToBase64 在部分机型或大文件场景会返回空串,或解码后字节数与 buffer.byteLength 不一致。处理顺序:
- 使用
uni.arrayBufferToBase64 - 校验 Base64 反推长度;不一致则改用 JS 分片编码
- 写入时按 4 的倍数分片 decode(Base64 分组要求)
const getArrayBufferBase64 = (buffer) => {
const expectSize = buffer.byteLength || 0
let base64 = cleanBase64(uni.arrayBufferToBase64(buffer))
if (base64 && getBase64ByteLength(base64) === expectSize) {
return { base64, source: 'uni.arrayBufferToBase64', byteLength: expectSize }
}
base64 = cleanBase64(arrayBufferToBase64ByChunk(buffer))
// 仍不一致则抛错,禁止写出空文件
return { base64, source: 'chunkFallback', byteLength: expectSize }
}
禁止整包 decode,禁止 JS 循环逐字节 write 到 Java。使用分片 + BufferedOutputStream,写完后校验 file.length()。
Android 10+ 写公共 Download
步骤:
ContentValues写入DISPLAY_NAME、MIME_TYPE、RELATIVE_PATH、IS_PENDING=1insert得到 UriopenOutputStream写入内容- 将
IS_PENDING置为0;失败则delete该 Uri
嵌套类使用 $ 导入,否则运行时无法读取常量:
const Downloads = plus.android.importClass('android.provider.MediaStore$Downloads')
const MediaColumns = plus.android.importClass('android.provider.MediaStore$MediaColumns')
resolver.insert is not a function
真机报错:
TypeError: resolver.insert is not a function
原因:getContentResolver() 返回对象上的 Java 方法未挂载为 JS 函数,直接调用 insert 得到 undefined。使用 plus.android.invoke 兜底:
const invokeAndroid = (target, method, ...args) => {
if (!target) return null
if (typeof target[method] === 'function') return target[method](...args)
return plus.android.invoke(target, method, ...args)
}
const resolver = invokeAndroid(main, 'getContentResolver')
const uri = invokeAndroid(resolver, 'insert', collection, values)
const outputStream = invokeAndroid(resolver, 'openOutputStream', uri)
Cursor.moveToFirst、DownloadManager 轮询查询使用同一方式调用。
Android 分享
私有文件禁止直接传递 file://。使用 FileProvider 生成 content Uri:
uri = FileProvider.getUriForFile(main, pkg + '.dc.fileprovider', file)
// 失败则尝试 pkg + '.fileprovider',再失败使用 Uri.fromFile
intent.putExtra(Intent.EXTRA_STREAM, uri)
main.startActivity(Intent.createChooser(intent, chooserTitle))
分享失败时检查原生工程 FileProvider 与 AndroidManifest 配置。
iOS 分享
禁止只使用 keyWindow.rootViewController。沿 presentedViewController 找到顶层 ViewController 再 present。写入使用 plus.io + writeAsBinary(base64)。
鸿蒙
实现位于 harmony.js,使用 uni.getFileSystemManager().writeFileSync(path, buffer)。图片使用 uni.shareWithSystem;其他文件使用 openDocument({ showMenu: true });两者都失败时将路径写入剪贴板并抛错。
nvue
禁止仅使用 document.addEventListener('plusready')。存在 DOM 时监听事件;无 DOM 时轮询 typeof plus。
uni-app x
插件提供 utssdk/(web / android / ios / harmony)。业务侧引入方式:
import { downloadOrShareArrayBuffer } from '@/uni_modules/lf-file-share'
用法
import {
downloadOrShareArrayBuffer,
downloadOrShareByUrl,
shareOnlineImage,
shareOnlineVideo,
shareResource
} from '@/uni_modules/lf-file-share'
// 在线 PDF
const result = await downloadOrShareByUrl('https://example.com/a.pdf', {
filename: '说明文档',
mime: 'application/pdf',
appAction: 'save',
saveDirName: 'MyApp'
})
// 图片 / 视频
await shareOnlineImage(url, { appAction: 'share' })
await shareOnlineVideo(url, {
appAction: 'save',
filename: 'demo.mp4',
saveDirName: 'MyApp'
})
// buffer 或 url
await shareResource({
buffer,
filename: '报表.xlsx',
appAction: 'save',
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
参数:
| 字段 | 说明 |
|---|---|
appAction |
share / save |
mime |
显式传入文件 MIME |
saveDirName |
Android 公共 Download 下的子目录名 |
saveLocation |
public(默认)或 sandbox |
chooserTitle |
系统分享面板标题 |
debug |
输出 Base64 转换与写入耗时 |
返回值示例:
{
platform: 'Android',
action: 'save',
path: '/storage/emulated/0/Download/MyApp/a.xlsx',
localPath: 'Download/MyApp/a.xlsx',
filename: 'a.xlsx',
size: 4024,
isPublic: true
}
真机问题
-
未设置
responseType: 'arraybuffer'
得到字符串,Excel / PDF 无法打开。 -
保存成功但「下载」中找不到文件
文件写入了沙盒。Android 保存不要设置saveLocation: 'sandbox',使用默认公共 Download。 -
resolver.insert is not a function
对 ContentResolver / Cursor 使用plus.android.invoke。 -
大文件
actual=0或写入过慢
禁止整包 decode,禁止逐字节 write。使用分片 + 缓冲流,写完校验长度。 -
分享到微信失败
检查 FileProvider、authority、是否使用了私有file://、mime 与文件后缀是否正确。 -
iOS 点击分享无弹窗
present 目标不是顶层 VC。取最上层presentedViewController。iPad 需配置 popover 锚点。 -
鸿蒙引入后无效果
实现写在#ifdef APP-PLUS中,鸿蒙未编译该分支。 -
nvue 中
plus为 undefined
未处理无 DOM 场景,需轮询plus就绪。 -
instanceof ArrayBuffer为 false
使用Object.prototype.toString.call(buffer) === '[object ArrayBuffer]',并兼容 TypedArray。 -
文件名含
\/:*?"<>|或超长
写入路径失败。插件会清洗文件名与目录名,调用方也应传入合法名称。 -
连续点击保存
同名文件并发写入导致大小校验失败。按钮增加 loading,禁止重复提交。 -
在小程序中使用本插件做系统文件分享
小程序无对应能力,插件不支持该平台。 -
Demo 在线 PDF 无法下载
示例地址依赖外网。内网环境改用 mock:pdf-demo/xlsx-report。 -
Android 9 及以下写入公共目录失败
在manifest.json配置WRITE_EXTERNAL_STORAGE。Android 10+ 使用 MediaStore 写入公共 Download,不依赖该权限。
排查
开启 debug: true:
await downloadOrShareArrayBuffer(buffer, filename, {
appAction: 'save',
mime: 'application/pdf',
debug: true
})
日志输出 Base64 来源与写入耗时。source=chunkFallback 表示 uni.arrayBufferToBase64 校验失败,已改用分片编码。
console.log(buffer.byteLength, result.size, result.path)
App 打开文件:
// #ifdef APP-PLUS
plus.runtime.openFile(result.path, {}, () => {
uni.showToast({ title: '未找到可打开此文件的应用', icon: 'none' })
})
// #endif
源码目录
uni_modules/lf-file-share/
├── index.js # H5 / APP-PLUS 主逻辑
├── harmony.js # 鸿蒙
├── mock.js # 本地 mock
├── utssdk/ # uni-app x
├── readme.md
├── changelog.md
└── article.md
演示页:pages/file-share/demo.vue(表格导出、大文件大图、其它 mock、在线 URL / 文本流)。
接入要求:
- 插件目录为
uni_modules/lf-file-share - Android 配置 FileProvider(系统分享)
- Android 9 及以下按需配置存储权限
- 导出接口设置
responseType: 'arraybuffer' - 保存 / 分享显式传入
mime与文件名 - 鸿蒙、uni-app x 在对应运行环境验证,不能只用 Android 基座代替
调用示例:
await downloadOrShareArrayBuffer(res.data, '销售报表.xlsx', {
appAction: 'save',
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
问题反馈
更多推荐

所有评论(0)