微信小程序上传图片视频到Flask后台:可运行Demo与避坑指南
简介:这份资源面向微信小程序开发者与Flask后端初学者,提供图片、视频上传至服务器存储的完整前后端示例,可直接运行调试,适合作为课程作业、功能原型或项目练手素材。压缩包共12个文件,约5KB,包含5个json配置、3个js逻辑脚本、2个wxss样式、1个wxml页面结构及1个py后端文件,前端小程序与后端上传接口一一对应,目录按前后端分置,便于快速定位与二次修改。已有421人学习下载,说明该示例在同类需求中具备一定参考价值。读者可从中获得小程序端选择媒体、发起上传请求的完整流程,以及Flask端接收文件并落盘存储的实现思路,配合配套博文教程可进一步理解参数传递与接口联调细节,遇到问题也可与作者交流讨论,适合希望打通小程序与后端文件上传链路的开发者参考。
1. 微信小程序上传图片视频到后台存储:一份能直接跑通的 Flask 落地 Demo
小程序端选完图、点上传,后台却只收到一个空文件或者 413,这种场景我见过太多次。这份 Demo 干的事很具体:把微信小程序里wx.chooseMedia选出来的图片和视频,通过wx.uploadFile传到 Flask 后台,落盘存储并返回可访问路径。它解决的是「前端选得了、后端收得住、文件找得回」这条最短链路,适合刚接手小程序文件上传需求的开发者、要做课程设计的学生,以及想给现有 Flask 项目补上传能力的后端同学。整套东西不依赖对象存储,本地磁盘就能跑,先把链路打通再谈扩展,这是我的一贯顺序。
2. 拆开这份 Demo:Flask 接收端与小程序上传端的职责边界
拿到一个上传 Demo,先别急着跑,得看清两端各自管什么。很多翻车不是代码错,是职责没分清——前端以为后端会压缩,后端以为前端已经限制了大小,结果两边都没做,一个 200MB 的视频直接把服务打挂。这一章把 Demo 的结构拆开讲,让你知道每个文件该放哪、每段逻辑为什么这么写。
2.1 项目结构与依赖清单
这份 Demo 的目录结构不复杂,但每个位置都有讲究。我一般会按下面这样组织,前后端分开,上传目录独立出来,方便后面挂载和清理:
upload-demo/ ├── server/ │ ├── app.py # Flask 主入口 │ ├── config.py # 上传大小、允许类型等配置 │ ├── uploads/ # 落盘目录(运行时生成) │ └── requirements.txt └── miniprogram/ ├── pages/upload/ │ ├── upload.js # 选图 + 上传逻辑 │ ├── upload.wxml │ └── upload.wxss └── app.json依赖就三个,别装一堆用不上的:
# server/requirements.txt Flask==3.0.3 Flask-Cors==4.0.1 Werkzeug==3.0.3Flask是主框架,Flask-Cors处理小程序开发阶段的跨域预检,Werkzeug里的secure_filename用来清洗文件名。这里有个血泪经验:secure_filename会把中文文件名几乎全部剥掉,如果你的业务需要保留原始中文名,得自己写一套清洗逻辑,不能直接用它返回的结果当最终文件名,否则用户上传「产品图.jpg」最后变成「.jpg」,排查起来很费劲。
2.2 Flask 接收端:从 request.files 到安全落盘
后端接收的核心是request.files,但真正决定稳不稳的是落盘前的校验。下面这段是我在这个 Demo 里用的接收逻辑,能直接抄:
# server/app.py import os import uuid from flask import Flask, request, jsonify from flask_cors import CORS from werkzeug.utils import secure_filename app = Flask(__name__) CORS(app) # 配置区:这几个值建议放 config.py,别硬编码在路由里 UPLOAD_DIR = os.path.join(os.path.dirname(__file__), "uploads") ALLOWED_EXT = {"jpg", "jpeg", "png", "gif", "mp4", "mov"} MAX_SIZE = 50 * 1024 * 1024 # 单文件 50MB 上限 os.makedirs(UPLOAD_DIR, exist_ok=True) app.config["MAX_CONTENT_LENGTH"] = MAX_SIZE def allowed_file(filename: str) -> bool: # 只认扩展名,且必须带点,防止无扩展名文件混入 return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXT @app.route("/api/upload", methods=["POST"]) def upload(): # 1. 字段名必须和小程序端 wx.uploadFile 的 name 一致 if "file" not in request.files: return jsonify(code=400, msg="缺少 file 字段"), 400 f = request.files["file"] if f.filename == "": return jsonify(code=400, msg="文件名为空"), 400 if not allowed_file(f.filename): return jsonify(code=400, msg="不支持的文件类型"), 400 # 2. 用 uuid 重命名,避免同名覆盖和路径穿越 ext = f.filename.rsplit(".", 1)[1].lower() safe_name = f"{uuid.uuid4().hex}.{ext}" save_path = os.path.join(UPLOAD_DIR, safe_name) # 3. 分块写入,大文件不至于一次性吃满内存 with open(save_path, "wb") as out: while chunk := f.stream.read(1024 * 1024): out.write(chunk) # 4. 返回相对路径,前端拼域名即可访问 return jsonify(code=0, msg="ok", data={"path": f"/uploads/{safe_name}"}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)逐段说下为什么这么写。第一,字段名file是硬约定,小程序端wx.uploadFile的name参数写什么,这里就必须是什么,两边对不上就是「缺少 file 字段」,这是最高频的报错。第二,用uuid4().hex重命名而不是保留原名,一是防同名覆盖,二是防../这类路径穿越,secure_filename虽然也能防,但会吃掉中文,所以我干脆自己生成。第三,f.stream.read(1024 * 1024)分块写,1MB 一块,50MB 的文件也就循环 50 次,内存占用可控;如果直接f.save(),小文件没问题,大视频在并发下容易把内存顶上去。第四,MAX_CONTENT_LENGTH是 Flask 层面的硬闸,超过直接返回 413,比在业务代码里判断更早生效。
参数上,MAX_SIZE设 50MB 是个折中值,微信小程序wx.uploadFile单文件本身有上限,视频稍微大一点就传不动,实际项目里视频建议走分片或者直传对象存储,这个 Demo 先覆盖图片和短视频场景。ALLOWED_EXT里我加了mov,因为 iOS 拍出来的视频常见这个格式,不加的话苹果用户一传就报「不支持的类型」。
2.3 小程序端:wx.chooseMedia 与 wx.uploadFile 的配合
前端这块,选媒体和上传是两个 API,中间要处理好临时路径。核心代码如下:
// miniprogram/pages/upload/upload.js Page({ data: { filePath: "", mediaType: "image", uploading: false, }, // 选图或选视频,mediaType 决定选哪种 chooseMedia() { const that = this; wx.chooseMedia({ count: 1, mediaType: [this.data.mediaType], // image / video / mix sourceType: ["album", "camera"], maxDuration: 30, // 视频最长 30 秒,和后台大小限制呼应 success(res) { const file = res.tempFiles[0]; that.setData({ filePath: file.tempFilePath, mediaType: file.fileType === "video" ? "video" : "image", }); }, fail(err) { console.error("选择失败", err); }, }); }, // 上传到 Flask 后台 doUpload() { if (!this.data.filePath) { wx.showToast({ title: "请先选择文件", icon: "none" }); return; } if (this.data.uploading) return; // 防重复点击 this.setData({ uploading: true }); const that = this; wx.uploadFile({ url: "http://127.0.0.1:5000/api/upload", // 真机调试换成局域网 IP filePath: this.data.filePath, name: "file", // 必须和后台 request.files 的键一致 formData: { source: "miniprogram" }, // 附加字段,后台可选用 timeout: 60000, success(res) { // 注意:res.data 是字符串,要手动 JSON.parse const body = JSON.parse(res.data); if (body.code === 0) { wx.showToast({ title: "上传成功" }); console.log("访问路径", body.data.path); } else { wx.showToast({ title: body.msg || "上传失败", icon: "none" }); } }, fail(err) { console.error("上传失败", err); wx.showToast({ title: "网络异常", icon: "none" }); }, complete() { that.setData({ uploading: false }); }, }); }, });几个关键点。wx.chooseMedia的mediaType传数组,["image"]只选图,["video"]只选视频,["mix"]混选,Demo 里用data.mediaType控制,方便切换。maxDuration: 30是视频时长上限,单位秒,和后台 50MB 的大小限制配合,避免用户选了个 5 分钟的视频传半天失败。wx.uploadFile的name必须和后台request.files["file"]对上,这是硬约束。还有一个新手常踩的点:success回调里的res.data是字符串不是对象,直接res.data.path会拿到undefined,必须JSON.parse一次,这个坑我在早期项目里栽过,排查了半天以为是后台没返回。
url里的127.0.0.1只在开发者工具里有效,真机调试必须换成电脑的局域网 IP,比如http://192.168.x.x:5000/api/upload,而且手机和电脑要在同一个网络下。这一步不做,真机上永远是「网络异常」。
3. 跑通全链路:从本地启动到真机验证的完整步骤
代码看懂了不代表能跑起来,上传这条链路涉及端口、网络、权限、格式四道关,任何一道没过都是失败。这一章按启动顺序走一遍,每步给出验证方法,让你知道卡在哪一环。
3.1 启动 Flask 并验证接口可用
先把后端跑起来,别急着开小程序。启动命令:
cd server pip install -r requirements.txt python app.py看到Running on http://0.0.0.0:5000就说明起来了。但「起来了」不等于「能用」,我习惯先用 curl 打一发,确认接口本身没问题:
# 用一个本地图片测试,-F 的 file 对应后台字段名 curl -X POST http://127.0.0.1:5000/api/upload \ -F "file=@./test.jpg" \ -v正常返回应该是{"code":0,"msg":"ok","data":{"path":"/uploads/xxxx.jpg"}}。如果返回 413,说明文件超了MAX_CONTENT_LENGTH;返回 400 且提示「不支持的文件类型」,检查扩展名是否在ALLOWED_EXT里;返回 400 提示「缺少 file 字段」,说明-F的键名写错了。这一步用 curl 先过,能把「后端自身的问题」和「前后端联调的问题」隔离开,省得两头猜。
还要注意uploads目录的写权限。Linux 下如果 Flask 以低权限用户跑,目录属主不对,open(save_path, "wb")会直接抛PermissionError,日志里能看到,但小程序端只会显示「上传失败」,容易误判成网络问题。启动前ls -ld uploads看一眼属主,不对就chown一下。
3.2 开发者工具里的域名与跨域配置
微信开发者工具默认会校验合法域名,本地127.0.0.1不在白名单里,直接请求会被拦。开发阶段的做法是:在开发者工具右上角「详情 → 本地设置」里勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」。这一步勾上,本地 HTTP 请求才能发出去。
跨域这块,Flask 端已经用CORS(app)全局放开了,开发阶段够用。但要注意,CORS(app)是允许所有来源,上线前必须收紧到具体域名,否则等于把接口裸奔在公网。我一般会在config.py里留一个ALLOWED_ORIGINS列表,开发时是["*"],上线前改成小程序后台配置的合法域名。
还有一个容易忽略的点:wx.uploadFile走的是multipart/form-data,不是普通POST,所以后台不能用request.get_json()去取参数,附加字段要用request.form.get("source")。这个区别在联调时经常让人困惑——明明前端formData传了值,后台get_json()却是None。
3.3 真机调试:局域网 IP、HTTPS 与大小限制
开发者工具跑通后,真机是另一道坎。三个必须处理的点:
第一,url换成局域网 IP。电脑上ipconfig(Windows)或ifconfig(macOS/Linux)查到本机 IP,比如192.168.1.100,小程序里改成http://192.168.1.100:5000/api/upload。手机连同一个 WiFi,浏览器先访问http://192.168.1.100:5000看能不能通,通了再传。
第二,真机默认要求 HTTPS。开发阶段可以在小程序后台「开发管理 → 开发设置 → 服务器域名」里配置,但 HTTP 的 IP 加不进去。常规做法是真机调试时打开「调试模式」,或者在开发者工具里用「真机调试」功能,它会临时放宽限制。正式环境必须上 HTTPS,这是硬要求,Demo 阶段先用调试模式绕过。
第三,大小限制要对齐。微信小程序wx.uploadFile单文件有上限,视频稍大就传不动,后台的 50MB 和前端maxDuration: 30要一起调。我见过前端不限时长、后台限 50MB 的配置,用户选个 2 分钟视频,传到 80% 失败,体验极差。两边阈值必须一致,或者前端选完就先判断tempFiles[0].size,超了直接提示,别等传一半才报错。
验证顺序建议:curl 通 → 开发者工具通 → 真机同 WiFi 通 → 真机调试模式通。每步都过了,这条链路才算真正跑通。
4. 避坑与排查:上传链路上最容易翻车的五个点
上传这活儿,代码量不大,坑却不少,而且大多表现为「上传失败」四个字,信息量极低。这一章按「现象 → 原因 → 解决」把高频问题列清楚,照着对,能省下大量瞎猜的时间。
4.1 现象:后台收到空文件或文件名丢失
原因通常是前端name和后台字段名不一致,或者用了secure_filename处理中文名。前者表现为request.files里根本没有对应键,后者表现为落盘文件名变成.jpg这种残缺形态。解决:先确认wx.uploadFile的name和request.files["file"]完全一致;中文名场景不要直接用secure_filename的返回值,改成「uuid + 原扩展名」,原始名单独存数据库字段。
4.2 现象:413 Request Entity Too Large
原因就是文件超了MAX_CONTENT_LENGTH。但要注意,这个限制是 Flask 层面的,Nginx 反代时还有一层client_max_body_size,两层都要调。解决:Flask 里设app.config["MAX_CONTENT_LENGTH"],Nginx 里设client_max_body_size 50m;,两边数值对齐。只调一边,另一边照样拦。
4.3 现象:真机上一直「网络异常」
原因多半是url还是127.0.0.1,或者手机和电脑不在同一网段。解决:换成电脑局域网 IP,手机浏览器先访问确认连通;再检查电脑防火墙是否放行了 5000 端口,Windows 防火墙默认会拦入站连接,这个特别隐蔽。
4.4 现象:iOS 上传视频报「不支持的类型」
原因是 iOS 拍的视频常见.mov格式,ALLOWED_EXT里没加。解决:把mov加进白名单,或者前端选完后判断fileType,非白名单格式直接提示用户。别指望用户自己转格式,体验上过不去。
4.5 现象:并发上传时部分请求失败或文件损坏
原因是多个请求写同名文件,或者磁盘 IO 打满。用 uuid 重命名能解决同名覆盖,但磁盘 IO 这块要靠限流。解决:前端加uploading状态防重复点击(Demo 里已有),后端如果并发高,考虑加队列或者直接上对象存储,本地磁盘不适合高并发写入场景。
5. 从 Demo 到可用:三个让上传链路更稳的改造技巧
Demo 跑通只是起点,真要用起来,还得补几块。这一章说三个我每次都会加的改造,都是踩过坑之后形成的习惯。
第一个是文件类型二次校验。光看扩展名不够,用户把.exe改成.jpg照样能过。稳妥做法是读文件头几个字节判断真实类型,图片看 magic number(JPEG 是FF D8 FF,PNG 是89 50 4E 47),视频看ftyp标识。下面这段是图片校验的简化版:
def sniff_image_type(path: str) -> str | None: # 读前 8 字节判断真实图片类型,防止改扩展名绕过 with open(path, "rb") as f: head = f.read(8) if head[:3] == b"\xff\xd8\xff": return "jpeg" if head[:8] == b"\x89PNG\r\n\x1a\n": return "png" if head[:6] in (b"GIF87a", b"GIF89a"): return "gif" return None落盘后调一次,返回None就删文件并报错。这一步能挡掉大部分伪装文件,成本很低。
第二个是上传进度反馈。wx.uploadFile返回的是UploadTask对象,可以监听onProgressUpdate,把百分比推到页面,用户不至于盯着转圈干等。视频文件尤其需要,30 秒的视频在弱网下能传一分多钟,没进度条用户早退了。
第三个是清理策略。uploads目录只增不减,跑几个月就撑爆磁盘。我一般加个定时任务,按修改时间清理超过 N 天的文件,或者落库时记录路径和创建时间,定期对账删除孤儿文件。这个不做,迟早有一天磁盘满了,上传全线挂掉,那时候再补就晚了。
| 改造项 | 解决什么问题 | 实现成本 |
|---|---|---|
| 文件头校验 | 改扩展名绕过类型限制 | 低,一个函数 |
| 进度反馈 | 大文件上传无感知 | 低,监听回调 |
| 定时清理 | 磁盘只增不减 | 中,需定时任务 |
这三个改造里,文件头校验是我最坚持的一个。早些年有个项目没做,用户传了个改名的脚本上来,虽然没造成实际影响,但事后复盘吓出一身冷汗。从那以后我每次做上传功能,落盘后的类型校验都强制走一遍,不管前端有没有限制——前端的限制永远只是体验优化,真正的安全边界必须在后端。希望这套东西能帮到你,少走几个我走过的弯路。
本文还有配套的精品资源,点击获取