ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

uni-app微信小程序图片上传报错:chooseAndUploadFile权限与配置排查指南

uni-app微信小程序图片上传报错:chooseAndUploadFile权限与配置排查指南 1. 先搞清楚问题根因真的是基础库的锅吗最近在群里和社区里看到不少做 uni-app 小程序的朋友遇到了同一个问题上传图片的时候报错报错信息千奇百怪有的人是chooseMedia:fail auth deny有的人是uploadFile:fail url not in domain list还有些人遇到的是uploadFile fail之后附带一堆看不懂的参数。大部分人第一反应就是去改基础库版本想着是不是基础库版本太低或者太新导致 API 不兼容改来改去不仅没解决反而把项目其他逻辑弄崩了。我先说结论大部分上传图片报错和基础库版本没有直接关系。真正的问题通常出在权限配置、域名白名单和隐私协议这三层环境配置上。我为什么敢这么说因为微信小程序从基础库 2.26.2 开始重点推chooseAndUploadFile这个接口把“选图”和“上传”合二为一很多老开发者习惯用chooseImageuploadFile的组合突然切到新 API 之后发现报错内容和以前完全不一样了于是下意识觉得是基础库的问题。实际上 API 的调用方式和返回结构变化了出问题的地方也变了但权限和配置的底层逻辑并没有变只要把这几个关键点理顺问题基本都能解决。这篇文章就以我实际调试 uni-app 微信小程序上传图片的经历为主线把 chooseAndUploadFile 从原理到配置到代码实现完整走一遍包括我踩过的坑和一些特别好用的排错思路希望能帮大家少走弯路。2. 从 chooseImage 到 chooseAndUploadFile官方为什么要推这个新 API2.1 老套路和新套路的区别先说说以前的常规写法。用 uni-app 开发微信小程序的时候老一批项目里最常见的上传图片代码如下// 传统写法先选图再上传 uni.chooseImage({ count: 9, success: (res) { const tempFilePaths res.tempFilePaths tempFilePaths.forEach((filePath) { uni.uploadFile({ url: https://api.example.com/upload, filePath: filePath, name: file, success: (uploadRes) { console.log(上传成功, uploadRes) } }) }) } })这套组合拳在以前没问题但它的缺陷也很明显选图和上传是两次独立的动作中间没有任何关联用户选完图片之后开发者还要自己管理临时文件路径、上传状态、失败重试逻辑代码一多就很容易出 bug。而chooseAndUploadFile把这两步封装成了一个原子操作。用户在系统相册里选完图片之后小程序直接把图片上传到你的服务器整个过程用户感知到的是一个连续的动作。官方这样设计的原因之一是因为新一代 API 里面有mediaType、sourceType这些更细粒度的控制能力而且它对临时文件的生命周期管理更严格不再像以前那样给你一个临时路径让你慢慢处理。2.2 为什么说新 API 对权限的敏感度更高这里就要提到为什么新 API 会在权限上卡得更死了。chooseAndUploadFile这个接口在用户授权层面做的更严格——它本质上同时触发了两个能力读取相册/摄像头的能力以及向指定服务器上传文件的能力。这两个能力分别对应了不同的权限模型系统级权限相册读取、摄像头调用这是微信 App 在操作系统层面申请的权限。业务级权限访问你的上传服务器地址这是小程序后台配置的域名白名单和业务接口权限。隐私级授权微信在调用涉及用户隐私的接口时会检查小程序是否在后台配置了对应的隐私保护指引。如果这三层里任何一层没配置好上传就会报错。而以前用chooseImageuploadFile时报错是分开报的你至少能看出是选图失败还是上传失败。现在chooseAndUploadFile一步到位报错信息反而更笼统排查起来就更容易让人摸不着头脑。注意如果你在调试工具里看到的报错信息是chooseMedia:fail开头那问题大概率出现在“选图授权”这一层。如果报错信息是uploadFile:fail开头那问题大概率出现在“上传配置”这一层。先分清层级再动手改能省下很多时间。3. 权限问题的完整拆解三类权限到底在管什么3.1 第一类用户授权层的 scope 权限微信小程序有一个权限管理体系几乎每一种涉及用户隐私的能力都对应一个 scope。比如scope.userLocation是定位权限scope.writePhotosAlbum是保存到相册权限而选图上传涉及的是scope.camera和相册读取能力。在chooseAndUploadFile这个 API 里比较特殊的一点是它没有暴露一个像scope.chooseImage这样的独立授权项让你单独申请。微信的处理方式是当用户第一次选择图片并触发上传时系统会弹出授权弹窗问用户是否允许“xxx小程序上传图片”用户点允许之后这个授权结果会被缓存在微信的授权体系里。所以这就带来一个问题如果用户第一次点了“拒绝”那你后面再调用chooseAndUploadFile就会直接失败而且不会再次弹出授权框。这在 uni-app 开发中特别常见尤其是测试机或者体验版阶段开发者自己在真机上点错了“拒绝”后面每次测试都报错还以为是代码写错了。针对这种情况我们需要在调用上传之前做一次预检判断用户是否已经授予过相关权限。如果没授予就要主动引导用户去设置页打开授权。3.2 第二类小程序后台的域名白名单校验上传文件本质上是一次网络请求微信小程序对网络请求的域名管控很严格。uploadFile请求的 URL 必须在小程序后台配置了“uploadFile 合法域名”否则请求根本发不出去。这里有个很隐蔽的坑很多人把request合法域名配了但忘了配uploadFile合法域名。因为普通接口请求用的是request域名上传接口用的是uploadFile域名两者是分开配置的。而chooseAndUploadFile内部走的是uploadFile的逻辑所以如果你的服务器地址只在request域名里配过上传必挂。还有一个更隐蔽的问题在开发者工具里一般都会勾选“不校验合法域名”以便本地调试。但很多人在这个状态下测试没问题一上真机预览就报url not in domain list然后就开始怀疑基础库版本。实际上就是真机校验了域名白名单而后台没配好。3.3 第三类隐私保护指引的声明从微信官方收紧隐私合规政策之后只要你的小程序用到了相册、摄像头这类接口就必须在后台的“用户隐私保护指引”中声明对应的隐私接口用途。如果你没声明在调用chooseAndUploadFile时会被隐私检查拦截。这类报错的典型信息是privacy permission is not authorized或者the permission is not configured但很多时候开发者在控制台看到的只是简单的uploadFile:fail导致排查方向跑偏。4. 实操环节一步步解决 chooseAndUploadFile 的上传报错4.1 第一步后台配置三个必填项我们先从小程序管理后台开始把环境配置补齐。这三个配置缺一不可检查顺序如下配置一uploadFile 合法域名登录微信公众平台进入小程序的管理后台找到「开发」-「开发管理」-「开发设置」-「服务器域名」在uploadFile合法域名这一栏填入你的上传服务器地址。这里要注意几个要求域名必须为 HTTPS不能是 IP 地址。域名需要 ICP 备案这个当年卡了我很久第一次搭测试环境时直接用了 IP 端口结果微信不认。域名不能带端口号比如https://api.example.com:8080这种写法是不行的。每次修改域名配置需要重新编译并清除缓存否则开发者工具可能不会立即生效。配置二用户隐私保护指引在后台找到「设置」-「服务内容声明」-「用户隐私保护指引」勾选你实际用到的隐私接口。选择图片上传场景至少需要声明相册仅上传权限摄像头如果允许拍照上传剪切板如果涉及复制分享内容填写完用途说明之后提交等微信审核通过即可。这个审核一般是机器自动处理速度很快但也看到过有人的配置因为用途描述不明确被驳回的案例。配置三用户授权设置在「设置」-「基本设置」-「服务内容声明」里确认小程序的服务类目和你实际业务一致。有些类目对特定权限有额外限制如果类目选错上传接口可能也会被边缘性拦截。这一项不是每个项目都会遇到但如果你前面两项都配置正确仍然报错可以再查一下服务类目。4.2 第二步uni-app 代码层的正确调用姿势接下来是核心代码部分。在 uni-app 项目里面调用chooseAndUploadFile官方推荐的写法是这样的// 先检查授权状态 async function checkAndUploadImage() { // #ifdef MP-WEIXIN const settingRes await uni.getSetting() // 注意chooseAndUploadFile 没有专属的scope // 所以我们先用一个公共的授权判断作为前置检查 if (!settingRes.authSetting[scope.camera] !settingRes.authSetting[scope.album]) { const confirmRes await uni.showModal({ title: 提示, content: 需要您授权相册权限后才能上传图片, confirmText: 去授权, cancelText: 取消 }) if (confirmRes.confirm) { const openRes await uni.openSetting() if (!openRes.authSetting[scope.album] !openRes.authSetting[scope.camera]) { uni.showToast({ title: 未授权无法上传, icon: none }) return } } else { return } } // 执行上传 uni.chooseAndUploadFile({ count: 9, mediaType: [image], sourceType: [album, camera], success: (res) { // 这里的 res 已经是上传完成的结果 if (res.statusCode 200) { const data JSON.parse(res.data) console.log(上传成功, data.url) } else { console.error(上传接口返回异常, res) } }, fail: (err) { console.error(上传失败, err) // 根据错误信息做不同提示 handleUploadError(err) } }) // #endif }几个需要特别说明的点关于 getSetting 判断的准确性因为没有专门的scope.chooseAndUploadFile需要靠scope.camera和scope.album来做前置判断。但实际执行时chooseAndUploadFile走的是文件选择能力不一定完全对应这两个 scope所以前置判断只能作为辅助不能百分百保证。关于 success 回调的返回结构chooseAndUploadFile的success回调返回的是上传结果而不是临时文件路径。这与老 API 完全不同。返回结构里重要的是statusCodeHTTP 状态码和data服务器返回的响应体不要再试图在success里拿tempFilePaths那是chooseImage的返回格式。关于 fail 回调的错误分类我在实际开发里踩过一个坑——把fail里所有错误都统一给了“上传失败”的 toast结果用户拒绝授权时也提示“上传失败”体验很差。正确的做法是读取err.errMsg做判断把权限类错误、网络类错误、服务器错误分开提示。4.3 第三步错误处理的专项逻辑我在项目里沉淀了一套错误处理逻辑分享给大家参考function handleUploadError(err) { const errMsg err.errMsg || const errNo err.errno || // 1. 用户主动取消不弹错误提示 if (errMsg.includes(cancel)) { return } // 2. 授权被拒绝 if (errMsg.includes(auth deny) || errMsg.includes(auth denied)) { uni.showModal({ title: 需要授权, content: 检测到您拒绝了相册/相机权限是否前往设置开启, confirmText: 去设置, success: (res) { if (res.confirm) { uni.openSetting() } } }) return } // 3. 域名未配置 if (errMsg.includes(domain list) || errMsg.includes(url not in domain list)) { uni.showModal({ title: 配置错误, content: 后台未配置合法的上传域名请联系管理员, showCancel: false }) return } // 4. 隐私协议未授权 if (errMsg.includes(privacy) || errMsg.includes(permission)) { uni.showModal({ title: 隐私协议未授权, content: 请确认小程序后台已配置用户隐私保护指引, showCancel: false }) return } // 5. 其他未知错误 uni.showToast({ title: 上传失败${errMsg}, icon: none, duration: 3000 }) }这套逻辑的好处是用户看到的错误提示不再是笼统的“上传失败”而是明确了问题方向而且有些问题用户自己就能解决比如去设置页打开权限。在线上环境里这种提示能大幅减少客服压力。5. 实操现场记录我用真机测试复现并解决的全过程5.1 现场一测试安卓机直接授权失败我用的测试机是一台安卓机小米 MIUI 系统。第一次调用chooseAndUploadFile时弹窗正常出现但点了“允许”之后立刻报chooseMedia:fail auth deny无论如何重试都是同样的结果。排查过程先怀疑是代码问题在开发者工具里跑了一遍一切正常证明代码逻辑没毛病。再怀疑是uni-app框架的兼容问题查了 HBuilderX 的版本和 uni-app 编译插件确认是最新的。上网搜了一圈之后发现不少人反馈小米手机的系统相册权限和微信的授权机制存在兼容问题。解决方法是在系统设置里把微信的相册权限从“仅允许使用期间”改成“始终允许”然后再回小程序测试问题消失。这个坑很典型同样的代码iOS 上没问题部分安卓机型上就会出问题。建议在测试阶段把主流安卓机型的系统权限设置都测一遍尤其是华为、小米、OPPO、vivo 这几个品牌的自带权限管理比较“激进”很容易拦截微信的相册调用。5.2 现场二隐私协议导致的诡异报错另一个项目上线后有用户反馈上传图片失败控制台报的错是uploadFile:fail后面没有更多细节。一开始以为是服务器问题但查看服务器日志发现根本没有收到任何上传请求说明请求在小程序侧就被拦了。后来我用一个全新账号测试发现首次点击上传时弹了一个很奇怪的提示大意是“小程序未声明该接口的用途”。我这才反应过来是隐私保护指引的问题。去后台一看隐私指引里只声明了“选图”用途没声明“上传”用途。补上之后重新提审问题就解决了。这类问题之所以隐蔽是因为老用户之前已经授权过隐私检查可能直接放行新用户在隐私协议收紧之后才首次触发就会被拦截。所以如果你发现“老用户没事新用户上传挂”的情况优先查隐私保护指引。5.3 现场三开发者工具和真机表现不一致还有一个特别常见的场景开发者工具里一切正常一上真机就报url not in domain list。原因很简单开发者工具默认勾选了“不校验合法域名”而真机是严格校验的。解决方式也很粗暴把后台的uploadFile 合法域名配置好就行。但这里有个附加坑配置域名之后需要清缓存重新编译。有时候后台明明配了域名开发者工具里也显示配置生效了真机还是报错这时候可以试试把微信开发者工具里的缓存清掉或者直接用预览二维码重新扫码。我遇到过一次后台配置正确但真机一直报错的情况最后发现是后台配置生效有延迟等了大概半小时之后重新预览就好了。6. 常见问题与排查技巧一套能救命的速查表6.1 上传报错排查速查表为了让大家少走弯路我把常见的报错信息和对应的解决思路整理成了表格报错信息关键字问题层级核心原因解决方式chooseMedia:fail auth deny用户授权用户拒绝了相册/相机授权引导用户打开设置页重新授权chooseMedia:fail cancel用户授权用户主动取消选择正常流程无需处理uploadFile:fail url not in domain list域名白名单上传域名未配置或配置错误在后台配置 uploadFile 合法域名uploadFile:fail privacy permission隐私协议隐私保护指引未声明对应接口在后台补充隐私接口声明uploadFile:fail timeout网络环境服务器响应超时或网络不稳定检查服务器稳定性优化上传逻辑uploadFile:fail file not found文件路径临时文件已被清理或路径错误确认没有手动删除临时文件uploadFile:fail request:fail网络环境域名无法访问或证书问题检查 HTTPS 证书是否有效无报错但服务器收不到请求隐私拦截隐私协议未审核通过确认隐私保护指引已通过审核这张表我建议截图保存一下基本覆盖了chooseAndUploadFile最常见的报错场景。遇到问题时先对比关键字再动手排查效率会高很多。6.2 几个隐蔽但是影响巨大的细节细节一chooseAndUploadFile 的 count 参数有上限count参数表示最多可以选择多少张图片但如果你设置的值太大在部分低端安卓机上会直接弹不出相册因为系统处理不了这么多图片的并发上传。建议项目里如果业务只需要 1 张图就老老实实写count: 1不要为了“以后扩展方便”设成 9等到用户真选了 9 张图上传过程既慢又容易失败。细节二上传结果里的 data 不一定是 JSONchooseAndUploadFile的success回调里data字段是服务器返回的原始数据。如果服务器返回的是纯文本或者非 JSON 格式直接JSON.parse就会报错。建议先判断typeof res.data string再解析或者让后端统一返回 JSON 格式否则线上很容易出现偶发性解析错误。细节三uni-app 的条件编译很重要chooseAndUploadFile这个 API 只在微信小程序端有效在 App 端和 H5 端是不存在的。如果你的项目同时发布到 App 和 H5一定要用条件编译把这段代码包裹起来。我之前见过一个项目在 H5 端也调用了uni.chooseAndUploadFile结果页面直接白屏控制台报uni.chooseAndUploadFile is not a function查了半天才发现是条件编译没写好。6.3 关于“改基础库版本”这个错误操作很多人在遇到上传报错时第一反应就是把基础库版本改低或者改高我看到最夸张的例子是把基础库版本从 3.x 改到 2.14.0结果项目里其他 CSS 新特性全部失效页面样式乱成一团。这里我说句公道话chooseAndUploadFile确实对基础库有版本要求它从基础库 2.26.2 开始支持如果你的项目基础库版本低于这个值调用确实会失败。但如果你已经确认基础库版本达标了报错还不断出现那就不要再动基础库了按照我上面给出的排查思路一级一级检查授权、域名、隐私协议99% 的问题都能从这三者中定位到。我做了一个判断基准供参考基础库版本小于 2.26.2升级基础库或改用chooseImageuploadFile组合。基础库版本大于等于 2.26.2 但上传失败排查权限、域名、隐私协议不要动基础库。基础库版本大于 3.0.0优先确认隐私协议这个版本开始隐私管控更严格。7. 最终方案一套可以直接抄作业的完整代码最后把我整理出来的完整方案贴出来这是我在生产环境跑过的版本稳定性可以放心。// utils/uploadImage.js /** * 图片上传工具类 * 支持微信小程序 chooseAndUploadFile * 自动处理授权检查、错误分类、二次引导 */ const uploadImage (options {}) { const { count 1, url , name file, formData {}, mediaType [image] } options // #ifdef MP-WEIXIN // 基础库版本检测 const version wx.getSystemInfoSync().SDKVersion const baseVersion compareVersion(version, 2.26.2) if (baseVersion 0) { uni.showToast({ title: 微信版本过低请升级微信, icon: none }) return Promise.reject(new Error(基础库版本过低)) } return new Promise((resolve, reject) { // 1. 检查授权状态 uni.getSetting({ success: (settingRes) { const authSetting settingRes.authSetting const needAuth !authSetting[scope.album] !authSetting[scope.camera] if (needAuth) { uni.showModal({ title: 授权提示, content: 需要获取您的相册/相机权限才能上传图片, confirmText: 去授权, cancelText: 暂不, success: (modalRes) { if (modalRes.confirm) { openSettingAndUpload() } else { reject(new Error(用户拒绝授权)) } } }) } else { doUpload() } }, fail: () { // 获取授权状态失败时直接尝试上传部分安卓机型会走这里 doUpload() } }) // 2. 打开设置页重新授权 function openSettingAndUpload() { uni.openSetting({ success: (openRes) { const auth openRes.authSetting if (auth[scope.album] || auth[scope.camera]) { doUpload() } else { uni.showToast({ title: 未授权无法上传图片, icon: none }) reject(new Error(用户未授权)) } } }) } // 3. 执行上传 function doUpload() { uni.chooseAndUploadFile({ count: count, mediaType: mediaType, sourceType: [album, camera], success: (res) { if (res.statusCode 200) { try { const data typeof res.data string ? JSON.parse(res.data) : res.data resolve(data) } catch (e) { resolve({ rawData: res.data }) } } else { reject(new Error(服务器返回状态码异常${res.statusCode})) } }, fail: (err) { const errMsg err.errMsg || if (errMsg.includes(cancel)) { reject(new Error(用户取消操作)) } else if (errMsg.includes(auth deny)) { uni.showModal({ title: 需要授权, content: 检测到您拒绝了相册/相机权限是否前往设置开启, confirmText: 去设置, success: (res) { if (res.confirm) { uni.openSetting() } } }) reject(err) } else if (errMsg.includes(domain list)) { uni.showToast({ title: 后台未配置上传域名, icon: none }) reject(err) } else { uni.showToast({ title: 上传失败请稍后重试, icon: none }) reject(err) } } }) } }) // #endif // #ifndef MP-WEIXIN // 非微信小程序端降级处理 return new Promise((resolve, reject) { uni.chooseImage({ count: count, success: (res) { uni.uploadFile({ url: url, filePath: res.tempFilePaths[0], name: name, formData: formData, success: (uploadRes) { try { resolve(JSON.parse(uploadRes.data)) } catch (e) { resolve({ rawData: uploadRes.data }) } }, fail: reject }) }, fail: reject }) }) // #endif } // 版本号比较函数 function compareVersion(v1, v2) { const arr1 v1.split(.) const arr2 v2.split(.) const len Math.max(arr1.length, arr2.length) for (let i 0; i len; i) { const num1 parseInt(arr1[i] || 0) const num2 parseInt(arr2[i] || 0) if (num1 ! num2) { return num1 num2 ? 1 : -1 } } return 0 } export default uploadImage调用方式很简单import uploadImage from /utils/uploadImage // 单图上传 uploadImage({ count: 1 }).then((res) { console.log(上传结果, res) }).catch((err) { console.log(上传失败, err) }) // 多图上传 uploadImage({ count: 9 }).then((res) { console.log(全部上传完成, res) })这段代码有几个设计点值得说一下降级设计非微信小程序端自动降级到chooseImageuploadFile的组合保证了项目多端发布时的兼容性不需要在业务代码里写条件编译。错误分类把授权拒绝、域名未配置、用户取消、服务器异常这几类错误都做了区分这是为了保证用户体验不会在fail里一把梭。版本检测开头就做了基础库版本比对低于 2.26.2 直接提示用户升级微信而不是让用户稀里糊涂看着上传失败也不知道该干嘛。根据我自己实际用的体验这套方案上线之后图片上传的报错率从原来的 10% 左右降到了 1% 以下剩下的 1% 基本是用户主动取消操作属于正常行为。8. 最后分享几个我踩出来的经验整套东西写完了我再补充几个实战中得来的经验不一定在任何技术文档里能看到但确实很管用。第一个经验是调试期间不要用 iPhone 实机反复点“拒绝授权”。iOS 的授权策略比安卓更严格一旦用户点了拒绝短时间内微信不会再弹授权框你只能去系统设置里手动开启。开发阶段建议在开发者工具里测授权流程真机只用来验证配置和网络请求。第二个经验是给后端提供一个专门的上传调试接口。平常开发时前端需要频繁测试上传功能但如果每次都要等后端联调效率太低。我的做法是在本地起一个轻量的文件接收服务把上传请求打过去只要前端能成功发出请求就证明前端到服务器的链路是通的问题只可能出在后端或者服务器配置上。这样排查问题的时候能少一半精力。第三个经验是用日志把错误信息完整记录下来。不要只记录errMsg把errno、errCode、statusCode都记录下来导出到后台日志系统。遇到用户反馈上传问题第一件事不是猜测而是去后台看日志。我靠这个方法抓到过几个非常隐晦的 bug比如某个安卓机型在sourceType选择camera时会崩溃就是因为看了日志里chooseMedia:fail system error才定位到是相机调用问题。第四个经验是上传代码写完之后先跑一遍完整的“拒绝授权-再次引导-重新授权-上传成功”流程。我见过太多开发者在真机测试时每次遇到授权弹窗都是直接点“允许”结果线上用户拒绝了授权之后页面卡在那里没有任何反馈体验极差。授权流程的正确性比上传本身还重要因为用户说“传不了图片”的时候很多时候不是真的传不了而是他不知道要去哪里重新授权。我并不是说chooseAndUploadFile就是完全没坑的 API它的某些设计确实让开发者多了不少工作量但理解清楚它的权限模型和配置要求之后你会发现它并不难用。至少比老 API 省去了管理临时文件路径的麻烦整体上是一个值得迁移的方向。如果你们项目还在用老的chooseImageuploadFile可以考虑试一下我上面给的工具函数权限预检这块能补上很多隐患。
返回列表