资讯详情

资讯详情

小程序文件分享实战:arrayBuffer落盘与shareFileMessage回调兜底

前几天接了一个文件分享需求后端把合同PDF以文件流形式下发小程序拿到arrayBuffer之后要能一键分享到聊天窗口。方案乍一看不复杂真落地的时候倒是踩了两个没想到的坑一个是原生shareFileMessage只认本地文件路径arrayBuffer必须先在本地落盘另一个是complete回调在真机上竟然不触发页面上的 loading 差点变成永动机。这篇文章就把这条分享链路从数据落盘到回调兜底完整拆一遍给准备做类似功能的朋友一个参考。1. 需求拆解文件从哪里来又要从哪里出去1.1 一个典型的文件分享场景这类需求在小程序里非常常见我这次遇到的是合同文件下载与分享。服务端返回的不是一个可直链下载的URL而是一个二进制流前端通过请求拿到arrayBuffer随后需要在页面里提供一个“分享文件”的按钮用户点击后可以把这份合同直接以文件形式发送到聊天会话里。除了合同这套流程也可以套用在很多场景上录音文件分享、canvas生成的图片/PDF导出、业务报表下载、Excel模板分发等等。共同点是底层数据都是二进制内容且都需要以“文件实体”的形式离开小程序。这里要强调一个容易混淆的点小程序里最常见的转发是wx.showShareMenu或右上角菜单转发那种方式分享的是“页面卡片”朋友点开之后进入的是一个H5页面本质是链接。而shareFileMessage分享的是真实文件对方收到的是可以直接打开、保存、转发的文件本体。两者解决的问题完全不同如果你的需求是“让用户下载一份文件并发给同事”那就应该走文件分享。1.2 接口能力边界先摸清调用shareFileMessage之前先看它的硬性要求必须传入一个本地文件路径也就是filePath参数分享时显示的文件名走fileName安卓端还需要根据文件后缀决定能否分享。并且这类原生分享面板依赖宿主客户端能力在开发者工具里是调不起来的必须在真机上跑。这个接口其实做过一次能力升级升级之后安卓端支持的文件类型变多了但依然不是任何类型都能分享。大概包括图片、视频、apk、doc、xls、ppt、pdf、docx、xlsx、pptx。而iOS端相对宽松基本任意后缀都能作为文件发出去。所以一套代码在两个端上分享成功率和体验可能完全不一样测试时千万别只测一端就上线。1.3 为什么arrayBuffer不能直接分享这是整个需求的第一个关键问题我手里是arrayBuffer分享接口却只要文件路径咋办因为shareFileMessage打开的是系统级分享面板面板要读取的是本地文件它不会接收内存里的二进制块。这个设计其实也合理分享面板不是只能读小程序内部数据的它需要把文件传递给其他应用就必须有一个文件系统层面的真实文件存在。打个比方你手里有一份数字文档在内存里想拿去快递站寄走快递员不会接收“脑子里的内容”你需要先把它打印成一张纸、装进信封再交给快递员。arrayBuffer就是内存里的内容writeFile就是打印过程写入后的本地文件就是那封信。所以链路就很清晰了arrayBuffer - 本地临时文件 - shareFileMessage - 系统分享面板2. 先落盘把 arrayBuffer 写成可靠的本地文件2.1 三个关键参数不能写错写文件用的是wx.getFileSystemManager()暴露的writeFile核心参数有三个filePath、data、encoding。const fs wx.getFileSystemManager() fs.writeFile({ filePath: tempFilePath, data: arrayBuffer, encoding: binary, success(res) { // 落盘成功准备分享 }, fail(err) { console.error(writeFile failed, err) } })最容易被忽略的是encoding。当data是ArrayBuffer时必须写成binary。如果写成utf8或者漏掉这个字段轻则写入后文件损坏分享出去的文件打不开重则直接写入空文件。我一开始就是从别处复制了一段代码里面带着utf8分享出去的文件在安卓上打开后内容全是乱码查了半天才发现是这里写错了。2.2 路径必须落在 USER_DATA_PATH 下写入路径不能随便写比如直接写/tmp/xxx.pdf这类绝对路径会报“路径不合法”。正确做法是以wx.env.USER_DATA_PATH作为根目录拼接一个具体文件名。const tempFilePath ${wx.env.USER_DATA_PATH}/share_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.pdfUSER_DATA_PATH是小程序专属的本地用户数据目录每个小程序是独立沙盒应用退出后文件还在直到被清理或小程序被删除。这个目录不需要预先创建直接写文件即可。路径里加Date.now()和随机数的原因很简单避免文件覆盖。如果不加时间戳第二次分享同名文件时会把第一次的覆盖掉如果恰好有另一个页面也在用相同名字的临时文件互相覆盖会导致数据错乱。分享文件这种操作文件内容错了比分享失败还麻烦。2.3 写入之后建议先校验writeFile的success只代表写操作执行完了不代表文件内容是对的。稳妥的做法是写完之后顺手读一下文件信息const res fs.getFileInfo({ filePath: tempFilePath })主要看两点文件是否存在、size是否大于0。如果写入出来的文件是0字节那后面分享出去的必然是一个空文件接收方打开就是“文件已损坏”。这种情况常见于arrayBuffer本身为空或者encoding写错导致数据没有被正确落盘。提示writeFile是异步接口不要在fail里什么都不做就直接调分享。更不要用wx.arrayBufferToBase64把数据转成字符串之后再写入这完全是绕远路还会带来Base64体积膨胀和字符串编码问题。2.4 fileName 和后缀设计是分享成败的隐形关键在分享环节fileName是用户实际看到的名字而extension和fileName的后缀共同影响安卓端的类型判断。实际操作中建议把文件后缀作为约定参数传入并确保写入的临时文件、分享的fileName、extension三者后缀一致。const shareName 项目合同_20240612.pdf中文文件名完全没问题分享面板能正常显示。但要注意如果fileName不带后缀安卓端很可能识别不了文件类型分享出去的卡片会变成一个无法预览的未知文件。所以无论调用方传不传extensionfileName里最好都带上后缀这是最省事的保证。用久了之后你会发现这个落盘动作其实是通用的建议封装成一个工具函数function writeArrayBufferToShareFile(arrayBuffer, ext pdf, prefix share) { const fs wx.getFileSystemManager() const time Date.now() const random Math.random().toString(36).slice(2, 8) const filePath ${wx.env.USER_DATA_PATH}/${prefix}_${time}_${random}.${ext} return new Promise((resolve, reject) { fs.writeFile({ filePath, data: arrayBuffer, encoding: binary, success() { resolve({ filePath, fileName: ${prefix}_${time}.${ext}, ext }) }, fail(err) { reject(err) } }) }) }3. 调用 shareFileMessage 的姿势和参数陷阱3.1 参数逐个拆开看真正调用分享的代码不复杂wx.shareFileMessage({ filePath: tempFilePath, fileName: 项目合同_20240612.pdf, extension: pdf, success(res) { console.log(share success, res) }, fail(err) { console.error(share fail, err) }, complete(res) { console.log(share complete, res) } })filePath必填就是刚才本地落盘的文件路径。fileName选填分享卡片上显示的文件名。不传则可能使用默认名但默认名往往不够友好建议显式传。extension选填但在安卓端意义很大它决定分享面板是否允许把这个文件发出去。比如一个文件后缀是.abc安卓端不在支持列表里面板可能直接打不开或分享失败。另外有一点容易被忽略filePath必须是真实存在的文件。如果文件已经被删除或路径写错接口会直接fail。所以分享按钮的点击处理函数里最好先确认本地文件还在再发起分享。3.2 安卓与 iOS 的行为差异两端的差异是这类需求里的重头戏值得单独列出来看。维度安卓iOS支持类型有限列表如图片、视频、pdf、office文档等任意类型文件均可分享文件名后缀影响后缀不在支持列表会失败或无法打开对后缀不敏感分享面板表现仅显示支持类型的文件显示所有文件这个差异直接决定你要不要做端型判断。如果产品要求Android、iOS一致的体验那么分享的文件类型应该控制在安卓支持列表内或者至少在安卓端提前做类型拦截给出友好提示。实际开发中还有个细节在安卓上如果你传入了extension但实际文件的二进制内容并不是这个格式比如后缀写pdf内容是文本分享面板仍可能放行但对方打开文件时会提示格式损坏。问题根源还是写入时的数据格式和后缀不匹配要在源头上约束好。3.3 回调在文档上怎么写和实际情况的差距正常理解下success表示用户完成分享fail表示用户取消或失败complete无论如何都会触发。文档也确实是这样写的。但真机上的实际表现并没那么理想尤其是在部分安卓机型和较旧的基础库环境中success、fail都有概率不触发complete也不是每次都会执行。这个问题不是偶发个例而是原生分享面板带来的天然限制分享面板是系统级的它和当前小程序页面不在同一个渲染进程里用户关闭面板、跳转到聊天页、再返回小程序的过程中回调事件链很容易断掉。这就意味着任何依赖complete去执行“关闭loading”“清理资源”“跳转页面”等关键逻辑的写法都存在卡死风险。4. complete 回调不触发的完整踩坑记录4.1 现象还原loading 转个不停我的项目里遇到的现场是这样的点击“分享文件”按钮后先走writeFile成功之后调用shareFileMessage在complete回调里写了一个wx.hideLoading()和页面状态重置逻辑。结果真机测试时发现第一次分享分享面板正常弹出选择聊天对象发送成功回到小程序页面loading 仍然在转complete里的代码没有执行退出页面重进后功能恢复正常但第二次分享又复现。刚开始怀疑是writeFile的问题后来单独在complete里加了日志发现整段日志根本没打出来确认是complete回调丢失。另一个更隐蔽的现场是在开发者工具里点击分享按钮后完全没有反应既不出分享面板也没有回调。后来排查确认是工具环境不支持这个原生面板不能作为判断依据必须真机验证。4.2 根因原生面板回调链不稳定针对这个现象我把可能的原因捋了一遍结论是shareFileMessage不是一个纯 JS 层的接口它打开的系统分享面板运行在原生层小程序侧只能被动等待回调。从用户点击发送到聊天会话写入完成再到宿主App把结果回传给小程序这条链路跨了多个进程任何一步掉了都会导致小程序的回调收不到。更麻烦的是一些版本里分享结果并不会回传而是静默结束。所以在写代码时不能把这个 API 当成像wx.request那样“必然有回调”的接口来处理要默认它可能没有回调。还有一个相关联的坑就算success触发了它代表的也只是“分享面板发起了分享”并不完全等于“对方百分百收到了可用的文件”。所以业务上不要过度解读success的含义。4.3 兜底方案一用页面生命周期替代回调既然shareFileMessage的回调不可靠那唯一稳定的事件节点就是页面自身的生命周期。分享文件的操作路径是点击分享 - 打开分享面板 - 用户选择会话并发送 - 面板关闭 - 回到小程序页面无论分享结果如何“回到小程序页面”这件事必然会发生对应的事件就是onShow。所以可以把关键逻辑从complete里挪出来放到onShow里处理const shareFlag { startTime: 0, needClean: false } function handleShareFile(filePath, fileName, ext) { shareFlag.startTime Date.now() shareFlag.needClean true wx.shareFileMessage({ filePath, fileName, extension: ext }) } onShow() { if (shareFlag.needClean) { // 执行关闭loading、刷新状态、清理临时文件等操作 shareFlag.needClean false } }这样即使complete完全不触发只要用户从分享面板返回页面逻辑就能继续往下走。注意onShow在首次进入页面时也会触发所以要用标志位控制不能在首次加载时就误触发清理逻辑。4.4 兜底方案二超时强制释放光有onShow还不够如果用户打开了分享面板但一直不操作或者分享面板直接异常关闭页面可能一直停在那个中间状态。稳妥的做法是加一个超时兜底function shareWithTimeout(options, timeout 10000) { let settled false return new Promise((resolve) { const done (res) { if (settled) return settled true resolve(res) } wx.shareFileMessage({ ...options, success(res) { done({ status: success, res }) }, fail(err) { done({ status: fail, err }) }, complete(res) { done({ status: complete, res }) } }) setTimeout(() { done({ status: timeout }) }, timeout) }) }这个封装的思路是不管complete有没有触发只要用户操作完成或超过10秒还没结束Promise 都会有一个结果。这样后续就不要再依赖具体的complete回调了统一用await拿到结果并且在拿到结果之后做统一的清理逻辑。用的时候要注意超时时间不能太短否则用户还在挑选聊天对象这边已经把loading关了体验很奇怪。我实际调下来10秒左右比较平衡用户有足够时间完成发送操作又不会无限等下去。4.5 兜底方案三失败降级和异常捕获如果shareFileMessage本身因为参数不合法、文件不存在、基础库版本过低等原因根本无法打开面板那它连 fail 都不一定会给。所以调用之前可以做一次基本的环境判断if (!wx.shareFileMessage) { showToast(当前客户端版本不支持文件分享) return }再做一次本地文件的access校验确保文件路径真实存在try { fs.accessSync(tempFilePath) // 继续分享 } catch (e) { showToast(文件不存在请重新生成) }这三层兜底合在一起基本能让这个功能在各种机型上都能体面地结束而不是卡死在某个界面上。5. 真机调试和上线自查清单5.1 开发工具里不能调真机才是唯一标准这个接口在开发者工具里是跑不起来的原因很简单开发者工具没有真实的系统分享面板。工具上最常见的表现就是点了按钮没反应不要在这个阶段怀疑代码问题直接上真机调试。真机调试时建议把分享目标选为“文件传输助手”这样发送后可以快速在聊天窗口里点开文件检查文件名、后缀、内容是否正常。安卓和iOS要分别走一遍完整流程因为两端的支持类型差异很大会直接影响成功率。如果在真机上仍然遇到回调不触发的情况不要反复刷新页面去猜先在complete里、onShow里分别加上日志通过日志判断是走到了哪一步。用日志还原现场比瞎猜高效得多。5.2 常见问题速查表现象可能原因解决方案写入文件报路径不合法路径没有以wx.env.USER_DATA_PATH开头统一用wx.env.USER_DATA_PATH拼接文件名分享面板打不开在开发者工具里调试真机预览/体验版调试分享出去的文件打不开fileName没有后缀或后缀不在安卓支持列表显式传extension确保后缀一致且为支持类型分享文件内容乱码writeFile的encoding不是binary改为encoding: binary分享回调完全不触发原生面板回调链不稳定使用onShow setTimeout 兜底分享出去的文件是0字节arrayBuffer为空或写入失败写入后调用getFileInfo校验size文件名显示为默认名没有传fileName显式传fileName带后缀5.3 上线前检查清单代码里是否还有依赖complete回调去关闭loading、清理临时文件如果有不要赌它能触发改成onShow或超时兜底。临时文件是否做过清理每次分享生成的临时文件会占用本地空间建议在分享结束后的安全时机删除或者在下一次分享前清理旧文件。文件大小是否合理把动辄几十MB的文件塞进分享面板生成和传输都会很慢产品层面最好限制一下文件体积。安卓和iPad/PC端是否都测过平台间差异可能导致同一个文件在一个端能分享、另一个端失败发布前至少把主流环境跑一遍。6. 最后再补充两个小经验第一个是临时文件的清理时机。我现在的做法是在每次写入新临时文件之前先尝试清空同目录下的旧分享文件避免无限堆积。清理动作放在writeFile之前不要放在complete之后因为回调不可靠。function cleanOldShareFiles(prefix share) { const fs wx.getFileSystemManager() const dirPath wx.env.USER_DATA_PATH try { const files fs.readdirSync(dirPath) files.forEach((file) { if (file.startsWith(prefix)) { fs.unlinkSync(${dirPath}/${file}) } }) } catch (e) { // 目录读取失败或文件不存在忽略即可 } }第二个是分享结果别看得太重。我实际用过一段时间后发现shareFileMessage的稳定性和普通接口不是一个级别产品文案里尽量不要出现“分享成功”这类强提示更不要基于success去做积分发放、状态同步等硬依赖。如果一定要获得分享结果优先考虑通过服务端下发的消息回调来确认而不是依赖小程序的回调事件。这类“文件分享”功能核心链路其实只有三步落盘、调接口、处理结果。但每一步都有不小的暗坑尤其是回调可靠性这块谁踩谁知道。希望这篇记录能让你少走一圈弯路。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →