ARTICLE DETAIL

资讯详情

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

小程序文件上传:wx.uploadFile + formData 传参全攻略

小程序文件上传:wx.uploadFile + formData 传参全攻略 小程序的售后工单里用户拍了一张商品照片要传上来同时还得带上订单号和退款原因。这个功能我第一次做的时候下意识想在小程序里 new FormData() 再加一个 File 对象——跟网页端写 axios 一样。结果发现小程序根本没有完整的 FormData 实现绕了一圈最后回到 wx.uploadFile 这个官方 API 上把 formData 里的文本字段和 file 文件一起发出去。这篇文章就把这套传参逻辑彻底捋一遍formData 里放什么、file 走哪条通道、后端怎么接、多图怎么传、以及文档里没写明白的那几个坑。1. 售后工单的真实需求文件传参为什么不能只靠 JSON1.1 字段拆开看问题就清楚了我在做小程序商城的售后模块时用户提交退款申请需要填订单号、联系人、退款原因还必须附上商品照片作为凭证。把需求拆开看这其实是两种类型完全不一样的数据普通文本字段订单号、原因、联系电话都是字符串量很小二进制文件数据图片可能是几百 KB 到几 MB无法直接塞进 JSON 字符串。小程序里最常见的传参方式是 wx.request JSON。JSON 对文本字段很友好但对文件极不友好。有人会想“把图片转 base64 塞进 JSON”这在单张几百 KB 时还能凑合一旦图片到了几 MBbase64 会让体积增加约 33%请求体变得巨大网关和服务器处理起来都很痛苦。很多后端网关对请求体有 1MB、2MB 的限制一上传就 413用户那边只看到“提交失败”体验非常差。1.2 一段式 vs 两段式先决定方案再动手这里值得先停下来选方案选错后面全是返工。业内有两条路线方案流程优点缺点一段式本文主打wx.uploadFile 发起 multipart/form-data 请求文本字段和文件一次带上单次请求原子化后端一次入库代码路径短多图时文本字段会重复上传某张失败整体失败进度不好单独展示两段式先用 wx.uploadFile 把文件传到文件服务拿到 URL/文件 ID再用 wx.request 提交 JSON 表单文件上传可以单独重试字段变化不影响已传文件后端逻辑简单若订单没提交会留下孤儿文件需要额外处理临时文件清理实际业务里我见过不少团队在一段式和两段式之间反复横跳。我的经验是如果只是“一张图 两三个字段”一段式简单直接如果是“九张图 多字段 长文本说明”两段式会稳妥很多因为你可以先展示每张图的上传进度失败单独重试最后再统一提交业务数据。本篇重点展开一段式因为标题问的正是“小程序使用 formData 传参 附件 file”把这段链路理解透两段式自然也会了。1.3 formData 在里面的角色一段式方案里wx.uploadFile 提供一个 formData 参数用来携带普通文本字段文件本体则通过 filePath 参数指定。这里最常见的误区是以为 formData 是“文件集合”能往里面塞 File 对象。它不是。formData 里的值会被微信展开成 multipart/form-data 请求体中的普通文本 part文件是单独的 file part。理解这个区别后面排查问题会快很多。2. 小程序没有完整的 FormData 对象先把环境边界摸清2.1 浏览器思维为什么在小程序里失灵在网页端我们写的是const formData new FormData(); formData.append(orderId, SO20241102120001); formData.append(file, fileInput.files[0]); axios.post(/upload, formData);浏览器会自己生成 multipart/form-data 请求体包含普通字段和文件字段。小程序不一样——官方没有给开发者提供完整的、可用的 FormData 构造函数你 new FormData() 大概率会报错或拿到一个残缺对象。不要浪费时间去找“小程序版 FormData polyfill”微信给的正规入口就是 wx.uploadFile以及包装它的各种第三方库。2.2 重新认识 wx.uploadFile 的几个参数wx.uploadFile 的完整签名大致是wx.uploadFile({ url: https://api.example.com/upload, filePath: /tmp/xxx.jpg, // 文件在本地文件系统中的路径 name: file, // 后端用来拿文件的字段名 formData: { // 其他普通字段 orderId: SO20241102120001, reason: 商品破损 }, header: { Authorization: Bearer token }, timeout: 20000, success: (res) {}, fail: (err) {} });filePath不是 File 对象也不是 ArrayBuffer是本地文件路径。这个路径通常由 wx.chooseMedia、wx.chooseMessageFile 返回。name文件在请求体里的字段名。后端 multer 的 upload.single(file) 里的 file 必须和它一致否则后端拿不到文件。formData只是普通键值对会被放到 multipart 请求体的普通 part 里后端通过 req.bodyNode 里或 RequestParamSpring 里取到。header可以放认证信息但不要手动塞 Content-Type原因后面专门说。2.3 底层到底长什么样一次 wx.uploadFile 发出去实际请求体长这样--upload-boundary Content-Disposition: form-data; nameorderId SO20241102120001 --upload-boundary Content-Disposition: form-data; namereason 商品破损 --upload-boundary Content-Disposition: form-data; namefile; filenamephoto.jpg Content-Type: image/jpeg 二进制文件内容 --upload-boundary--微信底层会把 formData 里的每个键值转成一个文本 part把 filePath 指向的文件转成一个文件 part这两类 part 共同组成一个合格的 multipart/form-data 请求体。这也是为什么前端不需要、也不应该手动拼 multipart——微信已经帮你拼好了你只要把参数摆对。3. 完整实操一份能跑通的工单上传代码3.1 选择图片并组装参数基础库里推荐使用 wx.chooseMedia2.10.0旧基础库用 wx.chooseImage。选择图片后需要从返回值里把 tempFilePath 取出来。wx.chooseMedia({ count: 1, mediaType: [image], sizeType: [compressed], sourceType: [album, camera], success: (res) { const filePath res.tempFiles[0].tempFilePath; this.uploadWorkOrder(filePath); } });为什么不选 original工单凭证这种场景压缩图足够看清商品瑕疵original 会显著增大上传体积和耗时。如果业务确实需要原图选择 original但要在后面做好大小限制。3.2 封装一个上传方法并处理业务回调直接调用 wx.uploadFile 时建议做一层薄封装把“网络层成功/失败”和“业务层成功/失败”分开。submitWorkOrder(filePath) { wx.uploadFile({ url: https://api.example.com/aftersale/report, filePath, name: file, formData: { orderId: SO20241102120001, reason: 商品破损申请退款, contact: 13800138000 }, header: { Authorization: Bearer token }, timeout: 20000, success: (res) { // 先看状态码 if (res.statusCode ! 200) { wx.showToast({ title: 服务异常, icon: none }); return; } // 再解析业务码 try { const data JSON.parse(res.data); if (data.code 0) { wx.showToast({ title: 提交成功, icon: success }); this.setData({ submitting: false }); } else { wx.showToast({ title: data.msg || 提交失败, icon: none }); } } catch (e) { console.error(响应不是合法JSON, res.data); } this.setData({ submitting: false }); }, fail: (err) { console.error(上传失败, err); wx.showToast({ title: 网络异常请重试, icon: none }); this.setData({ submitting: false }); } }); }代码里有两个关键点一是用 submitting 状态防止用户重复点击二是不要在 success 里直接认为成功必须检查 res.statusCode 和业务 code。很多新手在开发工具里一切正常一上线就发现偶发“已经传了但页面提示失败”多半就是没解析后端返回的业务码。3.3 后端如何接收Node.js multer 示例前端用 multipart/form-data 发请求后端就不要指望通过 express.json() 或者 req.query 拿到字段了。以 Node.js multer 为例const express require(express); const multer require(multer); const app express(); const upload multer({ dest: uploads/ }); app.post(/aftersale/report, upload.single(file), (req, res) { console.log(普通字段:, req.body); console.log(文件字段:, req.file); res.json({ code: 0, data: { fileId: req.file.filename, fields: req.body } }); }); app.listen(3000);只要前端 name 传的是 filemulter 的 upload.single(file) 就能收到formData 里的 orderId、reason、contact 会完整出现在 req.body 里。后端如果只配了 bodyParser json那它只管 application/json对 multipart 是无效的必须靠 multer 这类中间件解析。顺便说一句换成其他后端也一样对字段名Spring 里用 RequestParam(file) MultipartFile file、RequestParam(orderId) String orderIdPHP 里读 $_FILES[file] 和 $_POST[orderId]。字段名对齐是这一段的核心。3.4 联调时的验证方法本地联调最简单有效的验证就是在开发者工具的 Network 面板里看这次 upload 请求的 Request Payload。成功的 multipart 请求会显示 boundary 分隔的文本 part 和文件 part你可以直观确认 formData 字段是否都在、文件是否带上。后端再配合打印 req.body 和 req.file前后一对照问题基本一目了然。注意uploadFile 对域名要求和 request 一样开发版/体验版阶段可以在开发者工具“详情-本地设置”里勾选不校验合法域名但正式发布前一定要在微信公众平台配置好 uploadFile 合法域名否则真机上会报“xxx.xxx.com 不在以下 uploadFile 合法域名列表中”。4. 多图、进度条、大文件把上传体验从能用到好用4.1 一次选多张图怎么逐个上传wx.uploadFile 一次只能传一个 filePath。如果用户选了九张图有两条路串行一张传完再传下一张失败可单独重试并行几张同时传前提是后端撑得住且微信并发上限不建议超过 10 个。实际项目里我更常用串行加小并发控制。先写一个返回 Promise 的上传方法uploadFilePromise(params) { return new Promise((resolve, reject) { const task wx.uploadFile({ ...params, success: (res) { try { const data JSON.parse(res.data); if (res.statusCode 200 data.code 0) { resolve(data.data); // 拿到文件id或url } else { reject(new Error(上传失败: ${res.statusCode})); } } catch (e) { reject(e); } }, fail: reject }); }); }然后串行上传async function uploadImages(filePaths) { const results []; for (const filePath of filePaths) { try { const result await this.uploadFilePromise({ url: https://api.example.com/upload, filePath, name: file, formData: { scene: workorder } }); results.push(result); } catch (e) { // 失败重试一次 const retry await this.uploadFilePromise({ url: https://api.example.com/upload, filePath, name: file, formData: { scene: workorder } }); results.push(retry); } } return results; }代码里的重试逻辑看起来简单但很实用——移动端弱网环境下偶发失败远比想象中多。注意重试前建议加 300ms 左右的延迟避免在弱网瞬间连续重试打爆带宽。4.2 加上进度监控和取消wx.uploadFile 会返回一个 UploadTask 对象在 success/fail 触发前可以绑定进度const uploadTask wx.uploadFile({ /* 参数略 */ }); uploadTask.onProgressUpdate(res { console.log(进度, res.progress); console.log(已传, res.totalBytesSent, 共, res.totalBytesExpectedToSend); this.setData({ uploadPercent: res.progress }); }); // 用户取消 uploadTask.abort();进度数据适合用来做文件级进度条九张图就记录一个数组每张上传完成把对应 index 的进度设为 100再合并计算整体进度。这个体验比“干等 loading”好得多。4.3 大文件怎么办wx.uploadFile 本身没有硬性的大小限制但现实中的瓶颈很多后端网关通常限制 body 大小Nginx 常见 1~10M云函数/API 网关有的限制更小微信默认请求超时 60 秒大文件加上弱网很容易超时真机内存吃紧时一次性读大文件也可能出问题。我的实际处理顺序是优先压缩。chooseMedia 时 sizeType 用 compressed或后端提前告知可接受的图片尺寸与压缩策略前端先拿文件大小做预检。用 wx.getFileInfo 查 size超过阈值直接提示不发起无效请求超过几十 M 的视频类文件建议走分片上传用 FileSystemManager.readFile 按 position/length 分段读取每次写一个本地临时分片文件再用 wx.uploadFile 逐片上传后端负责合并。分片的工程成本不低不是所有项目都要上先确认业务真的需要。文件大小预检的代码很好写wx.getFileInfo({ filePath, success: (res) { if (res.size 5 * 1024 * 1024) { wx.showToast({ title: 图片不能超过5M, icon: none }); return; } } });5. 这些细节只有真跑一遍才会明白5.1 别在 header 里手动塞 Content-Type用过 axios 的人习惯手动设置 Content-Type。但在 wx.uploadFile 里如果你在 header 里写死“Content-Type: multipart/form-data”偏偏又没有微信自动生成的 boundary后端解析会失败或丢字段如果你写“application/json”微信底层可能根本不会把文件拼进去。正确做法是header 只放认证、版本这类信息Content-Type 交给微信自动生成。这是我在联调时踩的第一个坑报错信息非常难定位一度以为是自己代码传参传错了。5.2 回调与状态码的真相wx.uploadFile 的 success 回调只代表着“这次 HTTP 请求发出去了并拿到了响应”不代表后端业务成功。后端可能返回 500也可能返回 { code: 1, msg: 库存不足 }这都会走 success。所以解析顺序一定是先看 res.statusCode 是否为 2xx再 JSON.parse(res.data) 看业务 code。fail 回调主要触发在网络层错误无网络、超时、被 abort 等。如果哪天上线后发现上传明明有响应却一直提示失败先回来查这块。5.3 临时文件的生命周期和清理时机wx.chooseMedia 返回的 tempFilePath位于系统临时目录小程序退出或系统清理内存后可能就没了。如果你只是“选完立刻上传”直接用 tempFilePath无需保存。但工单场景里用户往往先填表、预览图片隔一会儿才提交这时建议用 FileSystemManager 把临时文件保存到本地用户目录const fs wx.getFileSystemManager(); fs.saveFile({ tempFilePath, success: (res) { // res.savedFilePath 是持久路径 const savedPath res.savedFilePath; } });提交成功后如果本地不再需要这张图记得用 removeSavedFile 把它删掉避免长期占用用户存储空间。我在一个旧项目里见过客户反馈“小程序越用越大”查下来就是每次工单预览都把图片 saveFile 了提交后忘了删。5.4 真机和开发者工具不一样的地方开发者工具里 Network 面板、临时目录、chooseMedia 模拟器都和真机有差别。最常见的是工具里上传一切正常一上真机就报域名不合法原因就是正式环境必须把上传域名配到 uploadFile 合法域名里request 域名和 uploadFile 域名是分开配置的。其次是真机上 chooseMedia 会唤起系统相机相册不同机型返回的 filePath 格式不一致个别手机会带中文文件名或特殊字符后端保存文件时要做好文件名处理不要直接用原始文件名拼路径。我遇到过一台安卓机传上来的文件名带空格和括号后端没有做转义保存时直接报错排查了半天。5.5 我现在选方案的朴素标准跑过一轮之后我现在的习惯很明确单张图、字段少直接用 wx.uploadFile formData 一段式代码最短后端一个接口搞定多图或字段复杂的业务先传文件拿 URL再走 wx.request 提交文本。formData 这个能力不是用来硬扛所有场景的它是你在“一段式”这条路上最顺手的工具。把它的边界认清遇到新需求时你就能快速判断该往哪边走而不是每次都在调试 multipart 解析错误。
返回列表