☰
用Flask+Vue打造在线音乐播放平台:前后端分离与音频流式播放实践
2026/10/20 18:09:01 网站建设 项目流程

在线音乐播放平台听起来门槛不高,但真要把“上传歌曲、流式播放、用户收藏、评论互动”这一整套跑通,再跟前端联调、部署上线,涉及的知识点一点不比电商平台少。我去年用 Flask 做后端、Vue 写前端、在 PyCharm 里完成整个开发流程,做出来一个支持登录注册、歌曲搜索、播放列表、收藏和评论的在线音乐播放平台,期间踩了不少坑,也总结了一套比较顺手的实践方案。如果你是刚学完 Python 基础、想拿一个完整项目练手,或者正在准备毕业设计、想做一个能演示的 Web 应用,这篇文章可以直接当作你的项目参考,我会把技术选型、数据库设计、音频流接口、前后端联调和常见问题全部拆开讲清楚。

1. 技术选型:Flask、Django、Vue和PyCharm怎么组合

1.1 Flask和Django同时出现在一个标题里,到底选哪个

很多初学者看到“基于flask和python框架”又带着“django”就会纠结:到底是两个框架都用,还是二选一?答案很直接:这个音乐平台的核心后端可以选 Flask,也可以选 Django,但没必要两个都写在同一个项目里。我当时选择 Flask,是因为它足够轻量,路由和视图函数写起来很自由,一个音乐平台的接口数量几十个,用 Flask 可以很清晰地按业务模块组织蓝图。Django 自带 Admin 后台、ORM、认证体系,开发效率高,但学习曲线更陡,启动一个项目会带出很多默认文件,初学者容易迷失在配置里。

如果你更喜欢 Django,也不是不行,用 Django REST Framework 同样能实现下面讲的全部功能。但要注意,Flask 和 Django 的路由定义、请求处理、ORM 写法都不一样,如果一会儿用 Flask 的@app.route,一会儿用 Django 的urls.py,项目会变得不伦不类。我建议:目标是在一周内快速做出可演示的在线音乐播放平台,选 Flask;目标是熟悉企业级框架、想用自带的权限体系和后台管理,选 Django。不要混着用。

1.2 前端为什么用Vue而不是传统页面

在线音乐播放平台对交互要求比较高,比如播放器需要实时更新进度条、音量滑块要立刻响应、搜索列表要无刷新展示,这些用传统的服务端渲染也能做,但会频繁刷新页面,体验很差。Vue 的响应式数据绑定刚好解决这个问题。我选 Vue 2 配合 Vue Router 和 Axios,因为生态最稳定、资料最多,遇到问题很好搜。你不需要在项目里引入特别复杂的状态管理库,用 Vuex 或 Pinia 管理用户信息和播放状态即可。

Vue 作为独立的前端工程,和后端 Flask 通过 HTTP 接口通信,相当于两个人各管一摊,前端只通过axios.get('/api/songs')拿数据,后端只负责返回 JSON。这种前后端分离的架构也是目前实际团队合作的主流方式。如果你是个人开发,分开写还能让代码职责更清晰,不会出现前端标签里夹着一堆 Jinja2 模板语法的情况。

1.3 PyCharm在整个开发流程里扮演什么角色

PyCharm 是 Python 后端开发非常顺手的 IDE,尤其是专业版,可以直接打开 Vue 项目、运行npm run dev,同时开启 Flask 调试服务器。我习惯用 PyCharm 的 Project 视图同时管理 backend 和 frontend 两个目录,左边写 Flask 代码,右边直接改 Vue 组件,调试时按 Ctrl+Shift+F10 运行后端,再开一个 Terminal 跑前端,很方便。

PyCharm 最有用的功能是内置的虚拟环境管理和解释器配置。新建 Flask 项目时,它会自动帮你创建venv,省去了手动python -m venv的步骤。数据库插件还能可视化查看 SQLite 表结构,调试在线音乐平台时的用户表和歌曲表关系非常直观。如果你用社区版,也完全可以,只是没有数据库图形界面和前端调试支持,但写后端代码没区别。

2. 在线音乐播放平台的功能拆解与数据设计

2.1 用户体系:注册登录和Token鉴权

音乐平台首先得有用户,不然收藏和评论无从谈起。我设计的用户表字段包括:id、username、password_hash、avatar、created_at。密码绝对不能明文保存,要用 Flask 的werkzeug.security库生成密码哈希,校验时用check_password_hash。登录成功后,后端生成一个 token 返回给前端。我一开始用的是简单的itsdangerous签名 token,后来换成了pyjwt,因为 JWT 是行业标准,以后接移动端也方便。

token 的过期时间我设置为 24 小时,前端拿到后存储在 localStorage 里,每次请求在请求头加Authorization: Bearer <token>。后端写一个装饰器@login_required,在需要登录的接口里先解析 token,再获取当前用户。这里有个小细节:修改密码或退出登录后,旧 token 在逻辑上不应该继续有效,但 JWT 天然是无状态的,服务端无法主动让 token 失效。我的方案是把token_version字段加到用户表,生成 token 时携带这个版本号,校验时比对,用户修改密码时把版本号加一,旧 token 就全部失效了。

2.2 歌曲信息与文件存储

在线音乐平台的核心资源是歌曲文件,但数据库里不该存音频二进制,而是存文件的路径或 URL。我建了一张songs表,字段有:id、title、artist、album、duration、file_path、cover_url、lyrics、play_count、created_at。

file_path我存的是服务器本地的相对路径,比如uploads/audio/123.mp3。cover_url是封面图片的访问路径,同样存在本地。为什么不存完整 URL?因为部署时域名或端口可能会变,相对路径更灵活。前端拿歌曲列表时,后端会动态拼接出绝对地址返回,比如http://192.168.1.10:5000/uploads/audio/123.mp3,这样前端可以直接使用。

歌曲的封面图片我用Pillow做了压缩处理,统一转成400x400的 JPG,避免用户上传一个大图导致加载缓慢。音频转码我用的ffmpeg,为了统一格式,上传的音频都转成 320kbps 的 MP3。这样播放器兼容性最好。转码操作比较耗时,我是在上传接口里同步处理的,因为是个人项目,并发量不大,同步也能接受。

2.3 收藏、播放列表和评论的外键关系

收藏和播放列表本质上都是用户与歌曲的多对多关系,需要中间表来关联。favorites表字段:id、user_id、song_id、created_at,加一个唯一的联合索引(user_id, song_id)防止重复收藏。播放列表需要支持创建多个歌单,我建了playlists表(id、user_id、name、description、created_at)和playlist_songs表(id、playlist_id、song_id、sort_order)。评论表更简单:id、song_id、user_id、content、created_at。

用 SQLAlchemy 定义这些模型时,重点在于relationship和cascade的设置。删除用户时,他的收藏和评论应该同步删除,播放列表也要删除,但歌曲本身不能删,因为歌曲可能是管理员上传的,属于公共资源。我在User模型上写了favorites = db.relationship('Favorite', backref='user', cascade='all, delete-orphan'),这样删除用户时 SQLAlchemy 会自动帮我清理关联记录,避免了孤儿数据。

3. 从零搭建项目:后端API和音频流播放的完整流程

3.1 初始化Flask项目和数据库连接

我先在 PyCharm 里新建了一个 Flask 项目,项目目录结构如下:

music-platform/ ├── backend/ │ ├── app.py │ ├── models.py │ ├── api/ │ │ ├── auth.py │ │ ├── songs.py │ │ ├── playlists.py │ │ └── comments.py │ ├── utils/ │ │ ├── token.py │ │ └── audio.py │ ├── uploads/ │ │ ├── audio/ │ │ └── covers/ │ └── requirements.txt └── frontend/ ├── src/ │ ├── api/ │ ├── views/ │ ├── components/ │ └── router/ └── package.json

后端我用 Flask 2.x 版本,数据库用 SQLite 起步,后期需要换 MySQL 的话,只要修改 SQLAlchemy 的数据库 URL 就行。app.py里先初始化应用和数据库:

from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS db = SQLAlchemy() def create_app(): app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///music.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 db.init_app(app) CORS(app) from api.auth import auth_bp from api.songs import songs_bp from api.playlists import playlists_bp from api.comments import comments_bp app.register_blueprint(auth_bp, url_prefix='/api/auth') app.register_blueprint(songs_bp, url_prefix='/api/songs') app.register_blueprint(playlists_bp, url_prefix='/api/playlists') app.register_blueprint(comments_bp, url_prefix='/api/comments') return app

这里有个关键点:CORS(app)这行不能漏。因为 Vue 开发服务器运行在 5173 端口,Flask 运行在 5000 端口,跨域请求会被浏览器拦截。加了 Flask-CORS 后,后端会返回Access-Control-Allow-Origin头,前端才能正常拿到数据。如果你临时不需要跨域,可以只在开发环境配置,生产环境放到同一个域名下就可以去掉。

3.2 设计RESTful API端点

在线音乐播放平台的后端接口要覆盖用户操作、歌曲浏览、播放列表管理和评论四个模块。我简单的端点列表如下:

功能模块请求方式接口路径说明
注册POST/api/auth/register提交用户名、密码
登录POST/api/auth/login返回 JWT token
获取当前用户GET/api/auth/me需要带 token
歌曲列表GET/api/songs支持关键词搜索和分页
歌曲详情GET/api/songs/<int:song_id>返回歌曲信息和播放地址
上传歌曲POST/api/songs需要管理员权限,multipart 表单
收藏歌曲POST/api/favorites需要登录
取消收藏DELETE/api/favorites/<song_id>需要登录
创建歌单POST/api/playlists需要登录
歌单添加歌曲POST/api/playlists/<id>/songs需要登录
获取歌单详情GET/api/playlists/<id>返回歌单内歌曲
发表评论POST/api/comments需要登录
获取歌曲评论GET/api/comments/song/<song_id>不需要登录

写接口时,我习惯把所有返回数据统一为{"code": 200, "message": "success", "data": {...}}这样的结构。前端通过code判断请求是否成功,方便统一拦截错误,不用每个接口单独处理异常。

3.3 音频流式播放的实现细节

这是整个项目里最容易出坑的地方。如果直接用 Flask 的send_file返回整个 MP3 文件,播放器会等待文件下载完才开始播放,几百兆的音频会卡很久。正确做法是支持 HTTP Range 请求,也就是流式传输。

前端播放器(比如 Vue 里用原生<audio>标签或封装组件)在播放时,浏览器会自动发送带Range: bytes=0-的请求。后端需要读取这个请求头,返回对应字节范围的数据,并带上206 Partial Content状态码。我用 Flask 写了一个专门处理音频流的接口:

from flask import request, Response, send_file import os def send_audio_file(file_path): file_size = os.path.getsize(file_path) range_header = request.headers.get('Range', None) if not range_header: return send_file(file_path, mimetype='audio/mpeg') start, end = range_header.replace('bytes=', '').split('-') start = int(start) if start else 0 end = int(end) if end else file_size - 1 length = end - start + 1 with open(file_path, 'rb') as fp: fp.seek(start) data = fp.read(length) response = Response(data, status=206, mimetype='audio/mpeg') response.headers['Content-Range'] = f'bytes {start}-{end}/{file_size}' response.headers['Accept-Ranges'] = 'bytes' return response

这里有几个细节要注意:Range头可能不带结束位置,比如bytes=1024-,也可能不带开始位置,比如bytes=-2048,需要同时处理这两种情况。实际项目里,我还加了对If-Range的支持,避免文件在传输过程中被修改导致数据错乱。所有歌曲的访问地址都统一走/api/songs/<id>/stream,前端audio标签的src直接指向这个接口,播放、拖动进度条都会自动发起 Range 请求。

3.4 Vue前端页面和后端接口联调

前端用 Vue CLI 初始化项目,安装vue-router、axios、vuex(或 Pinia)。核心页面有首页(推荐歌曲列表)、搜索页、歌单页、歌曲详情页、用户中心。播放器组件做成全局的,放在App.vue中,无论怎么切换路由,播放器都保持在页面底部。

我在src/api目录里统一封装了请求函数:

import axios from 'axios' const service = axios.create({ baseURL: 'http://127.0.0.1:5000/', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) export default service

联调时最容易出现的问题是前端请求的端口和后端不一致。我用 Vue CLI 的vue.config.js配置了代理,这样前端请求路径变成了/api/songs,不会出现跨域:

module.exports = { devServer: { proxy: { '/api': { target: 'http://127.0.0.1:5000', changeOrigin: true } } } }

配置代理后,前端的baseURL可以直接写空字符串,所有请求走相对路径'/api/songs',开发环境由 webpack 转发给 Flask,生产环境由 Nginx 转发,整体切换非常平滑。

4. 项目上线前遇到的坑和排查记录

4.1 Vue请求Flask接口的跨域问题

这个问题几乎每个前后端分离的项目都会遇到。现象是浏览器控制台报Access to XMLHttpRequest has been blocked by CORS policy。原因是我一开始在 Flask 端用了CORS(app),但 Vue 端也用axios.defaults.baseURL写成了 Flask 的绝对地址,然后又在 Flask 端带了凭证withCredentials: true,而 Flask-CORS 默认没有允许携带凭证,所以请求被拦了。

解决办法有两个方向:一是开发环境就用 Vue 的 proxy,让浏览器认为请求是同源的;二是后端明确配置CORS(app, supports_credentials=True),并且在前端不能随便设置withCredentials,因为这要求后端指定的Access-Control-Allow-Origin不能是*,必须是具体域名。我个人更推荐第一种方式,因为代理在开发阶段最稳定,而且生产环境本来也是通过 Nginx 转发,不存在跨域问题。

4.2 播放长音频时莫名其妙中断

有段时间播放一首 50 分钟的电台节目,总是播到 15 分钟左右就断掉,重新加载才能继续。第一次怀疑是前端音频组件的问题,换了播放器组件还是复现。最后排查到 Flask 开发服务器对客户端断连异常处理不完善,而且我那个流式播放接口没有设置超时或心跳机制,浏览器和服务器之间连接被防火墙判定为空闲,直接切断了。

我做的修复有三条:第一,在响应头里显式加上Connection: keep-alive和Cache-Control: no-cache;第二,把开发服务器的threaded=True打开,让每个音频请求都在独立线程里处理,避免阻塞;第三,前端在audio事件里检测到stalled状态时自动重新加载。这些改动之后,长音频播放稳定了很多。真正部署到生产环境时,我建议用 Nginx 托管静态音频,让 Nginx 处理 Range 请求,Flask 只负责返回音频的元数据,性能和稳定性都会更好。

4.3 中文歌曲名在响应里变成乱码

前端传?keyword=忽然之间给后端,后端返回歌曲列表,标题却显示成类似çªåæä¹é´的乱码。这是典型的 URL 编码和数据库字符集不一致导致的问题。Flask 的request.args.get('keyword')会按照 UTF-8 解码,但我的 SQLite 连接字符串没有指定charset,加上前端 axios 默认也会把参数编码,导致后端拿到的是双重编码后的内容。

解决方法是统一编码规范。后端数据库连接字符串写成sqlite:///music.db?charset=utf8mb4,前端在定义 axios 实例时不要手动encodeURIComponent参数,让 axios 自动处理。另外,在 Flask 入口文件里加一行app.config['JSON_AS_ASCII'] = False,这样 Flask 返回 JSON 时不会把中文转成\uXXXX转义序列,前端拿到中文更直观,调试也更方便。

4.4 用户登录后刷新页面就丢了登录态

Vue 应用是单页应用,刷新后内存中的用户状态会被清空。我当时保存在 Vuex 里的userInfo刷个页面就没了,导致每次刷新都要重新登录。后来把 token 保存到 localStorage,并在应用初始化时根据 token 重新请求/api/auth/me获取用户信息,把这个逻辑放在router.beforeEach里面,页面刷新后先校验 token,再进路由,这样用户就不会有感知。

还有一个小坑是 token 过期后,接口返回 401,但页面没有跳转登录页。我在 axios 响应拦截器里统一判断response.data.code === 401或 HTTP 状态为 401,清除本地登录态并跳转到/login。注意不要在每个页面手动处理,否则代码会非常冗余。

5. 部署上线与性能优化经验

5.1 用Nginx托管静态文件和转发API

开发完成后不能一直跑flask run,我部署到一台 Linux 服务器上,用 Nginx 做反向代理。Nginx 配置里,/api开头的请求转发到 Flask 的 5000 端口,/uploads开头的请求直接指向本地音频和封面图片目录,/路径指向 Vue 打包后的dist/index.html。这样音频文件的请求完全由 Nginx 处理,Flask 压力小很多。

线上跑 Flask 用的是gunicorn,起步命令是gunicorn -w 4 -b 127.0.0.1:5000 app:app。我实测单机 4 个 worker 能支撑日常并发播放。如果音频文件多,建议用更大的磁盘或者直接对接对象存储服务,上传时把音频传到对象存储,数据库里存对象的 key,播放地址变成 CDN 加速链接,这样体验会更好。

5.2 歌曲列表接口性能优化

列表页有几百首歌曲时,接口响应会变慢。最直接的办法是分页,我用的page和per_page参数配合 Flask-SQLAlchemy 的paginate方法。但有些场景需要搜索,比如用户输入关键词,SQLite 的LIKE '%关键词%'查询在数据量大时会全表扫描。我给songs表加了title和artist的索引,虽然LIKE '%xxx%'不一定走索引,但精确匹配或前缀匹配会快很多。

推荐歌单接口需要统计播放次数,我在Song模型上加了play_count字段,每次播放请求到达时异步加一。注意不要用更新字段后再查回的方式来返回播放次数,因为高并发下会互相覆盖。我用的是 SQLAlchemy 的update().where(Song.id == song_id).values(play_count=Song.play_count + 1),这个操作在数据库层面是原子的。

5.3 前端Webpack打包体积过大

Vue 项目打包后体积有 2MB 多,首次加载要好几秒。我优化了几个地方:引入路由懒加载,每个页面组件单独拆包;播放器组件和第三方音频插件用动态导入;全局 Element UI 改成按需引入。打包后首屏体积降到 500KB 左右。个人项目不需要追求极致优化,但这些做法值得养成习惯,以后做更大的项目也用得上。

6. 一些实际操作中的体会

做这个在线音乐播放平台的过程中,我最大的体会是技术难点往往不在写代码本身,而在于几个模块的衔接。前端的播放器、后端的音频流接口、服务器的文件存储,这三者只要有一个环节没对齐,歌就放不出来。比如浏览器发送 Range 请求,后端如果返回了完整的 200 响应,播放器虽然也能播,但进度条拖动就会失灵。这些看似边缘的情况,恰恰是实际用户最容易遇到的问题。

如果这个项目后续还要扩展,我会优先做这两件事:一是把音频上传改成异步任务,用消息队列处理转码,避免用户上传大文件时一直卡在等待页面;二是给播放列表加导入导出功能,支持用户把自己的歌单分享出去。另外,如果你想把这个项目作为求职项目展示,建议再加上一个简单的数据统计页,展示歌曲播放量排行、用户活跃度,这些内容会让面试官觉得你有数据思维。

最后再分享一个小技巧:开发时尽量不要用默认的 SQLite 数据库文件名字instance/music.db,我因为没注意 SQLAlchemy 默认把 SQLite 数据库放在instance目录里,导致git init时差点把数据库文件提交上去。在.gitignore里加上*.db和uploads/,避免把大文件和本地数据传到仓库,这个习惯能帮你省掉很多麻烦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询