
聊个最近折腾完的项目——一个基于Vue和Python的音乐播放系统。技术栈不算新奇但胜在把前后端打通的过程中踩过的坑和沉淀下来的方案都比较完整。项目最终效果是前端Vue负责歌单展示、搜索、播放器交互后端用Python提供歌曲元数据和流媒体接口开发全程在PyCharm里完成。如果你正在纠结Flask和Django怎么选或者刚接触Vue和Python接口对接这篇应该能帮上忙。这类音乐播放系统最常见的落地场景其实就是个人云歌单、企业内部音频资料管理、培训平台的多媒体资源展示或者毕设项目。核心链路不复杂——歌曲从哪来、怎么组织、怎么流畅播出去但在选型、环境搭建、播放器接入这几个环节网上的资料大多零散不同版本之间差异又大很多人卡在环境上没走出来。这篇文章就按我实际开发顺序来拆先讲技术栈选型为什么这么做再讲开发环境怎么搭然后到核心功能实现、联调排查最后聊部署和扩展。每一步都会给出我在PyCharm里实际跑通的方案和代码。1. 立项思考音乐播放系统的技术栈到底怎么选1.1 这个系统要解决什么问题音乐播放系统本质上要解决三件事歌曲资源的存储与组织、按用户意图检索和编排、把音频流畅地送到浏览器里播出来。桌面播放器扫本地盘就行但Web版不行。前端页面只管界面和播放交互数据从哪来、音频文件放在哪、权限怎么控制这些必须由后端来处理。所以我做这套系统时给自己定了明确的边界不做花哨的推荐算法不做P2P分发老老实实覆盖“歌单—列表—播放”这条主链路同时把工程化做好让代码结构清晰、可以继续扩展。目标用户是三类人想给自己搭私人云歌单的开发者、需要内部音频/视频资料管理平台的企业或机构、以及用这类项目练手的在校学生。这个定位决定了技术选型不需要太激进。前端要能写组件、管状态、处理路由后端要能提供API、管理数据、输出音频流开发环境要让人能舒舒服服调代码。把需求拆明白之后选型就不会纠结太久。1.2 Flask和Django不是对手是两种开发习惯标题里同时出现Flask和Django这是很多刚接触Python Web开发的人都会纠结的问题。我的看法是这两个框架不是竞争关系而是两种开发习惯。Flask走的是“微框架”路线核心只做路由和请求分发其他东西全凭自己组合Django走的是“全家桶”路线ORM、Admin后台、表单、认证、迁移工具全部内置。回到音乐系统这个具体项目我当时的感觉非常明确如果核心是管理歌曲、专辑、歌手、歌单这些数据还要一个能用的后台让运营或自己录入数据Django几乎是最省力的方案。自带Admin后台配个美化插件歌曲表的管理界面基本就现成了省掉大量重复的增删改查页面。而Flask虽然灵活但要把后台管理、数据库迁移、序列化这些能力一个个拼起来工程量会明显增加。我也见过很多Flask做得非常漂亮的音乐类项目配Flask-SQLAlchemy、Flask-Admin、Flask-RESTful确实能做出很整洁的结构。适用场景不一样接口少、想自己把握每个环节Flask很舒服数据模型多、后台管理需求重、希望开箱即用Django更合适。这篇博文的主体按Django来写因为它在数据管理上的优势更适合音乐系统Flask版的路由和API设计思路也会穿插着讲方便对比。1.3 为什么前端框架选了Vue前端框架这些年其实可选范围很广但Vue在这个项目里特别合适。原因有三个第一上手曲线平滑模板语法直观不用像某些框架那样先理解一堆函数式概念第二生态成熟Vue Router做页面路由、Pinia或Vuex管播放器状态、Axios发请求这些都是经过大量项目验证的稳定组合第三开发体验好配合Vite的热更新改完代码页面立刻刷新调试效率很高。音乐播放器这类界面的痛点在于交互状态特别多当前播放的是哪首、播放列表是什么、进度条走了多少、音量多大、播放模式是单曲还是列表循环。这些状态如果不用框架来管理代码很快就会变成一团乱麻。Vue的响应式数据模型天然适合这种场景组件化开发也能把“歌曲列表”“播放器控制条”“搜索框”这些模块拆开维护互不干扰。另外搜索热词里反复出现“vue播放m3u8”“vue视频m3u8”说明大家对流媒体播放的关注度很高。Vue在这个场景也没有包袱不管是普通音频还是m3u8切片流都能通过第三方库很好地接入后文我会专门写这一块的实现。2. 开发环境PyCharm里把Python和Vue的开局盘活2.1 Python环境准备与PyCharm配置说实话很多项目死在起跑线上不是代码难写而是环境没配好。Python环境这块我建议直接用PyCharm社区版就够了——至少对这个项目来说专业版那些数据库工具和前端支持属于锦上添花社区版完全能承载Django开发和Vue联调。安装Python时有一个容易被忽略的细节安装向导里要把“Add Python to PATH”勾上否则终端里敲python会提示找不到命令。Windows和Linux的安装方式不一样Windows建议直接从官网下载安装包Linux可以用系统包管理器但要注意不同发行版可能默认装的是python3而不是python。我个人的习惯是开发项目一律用虚拟环境不用系统级Python装包。在PyCharm里创建新项目时选择Virtualenv环境PyCharm会自动生成独立的Python解释器后面用pip安装任何包都不会污染系统环境。再就是版本问题。Python 3.10以上跑Django 4.x和Flask 3.x都没有问题。Django和Flask虽然支持较新的Python但有些依赖库比如后面要讲的mysqlclient在Windows下可能编译困难所以稳定版本优先不要追新追到让自己难受。装完Python之后建议顺手在PyCharm终端里跑一下python --version和pip --version确认基础环境没问题再往下走。2.2 用Vite初始化Vue前端工程Vue工程的搭建现在主流方案是Vite比早期的vue-cli启动速度快一个量级。创建项目的命令是npm create vitelatest music-front -- --template vue它会生成一个带Vue 3的工程骨架目录简洁src下分为components、router、views这些惯用目录。初始化之后进入工程目录安装核心依赖cd music-front npm install npm install vue-router4 pinia axiosnpm install这一步在国内网络环境下可能比较慢可以配置镜像源来解决在项目根目录建一个.npmrc文件写入registryhttps://registry.npmmirror.com关于Vue版本直接上Vue 3。Vue 2虽然在存量项目里还有大量存在但新项目没必要从旧版开始。Vue 3的组合式API写播放器状态比选项式API利落得多。装好依赖之后跑npm run dev看到Vite启动界面前端工程就算活了。2.3 后端工程结构与目录设计后端我用Django做主框架。创建项目和应用django-admin startproject music_project cd music_project python manage.py startapp music目录结构我按这样的思路组织music_project/ ├── music_project/ # 项目配置settings.py、urls.py ├── music/ # 音乐应用models、views、api接口 ├── media/ # 音频文件、封面图上传文件存储位置 ├── static/ # Django静态文件如果前端独立部署则很少用 ├── manage.py └── requirements.txtsettings.py里有几个必须改的地方。首先把music应用注册到INSTALLED_APPS里然后配置MEDIA_ROOT和MEDIA_URL这是音频文件能不能被浏览器访问到的关键如果前端独立部署在Vite开发服务器上还要处理跨域或者用Vite代理转发。Django的media配置是这样的import os MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media)同时在主urls.py里把media目录暴露出来from django.conf import settings from django.conf.urls.static import static urlpatterns [ path(admin/, admin.site.urls), path(api/, include(music.urls)), ] static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)开发阶段这样处理完全没有问题生产部署时静态文件和媒体文件的托管会交给Nginx来做后文部署部分会详细讲。Flask版的工程结构我会这样组织flask_music/ ├── app.py # 应用入口注册蓝图 ├── models.py # SQLAlchemy模型 ├── api/ │ ├── songs.py # 歌曲接口蓝图 │ └── playlists.py # 歌单接口蓝图 ├── media/ └── requirements.txtFlask的蓝图机制和Django的app概念功能上是对应的都是把不同业务模块拆开管理避免所有路由堆在入口文件里。3. 核心功能实现从数据模型到播放器闭环3.1 音乐数据模型设计音乐系统的数据模型核心就几张表歌曲、歌手、专辑、歌单以及“歌单-歌曲”的关联关系。Django的ORM里模型继承models.Model字段类型有明确的语义。from django.db import models class Song(models.Model): title models.CharField(max_length200, verbose_name歌曲名) artist models.ForeignKey(Artist, on_deletemodels.CASCADE, related_namesongs) album models.ForeignKey(Album, on_deletemodels.SET_NULL, nullTrue, blankTrue) duration models.IntegerField(help_text时长单位秒) genre models.CharField(max_length50, blankTrue, verbose_name风格) file_url models.FileField(upload_tosongs/, verbose_name音频文件) cover_url models.ImageField(upload_tocovers/, nullTrue, blankTrue, verbose_name封面图) play_count models.IntegerField(default0, verbose_name播放次数) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table song ordering [-created_at] def __str__(self): return self.title这个设计里有几个关键决策值得说明。歌曲与歌手用外键关联on_delete设为CASCADE意思是歌手删除时他的歌曲一起删除符合直觉歌曲与专辑用SET_NULL专辑删了歌曲仍然保留只是专辑字段置空。file_url字段用的是FileFieldDjango会把上传文件存到MEDIA_ROOT下同时数据库里保存的是相对路径这样做的好处是文件系统里的音频文件都有统一的管理位置不容易散落。歌曲和歌单之间是典型的多对多关系一首歌可以出现在多个歌单里一个歌单包含多首歌。Django里直接用ManyToManyFieldclass Playlist(models.Model): name models.CharField(max_length100) description models.TextField(blankTrue) songs models.ManyToManyField(Song, related_nameplaylists, throughPlaylistItem) created_at models.DateTimeField(auto_now_addTrue) class PlaylistItem(models.Model): playlist models.ForeignKey(Playlist, on_deletemodels.CASCADE) song models.ForeignKey(Song, on_deletemodels.CASCADE) sort_order models.IntegerField(default0) class Meta: ordering [sort_order]中间表PlaylistItem的存在是为了给歌单里的歌曲加sort_order排序字段。如果不考虑歌单内排序直接用默认的自动中间表也行但做音乐播放器歌单排序是刚需所以手动指定中间表。设计完成之后执行数据库迁移python manage.py makemigrations music python manage.py migrate数据库我开发阶段直接用SQLite零配置、单文件、好备份。等要上生产再切MySQLDjango的ORM层面基本不用改只改settings里数据库配置后面会谈到这个切换过程。3.2 API接口设计与实现接口是前后端的唯一交流通道。我的设计原则是URL语义清晰、返回JSON、错误有明确状态码。基础接口如下GET /api/songs/ 歌曲列表分页 GET /api/songs/id/ 歌曲详情 GET /api/songs/search/?q关键词 搜索歌曲 GET /api/playlists/ 歌单列表 GET /api/playlists/id/songs/ 歌单内的歌曲列表Django里实现这些接口有两种方式直接用JsonResponse手写或者用Django REST frameworkDRF。我建议接口超过三四个就上DRF它解决了序列化、分页、过滤、权限这些重复劳动。# music/serializers.py from rest_framework import serializers from .models import Song, Playlist class SongSerializer(serializers.ModelSerializer): artist_name serializers.CharField(sourceartist.name, read_onlyTrue) album_name serializers.CharField(sourcealbum.title, read_onlyTrue) class Meta: model Song fields [id, title, artist_name, album_name, duration, genre, file_url, cover_url, play_count] # music/views.py from rest_framework import viewsets, filters from .models import Song, Playlist from .serializers import SongSerializer, PlaylistSerializer class SongViewSet(viewsets.ModelViewSet): queryset Song.objects.all() serializer_class SongSerializer filter_backends [filters.SearchFilter] search_fields [title, artist__name, album__title]SearchFilter支持按歌曲名、歌手名、专辑名搜索Django ORM里artist__name这种双下划线写法能跨表查询前端传一个q参数来调用这个体验很顺。如果坚持用Flask同样的接口会写成这样from flask import Blueprint, jsonify, request from models import db, Song bp Blueprint(songs, __name__, url_prefix/api/songs) bp.route(/) def list_songs(): q request.args.get(q, ) query Song.query if q: query query.filter(Song.title.ilike(f%{q}%)) songs query.all() return jsonify([song.to_dict() for song in songs])对比之下DRF帮你把分页、过滤、序列化的活都揽过来了Flask则给你最大的自由度。两种风格没有绝对优劣项目规模决定了选择。3.3 播放器模块普通音频与m3u8流媒体播放播放器是整个系统的门面用户看不看得到其他功能未必但播放器不好用一定会被吐槽。前端核心代码用原生audio元素实现template div classplayer audio refaudioRef :srccurrentSong.fileUrl timeupdateonTimeUpdate endedonEnded/audio button clicktogglePlay{{ isPlaying ? 暂停 : 播放 }}/button span{{ formatTime(currentTime) }} / {{ formatTime(duration) }}/span input typerange :maxduration :valuecurrentTime inputseekTo / /div /template浏览器原生audio标签支持mp3、ogg、aac、wav等格式源码指向后端返回的file_url就能直接播放。但很多人会遇到“视频/音频播不了”的问题其中一个重要场景就是m3u8格式。m3u8是HLSHTTP Live Streaming协议的索引文件本身不存音视频数据它指向一堆.ts切片文件播放器按顺序加载。这种格式在视频直播、长音频场景里很常见。难点是原生audio标签不支持直接解析m3u8需要hls.js这个库来处理。我在Vue组件里封装了一个带m3u8支持的播放器script setup import Hls from hls.js; const audioRef ref(null); const currentSong ref({}); watch(currentSong, (song) { const audio audioRef.value; if (!audio || !song.fileUrl) return; const isM3u8 song.fileUrl.endsWith(.m3u8); if (isM3u8 Hls.isSupported()) { const hls new Hls(); hls.loadSource(song.fileUrl); hls.attachMedia(audio); hls.on(Hls.Events.MANIFEST_PARSED, () audio.play()); } else if (audio.canPlayType(application/vnd.apple.mpegurl)) { // 兼容Safari原生支持 audio.src song.fileUrl; } else { audio.src song.fileUrl; } }); /script这段代码的逻辑很关键优先判断Hls.isSupported()支持就走hls.jsSafari浏览器原生支持m3u8直接赋值src即可。如果后端音频不是m3u8而是普通mp3就走else分支。一个组件同时兼容两种播放模式。播放时长显示、进度拖动更新、播放结束自动切下一首这些交互逻辑可以用Vue的响应式数据管理。播放进度和总时长通过timeupdate事件拿到seek时把audio.currentTime直接赋值即可。3.4 后台管理和数据导入有了Django Admin管理歌曲数据的工作量减少一大截。默认的Admin界面样式朴素但功能都在每个模型自动生成列表页支持搜索、分页、新增、编辑、删除。我用django-simpleui做了一下界面美化视觉效果立刻跟上配置方法很简单pip install django-simpleuisettings.py的INSTALLED_APPS里把simpleui放到django.contrib.admin之前重启服务就能看到效果。不过光靠手录数据效率还是低我给后台加了一个批量导入脚本遍历media/songs目录把文件名、时长、风格等信息自动抽出来写入数据库。时长的获取可以用mutagen这个Python库from mutagen.mp3 import MP3 import os for root, dirs, files in os.walk(media/songs): for name in files: if not name.endswith(.mp3): continue audio MP3(os.path.join(root, name)) duration int(audio.info.length) # 根据文件名风格自定义规则写入Song表其实后台管理还会有很多可扩展功能比如按歌手筛选、批量修改专辑信息、清空播放次数。Django Admin支持在ModelAdmin里配置list_filter、list_display、search_fields等属性半小时就能做到“上传文件后后台自动识别并入库”的体验。4. 联调、测试与问题排查实录4.1 Vite开发代理与跨域前后端联调时第一个拦路虎就是跨域。前端跑在5173端口Django跑在8000端口浏览器直接请求肯定报CORS错误。解决方式有两种后端加django-cors-headers或者前端配开发代理。我更推荐开发代理因为生产环境里前端静态资源和后端API通常都在同一个域下用代理模拟这个场景提前发现潜在的路径问题。在Vite工程根目录下找到vite.config.js增加server.proxy配置import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true }, /media: { target: http://127.0.0.1:8000, changeOrigin: true } } } })这样前端请求/api/xxx和/media/xxx时Vite会自动转发到Django服务。前端代码里请求地址不用写绝对路径统一以“/api”开头就行后续迁移到生产环境也只需要改代理或Nginx的转发规则。4.2 播放器不出声的排查流程播放器点下去不出声这个问题我在项目中遇到过好几次每次排查思路都在下面这条链路里。先打开浏览器开发者工具看Network标签里音频请求是否200这一步能过滤掉一半问题。第一是路径问题。如果音频请求返回404检查数据库中的file_url是否完整比如实际文件在/media/songs/xxx.mp3但库里存成了/songs/xxx.mp3路径对不上就找不到文件。Django的MEDIA_URL是/media/开头Access控制台确认一下真实路径。第二是跨域问题。如果音频请求返回CORS错误说明音频资源本身也被跨域限制了。这个问题颠簸在开发代理配置里容易漏掉/media目录我就在代理配置里单独加了一条。第三是格式支持问题。浏览器不是所有音频格式都支持比如部分老浏览器对FLAC支持度不高最好转成mp3或aac格式。我自己踩过的坑是录了一段WAV格式的长音频Chrome能播但在移动端Safari上直接黑屏不出声。第四是m3u8本身的问题。如果确定是m3u8播放失败看两点m3u8索引文件里的.ts切片路径是否为绝对URL以及切片请求是否被CORS拦截。后端返回的m3u8文件里如果切片是相对路径浏览器会基于当前域名去拼接这在某些代理配置下会导致404。还有一个隐藏问题——如果后端用StreamingHttpResponse输出音频内容但没有写正确的Content-Type浏览器会当成普通文本处理。写响应头时要指定audio/mpeg或application/vnd.apple.mpegurl。4.3 数据库连接mysqlclient与SQLite的取舍开发阶段用SQLite非常舒服零配置、单文件、备份简单。但项目要上线或者多人协作时一般会切到MySQL或PostgreSQL。Django切库在ORM层面几乎零改动但Windows下装mysqlclient经常踩坑这也是搜索词里“django install mysqlclient”被频繁搜索的原因。直接在Windows上pip install mysqlclient大概率报错提示需要Microsoft Visual C Build Tools。解决办法有三种第一种偷懒方案——用PyMySQL替代。在Django项目的__init__.py里加import pymysql pymysql.install_as_MySQLdb()然后settings.py配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: music_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, } }第二种换用PostgreSQLpsycopg2在Windows下有预编译wheel包安装顺利很多性能和功能也更强。第三种从官网下载mysqlclient的预编译whl文件离线安装但找对应Python版本的wheel有时候很费劲不如PyMySQL省事。我自己的经验是开发阶段SQLite部署阶段换PostgreSQL或MySQL两种方案都走通了。用ORM是划算的换库时模型层一行没改只改settings配置。4.4 依赖与环境高频问题速查表联调过程中环境问题占了很大比重。我把实际操作中遇到的高频问题整理成一个速查表方便排查时直接对照。问题现象可能原因解决办法pycharm终端里敲python提示找不到命令安装时没勾选Add to PATH重装Python勾选PATH或手动添加环境变量pip安装慢或超时默认源在海外配置国内镜像源修改pip.ini或使用-i参数npm install报权限错误或EACCESNode安装在系统目录用nvm管理Node版本不要用sudo npmDjango启动后页面样式全丢了DEBUGFalse时static未处理开发阶段保持DEBUGTrue或配置STATIC_ROOT并collectstatic前端npm run build后页面空白部署路径不在根目录vite.config.js里设置base: ./或用绝对路径部署django migrate提示No migrations to apply模型改动后忘记makemigrations先makemigrations再migrate两步缺一不可axios请求一直pending不返回后端服务没启动或代理配置错误确认Django运行在8000端口检查vite proxy的target上传歌曲后列表不刷新缓存问题或接口无缓存策略强制刷新浏览器或检查响应头中的Cache-Control还有一个特定场景值得单独说Vue打包后布局异常。开发时候好好的npm run build之后部署到服务器发现样式全错位。我排查之后发现根源在于路由模式选的是history部署到Nginx子路径时如果没有做try_files回退刷新页面会直接404导致路由对应的CSS和JS加载不完整。解决方法有两个路由改为hash模式或者Nginx配置try_files $uri $uri/ /index.html。我个人建议生产环境用Nginx配置回退方案保留history模式URL更干净。5. 部署上线与项目演进5.1 Vue打包与Django后端部署前端工程完成后构建部署是至关重要的一步。Vue的执行打包指令很快npm run build构建产物在dist目录里面是纯静态文件扔到任意Web服务器就能跑。Django那边用gunicorn跑应用pip install gunicorn gunicorn music_project.wsgi:application --bind 0.0.0.0:8000Nginx配置两个核心转发静态资源和API转发。前端静态资源直接用alias指向dist目录后端API反向代理到8000端口媒体文件单独一条locationserver { listen 80; server_name your-domain.com; location / { root /var/www/music-front/dist; index index.html; 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/music_project/media/; } }部署这块如果要进一步上强度可以把Django的static用collectstatic命令收集起来统一由Nginx托管数据库切到正式的MySQL或PostgreSQL文件存储迁到OSS或者对象存储。但对于个人项目和中小型应用上面这套Nginxgunicorn方案已经非常稳了。5.2 这个系统还能怎么长骨架搭好之后后续扩展方向其实挺多的。从实际价值角度我梳理了三个最值得做的方向第一个是歌词同步功能。歌曲表加一个lyrics字段存LRC格式的歌词内容前端按时间戳解析并高亮当前行体验会直接上一个档次。第二个是播放数据统计。现在song表已经有play_count字段可以扩展成独立的播放日志表记录每次播放的时间、来源、是否完整播完。基于这些数据再往推荐算法方向演进就有素材了。第三个是移动端适配或者多端同步。Vue生态在这个方面非常成熟可以把现在这套工程套上Vant或NutUI做一套移动端H5版本后端接口完全复用前端工作量主要在重新排版播放器交互。我在实际折腾这个项目时的体会是做这类系统不要一上来就奔着堆技术去先把主链路跑通再说。第一版只要能做到“打开页面-看到歌单-点播放-声音出来”整个工程就立住了。后面加歌词、加推荐、加移动端都只是在这个闭合链路上继续延伸。最开始卡环境卡了好几天的难受和现在看到播放器跑起来听到声音的踏实感相比都值了。