ARTICLE DETAIL

资讯详情

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

Flet + FastAPI 文件上传组件封装指南:从选文件到落盘全解析

Flet + FastAPI 文件上传组件封装指南:从选文件到落盘全解析 简介一套基于Flet与FastAPI的前后端分离文件上传自定义组件模板面向需要快速实现文件上传功能的Python开发者。模板解决了传统上传方案中前端交互繁琐、后端处理易卡顿的问题适用于文档管理、媒体库或项目存储等场景。压缩包共5个文件包含3个Python脚本、1个txt说明文档和1个GIF演示动图整体仅108KB结构紧凑便于阅读。目前已有102人学习下载。资源提供了Flet前端多文件选择、上传进度实时显示以及FastAPI后端异步接收并自动保存文件的完整代码同时通过环境变量FLET_SECRET_KEY示例展示安全配置方式可作为自定义组件直接集成到项目中也可作为前后端交互的参考模板进行二次开发。1. Flet 框架做文件上传一个值得做成模板的组件是什么样的临时搭内部工具的时候文件上传这个功能我至少写过三遍。用 Flet 做前端、FastAPI 做后端听起来不复杂实际做起来会在 FilePicker 和文件传输这里卡一阵因为 Flet 并不是传统意义上的“前端框架”它把 Python 代码跑在服务端界面渲染在客户端文件到底是落在哪一边、以什么方式传直接影响你的实现思路。这篇文章要拆的就是一个能直接复用的上传组件模板Flet 端封装好选文件、展示文件信息、提交上传FastAPI 端封装好接收、落盘、返回结构化响应。适合正在用 Python 搭内部系统的开发者也适合想搞清楚 Flet 文件处理机制、想少踩一轮坑的人。2. 上传前的第一件事先弄清 Flet 的运行模型与两种上传姿势2.1 Flet 不是传统意义上的“前端”你的 Python 代码跑在服务端Flet 应用启动之后实际上起了一个本地服务浏览器或者桌面窗口只是连接这个服务的渲染端。你写的 Python 代码全部在服务端执行包括按钮回调、文件读取、逻辑判断而 FilePicker 弹出的是客户端操作系统的原生文件选择框选中文件后相关信息通过回调返回给服务端这边的 Python 代码。理解这层模型对文件上传最大的影响是文件实体到底在哪一侧。桌面模式下FilePicker 回调里的file.path指向客户端本地文件路径服务端代码可以open(path, rb)直接读但 Web 模式下浏览器出于安全原因不会交出真实路径file.path往往是空的此时只能依赖file.content这类内存对象。很多人在 Flet 里上传文件翻车根源都是没意识到“我以为我在前端拿到了文件其实回调发生在服务端”。下面是最小触发 FilePicker 的骨架先确认你的环境能弹出窗口import flet as ft def main(page: ft.Page): result_text ft.Text(未选择文件) def on_pick(e: ft.FilePickerResultEvent): if e.files: f e.files[0] result_text.value f{f.name} | {f.size} bytes | path{f.path} else: result_text.value 已取消 page.update() picker ft.FilePicker(on_resulton_pick) page.overlay.append(picker) # 必须挂到 overlay否则按钮点了没反应 page.add( ft.Column([ ft.ElevatedButton(选择文件, on_clicklambda _: picker.pick_files()), result_text, ]) ) ft.app(targetmain)代码里的关键点有两个on_result是选择动作完成后的唯一回调入口不管用户选了还是取消了都会触发page.overlay.append(picker)不是可选项漏掉这一步 FilePicker 无法弹出。我在第一次写的时候就是漏了这一行点了按钮毫无反应还以为是控件版本问题。2.2 两种上传姿势一体直写与前后端分离明确了运行模型再来看上传方案。Flet 场景下有两种常见做法方案实现方式服务数量Web 支持复杂度适用场景一体直写桌面端 FilePicker 拿到 pathPython 直接把文件写到服务器目录1 个 Flet 进程不支持Web 端拿不到 path低纯本机工具、临时脚本前后端分离Flet 只负责选文件和展示文件数据 POST 给 FastAPI 保存2 个进程支持中内部系统、多人使用、需要迁移后端我给的模板选了第二种理由有三个方面。第一Flet 的 Web 模式是它很重要的使用场景一体直写方案在 Web 端天然废掉而分离方案里不管 path 有没有前端都可以把文件内容读取出来、以multipart/form-data形式 post 给后端。第二后端单独跑一个 FastAPI 服务保存目录、文件名策略、存储位置都是后端说了算和前端彻底解耦以后想换成对象存储服务前端一行代码都不用动。第三多人同时使用内部工具时Flet 进程主要负责 UI 交互和回调调度文件落盘这种 IO 密集操作丢给独立后端不会把界面交互拖死。选择分离方案后通信协议也顺势定了前端用httpx发 HTTP 请求后端用 FastAPI 的UploadFile接收文件字段名统一叫file。接下来两章分别拆前端组件和后端接口最后再谈那些我实际翻过车的细节。3. 前端实现FilePicker 选文件与自定义组件封装3.1 选文件回调从弹出系统框到回填文件信息先把选文件这一环单独讲透因为它是一切上传的基础。FilePicker 使用时有几个高频参数需要记住参数作用我常用的值allow_multiple是否允许多选单传场景设False批量导入设Trueallowed_extensions过滤可选择的扩展名比如[xlsx, xls]不设则全部可选initial_directory初始定位目录一般是空让用户自己找file_type仅选择文件还是目录默认FilePickerFileType.FILE回调里拿到的e.files是一个列表列表元素带name、size、path三个基础字段。注意size是字节数展示给用户时最好转成 KB/MBpath在 Web 模式下可能是None代码里要做空值兜底不要直接拿去open()。def on_pick(e: ft.FilePickerResultEvent): if e.files is None or len(e.files) 0: file_info.value 未选择文件 status.value 用户取消了选择 page.update() return file e.files[0] file_info.value f文件名: {file.name} size_kb file.size / 1024 status.value f大小: {size_kb:.1f} KB | 路径: {file.path or 仅内存模式} page.update()这段代码里我做了两件容易被忽略的事一是对e.files做空判断用户点“取消”时e.files为空不做判断直接取[0]会抛异常二是展示路径时用了or 仅内存模式这样 Web 端运行也能看到友好提示而不是一个刺眼的None。选文件到这里截止真正上传在下一小节。3.2 把“选文件 上传”封装成可复用的 FileUploadComponent每次重新写一遍选文件逻辑没有意义下面这个组件是模板的核心直接继承ft.Column实例化时只需要传后端地址和成功回调。我把异步上传写进了组件内部避免 Flet 事件循环被同步网络请求阻塞。import httpx import flet as ft class FileUploadComponent(ft.Column): 可复用的文件上传组件。 upload_url: 后端接收接口FastAPI 路由为 /upload on_success: 上传成功后回调参数是后端返回的 JSON def __init__(self, upload_url: str, on_successNone): super().__init__(spacing10) self.upload_url upload_url self.on_success on_success self._selected None # 当前选中的文件对象 self.name_text ft.Text(未选择文件, size14) self.size_text ft.Text(, size12, colorft.colors.GREY_500) self.status_text ft.Text(, size12) self.progress_bar ft.ProgressBar(width320, visibleFalse) # FilePicker 必须挂到 page.overlay组件自己暴露 picker 给外层挂载 self.picker ft.FilePicker(on_resultself._on_pick_result) self.controls [ ft.Row([ ft.ElevatedButton( 选择文件, iconfolder_open, on_clicklambda _: self.picker.pick_files(allow_multipleFalse), ), ft.ElevatedButton( 上传, iconcloud_upload, on_clickself._upload, ), ]), self.name_text, self.size_text, self.status_text, self.progress_bar, ] def _on_pick_result(self, e: ft.FilePickerResultEvent): if e.files is None or len(e.files) 0: self.name_text.value 未选择文件 self.size_text.value self.status_text.value self.update() return self._selected e.files[0] self.name_text.value f文件名: {self._selected.name} self.size_text.value f大小: {self._selected.size / 1024:.1f} KB self.status_text.value self.update() async def _upload(self, e): if self._selected is None: self.status_text.value 请先选择一个文件 self.update() return self.progress_bar.visible True self.progress_bar.value None # None 表示不定进度动画 self.status_text.value 上传中... self.update() try: # Web 端 path 为空优先走 content桌面端走 path 读文件 if self._selected.path: with open(self._selected.path, rb) as f: data f.read() else: data self._selected.content async with httpx.AsyncClient(timeout60) as client: resp await client.post( self.upload_url, files{file: (self._selected.name, data)}, ) resp.raise_for_status() self.status_text.value 上传成功 if self.on_success: self.on_success(resp.json()) except Exception as exc: self.status_text.value f上传失败: {exc} finally: self.progress_bar.visible False self.update()组件里的几个设计点值得说明。第一_upload是async def内部全部用httpx.AsyncClient这是避免界面卡死的关键。第二读取文件时分了桌面和 Web 两条路桌面用open(path)读Web 用self._selected.content在模板里统一封好使用者不需要关心自己跑在哪种模式。第三progress_bar.value None表示展示不确定进度动画上传完成后再隐藏虽然这里还没有真实进度值但至少视觉上给了反馈。使用这个组件只需要三步实例化、挂载 picker 到 overlay、把组件加到页面。def main(page: ft.Page): page.title Flet 上传组件示例 def on_success(resp): print(后端返回:, resp) uploader FileUploadComponent( upload_urlhttp://127.0.0.1:8000/upload, on_successon_success, ) page.overlay.append(uploader.picker) # 别漏 page.add(uploader) ft.app(targetmain)这段代码开头用了两步去解释组件用法实际项目里你也可以把page.overlay.append(uploader.picker)封装成uploader.mount(page)方法模板简化处理没有加。重点是overlay.append那行必须执行否则选择按钮点了没响应这一点后面避坑章节还会着重讲。4. 后端实现用 FastAPI 接收文件并落盘4.1 一个最小可用的接收保存接口后端不复杂但有几个细节直接影响模板好不好用。最小实现如下UploadFile是 FastAPI 对multipart/form-data中文件字段的封装它本身是异步接口read()可以直接await。import os import uuid from fastapi import FastAPI, UploadFile, File from fastapi.middleware.cors import CORSMiddleware app FastAPI() # Flet Web 前端和后端端口不同需要放开跨域 app.add_middleware( CORSMiddleware, allow_origins[*], # 内网工具可以放开生产环境请收窄到具体域名 allow_methods[*], allow_headers[*], ) UPLOAD_DIR uploads os.makedirs(UPLOAD_DIR, exist_okTrue) app.post(/upload) async def upload_file(file: UploadFile File(...)): 接收 file 字段的文件流式写入 uploads 目录。 raw_name file.filename or unnamed # 用 uuid 重命名落盘文件名原始名只在响应里返回 ext os.path.splitext(raw_name)[1] saved_name f{uuid.uuid4().hex}{ext} save_path os.path.join(UPLOAD_DIR, saved_name) total 0 with open(save_path, wb) as buffer: # 每次读 1MB避免大文件一次性读进内存 while chunk : await file.read(1024 * 1024): buffer.write(chunk) total len(chunk) return { code: 0, msg: ok, data: { saved_name: saved_name, original_name: raw_name, size: total, }, }接口的入参定义是file: UploadFile File(...)File(...)是必填标记前端如果不带file字段会直接返回 422。落盘时我没有直接用raw_name而是用uuid.uuid4().hex生成新文件名只保留原始扩展名。这样做有两点考虑一是中文文件名在浏览器 HTTP 头和操作系统文件系统之间的编码差异容易出乱码二是用户可控的文件名里可能夹带..这类特殊字符即使 FastAPI 层不会直接产生路径穿越落盘文件名可信度也值得警惕。原始文件名放在响应体original_name里返回给前端后续要记录或展示再用它查。4.2 联调时的请求格式与依赖清单前后端联调时最容易错的是请求格式。前端模板里已经用files{file: (filename, data)}处理好了对应到后端就是字段名file、文件内容、可选文件名三要素。如果你要换工具测试curl 等价命令是这样curl -X POST http://127.0.0.1:8000/upload \ -F file/path/to/local/file.xlsx-F表示构造multipart/form-datafile是字段名后面跟本地文件路径。后面跟filenamexxx可以覆盖上传文件名。我用这套组合自测过响应里的data.size应该和本地文件字节数一致这是确认上传链路完整的最快方式。依赖清单方面整个模板只需要四个库我建议拆成 requirements.txt 方便复现依赖版本要求用途flet0.20前端界面与组件基础httpx任意近期版本前端异步上传请求fastapi任意近期版本后端接收接口uvicorn任意近期版本后端启动服务启动后端用uvicorn main_upload:app --host 0.0.0.0 --port 8000前端用flet run main_front.py或直接python main_front.py跑起来。两个进程都在本地时前端代码里upload_url写http://127.0.0.1:8000/upload即可。注意如果后端写0.0.0.0监听同一局域网内其他机器的 Flet Web 端也可以访问这对团队内部工具很实用。5. 避坑笔记Flet 上传最容易翻车的五个场景5.1 点了“选择文件”按钮系统选择框没弹出来现象按钮点击后没有任何反应不报错也不弹窗。原因FilePicker 没有挂到page.overlay。Flet 的 FilePicker 不是普通控件直接page.add(picker)不会建立有效的挂载关系选择框自然无法触发。解决在初始化后立刻执行page.overlay.append(picker)。如果封装在自定义组件里组件必须暴露picker属性或提供mount(page)方法由使用方挂载。我后来写的所有组件都默认带一个mount方法内部完成page.overlay.append(self.picker)少让使用方记这件事。5.2 Web 模式下file.path为空直接 open 报错现象桌面模式测试一切正常换到 Web 模式后上传必失败报错 FileNotFoundError。原因浏览器安全机制不向页面脚本暴露本地绝对路径Flet Web 模式下 FilePicker 回调里的path是空。解决模板里已经做了双分支path存在走文件读取否则用self._selected.content。如果你不想兼容 Web 模式可以在选文件回调里对path为空的情况做拦截并提示“请使用桌面客户端”。做内部工具时我一般直接要求用桌面版开发省心。5.3 上传后文件名乱码或保存后打不开现象后端保存的文件名变成了类似测试.xlsx的乱码虽然文件能打开但名字不可读。原因HTTP 传输阶段中文文件名在不同编码间转换加上部分操作系统文件系统默认编码不是 UTF-8就会落盘成乱码。解决后端不要直接拿file.filename落盘。模板采用 uuid 重命名原始名放在响应体original_name字段。如果确实需要保留可读文件名至少要先用urllib.parse.unquote解一次码再确认目标文件系统兼容然后决定是否写库而不写盘名。5.4 上传大文件时界面卡死按钮变灰不响应现象上传几十 MB 文件时Flet 窗口转圈整个界面点不动。原因回调里用了同步httpx.post或者同步read()阻塞了 Flet 事件循环。Flet 的回调本身是事件驱动的同步阻塞会让后续所有 UI 更新排队。解决上传逻辑全部改成async defawait用httpx.AsyncClient。模板里的_upload已经是异步版。另外文件读取也不要贪快一次性read()全部字节后端已按 1MB 分块读前端如果读本地大文件一次性进内存也容易吃紧可以按需分批。5.5 浏览器控制台报 CORS 错误上传请求被拦截现象Flet Web 页面访问http://127.0.0.1:8000/uploadNetwork 面板显示请求发出去了但被拦截控制台报Access-Control-Allow-Origin。原因Flet 渲染后的页面默认跑在随机分配的本地端口上和后端 8000 端口不同源浏览器跨域策略拦截了响应。解决后端加CORSMiddleware。开发阶段allow_origins[*]最省事但要清楚这是全部放开部署到生产环境把allow_origins收窄成 Flet 实际使用的域名和端口列表不要长期挂通配符。6. 进阶改造给组件加真实进度和类型白名单模板目前的上传进度只是一个“上传中”的动画没有真实百分比。FastAPI 默认要等整个文件接收完才返回响应所以想拿到实时进度思路要从“一次性上传”改成“分块上传”前端把文件切成固定大小的小块逐块 POST 给后端每成功一块就更新进度条。后端对应改成分段接收接口按块号写入同一个临时文件最后合并。切片可以用一个简单的生成器def split_bytes(data: bytes, chunk_size: int 1024 * 1024): for i in range(0, len(data), chunk_size): yield data[i:i chunk_size]上传循环里维护一个已发送计数progress_bar.value直接设为idx / total这样就是真实进度而不是动画chunks list(split_bytes(data)) for idx, chunk in enumerate(chunks, start1): async with httpx.AsyncClient(timeout30) as client: resp await client.post( self.upload_url, files{file: (f{self._selected.name}.{idx}, chunk)}, ) self.progress_bar.value idx / len(chunks) self.update()配套的后端需要把每个分块追加到同一个目标文件并在请求里带上总块数收完最后一块后做完整性校验。这是通用做法局域网内几百 MB 的包也能看到平滑进度。类型白名单更简单前端在pick_files里加allowed_extensions后端再做一层兜底校验。后端校验可以直接看splitext(file.filename)[1].lower()是否在集合内不在就返回 400。ALLOWED_EXTS {.xlsx, .xls, .csv} if os.path.splitext(raw_name)[1].lower() not in ALLOWED_EXTS: return {code: 1, msg: 文件类型不允许}, 400前后端双重校验的目的不是防攻击是给使用者更快的反馈路径——选文件阶段就过滤掉大部分误选后端兜底防止绕过前端直连接口的情况。说起这轮改造我印象最深的就是那个 update 的坑。最早写 Flet 组件时我在回调里改了一堆控件的value界面纹丝不动一度怀疑是组件继承写错了。后来才反应过来Flet 里你对控件属性的修改不会自动推送到界面必须手动调update()。从那以后我写每个自定义组件不管回调还是异步方法固定先完成数据修改再在方法末尾统一调一次self.update()宁可多刷一次也不赌它自动刷新。这个小习惯让我后续的列表刷新、状态切换再也没有出现过“代码执行了但界面没变”的玄学问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表