我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Flask+Vue河南庙会数字化展示与定制项目实战解析

Flask+Vue河南庙会数字化展示与定制项目实战解析 做河南庙会数字化这个项目前后折腾了快一个月踩了不少坑也攒了不少能直接套用的经验。今天就把整个“基于Flask的河南庙会文化艺术展示与定制”项目拆开聊一聊从技术选型到Vue前端互动再到PyCharm环境配置和实际部署把关键环节都过一遍。这套东西适合两类人一是想拿非遗文化、地方活动做成线上展示产品的朋友二是刚好在学Flask、Vue、Django这几个技术栈想找一个综合项目练手的人。我最早想得很简单做一个网页把庙会的时间、地点、节目单放上去就行。真做起来才发现庙会文化的核心不只是“哪天有会”而是那些传承了很多年的民间艺术浚县泥咕咕、淮阳泥泥狗、朱仙镇木版年画、豫剧清唱、高跷、旱船、打铁花……这些内容形态极多有图片、有视频、有传承人故事如果只是堆在页面上用户根本看不下去。所以后来把项目定位改成“展示 定制”一方面把庙会和非遗项目做成结构化的浏览体验另一方面让用户能挑选喜欢的文化元素生成一张带自己署名的纪念海报或文创卡片。这个“定制”功能反而成了整个项目里最有意思的部分。下面我就按实际开发的顺序把项目整个拆开讲。1. 为什么是Flask Vue项目整体设计与技术选型心路1.1 先聊选型Flask比Django更适合这个项目的原因项目标题里有“基于flask”但也有不少人第一反应是为什么不用Django毕竟Django在做内容管理、后台数据录入方面确实方便热搜词里也有一堆和Django相关的内容比如“django项目实战新手”“django创建app”“django之MTV模式有什么作用”。这问题我在项目初期也认真纠结过。Django的优势是“全家桶”自带ORM、Admin后台、认证系统、表单处理MTV模式更是把“数据模型-模板渲染-视图控制”理得很顺。但如果你的团队不大、项目边界清晰Django反而会有一种“被框架拖着走”的感觉。尤其是我这个项目的核心是“文化展示接口 定制服务”后台管理只是辅助不会出现几十个复杂权限角色。用Flask这种微框架我可以把所有代码都捏在自己手里每个视图函数都看得懂遇到需要改的地方直接在app.py或blueprint里改而不需要翻Django的settings和各种中间件。另外Flask和SQLAlchemy的组合足够覆盖这个项目的持久化需求。对于“庙会活动表”“非遗项目表”“定制订单表”这种关系相对简单、查询也不复杂的场景Flask-SQLAlchemy的表现完全不输Django ORM。所以我的结论是项目体量不大、追求灵活可控选Flask项目一上来就有复杂的权限体系、成熟的内容工作流才更需要Django这种重型框架。这个项目显然属于前者。1.2 四条业务线展示、搜索、定制、管理怎么划分在动手写代码前我先把项目功能拆成了四条业务线这也是Flask蓝图Blueprint的划分依据展示线庙会日历、非遗项目库、图片库、视频库核心是让用户“逛得明白”。用户进入首页能看到近期庙会活动列表点进详情页能看到庙会对应的非遗艺术、传承人故事、现场图片和视频。搜索线支持按庙会名称、地区、非遗类型三个维度筛选。比如用户搜索“浚县”就能把浚县正月庙会、泥咕咕项目全部带出来。定制线这是项目的亮点。用户在看某个非遗项目时如果喜欢对应的纹样或图片可以选择底图、填写祝福文案、选择卡片风格系统在服务端生成一张定制海报提供高清PNG下载。管理线后台只保留最基础的数据维护能力新增庙会、新增非遗项目、删除下架内容。我直接用Flask-Admin实现五分钟搭一个后台后面在“常见问题”里会说清楚为什么不用自写后台。四条线互不干扰恰好对应四个Flask蓝图catalog、search、customize、admin。开发时每一条线都能独立调试不用在几百行的路由文件里找代码。2. 庙会文化数据的结构设计非遗内容如何变成可管理的数据2.1 数据模型拆分从“庙会”到“文化要素”的三层结构文化类项目最容易犯的错就是把所有信息塞进一个大表格。比如一个“庙会活动表”里既有庙会名称、时间、地点又把表演项目、非遗图片全用JSON字段塞进去。短时间看是省事后面做筛选、定制、推荐的时候查数据会非常痛苦。我最终拆成了三个核心模型Festival庙会活动字段包括id、名称、地区、开始日期、结束日期、简介、主图地址。一个庙会可以关联多个非遗项目新的庙会信息由后台维护人员录入。HeritageItem非遗项目字段包括id、名称、类型、所属地区、传承人、介绍、图片地址、视频地址。这里用festival_id做外键表示这个项目属于哪一场庙会同时允许一个项目出现在多个庙会中用关联表维护多对多关系。CustomOrder定制订单字段包括id、底图地址、文字内容、风格参数、生成结果地址、创建时间。这个表的用途是记录每一次定制行为方便后面统计哪个纹样最受欢迎。选这段结构的关键原因是用户浏览路径是“庙会 → 非遗项目 → 定制文创”数据模型必须跟着这个路径走才能保证前端请求一次就能把详情页需要的所有数据拼出来。如果把图片、视频、项目介绍全堆在同一张表里后面做定制功能的材料匹配时就得反复拆JSON非常难维护。2.2 中文标签与分类体系如何避免“类型”字段失控河南庙会里的非遗内容五花八门如果分类做得太粗比如只分“表演类”“手工艺类”那泥咕咕和朱仙镇年画会被堆在一起用户搜索时无法精准命中如果做得太细比如按每个村的手艺分后台维护成本又太高。我的做法是“两级分类”一级分类category固定几个大项表演艺术、传统技艺、民俗活动、传统美食。二级分类tags用逗号分隔的标签比如“泥塑、彩绘”“戏曲、豫剧”“社火、高跷”。一级分类用字典表约束二级分类允许自定义。这个方案兼顾了结构化查询和灵活性。写SQLAlchemy查询时按一级分类过滤非常快按标签搜索时直接用LIKE %泥咕咕%或者加了简单的倒排缓存也能满足小规模访问量。如果你一上来就上Elasticsearch反而有点杀鸡用牛刀。2.3 图片/视频文件规划m3u8播放背后的存储约定庙会现场的视频大多是长视频如果直接放MP4用户加载起来很慢。这里就要提到热搜词里反复出现的“vue播放m3u8、vue播放m3u8免安装”。m3u8是HLS流媒体协议里的索引文件视频被切成很多个.ts小片段播放器通过索引文件按需加载它的好处是加载快、支持拖动、对服务器带宽压力小。我在项目里对文件目录做了统一约定media/ festivals/ {festival_id}/ cover.jpg images/ videos/ index.m3u8 ts/视频上传后用FFmpeg把MP4转成HLS切片ffmpeg -i input.mp4 -codec copy -hls_time 6 -hls_list_size 0 -hls_segment_filename ts/output_%03d.ts output.m3u8这条命令的意思是不重新编码-codec copy以节省CPU每个切片6秒生成output.m3u8索引文件和按顺序编号的ts切片。-hls_list_size 0表示保留所有切片不生成滚动播放列表。文件存储做好约定之后Flask和Vue只需要按路径规则访问就行。前端播放m3u8的细节我在后面专门讲。3. 从零搭起项目PyCharm、Flask后端与Vue前端的完整实操3.1 PyCharm里创建Flask工程并启动的详细步骤如果你在热搜里搜过“pycharm安装教程”“pycharm安装flask”说明你大概率卡在了环境配置这一步。这里我说一套最省心的流程用PyCharm专业版或社区版都能走通。第一步打开PyCharm选择New Project左侧选Flask。注意PyCharm新建Flask项目时会自动创建一个app.py和一个templates目录这对小项目没问题但我们后面要拆蓝图所以只把它当成脚手架。社区版也可以只是少了Flask模板的快捷入口手动建目录也一样。第二步在PyCharm底部的Terminal里激活虚拟环境并安装依赖。如果有venv目录Windows下用venv\Scripts\activate然后安装核心依赖pip install flask flask-sqlalchemy flask-cors flask-admin pillow waitress这里解释一下每个包的用途flask基础Web框架。flask-sqlalchemy把SQLAlchemy集成到Flask负责ORM映射。flask-cors解决前端Vue开发服务器和后端Flask端口不一致时的跨域问题。flask-admin做成管理后台不用自己写增删改查页面。pillow定制海报时做图片合成。waitressWindows下常用的生产级WSGI服务器后面部署讲。第三步写最简单的启动入口确认环境没问题from flask import Flask app Flask(__name__) app.route(/) def index(): return 庙会文化项目启动成功 if __name__ __main__: app.run(debugTrue, port5000)在PyCharm里直接右键运行浏览器访问http://127.0.0.1:5000看到提示文字就说明环境通了。这一步看起来很简单但我见过很多新手卡在“为什么pip install之后PyCharm还是找不到flask”——原因基本都是PyCharm当前解释器没指向虚拟环境需要在Settings - Project - Python Interpreter里手动选择venv下的Python。3.2 Flask后端REST风格接口和跨域处理项目到了正式写接口的阶段我按REST风格定义了最核心的几个API端点方法路径说明GET/api/festivals庙会列表支持地区、日期筛选GET/api/festivals/ int:id庙会详情附带关联非遗项目GET/api/heritage/ int:id非遗项目详情POST/api/custom/orders提交定制订单GET/api/custom/orders/ int:id查询定制订单状态和下载地址写接口时有个关键点前端Vue的开发服务器默认跑在5173端口Flask跑在5000端口两者端口不同浏览器默认会拦截跨域请求。解决办法是加flask-corsfrom flask_cors import CORS app Flask(__name__) CORS(app, resources{r/api/*: {origins: *}})origins这里开发阶段用通配符*图省事部署上线后建议改成前端真实域名。我遇到过因为跨域配置太宽松被临时部署的测试域名白嫖接口的情况所以生产环境一定不要图方便。另外Flask接口返回数据时统一用jsonify并且给每个接口都套一层code、data、message结构。这样前端拿到响应后先判断code再取数据后面写Vue代码会清爽很多。3.3 Vue前端工程axios、路由和庙会详情页前端部分我用Vue 3 Vite。创建项目之前先确认Node环境装好然后在PyCharm终端里执行npm create vuelatest这里会问你需不需要TypeScript、Vue Router、Pinia等按需选择。我建议新手第一次做项目可以直接全选默认先跑通再逐个剥掉。项目创建完进入目录安装依赖npm install npm install axios安装依赖慢是另一个常见痛点Windows下如果遇到网络问题把npm镜像切到国内源会明显改善。切源的方法很简单在项目根目录建一个.npmrc文件写入registryhttps://registry.npmmirror.com前端页面结构并不复杂核心是三个视图HomeView.vue展示庙会列表FestivalDetail.vue展示庙会详情和关联的非遗项目CustomizeView.vue承载定制功能。路由用Vue Router管理典型配置import { createRouter, createWebHistory } from vue-router import HomeView from ../views/HomeView.vue import FestivalDetail from ../views/FestivalDetail.vue import CustomizeView from ../views/CustomizeView.vue const router createRouter({ history: createWebHistory(), routes: [ { path: /, name: home, component: HomeView }, { path: /festival/:id, name: festival-detail, component: FestivalDetail }, { path: /customize/:heritageId, name: customize, component: CustomizeView } ] }) export default router这里有个容易踩的坑用createWebHistory时在Vite开发服务器没问题但打包后部署到Nginx如果用户直接刷新/festival/1这个地址Nginx会返回404。原因是没有配置前端路由回退需要把Nginx的try_files指到index.html。这个我在后面部署章节会详细说。调用后端接口时统一封装一个request.js用axios实例的方式import axios from axios const request axios.create({ baseURL: http://127.0.0.1:5000/api, timeout: 10000 }) request.interceptors.response.use( response response.data, error { console.error(请求出错, error) return Promise.reject(error) } ) export default request这样每个页面里只需要写import request from /utils/request const res await request.get(/festivals/${route.params.id})把baseURL统一管理之后后面前端联调或者部署换地址只改一个文件不用在几十个组件里搜API链接。3.4 庙会视频的m3u8在Vue中播放的完整配置前面说了视频会转成m3u8格式前端播放是另一个容易卡住很多人的地方。Vue里播放m3u8我推荐用hls.js因为流程简单、兼容性好。先安装npm install hls.js然后在组件里写一个播放封装template video refvideoRef controls classvideo-player/video /template script setup import { ref, onMounted, watch } from vue import Hls from hls.js const props defineProps({ src: { type: String, required: true } }) const videoRef ref(null) function playHls(url) { const video videoRef.value if (Hls.isSupported()) { if (hlsInstance) { hlsInstance.destroy() } hlsInstance new Hls() hlsInstance.loadSource(url) hlsInstance.attachMedia(video) hlsInstance.on(Hls.Events.MANIFEST_PARSED, () { video.play() }) } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // Safari直接支持HLS video.src url video.play() } } onMounted(() { if (props.src) { playHls(props.src) } }) watch(() props.src, (newVal) { if (newVal) { playHls(newVal) } }) /script这段代码里有一个细节值得注意hlsInstance必须存储在组件作用域里避免音频和视频播放下一个文件时上一个Hls实例还在后台占用内存和请求。我在调试时遇到过连续切换几个庙会视频后页面卡死的现象后来发现就是Hls实例没有销毁。还有一个和m3u8播放强相关的问题CORS。m3u8索引文件后面跟着几十个ts片段浏览器会逐个请求这些ts文件如果服务器没给ts文件设置正确的CORS头就算m3u8能加载视频画面也黑屏。Flask里处理办法是给媒体目录单独设置响应头app.after_request def add_media_cors_headers(response): if request.path.startswith(/media/): response.headers[Access-Control-Allow-Origin] * return response这个坑非常隐蔽因为浏览器控制台可能只报一个Failed to load resource不会明确告诉你是哪个ts文件被CORS拦截了。排查的时候要先打开Network面板过滤m3u8和ts请求看响应头里有没有Access-Control-Allow-Origin。4. 定制功能落地从“浏览文化”到“带走文化”4.1 定制表单与订单状态机设计“定制”听起来很玄落到功能上就是三步选底图、填文案、生成图片。定制页面的表单字段我设计成heritageId当前正在浏览的非遗项目ID决定有哪些纹样素材可选用。templateId用户选择的底图模板ID模板决定了海报的整体色调和构图。senderName署名比如“张三 敬献”。message一句祝福或介绍文字比如“中原遗韵匠心永传”。style配色风格比如“绛红”“墨青”“宣纸黄”。订单表里除了这些字段还要有一个状态字段status我用字符串表示pending刚提交服务端还没处理。processing服务端正在生成图片。done图片生成完成提供下载地址。failed生成失败记录了失败原因。状态机看着简单实际开发时很有价值。因为图片合成是耗时操作如果直接在请求里同步生成用户等待时间可能超过5秒前端axios默认超时时间不够体验极差。我的做法是提交订单后立刻返回orderId和pending状态前端轮询查询接口等后端把图片合成好后返回done。这样用户界面可以先显示“正在排版请稍候”而不是卡在请求里。4.2 用Pillow生成定制海报的轻量实现定制功能的核心是服务端用Pillow合成图片。我把整个流程放在一个独立模块poster.py里关键代码如下from PIL import Image, ImageDraw, ImageFont def generate_poster(order): base Image.open(order.base_image_path).convert(RGB) draw ImageDraw.Draw(base) # 在底图下方叠加半透明色带保证文字可读性 overlay Image.new(RGBA, base.size, (0, 0, 0, 0)) overlay_draw ImageDraw.Draw(overlay) width, height base.size overlay_draw.rectangle( [0, int(height * 0.75), width, height], fill(0, 0, 0, 160) ) base Image.alpha_composite(base.convert(RGBA), overlay).convert(RGB) draw ImageDraw.Draw(base) # 写文案 font_path fonts/SourceHanSerifSC-Regular.otf title_font ImageFont.truetype(font_path, 48) text_font ImageFont.truetype(font_path, 32) draw.text((50, int(height * 0.78)), order.message, fill(255, 255, 255), fonttitle_font) draw.text((50, int(height * 0.78) 80), f{order.sender_name} 敬献, fill(230, 220, 200), fonttext_font) output_path foutput/poster_{order.id}.png base.save(output_path) return output_path这里有两个特别需要注意的坑一是字体问题。Pillow默认字体不支持中文如果直接画中文会得到一堆方框。必须准备中文字体文件思源宋体、思源黑体都行而且路径要放到项目里随代码一起管理不要依赖操作系统的字体目录否则服务器上一换环境中文就乱码。二是图片合成时的模式转换。底图可能是JPEG没有Alpha通道而半透明遮罩是RGBA直接粘贴会报错。必须先convert(RGBA)做合成再转回RGB保存为PNG。我第一次写的时候忘了转回RGB保存出来的图背景是全黑的排查了半天才发现是模式问题。定制结果生成以后Flask提供一个下载接口这里需要设置Content-Disposition头让浏览器把响应当附件下载而不是直接打开from flask import send_file app.route(/api/custom/orders/int:order_id/download) def download_custom_order(order_id): order CustomOrder.query.get_or_404(order_id) if order.status ! done: return jsonify(code400, message订单尚未完成), 400 return send_file( order.result_path, mimetypeimage/png, as_attachmentTrue, download_namefposter_{order_id}.png )如果你在热搜里看到“django streaminghttpresponse 参数content_type和content-disposition”其实解决的就是同一个问题文件下载时响应头里Content-Type告诉浏览器文件类型Content-Disposition里的attachment告诉浏览器是附件而不是内联展示。Flask的send_file用as_attachmentTrue和download_name就能完成底层道理和Django是一模一样的。4.3 管理后台Flask-Admin还是自写项目后台只需要录入数据我直接用Flask-Admin十分钟就能跑起来from flask_admin import Admin from flask_admin.contrib.sqla import ModelView admin Admin(app, name庙会文化管理后台) admin.add_view(ModelView(Festival, db.session)) admin.add_view(ModelView(HeritageItem, db.session)) admin.add_view(ModelView(CustomOrder, db.session))ModelView会自动根据SQLAlchemy模型生成列表页、新增页、编辑页、删除按钮。对于运营人员来说能录入数据、能改信息就够了没必要费劲用Vue写一个独立后台。但这里要提醒一点如果把CustomOrder也交给Flask-Admin管理默认情况下运营人员可以直接看到用户的署名、联系方式如果有的话也能编辑订单状态。这在内部使用没问题一旦项目要对外开放并且涉及用户敏感信息就必须对后台做权限控制至少加一个简单的登录认证。Flask-Admin支持用flask-login包装一层但能查看到什么字段、能不能导出建议单独配置成只读视图避免误操作。5. 常见问题与排查技巧实录5.1 PyCharm与Vue环境配置的经典报错这个项目开发环境里最密集的问题基本都集中在“环境配置”而不是业务逻辑上。第一个高频报错是pip install flask时报错Microsoft Visual C 14.0 is required。这个通常是某些Python包需要编译C扩展Windows下没有C构建工具。解决方法是下载“Microsoft C Build Tools”安装或者尽量选有预编译wheel文件的包版本。在安装pillow时如果遇到这个报错直接去PyPI下载对应Python版本的pillow.whl手动安装能绕开编译过程。第二个高频问题是PyCharm里安装Flask后代码还是飘红。这时要检查右下角解释器状态确保当前项目使用的是虚拟环境里的Python而不是PyCharm自带的系统解释器。具体路径是File - Settings - Project - Python Interpreter选择venv目录下的Python.exe。第三个高频问题是Vue安装依赖时卡在npm install。除了切换镜像源还可以试一下npm install --registryhttps://registry.npmmirror.com。如果node_modules已经装了一半先删除node_modules和package-lock.json再重新安装不要直接在失败现场反复install。5.2 m3u8播放黑屏与请求拦截图解我在庙会详情页里嵌了视频播放自测时发现一个诡异现象m3u8本身能加载出来播放器初始化正常但画面始终黑进度条可以拖动就是没有声音没有画面。排查步骤打开浏览器Network面板筛选m3u8确认索引请求返回200。筛选ts发现大量ts请求返回200但看Response响应体长度有些是0。直接复制一个ts链接到新标签页打开发现能正常播放。查看这个ts请求的响应头发现没有Access-Control-Allow-Origin。问题就出在ts文件的CORS响应头上。Flask里对/media/的请求加了after_request处理生产环境用Nginx托管媒体文件时又需要再检查Nginx配置里的add_header是否写对了位置。如果你在Flask里加了CORS头但Nginx又托管了媒体目录那Nginx很可能把Flask的响应头直接覆盖掉了。正确做法是在Nginx的location /media/块里单独加location /media/ { add_header Access-Control-Allow-Origin *; }另外如果你在开发环境把m3u8索引和ts切片放在不同域名下情况会更复杂因为浏览器对每个资源都会单独发CORS预检。最省事的方案就是让m3u8和ts同源。5.3 部署上线waitress Nginx的组合项目开发完成后的部署我选了waitress Nginx。热搜词里也有“python django windows10 waitressnginx部署”其实waitress不区分Flask还是Django它就是一个纯Python的WSGI服务器Windows下用起来比gunicorn省心得多因为gunicorn在Windows上支持并不好经常需要借助WSL才能跑。生产启动命令很简单waitress-serve --host0.0.0.0 --port8000 app:appapp:app表示从app.py中导入app这个Flask实例。注意生产环境一定不要再开Flask的debug模式那会带来很大的安全风险我早期有一次忘了关debug直接把项目搁在一个公网测试服务器上结果后台暴露了调试器点开哪个请求都能看到服务端完整报错信息包括文件路径和部分代码片段。好在只是测试环境不然后果很麻烦。Nginx在部署里的职责主要是两个一是托管dist目录下的Vue静态文件二是把/api请求反向代理到waitress的8000端口。前端Vue打包先执行npm run build会生成dist目录。Nginx配置核心块server { listen 80; server_name your-domain.com; root /path/to/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /media/ { alias /path/to/media/; } }这段配置里最关键的是try_files $uri $uri/ /index.html。没有这一行用户在前端路由下刷新页面就会Nginx 404这正是Vue Router使用HTML5 History模式时的经典问题。5.4 缓存策略与海报下载文件名乱码还有一个我踩过的小坑定制海报下载时如果用户直接点send_file返回的接口文件名可能是poster_12.png这种格式浏览器下载时没问题。但如果文件名里有中文比如我想让用户下载的是“浚县泥咕咕定制海报.png”必须给download_name传带UTF-8编码的值同时要确保响应头里的Content-Disposition正确编码。Flask的send_file已经处理了download_name的URL编码但我遇到过前端拿到下载地址后用window.open打开某些浏览器会把中文文件名变成一串百分号。后来我干脆写了一个前端下载辅助函数先用fetch拿到Blob再用本地临时URL触发下载这样文件名由前端决定不再依赖服务器的Content-Dispositionasync function downloadPoster(orderId) { const res await fetch(/api/custom/orders/${orderId}/download) const blob await res.blob() const url URL.createObjectURL(blob) const a document.createElement(a) a.href url a.download 定制海报.png a.click() URL.revokeObjectURL(url) }这个方法虽然多了一步但能确保中文文件名在所有浏览器里都正常。如果你既想让浏览器直接下载又希望文件名是中文那也可以继续用send_file的download_name前提是客户端不额外处理响应。项目做下来的三点体会如果回头复盘这个项目我最想强调的不是Flask怎么写、Vue怎么用而是“文化数据的颗粒度”要先想清楚。庙会文化不是几个字段能装完的但也不能一开始就把模型设计得无比复杂。先用“庙会-非遗项目-定制订单”这三张表把核心链路跑通后面加传承人、加展演日历、加数字藏品都是在稳定骨架上长肉。另外一点就是前后端联调时一定要尽早把接口返回结构定下来不要前端一套、后端一套。我在项目初期吃够了乱改接口的亏后来把所有API响应统一成{ code, data, message }前后端各写一份接口文档问题立刻少了大半。最后说一个可以继续扩展的方向。现在的定制功能只支持静态海报和图片下载后续可以考虑把定制结果做成动态页面生成带独立链接的H5分享页用户在手机上打开就能看到自己定制的庙会艺术卡片还能转发给朋友。技术上并不复杂就是在订单完成后多生成一个HTML模板用Vue做分享页渲染Flask只需要提供一个公开只读的分享接口。这样整个项目就从“展示 定制”进一步升级成了“展示 定制 分享”文化传播的链条才算真正闭合。
返回列表