写在开头我自己前后做了几个学习类的小项目最后发现英语学习网站这个方向特别适合拿来练手。项目名就叫“基于Flask的在线英语学习网站”前端用Vue后端用Python Flask整体是一个前后端分离的Web应用。它能做的不只是展示单词和课文而是完整覆盖用户注册登录、单词背诵、课程学习、在线测验、错题统计、学习进度记录这些环节。如果你正在学Python Web开发或者刚接触Vue生态想找一个“后端有东西可写、前端有页面可做”的综合项目这篇内容正好可以拿来参考。整套系统麻雀虽小五脏俱全涉及的技术点很实在Flask的蓝图拆分、SQLAlchemy模型设计、JWT用户认证、Vue Router路由权限控制、Axios请求封装、Nginx反向代理和Gunicorn部署。下面我就从设计思路、核心实现、部署流程到排查过的坑一条一条拆开讲。1. 整体设计与技术选型思路1.1 为什么是Flask加Vue而不是别的组合很多人在做Web项目时会在Flask和FastAPI之间纠结也会在Vue和React之间犹豫。我的选择逻辑很简单Flask是目前Python生态里最容易上手的Web框架它的文档和社区资料非常丰富Flask-SQLAlchemy、Flask-JWT-Extended这些扩展链都很成熟官方维护稳定遇到问题基本一搜就能找到解决方案。FastAPI在性能上有优势天然支持异步和高并发接口文档自动生成也很香但对于一个学习内容管理、用户交互为主的应用来说它的这些强项暂时用不满而Flask的模型定义、请求处理和调试体验反而更平易近人。前端选Vue也是一样的逻辑。Vue的模板语法对从HTML和JavaScript转过来的开发者极其友好Vue 3的组合式APIComposition API让组件逻辑复用变得干脆配合Vite构建工具开发服务器的热更新速度非常快几乎不用等待编译。React当然也很强但Vue的上手曲线更低中文文档质量也更好项目里要用的路由、状态管理、组件通信这些核心概念Vue都是有最佳实践的。1.2 项目整体模块怎么拆英语学习网站不是单纯的信息展示它有明显的业务层次。我把整个系统拆成五大模块尽量做到前后端职责清晰模块之间通过接口契约配合用户模块注册登录、个人信息、学习档案单词模块单词列表、查询、收藏、错词本课程模块课文阅读、视频课程HLS流媒体播放、音频跟读练习模块单词测验、课文理解题、答题记录与成绩统计数据统计模块学习打卡日历、掌握度分析、趋势图表拆模块时我遵循一个原则一个功能闭环放在一个蓝图Blueprint里。比如用户相关的注册登录逻辑全部放在auth蓝图单词背诵的接口放在word蓝图这样文件结构一眼就能看懂后面对接前端也好找接口。实际目录结构大致是这样的flask-english/ ├── app/ │ ├── __init__.py # 工厂函数创建Flask实例 │ ├── models/ # SQLAlchemy模型 │ │ ├── user.py │ │ ├── word.py │ │ └── course.py │ ├── api/ # 蓝图接口 │ │ ├── auth.py │ │ ├── word.py │ │ ├── course.py │ │ └── practice.py │ ├── utils/ # 工具函数与装饰器 │ └── config.py # 配置 ├── migrations/ # 数据库迁移 ├── run.py # 开发入口 └── requirements.txt前端Vue项目则按照页面和组件来组织router集中管理路由store用Pinia管理用户状态api目录统一封装请求。前后端分离之后只要接口约定好两边完全可以并行开发。1.3 为什么前后端分离对这类项目有利最初我也考虑过直接使用Flask加Jinja2模板渲染出页面这样开发量会小一些。但仔细想了一下英语学习网站要承载交互性强的练习页面、学习记录表格、播放器组件传统服务端渲染的方式会让页面刷新和前后端逻辑耦合很难受。前后端分离之后Vue负责交互体验Flask只负责提供JSON数据接口接口可以在Postman里独立测试前端也可以直接用mock数据先行开发。后期部署时Vue打包成静态文件交给Nginx托管Flask进程只处理API请求职责分离排查问题的时候边界清楚得多。2. 后端核心实现与接口设计2.1 数据库模型怎么设计数据库设计是整个后端的地基英语学习网站的核心实体其实不多用户、单词、课程、练习记录、错题记录。我用SQLAlchemy来定义模型选择MySQL作为生产库开发阶段为了省事直接用SQLite两者通过配置切换模型代码几乎不用改。用户和单词之间是多对多的关系用户收藏了哪些单词、哪些词进入了错词本都要有中间表支撑。这样的设计可以让查询变得灵活也方便后续做掌握度分析。单词模型的核心字段包括单词本身、音标、释义、例句和分类标签分类标签很重要后面做场景化学习比如四级词汇、核心高频词全都靠它。from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class User(db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(64), uniqueTrue, nullableFalse) email db.Column(db.String(128), uniqueTrue, nullableFalse) password_hash db.Column(db.String(256), nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.utcnow) class Word(db.Model): __tablename__ words id db.Column(db.Integer, primary_keyTrue) spelling db.Column(db.String(128), uniqueTrue, nullableFalse) phonetic db.Column(db.String(128)) meaning db.Column(db.String(512), nullableFalse) example db.Column(db.Text) category db.Column(db.String(64), indexTrue) difficulty db.Column(db.Integer, default3) class StudyRecord(db.Model): __tablename__ study_records id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(users.id)) word_id db.Column(db.Integer, db.ForeignKey(words.id)) status db.Column(db.String(16), defaultlearning) # learning, mastered, wrong review_count db.Column(db.Integer, default0) last_review_at db.Column(db.DateTime) updated_at db.Column(db.DateTime, defaultdatetime.utcnow, onupdatedatetime.utcnow)这里有个细节值得注意密码字段存的是hash值用Werkzeug的generate_password_hash和check_password_hash处理绝不能用明文存密码。学习记录表加上了review_count和last_review_at这两个字段对后面的间隔重复Spaced Repetition功能非常有价值是后续做智能复习计划的基础数据。2.2 JWT认证与用户权限控制在线学习网站肯定需要用户登录之后才能记录学习进度所以认证模块是后端的第一个完整功能。我采用JWTJSON Web Token方案用Flask-JWT-Extended扩展来实现。为什么不用传统的Session因为前后端分离后前端部署在Nginx后端是独立的API服务如果依赖Session就需要处理跨域Cookie还要管理会话存储JWT在请求头里带Token的方式明显更契合无状态API的设计。实现上要注意Token过期时间不要太长我设置access_token有效期2小时refresh_token有效期7天前端拦截到401时自动调用刷新接口拿新的Token。下面这段代码是登录接口和装饰器用法的核心from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity from werkzeug.security import check_password_hash auth_bp.route(/login, methods[POST]) def login(): data request.get_json() user User.query.filter_by(usernamedata.get(username)).first() if not user or not check_password_hash(user.password_hash, data.get(password)): return {code: 40001, msg: 用户名或密码错误}, 401 token create_access_token(identitystr(user.id)) return {code: 0, data: {token: token, userinfo: {id: user.id, username: user.username}}} word_bp.route(/words, methods[GET]) jwt_required() def get_words(): user_id get_jwt_identity() # 查询逻辑...需要注意的是get_jwt_identity返回的是字符串查数据库时要转换类型不然会查出空结果。另外建议把jwt所需的配置统一放在config.py里比如过期时间、签名密钥密钥一定通过环境变量或外部配置传入不要写死到代码库里。2.3 学习内容接口怎么组织学习内容的接口我分成三层来设计。第一层是资源层比如获取单词列表、获取课程详情这些接口只读数据第二层是用户行为层比如标记某个单词已掌握、收藏单词、提交练习答案第三层是统计层比如拉取学习天数、正确率曲线。这样分层的好处是前端调用时思路清晰后端做权限控制也方便行为层和统计层的接口一律要求JWT认证。举个例子错词本的逻辑是这样的用户做测验时如果答错了后端就把这道题对应的单词id和user_id写入学习记录表状态置为wrong。当用户再次答对3次以上状态自动改为mastered从错词本中移出。这个规则在接口内部处理前端只需要调提交答案的接口看到的始终是处理完的状态。类似这种业务规则我的经验是尽量放在后端不要散落前端代码里否则后续改规则成本很高。接口返回格式我也统一了不管成功还是失败都返回一个code字段加data字段的结构错误码用业务码而不是HTTP状态码直接代替这样前端拦截器可以统一处理。3. 前端Vue工程化与页面实现3.1 Vue项目初始化和路由架构前端项目我用的Vue 3加Vite创建一个项目非常快推荐大家先配置好Node.js环境然后执行npm create vite。项目创建后第一件事是装依赖vue-router负责路由pinia管理状态axios发请求element-plus做UI组件库。这些是核心依赖安装命令整理如下npm create vitelatest frontend -- --template vue cd frontend npm install npm install vue-router4 pinia axios element-plus npm run dev路由架构是前端比较关键的一步我设计了两种路由公开路由和需要登录的路由。公开路由包括首页、单词浏览、课程列表需要登录的路由包括学习中心、错题本、个人统计。用Vue Router的全局前置守卫来做拦截逻辑很简单本地有Token就放行没有就去登录页。router.beforeEach((to, from, next) { const token localStorage.getItem(access_token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })这种处理方式对于中小型项目足够用。如果你后面做到类似后台管理的系统需要按角色控制菜单显隐那就要配置动态路由了Vue Router 4的动态添加路由API addRoute可以处理但开始做项目时不要过度设计。3.2 核心页面与组件拆解主页面我做成三栏布局左侧是侧边导航中间内容区右侧是学习进度卡片。这个布局拆成四个组件AppHeader、SideNav、LearningPanel、DailyProgress。单词学习页是最核心的功能页。在这个页面里用户会看到单词卡片、音标和释义下面有“我认识”和“不认识”两组按钮。点击后前端调后端的记录接口然后自动切换到下一个单词。这里我踩过一个坑状态切换的动画如果采用v-if叠加CSS过渡在快速点击时会出现元素闪烁后来改为teleport加transition-group的方式并且用一个定时器防止双击重复提交。课程详情页里视频播放用的是HTML5 video标签对这个项目来说已经够用。如果你有直播或者高清视频需要防盗链那就要考虑m3u8流媒体了Vue生态里有vue-video-player和hls.js可以解析m3u8流免安装直接用播放器组件的方案也是有的。我的建议是项目初版先把点播流程跑通再考虑流媒体切片别被一条m3u8链接卡住半天。练习题页面用了单选、填空和听力题三种类型。听力的实现思路是预加载一段mp3音频用户播放后选择听到的内容提交时把用户选项和正确答案都发给后端后端做匹配。前端要注意音频加载的时机我选择在用户点击“开始本节练习”后才创建Audio实例避免页面加载时产生大量音频请求导致白屏。3.3 与后端联调中的关键处理前后端联调是实践里最花时间的地方。跨域问题首先就绕不开。开发环境下Flask跑在5000端口Vite跑在5173端口两者端口不同必须解决CORS。后端我用了flask-cors处理设置origins为具体的前端地址生产环境则统一由Nginx同域代理从这个角度讲开发环境用CORS、生产环境用反向代理是最符合习惯的做法。请求封装上我把axios实例单独抽出来baseURL根据环境变量区分开发环境用http://localhost:5000/api生产环境用/api。axios拦截器做两件事请求拦截器里带上Authorization头响应拦截器里统一处理code字段遇到401就清理本地登录状态并跳转登录页。下面是响应拦截器的核心代码service.interceptors.response.use( response { const res response.data if (res.code ! 0) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(access_token) router.push(/login) } ElMessage.error(error.response?.data?.msg || 网络异常) return Promise.reject(error) } )这样统一处理后每个页面的业务代码不需要反复写loading、错误提示联调效率提升很明显。4. 部署上线与性能优化4.1 Flask生产环境部署方式开发环境下直接python run.py确实方便但生产环境绝不能这么做。Flask自带的开发服务器是Werkzeug单进程、性能有限而且很多文档里会提醒不要用于生产。我选择Gunicorn作为WSGI服务器配合多worker运行。部署时服务器选用Linux云主机环境准备步骤大致是安装Python 3.10和虚拟环境工具创建项目虚拟环境安装requirements.txt的依赖然后拉取代码。数据库我生产环境用MySQL启动前先执行数据库迁移把表结构创建好。项目入口文件run.py需要能被Gunicorn正确导入所以Flask实例一定要通过工厂函数创建并且暴露成模块级别的变量。gunicorn -w 4 -b 127.0.0.1:8000 run:app这里我解释一下参数-w 4表示启动4个worker进程能充分利用CPU多核-b 127.0.0.1:8000表示监听本地8000端口不直接暴露公网由Nginx做统一入口转发。如果不加超时参数某些耗时接口可能出现worker卡死的情况所以我一般会加上--timeout 60。4.2 Nginx反向代理与前端静态资源托管前端构建后把dist目录传到服务器的/var/www/english-frontend下。Nginx配置的关键是处理好两类请求静态资源直接由Nginx返回带/api/开头的请求反向代理到Gunicorn所在的8000端口。这里注意代理时要携带Host头否则Flask生成的一些绝对链接可能出现host不对的问题。server { listen 80; server_name your_domain.com; root /var/www/english-frontend; index 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }try_files那行是Vue路由的history模式必备配置。如果你的路由用了history模式刷新子页面时Nginx如果直接找对应的文件会404try_files会把不存在的路径回退到index.html由Vue Router接管。4.3 性能与安全优化建议部署完成只是起点真正上线后你会看到一些性能问题。我做了几件事实测效果还不错。第一是给静态资源加缓存策略图片、CSS、JS这些文件加上expiresNginx直接返回304第二是给Flask接口写统一的错误处理避免500异常直接暴露堆栈信息第三是数据库查询优化凡是列表接口尽量带上分页单词列表这种数据量大的接口一定不要全量返回。安全方面JWT密钥务必从环境变量读取Nginx层面对请求体大小做限制防止过大的上传请求。管理端的操作接口建议再加一个角色校验装饰器。虽然在线学习网站不比支付系统那么高危但这些基本卫生习惯还是要有的我在实际项目中见过把数据库密码写进Git仓库然后被自动扫描工具泄露的案例这种坑一次都不能踩。5. 常见问题与排查技巧实录5.1 后端接口排查记录问题一SQLite迁移后字段变了但查询报错。这个情况多半是旧数据库文件没有重新初始化。SQLite的结构变更不自动开发时如果改了模型直接删除开发库文件重新生成表是省事的但生产库不能删所以从第一天就要用Flask-Migrate来管理迁移而不是手动改表。问题二JWT报Expired signature但前端明明刚登录。排查下来是因为服务器时区设置问题导致的Token时间判断异常。解决方法是确认系统时区与JWT配置中使用的时区一致建议统一使用UTC存储展示时再转成北京时间。问题三接口数据返回正常但前端拿到后页面不更新。这是典型的Vue响应式问题。如果后端返回的数据格式是嵌套的JSON初始化时没有把对应的字段声明成响应式引用赋值就不会触发视图更新。解决办法是用reactive或者ref预定义完整的初始结构不要动态添加顶层字段。5.2 前端与Vue环境配置疑难杂症问题一npm install一直失败报错内容指向node-sass或者python环境。这大概率是Node版本和依赖版本冲突。我统一用node-sass替代为sass纯JS版本并锁定Node.js的LTS版本。如果公司的网络环境安装依赖很慢可以配置npmmirror镜像源下载速度会快很多。问题二Vue Router切换页面后滚动条不回到顶部。这是单页应用的老问题。解决方案是在全局路由afterEach里调用window.scrollTo(0, 0)但要注意如果是带tab切换的页面就不要每次都置顶了要有选择地处理。问题三前端打包后图片资源404。因为我把public目录下的图片通过绝对路径引用了但打包部署到子路径时路径对不上。解决方法是把Vite的base配置成./或者所有资源引用统一用import方式引入最终由打包工具解析路径。5.3 个人踩过的坑与状态管理心得整个项目从开发到上线我最大的体会是状态管理要克制。Pinia很好用但不代表所有状态都要放进 Store。像单词卡片的当前索引、练习题里用户正在作答的内容这些局部状态直接写在组件里就够了放进全局Store反而会造成跨页面数据污染。我遇到的经典bug是用户A在学习中心打卡后退出登录用户B登录进来页面上竟然还残留着A的学习数据。这就是把用户信息存进Vuex/Pinia后没有在登出流程中正确清理导致的。后来我整理了Store的初始化与重置规范用户Store包含一个reset方法登出接口成功后无论成功还是失败前端都立即清理Store数据和localStorage并且跳转登录页时带上redirect参数登录完成后自动回到原页面这样用户从任何一个地方点退出都不会串数据了。结语这里不整什么宏大总结了我就说一点最实在的感受做这种在线学习网站难点其实不在某个框架的API怎么调用而在于把“用户今天该复习哪些单词”“这道题答错后下次什么时候出现”这类学习业务逻辑翻译成后端接口和前端交互的完整闭环。先把单词表、课程、测验这些最小可用的功能跑通让前后端的数据流走顺畅再回头优化记忆算法、提升页面动效你就已经比只会“照着教程敲demo”的阶段强出一截了。希望这篇里的选型思路和避坑记录能帮你少熬夜调试几次。
阅读完成 · 觉得有帮助?