资讯动态

微信小程序上传图片视频到后台存储完整方案与避坑指南

发布时间:2026/9/9 9:26:09 来源:尧图企业网站定制
简介这是一份面向微信小程序开发者的前后端完整示例核心解决图片与视频上传至后台存储的常见需求适合刚接触小程序上传功能的初中级开发者作为参考模板。前端demo包含照片、视频两个独立上传入口覆盖了页面布局、事件绑定与提交逻辑后端使用Flask编写uploadTest.py内置images和videos目录分别存放上传文件运行后即可接收并保存多媒体数据。压缩包内共12个文件以json配置、js逻辑、wxss样式、wxml页面结构以及Python后端为主整体仅5KB代码量小且结构清晰便于逐行阅读和按需拆改。目前已有419人学习下载作者还在CSDN提供配套教程可结合博客中的部署说明与参数配置快速验证效果。通过该示例读者能完整掌握从小程序端选择文件、构造请求到Flask端接收、存储与返回结果的链路并可直接将这套流程迁移到自己的实际项目中使用。 做微信小程序开发这几年被问得最多的一个需求就是“怎么把用户拍的图片、视频传到后台服务器存起来”。这个功能看起来简单真做起来坑却不少——选完图片传不上去、视频传上去黑屏、安卓能用iOS失灵、后台收到的文件名乱码随便一个都能卡你半天。我这次干脆整理了一个前后端完整可跑通的微信小程序上传图片视频到后台存储demo把整个链路拆开讲清楚代码可以直接拿去用也把我在实际项目中踩过的坑一并写出来。这个demo适合谁看正在做小程序毕设、刚接外包项目、或者在小程序公司里被派了“上传功能”这类需求的人。你不需要多深的经验前端会用点小程序语法后端能跑Node.js就够了。我尽量把每一步为什么这么做讲明白而不是只丢一段能跑代码。1. 这个demo到底解决什么问题1.1 一次完整上传的链路很多人第一次做上传功能脑子里只有“小程序把文件发给服务器”这一个模糊概念真上手才发现链路比想象的长。完整走一遍是这样的用户在小程序里点击按钮通过wx.chooseMedia打开系统相册或相机选择图片或视频。小程序拿到临时文件路径比如wxfile://tmp_xxx.jpg这个路径是本地缓存别人访问不到必须主动上传。通过wx.uploadFile把文件以multipart/form-data格式发到后端接口。后端接口接收到文件流落盘保存到服务器磁盘目录。后端把保存后的文件访问URL比如http://ip:3000/uploads/xxxx.jpg返回给小程序。小程序拿这个URL做预览、展示、或写入数据库。我在demo里把这条链路完整实现了前后端加起来大概200行代码。别看简单它背后的几个知识点——临时文件生命周期、multipart协议、文件类型校验——才是以后做任何文件上传功能的地基。1.2 为什么用multipart/form-data而不是base64你可能会问我用wx.request把图片转成base64字符串塞进JSON里发给后端不也能存吗能存但非常不推荐主要有三个原因。base64会让文件体积膨胀约33%。一个5MB的视频转成base64大概变成6.7MB对小程序这种流量敏感的场景很不友好用户手机流量直接就顶不住了。而且wx.request默认有10秒超时限制大文件极容易超时。更麻烦的是后端如果用云函数或云存储对请求体大小往往有限制base64方式很容易撞上这个上限。wx.uploadFile走的是文件流不会整包加载到内存里转字符串官方也是专门为文件上传设计的API。微信还贴心地在 submit 之前帮你把临时文件自动转化成可上传的 http 格式我们只需要管好 formData 那部分字段。这就是为什么行业里所有成熟方案都走multipart上传这个demo也不例外。1.3 demo的技术选型前端我用的是微信小程序原生语法没接框架。用原生是降低复现成本你不需要理解vue或react的封装逻辑直接看wx.chooseMedia、wx.uploadFile怎么用就行。后端选了 Node.js Express Multer。Multer 是Express生态里最成熟的上传中间件封装了multipart解析逻辑。如果你熟悉别的后端栈比如Java的Spring或PHP的Laravel原理一样只是代码风格不同。Node.js相对最容易上手一个文件就能跑起来不用配置环境所以我拿它做演示。存储层面demo先用本地磁盘目录/uploads落盘这是最直观、最能讲清楚原理的方案。最后我还会说说生产环境为什么建议换成腾讯云COS、阿里云OSS这类对象存储以及怎么平滑切换。2. 小程序端选图、选视频然后把文件交出去2.1 页面搭建与数据模型设计页面核心就三个部分选择按钮、文件列表展示、上传进度条。为了简单我把所有状态都放在page的data里view classcontainer button typeprimary bindtapchooseMedia选择图片/视频/button view classfile-list view classfile-item wx:for{{fileList}} wx:keyindex image wx:if{{item.type image}} src{{item.path}} modeaspectFill / video wx:elif{{item.type video}} src{{item.path}} / view classfile-info {{item.name}} / {{item.size}}KB /view progress percent{{item.progress}} wx:if{{item.uploading}} / /view /view /viewdata里维护一个fileList数组每个元素对应一个待上传文件记录类型、本地路径、格式化大小、上传进度。这样选完文件后我们可以批量把列表里的文件逐个上传页面也能实时反映每个文件的状态。这里有个容易踩的坑本地临时文件路径tempFilePath只在当前小程序会话内有效用户杀掉小程序重启后就失效了。所以你必须在拿到临时文件后尽快上传不要把它存到数据库里供以后使用。我在实际项目里见过有人把本地路径存进数据库第二天打开就展示不出来了就是没搞清楚临时文件的生命周期。2.2 wx.chooseMedia一个API同时搞定图片和视频旧版API是wx.chooseImage和wx.chooseVideo分开用一个管图片一个管视频很不方便。基础库2.10.0之后官方推出了wx.chooseMedia可以同时选择图片和视频demo里用的就是它chooseMedia() { wx.chooseMedia({ count: 9, // 最多选择9个文件 mediaType: [image, video], // 图片和视频都允许 sourceType: [album, camera], // 相册和相机都允许 maxDuration: 30, // 视频最长30秒仅视频时生效 success: (res) { const list res.tempFiles.map(item { const isVideo item.fileType video; return { type: item.fileType, path: item.tempFilePath, size: (item.size / 1024).toFixed(1), name: isVideo ? video_${Date.now()}.mp4 : image_${Date.now()}.jpg, uploading: false, progress: 0 }; }); this.setData({ fileList: this.data.fileList.concat(list) }); list.forEach(item this.uploadFile(item)); } }); }几个参数你结合实际场景调count限制一次最多选几个9个是上限mediaType如果只传[video]那界面就只会出现视频选择入口反过来同理sourceType如果不传默认相册和相机都可以选。注意res.tempFiles返回的fileType只有image和video两种值我直接用这个字段区分渲染image还是video组件。tempFilePath在小程序里是类似http://tmp/xxx.jpg的临时路径wx.uploadFile能直接识别它不需要额外处理。选完之后立刻调uploadFile上传顺序执行。你要注意这里的个体差异用户可能一次选了9个文件每个都触发 upload如果同时发起9个请求慢网络下很容易排队严重。我在生产项目里一般加个队列每次并发控制到2到3个体验会好很多。demo里为了直观没做队列你接手后可以补上。2.3 wx.uploadFile上传的核心入口wx.uploadFile是小程序上传文件的唯一官方API它内部构造了一个multipart请求体。核心JSON参数就这几个uploadFile(item) { item.uploading true; this.setData({ fileList: this.data.fileList }); const task wx.uploadFile({ url: http://localhost:3000/upload, // 后端接口 filePath: item.path, // 本地文件路径 name: file, // 后端接收字段名 formData: { type: item.type }, // 额外的业务字段 success: (res) { const data JSON.parse(res.data); console.log(上传成功:, data.url); wx.showToast({ title: 上传成功, icon: success }); }, fail: (err) { console.error(上传失败:, err); wx.showToast({ title: 上传失败, icon: none }); } }); task.onProgressUpdate((res) { item.progress res.progress; this.setData({ fileList: this.data.fileList }); }); }name字段太重要了它对应后端multer.single(file)里的file。如果前端传name: file后端就用upload.single(file)接收两边必须一致否则后端拿到的是undefined。formData是附加到multipart请求体里的普通字段可以用来传文件类型、用户ID、业务ID等上下文信息。我在demo里只传了一个type字段后端可以根据它来做不同业务的存储目录。wx.uploadFile返回的是一个UploadTask对象它提供了onProgressUpdate监听上传进度回调。这个回调的res.progress是0到100的整数我直接赋值给 item 上的 progress再 setData 刷新页面。实际项目中如果你上传多个文件要注意每个task对应的是哪个item最好在循环里用闭包把任务和item绑定起来不然会出现进度条串位的诡异现象。2.4 完整的小程序端代码总览把上面几段拼起来再加一个容器样式就是完整实现。我把完整js贴在下面方便你直接对照Page({ data: { fileList: [] }, chooseMedia() { wx.chooseMedia({ count: 9, mediaType: [image, video], sourceType: [album, camera], maxDuration: 30, success: (res) { const list res.tempFiles.map(item { const isVideo item.fileType video; return { type: item.fileType, path: item.tempFilePath, size: (item.size / 1024).toFixed(1), name: isVideo ? video_${Date.now()}.mp4 : image_${Date.now()}.jpg, uploading: false, progress: 0 }; }); this.setData({ fileList: this.data.fileList.concat(list) }); list.forEach(item this.uploadFile(item)); } }); }, uploadFile(item) { item.uploading true; this.setData({ fileList: this.data.fileList }); const task wx.uploadFile({ url: http://localhost:3000/upload, filePath: item.path, name: file, formData: { type: item.type }, success: (res) { const data JSON.parse(res.data); console.log(上传成功:, data.url); wx.showToast({ title: 上传成功, icon: success }); }, fail: (err) { console.error(上传失败:, err); wx.showToast({ title: 上传失败, icon: none }); } }); task.onProgressUpdate((res) { item.progress res.progress; this.setData({ fileList: this.data.fileList }); }); } });这套代码在微信开发者工具里直接就能跑只要保证后端服务起来了页面逻辑完全不用改。真机上域名校验的问题在第四部分讲。3. 后端存储Node.js接收文件并落盘3.1 选型理由与项目初始化后端我选了 Express Multer。Multer 是处理multipart/form-data最成熟的中间件几行代码就能接住前端发来的文件流。先初始化项目mkdir upload-demo-server cd upload-demo-server npm init -y npm install express multer然后创建app.js代码一次写入const express require(express); const multer require(multer); const path require(path); const fs require(fs); const app express(); const PORT 3000; const uploadDir path.join(__dirname, uploads); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir); } app.use(/uploads, express.static(uploadDir)); app.listen(PORT, () { console.log(服务已启动: http://localhost:${PORT}); });先跑通这个空壳再往里面加上传逻辑。这样排错时能定位到底是服务问题还是上传逻辑问题。3.2 上传接口与存储目录设计核心是multer.diskStorage。它把文件写入磁盘并允许你自定义文件的最终名字。直接看代码const ALLOWED_TYPES { image: [image/jpeg, image/png, image/gif, image/webp], video: [video/mp4, video/quicktime, video/webm] }; const storage multer.diskStorage({ destination: (req, file, cb) cb(null, uploadDir), filename: (req, file, cb) { const ext path.extname(file.originalname); const uniqueName ${Date.now()}_${Math.round(Math.random() * 1e9)}${ext || .tmp}; cb(null, uniqueName); } }); const upload multer({ storage, limits: { fileSize: 100 * 1024 * 1024 }, fileFilter: (req, file, cb) { const mimetype file.mimetype; const allowed Object.values(ALLOWED_TYPES).flat(); if (allowed.includes(mimetype)) { cb(null, true); } else { cb(new Error(文件类型不允许)); } } }); app.post(/upload, upload.single(file), (req, res) { if (!req.file) { return res.status(400).json({ code: 400, message: 没有接收到文件 }); } const fileUrl http://localhost:${PORT}/uploads/${req.file.filename}; res.json({ code: 200, message: 上传成功, url: fileUrl }); });filename里我用Date.now()_随机数拼文件名避免不同用户上传同名文件互相覆盖。看似简单却是很多新手会栽的地方——直接用file.originalname存盘两天后文件就被撞没了。一个小问题是前端选择的临时文件在部分手机上拿不到扩展名导致path.extname(file.originalname)返回空字符串。我兜底加了|| .tmp。其实更稳妥的做法是根据mimetype映射后缀名比如image/jpeg对应.jpg、video/mp4对应.mp4。你自己扩展时可以把这部分做成字典存盘格式会更规范。3.3 文件安全类型白名单、大小限制、扩展名处理上传接口只暴露给小程序端不代表你可以裸奔。我在multer配置里做了三道防线。第一道是大小限制limits.fileSize设置最大100MB。这个值要根据你的业务场景调比如只做头像上传可以收紧到2MB而做视频号这类场景可能需要放宽到200MB甚至更大。第二道是类型白名单在fileFilter里检查mimetype。这里要注意前端wx.chooseMedia选的类型不可信因为请求可以被伪造。所以后端必须独立校验一次这是文件上传安全的第一原则。第三道是express.static静态目录不要和用户上传目录弄混。我用app.use(/uploads, express.static(uploadDir))把上传目录映射成静态资源路径这样浏览器和video组件直接通过http://localhost:3000/uploads/xxx.mp4就能访问。如果你把上传目录叫作public那所有上传文件都会被直接当静态资源公开存在越权访问风险。实际生产项目里文件校验还会加一步读取文件二进制头部magic number验证真实类型而不是只看mimetype。因为mimetype可以由请求方随意伪造而 JPEG 文件头固定是FF D8 FFPNG 是89 50 4E 47MP4 也有自己的box头。用文件头判断能挡住不少恶意上传。3.4 从本地存储平滑切换到对象存储demo里文件落在服务器本地磁盘好处是简单直观坏处也很明显磁盘有容量上限、服务器重启文件不丢但不好做扩容、文件访问占服务器带宽。如果这个demo后面要上生产我建议直接换对象存储比如腾讯云COS或阿里云OSS。改动成本其实比你想象的低后端收到文件后不再用multer的diskStorage落盘而是用multer.memoryStorage把文件保持在内存再调云存储SDK的putObject接口传上去返回的URL就是对外访问地址。小程序端几乎不用改。对象存储的好处是容量无限、上传走内网不占公网带宽、自带CDN加速、还能配防盗链。唯一的代价是要多花点钱但对个人项目和毕设来说本地磁盘方案也完全够用。4. 联调和真机运行让demo动起来4.1 本地快速启动后端后端代码写完启动很简单node app.js看到控制台输出服务已启动: http://localhost:3000就说明服务起来了。这时候你就可以在浏览器里直接访问http://localhost:3000/uploads/测试静态资源目录如果能打开说明express的静态服务是通的。然后打开微信开发者工具导入小程序项目。有一点你必须在开发者工具右上角“详情 - 本地设置”里勾上“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”否则本地调试时http://localhost:3000根本发不出去。这是新手第一坑基本都会遇到。4.2 小程序开发者工具里的域名校验处理勾选“不校验合法域名”只是本地调试的做法。真机预览、线上发布时你必须在小程序后台配置合法域名否则请求会直接被微信拦截。先说开发阶段怎么操作。微信开发者工具里你的本地请求会带上localhost真机预览时手机访问不到localhost——那是手机自己的回环地址。你需要把后端接口地址改成电脑的局域网IP比如http://192.168.31.100:3000/upload并保证手机和电脑在同一个WiFi下。然后在小程序后台的“开发管理 - 开发设置 - 服务器域名”里把http://192.168.31.100:3000加到uploadFile合法域名里。注意微信要求uploadFile合法域名必须是HTTPS但开发阶段可以用IP和HTTP凑合正式发布前一定要配好HTTPS证书。如果你只是自己体验也可以不改后台配置直接在开发者工具菜单栏选“预览”后在手机上打开调试模式右上角胶囊按钮 - 打开调试这样手机端也会跳过域名校验。这个方法对临时调试特别管用我经常在给客户演示demo时用这招。4.3 真机调试时的几个区别真机上传和工具模拟器最大的区别在于手机上的临时文件路径更长、文件大小更真实、网络环境更复杂。你在开发者工具里传一个100KB的图片很顺滑到了真机上传一个50MB的视频可能就会遇到超时、进度卡住、甚至内存告警。我在真机调demo时遇到过一个问题iPhone上传视频之后后端拿到的originalname没有扩展名导致存盘文件名变成xxxx.tmp。排查后发现是iOS微信对chooseMedia返回的临时文件处理方式和安卓不一样安卓通常带mp4后缀iOS的临时路径直接没有扩展名。解决办法就是3.3节说的mimetype映射后缀或者后端根据文件二进制头识别类型补全扩展名。另外真机上如果上传失败开发者工具的“Network”面板看不到手机上的请求。建议用真机调试模式或者在后端接口加日志打印req.file和req.body看请求到底到没到后端。没有日志就没有真相排查上传问题第一件事是加日志。5. 常见问题与排查技巧5.1 上传失败提示“网络请求错误”或“系统错误”搜索热词里出现频率最高的就是这类报错。遇到这个问题先别慌按顺序排查三件事。第一看域名配置。本地调试未勾选“不校验合法域名”真机调试未打开调试开关或者正式环境域名没备案、没配置为HTTPS都会导致请求发不出去。把这三个环节检查一遍基本能消除一半问题。第二看后端是否真的收到了请求。在后端接口入口处console.log(req.headers)和console.log(req.body)如果日志完全没打印那就是请求根本没到后端问题出在网络链路如果日志打印了但req.file是空那问题在multipart字段名不一致。第三看文件大小是否超过限制。小程序端和服务端各有文件大小限制比如后端limits.fileSize设为100MB前端传了一个120MB视频Multer 会抛异常前端就会收到“系统错误”。这种报错信息不够准确但你加个错误中间件打印err.message就能立刻定位。我在demo里已经写了统一的错误处理中间件app.use((err, req, res, next) { if (err instanceof multer.MulterError) { return res.status(500).json({ code: 500, message: 上传出错${err.message} }); } res.status(500).json({ code: 500, message: err.message }); });这样前端fail回调里能拿到更清晰的错误信息而不是笼统的“系统错误”。5.2 视频传上去播放不了这个问题也特别常见上传成功了、URL也返回了但在video组件里就是黑屏或转圈。原因基本是编码格式不兼容。微信小程序video组件支持的视频编码格式比较有限最稳妥的是H.264 AAC编码的MP4文件。很多手机录的视频是HEVCH.265编码直接传给video组件安卓上大概率播放不了。iPhone上录的视频尤其容易出现这种情况。解决方案有几个方向。第一小程序端在chooseMedia时设置maxDuration和camera参数但无法控制输出编码。第二后端收到视频后做转码用FFmpeg把各种格式统一转成H.264 MP4。转码会增加服务端CPU开销但为了兼容性正规视频类小程序都会做这一步。第三直接用云点播服务上传后会自动转码生成多清晰度版本。我做毕设那个阶段没搞转码直接让用户录短视频发现安卓和iOS各有几台机器录出来的格式播不了。后来后端加了FFmpeg转码问题才彻底解决。5.3 iOS和安卓的兼容差异前面提到的originalname扩展名问题只是iOS和安卓差异的一个体现。另一个典型差异是大小写敏感。安卓文件系统区分大小写iOS不区分所以你在文件名里用大写.MP4和小写.mp4在不同平台上访问结果可能不一样。我统一在后端把扩展名转成小写能避开很多莫名问题。还有一个表现是chooseMedia返回的tempFiles顺序。iOS返回的顺序和用户选择的顺序一致安卓则不一定如果要保证“用户先选的放在前面”需要自己在success回调里再排一次序不能依赖返回值顺序。5.4 swiper组件嵌套video导致全屏错位这是更新一个问题搜索热词里都有专门的词条。在小程序里如果在swiper里直接放video手机上看第一页还好滑动到第二页再点全屏视频会错位、黑屏、甚至整个页面卡死。原因在于video是原生组件层级最高swiper的滚动逻辑和原生组件全屏逻辑冲突。社区里比较常用的解决思路有三个一是视频不全屏播放用自己写的全屏容器模拟全屏效果比如点击全屏按钮盖一个全屏的view上去二是每个swiper-item里只放视频封面图和播放按钮点击后跳转到独立的视频播放页不做内嵌播放三是升级到新版基础库把video组件替换成官方同层渲染后的版本但仍然不建议在 swiper 里直接嵌套。我个人经验是遇到 swiper video 组合直接改成“封面图 跳转播放页”方案最稳。用户看到的交互差距不大但在兼容性上省了无数心。5.5 大文件上传超时的优化思路100MB的视频用wx.uploadFile直传在普通4G网络下很容易超过60秒微信默认超时是60秒体验会很差。项目里如果确认要做大文件上传有两条路可以走。云存储直传小程序端调用云存储SDK的chooseMedia拿到临时文件后直接调wx.cloud.uploadFile上传到云开发存储不走自己的后端服务器。云开发自带CDN和鉴权小程序端直传不掉链子代价是后端无法直接拿到文件做业务处理。后端分片上传把大文件切成多个小块用wx.uploadFile逐个上传后端每块记录存储全部传完后合并。实现成本高一些但能绕开微信单次请求的超时限制。我见过有人用前端worker做分片和进度上报页面不卡体验很顺滑。如果你的应用主要是图片直接wx.uploadFile完全够用。如果有高清视频上传需求建议一开始就把云存储直传纳入方案别等上线后流量大了再改。我做这个demo时最大的感受是上传功能的技术门槛不高真正费时间的是各种兼容性和边界情况。多准备几台测试机、多在后端打印日志、多在设计阶段想清楚文件到底存哪比到时候手忙脚乱排查强得多。你在自己的项目里跑通这个demo后可以试着把图片和视频分开放到不同目录再根据业务字段动态生成存储路径基本上就能满足绝大多数实际场景了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价