作为一个长期从事Web开发的工程师,我最近用Flask+Vue.js技术栈完成了一个音乐分享平台的全栈项目。这个平台的核心目标是让音乐爱好者能够自由上传、分享和发现优质音乐内容,同时建立基于共同音乐品味的社交关系网络。
选择Flask作为后端框架主要基于以下几点考量:
前端技术选型上,Vue.js的渐进式特性让我们可以按需引入功能模块。特别是配合Vuex的状态管理,能优雅地处理跨组件的数据流(比如全局播放器状态)。实测下来,Vue 3的Composition API比Options API更利于复杂交互的逻辑组织。
整个后端采用经典的三层架构:
code复制app/
├── controllers/ # 路由层
├── services/ # 业务逻辑
├── models/ # 数据模型
└── utils/ # 工具类
数据库选用MySQL 8.0,主要考虑到:
一个典型的歌曲模型定义如下:
python复制class Song(db.Model):
__tablename__ = 'songs'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
artist = db.Column(db.String(100))
duration = db.Column(db.Integer) # 秒数
file_path = db.Column(db.String(255))
uploader_id = db.Column(db.Integer, db.ForeignKey('users.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 关系定义
comments = db.relationship('Comment', backref='song', lazy='dynamic')
likes = db.relationship('Like', backref='song', lazy='dynamic')
前端项目采用Vue CLI搭建,主要模块划分:
code复制src/
├── api/ # 接口封装
├── assets/ # 静态资源
├── components/ # 公共组件
├── router/ # 路由配置
├── store/ # Vuex状态
└── views/ # 页面组件
特别值得分享的是音频播放器的全局状态管理方案:
javascript复制// store/modules/player.js
const state = {
currentSong: null,
playlist: [],
isPlaying: false,
currentTime: 0
}
const mutations = {
SET_CURRENT_SONG(state, song) {
state.currentSong = song
state.isPlaying = true
},
TOGGLE_PLAY(state) {
state.isPlaying = !state.isPlaying
}
}
// 在组件中使用
this.$store.commit('player/SET_CURRENT_SONG', songData)
文件上传是系统的核心功能之一,我们实现了:
python复制ALLOWED_EXTENSIONS = {'mp3', 'wav', 'ogg'}
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return {'error': 'No file part'}, 400
file = request.files['file']
if file.filename == '':
return {'error': 'No selected file'}, 400
if not allowed_file(file.filename):
return {'error': 'Invalid file type'}, 400
# 生成唯一文件名
filename = secure_filename(file.filename)
unique_name = f"{uuid.uuid4().hex}_{filename}"
save_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_name)
file.save(save_path)
# 提取音频元数据
try:
audio = TinyTag.get(save_path)
duration = audio.duration
except:
duration = 0
return {
'filename': unique_name,
'duration': duration
}, 200
采用WebSocket实现实时评论推送:
python复制# WebSocket路由
@socketio.on('connect')
def handle_connect():
emit('response', {'data': 'Connected'})
@socketio.on('new_comment')
def handle_new_comment(data):
song_id = data.get('song_id')
content = data.get('content')
# 保存到数据库
comment = Comment(
content=content,
user_id=current_user.id,
song_id=song_id
)
db.session.add(comment)
db.session.commit()
# 广播新评论
emit('new_comment', {
'user': current_user.username,
'content': content,
'time': datetime.utcnow().isoformat()
}, broadcast=True, room=song_id)
前端对应实现:
javascript复制// 建立WebSocket连接
const socket = io.connect('http://localhost:5000')
// 监听特定歌曲的评论
socket.on('new_comment', (data) => {
this.comments.unshift({
user: data.user,
content: data.content,
time: formatTime(data.time)
})
})
// 提交评论
methods: {
submitComment() {
socket.emit('new_comment', {
song_id: this.songId,
content: this.commentText
})
this.commentText = ''
}
}
针对热门歌曲页面的N+1查询问题:
python复制# 反例 - 产生N+1查询
songs = Song.query.all()
for song in songs:
print(song.comments.count())
# 优化方案 - 使用joinedload
from sqlalchemy.orm import joinedload
songs = Song.query.options(
joinedload(Song.comments),
joinedload(Song.likes)
).all()
实现图片和音频的按需加载:
vue复制<template>
<img v-lazy="song.coverUrl" alt="cover">
</template>
// 在main.js中注册指令
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
preLoad: 1.3,
attempt: 3
})
使用Docker Compose编排服务:
yaml复制version: '3'
services:
web:
build: .
ports:
- "5000:5000"
environment:
- FLASK_ENV=production
depends_on:
- db
- redis
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=secret
- MYSQL_DATABASE=music_app
volumes:
- db_data:/var/lib/mysql
redis:
image: redis:alpine
volumes:
db_data:
优化静态资源服务:
nginx复制server {
listen 80;
server_name example.com;
location / {
proxy_pass http://web:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /static/ {
alias /app/static/;
expires 30d;
add_header Cache-Control "public";
}
location /media/ {
alias /app/uploads/;
expires 30d;
add_header Cache-Control "public";
}
}
python复制from flask_cors import CORS
CORS(app, resources={
r"/api/*": {
"origins": ["http://localhost:8080"],
"methods": ["GET", "POST", "PUT", "DELETE"],
"allow_headers": ["Content-Type", "Authorization"]
}
})
javascript复制const audio = document.createElement('audio')
const canPlayMP3 = audio.canPlayType('audio/mpeg') !== ''
const canPlayOGG = audio.canPlayType('audio/ogg') !== ''
// 根据支持情况返回对应格式的URL
getAudioUrl() {
return canPlayMP3 ? this.song.mp3Url : this.song.oggUrl
}
python复制app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB
app.config['UPLOAD_TIMEOUT'] = 3600 # 1小时
这个项目从技术选型到最终上线历时3个月,最大的收获是深入理解了现代Web应用的完整生命周期。特别是在处理音频流和实时交互时,需要考虑的边界情况远比静态内容复杂得多。如果让我重新设计,可能会尝试用WebRTC来实现更高效的音频传输方案。