SpringBoot+Vue实现个性化音乐推荐系统:协同过滤与工程化实践
2026/9/24 19:32:20 网站建设 项目流程

做音乐推荐系统之前,我以为最麻烦的部分是推荐算法。真正把SpringBoot后端和Vue前端完全打通、让推荐结果在页面上流畅跑起来之后,我才意识到,算法只占三成工作量,剩下七成全在工程化——用户行为怎么埋点、数据怎么清洗、冷启动怎么解决、前后端怎么联调、部署环境怎么兼容。这篇博文就是把我从零搭完这套“基于SpringBoot + Vue的个性化音乐推荐系统”的完整过程、技术选型逻辑和踩坑记录整理出来,给正准备做同类项目或者想入门推荐系统实战的同学一个可复现的参考。

1. 整体设计与技术选型思路

1.1 项目定位:先想清楚是“练手项目”还是“可上线系统”

开始动工之前,我反复问自己一个问题:这个音乐推荐系统做出来是给自己交作业用的,还是真的能支撑小规模用户使用的?这个定位差别非常大,直接影响技术选型和代码组织方式。

如果只是一个课程设计级别的demo,完全可以用内存HashMap模拟用户行为数据,前端页面走个过场,算法随便写个相似度计算就能交差。但如果想做成一个架构清晰、能扩展、能部署到服务器上给别人试听的项目,就必须把以下问题全部考虑进去:

  • 用户行为数据(播放、收藏、下载、跳过)需要持久化存储,并且要支持后续增量更新;
  • 推荐引擎要独立于业务代码,至少能做到每天定时算一次,而不是每次请求都全量计算;
  • 前后端要彻底分离,接口要标准化,前端不能依赖后端模板渲染;
  • 歌曲文件、封面图片需要静态资源管理方案,音频流播放要解决跨域和格式兼容问题。

我自己定的目标是做一个“介于demo和商业系统之间”的项目:前后端分离、具备完整的用户体系、推荐算法可选择离线计算或实时计算两条路径、部署方案支持Docker。这样项目既能拿得出手讲架构,又不会因为过度设计把自己拖死。

1.2 后端为什么选SpringBoot,而不是其他框架

现在Java后端框架基本被SpringBoot统治了,选它不稀奇,但我还是想说说它在这个项目里的几个不可替代的优势。

首先是快速构建RESTful API的能力。音乐推荐系统本质上是一个接口密集型项目,用户登录注册、歌曲搜索、歌单管理、行为上报、推荐拉取,大大小小加起来有四五十个接口。SpringBoot的starter机制加上注解式开发,让我不用关心复杂的XML配置,一个@RestController就能搞定一个模块。配合Lombok,实体类也不用写一堆getter/setter,开发效率比传统SSH高了好几个档次。

其次是生态成熟度。推荐系统绕不开数据存储和缓存,SpringBoot对MySQL、Redis、MongoDB都有非常成熟的starter支持。我在项目里用Redis缓存热门歌曲和用户推荐结果,只需要几行配置就能接入,省去了大量底层代码。

第三是部署友好。项目最终要跑在Linux服务器上,SpringBoot内嵌Tomcat,打包成jar直接java -jar启动,不需要单独安装Tomcat容器。这一点在做Docker镜像时非常省事——基础镜像基于openjdk,拿到jar包就能跑,体积小、启动快。

当然,SpringBoot也有它的问题,最大的就是版本兼容性。如果你的JDK版本比较新(比如JDK17+),老版本的SpringBoot(2.x)会有一些潜在地雷,后面我会单独讲这块。

1.3 前端选Vue 3而不是Vue 2的原因和代价

前端框架我选的是Vue 3 + Vite + Element Plus这套组合。热搜词里一大堆vue安装、vue路由、vue播放m3u8的问题,说明很多人在Vue环境上卡过壳,所以我额外多说几句选型理由。

Vue 3相比Vue 2最大的提升是Composition API。做音乐播放器这种状态复杂的界面,逻辑复用非常关键。我用Composition API把播放逻辑抽成了一个usePlayer的hook,页面组件里只需要调用这个hook就能控制播放、暂停、切歌、管理播放列表,代码复用性比Options API好了不只一个档次。如果是Vue 2,这套逻辑写起来会非常别扭,mixin的命名冲突问题也让人头疼。

Vite替代Webpack也是一个加速选择。项目开发阶段,Vite的冷启动速度几乎秒开,热更新也快,不像Webpack改动一个文件要等好几秒。代价是Vite默认只支持ESM,有些老库需要额外配置,遇到兼容性问题要多花点时间。

UI组件库选的Element Plus,表格、表单、分页、弹窗这些后台管理界面需要的组件都有现成的,配合Vue 3的响应式系统,开发效率非常高。唯一需要注意的是Element Plus按需引入的配置,网上教程很多版本不一致,照着官方文档来最稳妥。

1.4 推荐算法选型:不迷信复杂模型,先跑通协同过滤

推荐算法我用了经典的协同过滤(Collaborative Filtering),具体来说是物品协同过滤(ItemCF)。为什么不上深度学习模型?原因很简单:数据集规模不够。这类个人项目一般也就几千首歌曲、几百个用户,这个规模下深度模型不仅训练慢,效果也不一定比协同过滤好。协同过滤的优点是算法直观、实现成本低、效果可解释,只要代码写得干净,性能完全够用。

ItemCF的核心逻辑就一句话:根据用户的历史行为找到与用户喜欢的歌相似的其他歌曲,然后推荐给用户。相似度的计算基于“喜欢歌曲A的用户是否也喜欢歌曲B”,也就是通过用户行为构建歌曲之间的共现矩阵,再算余弦相似度。这种方式不需要歌曲本身的任何内容特征,纯靠用户行为驱动,非常符合“个性化”这个项目的核心卖点。

除了ItemCF,我还做了一个基于内容的粗糙推荐作为补充:给每首歌曲打标签(风格、语种、年代),用户听过的歌的标签加权平均,继续推荐同一风格的歌曲。这部分代码量不大,但能解决协同过滤的冷启动问题。

1.5 数据存储设计:MySQL存业务数据,Redis管热数据

数据存储我用了双轨方案:MySQL存用户、歌曲、歌单、行为记录、推荐结果快照等业务数据;Redis缓存热门歌曲、在线用户状态、推荐接口的热数据。

MySQL表结构设计上特别注意了行为记录表(user_behavior)的数据量预期。即使只有几千用户,每个人的播放行为累积起来也会有几十万条记录,所以这张表必须建索引,而且查询要限定时间范围和用户维度。我在user_id和song_id上都建了联合索引,查询效率在百万级数据下基本在毫秒级。

Redis在项目里承担了三个职责:一是存热门歌曲榜,二是存每个用户的推荐结果缓存,三是做登录token的会话管理。推荐结果用Redis缓存这个设计很关键,因为ItemCF离线计算完成后会写库,但用户频繁刷新推荐页不可能每次都去数据库重新拉全量结果,直接从Redis取字符串格式的歌曲ID列表,性能差出了几个量级。

2. 核心功能模块拆解与数据库设计

2.1 从用户视角梳理功能清单

做后台系统之前,我习惯先站在用户视角画一遍使用流程。整个音乐推荐系统的用户侧功能,我提炼为五个大模块:

  • 用户认证模块:注册、登录、退出、个人信息维护。登录成功后签发JWT,前端把Token存localStorage,每次请求带着Token,后端通过拦截器校验身份。
  • 歌曲浏览模块:歌曲列表、歌手列表、专辑列表,支持按风格、语种、年代筛选,支持模糊搜索(歌名、歌手、歌词)。
  • 行为记录模块:用户在页面上播放、切换、收藏、取消收藏、下载歌曲时,前端向后端上报行为事件,后端写user_behavior表。
  • 推荐模块:首页“猜你喜欢”推荐流、每日推荐歌单、相似歌曲推荐。推荐结果既支持离线计算好的,也支持用户实时行为触发的小规模计算。
  • 播放器模块:前端播放器支持音频播放、进度拖拽、音量调节、循环模式切换、播放列表管理。版权合规范围内提供试听,音频文件用静态资源服务器托管。

管理端功能相对简单:歌曲管理(增删改查、上下架)、用户管理、行为数据统计、推荐算法参数配置(排行榜权重比例、推荐数量上限)。

2.2 数据库表结构设计,附关键字段解释

数据库我建了8张核心表,这里挑几张关键的说说设计思路和容易踩的坑。

user表:用户表,字段包括id、username、password(BCrypt加密存储)、nickname、avatar、created_time。注意username要做唯一索引,注册接口要处理并发情况下的重复注册问题。

song表:歌曲表,字段包括id、title、singer、album、duration、style、language、publish_date、audio_url、cover_url、play_count、status。其中audio_url字段在高潮部分有坑——部分音乐源返回到M3U8格式的流媒体地址,前端直接放audio标签是播放不了的,后面排查篇会详细讲。

user_behavior表:用户行为表,字段包括id、user_id、song_id、behavior_type(play、collect、download、skip)、create_time。这张表是推荐算法的数据基础,也是数据量最大的表。必须在user_id和song_id上建联合索引,behavior_type建议用int类型枚举而不是varchar,查询更快,存储更省。

recommend_result表:推荐结果快照表,字段包括id、user_id、song_ids(JSON字符串)、strategy_type、update_time。离线推荐任务跑完后写入这张表,前端拉推荐接口时直接从这张表取。

playlist表和playlist_song表:歌单表与歌单歌曲关联表。维护用户自定义歌单,playlist_song表用联合主键(playlist_id, song_id)防止重复添加。

系统还设计了一张behavior_config表,用来配置不同行为类型的权重分数,因为推荐算法计算用户偏好分数时不是简单计数,而是加权求和:播放一次算1分,收藏算3分,下载算5分,完整听完额外加2分,跳过一首歌扣1分。这个权重是可配置的,能通过管理页面调整,非常实用。

2.3 推荐模块的数据流:从原始行为到推荐结果

推荐模块的完整数据流,我总结为四个阶段:

阶段一是原始行为数据采集。前端各类播放事件触发后,上报到后端的/behavior/report接口,接口校验Token拿到userId,将行为数据写入user_behavior表。

阶段二是离线计算任务。系统每天凌晨跑一次定时任务,扫描最近30天的用户行为数据,构建用户-歌曲评分矩阵,计算歌曲之间的相似度矩阵,然后为每个用户生成TopN推荐列表,写入recommend_result表。

阶段三是推荐结果缓存。定时任务跑完后,把每个用户的推荐歌曲ID列表同步写入Redis,key的格式是recommend:user:{userId},过期时间24小时。

阶段四是前端推荐展示。用户打开推荐页,前端调用/recommend/list接口,后端优先查Redis,如果Redis没有再查MySQL,返回歌曲ID列表后再通过批量查询接口拿歌曲完整信息,最后渲染时过滤掉已经下架的歌曲。

这套数据流的好处是把计算密集型的推荐任务与在线业务解耦。用户请求推荐永远不会触发重计算,最坏的情况也只是查一次MySQL,接口响应时间能控制在100ms以内。

3. 推荐系统核心算法原理与实现

3.1 ItemCF算法公式拆解,用大白话讲清楚

物品协同过滤(ItemCF)的核心是计算物品之间的相似度。我先把公式逻辑讲透,再给代码。

相似度计算公式采用余弦相似度:

similarity(i, j) = |N(i) ∩ N(j)| / sqrt(|N(i)| * |N(j)|)

其中N(i)表示喜欢/点击过物品i的用户集合。这个公式的意义是:如果很多用户都同时喜欢物品i和物品j,那么i和j的相似度就高。分母的sqrt是为了惩罚热门物品——一个被所有人点过的歌,它和任何歌的共现次数都可能高,但不代表真的相似,用分母抵消热门度偏差。

得到物品相似度矩阵后,用户u对物品j的预测兴趣分:

p(u, j) = Σ(w(u, i) * similarity(i, j))

即用户喜欢过的所有物品i的权重(行为分数就是权重),乘以i和j的相似度,累加求和。按得分从高到低排序,去掉用户已经听过的物品,取topN,就是推荐结果。

这套算法我实现的时候做了两个优化:一是只计算与用户行为物品有相似关系的候选物品,避免全物品遍历;二是设置了相似度阈值(比如小于0.1的直接扔掉),减少噪声。

3.2 代码实现:相似度矩阵计算与推荐生成

算法部分我用Java实现,核心代码如下,我加了详细的注释:

public class ItemCFRecommender { /** * 构建用户-歌曲行为数据集 * key: userId, value: Map<songId, 行为得分> */ private Map<Long, Map<Long, Double>> userItemPref; /** * 物品相似度矩阵 * key: songId_i, value: Map<songId_j, 相似度> */ private Map<Long, Map<Long, Double>> itemSimMatrix; public void fit(List<UserBehavior> behaviors) { // 第一步:统计数据集中不同行为类型的权重 // behaviorType: play=1.0, collect=3.0, download=5.0, skip=-1.0 Map<Long, Map<Long, Double>> pref = new HashMap<>(); for (UserBehavior behavior : behaviors) { pref.computeIfAbsent(behavior.getUserId(), k -> new HashMap<>()) .merge(behavior.getSongId(), behavior.getWeight(), Double::sum); } this.userItemPref = pref; // 第二步:计算物品共现矩阵 Map<Long, Map<Long, Integer>> cooccurrence = new HashMap<>(); Map<Long, Integer> itemUserCount = new HashMap<>(); for (Map<Long, Double> itemPrefs : pref.values()) { List<Long> items = new ArrayList<>(itemPrefs.keySet()); for (Long itemI : items) { itemUserCount.merge(itemI, 1, Integer::sum); for (Long itemJ : items) { if (!itemI.equals(itemJ)) { cooccurrence.computeIfAbsent(itemI, k -> new HashMap<>()) .merge(itemJ, 1, Integer::sum); } } } } // 第三步:计算余弦相似度 itemSimMatrix = new HashMap<>(); for (Map.Entry<Long, Map<Long, Integer>> entry : cooccurrence.entrySet()) { Long itemI = entry.getKey(); double denominator = Math.sqrt(itemUserCount.getOrDefault(itemI, 1)); for (Map.Entry<Long, Integer> simEntry : entry.getValue().entrySet()) { Long itemJ = simEntry.getKey(); double sim = simEntry.getValue() / (denominator * Math.sqrt(itemUserCount.getOrDefault(itemJ, 1))); if (sim >= 0.1) { // 过滤低相似度噪声 itemSimMatrix.computeIfAbsent(itemI, k -> new HashMap<>()).put(itemJ, sim); } } } } /** * 为用户生成TopN推荐 */ public List<Long> recommend(Long userId, int topN) { Map<Long, Double> pref = userItemPref.getOrDefault(userId, Collections.emptyMap()); Map<Long, Double> scores = new HashMap<>(); // 对用户行为过的每个物品,找出相似物品累加得分 for (Map.Entry<Long, Double> prefEntry : pref.entrySet()) { Long itemI = prefEntry.getKey(); double prefScore = prefEntry.getValue(); Map<Long, Double> simItems = itemSimMatrix.getOrDefault(itemI, Collections.emptyMap()); for (Map.Entry<Long, Double> simEntry : simItems.entrySet()) { Long itemJ = simEntry.getKey(); if (pref.containsKey(itemJ)) { continue; // 除去用户已经听过的 } scores.merge(itemJ, prefScore * simEntry.getValue(), Double::sum); } } return scores.entrySet().stream() .sorted(Map.Entry.<Long, Double>comparingByValue().reversed()) .limit(topN) .map(Map.Entry::getKey) .collect(Collectors.toList()); } }

3.3 冷启动问题的三种应对策略

协同过滤最大的天敌是冷启动。新用户没有行为记录,算法算不出任何东西;新歌曲没有用户行为,永远不会被推荐。我做了三层防护。

第一层,新用户策略。用户注册成功、第一次打开推荐页时,后端发现该用户没有行为记录,直接返回一个基于热门歌曲和精选歌单的“新人推荐列表”。这个列表的数据来源是Redis缓存的全站热门Top100,加上管理员手动置顶的推荐位歌曲。等用户产生了行为再切换到个性化推荐。

第二层,新歌曲策略。歌曲上传时不进入推荐候选池,而是先通过内容标签(风格、语种、年代)与老歌建立弱关联,等积累到一定量的用户行为后,再参与协同过滤计算。这样可以避免新歌完全消失的情况。

第三层,基于内容的兜底推荐。用歌曲的风格标签算一个粗糙的相似度:风格相同权重1.0,语种相同权重0.5,年代相近权重0.3。当协同过滤结果太少或者置信度低时,用内容相似度补满推荐坑位。这套兜底代码逻辑不复杂,但亲自测试后发现对用户的体验提升非常大——推荐结果再差,也不至于空着。

3.4 推荐效果的简单评估方法

作为个人项目,不可能像大厂一样搞AB测试,但我还是设计了一套简易评估法。从user_behavior表里把最近30天的数据按时间切分:前20天当训练集,后10天当测试集,用训练集生成推荐结果,看测试集里用户实际播放的歌曲有多少出现在推荐列表中,算一个“推荐命中率”。我本地跑完,命中率大概有18%到23%,比随机推荐高了将近10倍,说明算法是有效果的。

另一个评估维度是覆盖率,也就是推荐池里有多少歌曲能被推荐出去。ItemCF如果过度热门导向,会导致大量长尾歌曲永远没机会出现,所以我在生成推荐结果时,从相似度Top200里随机取一部分低热度歌曲,保证结果多样性。这个“多样性注入”的经验值是推荐列表里保留20%到30%的长尾位。

4. 后端核心接口设计与关键实现

4.1 接口规范与统一响应结构

前后端分离项目,接口规范定了,后面联调就能省掉一堆破事。我用了RESTful风格,所有接口返回统一结构:

{ "code": 200, "message": "success", "data": {} }

Java对应的实体类是Result ,code用int类型,200表示成功,401表示未认证,403表示无权限,500表示服务器内部错误。前端Axios封装了响应拦截器,根据code做统一处理,比如401的时候自动清除本地Token并跳转登录页。

接口路径我按模块划分,列举几个核心的:

  • POST /api/auth/register 用户注册
  • POST /api/auth/login 用户登录
  • GET /api/song/page 歌曲分页列表
  • GET /api/song/search?keyword=xxx 歌曲搜索
  • POST /api/behavior/report 上报用户行为
  • GET /api/recommend/list 获取个性化推荐
  • GET /api/playlist/my 我的歌单

4.2 JWT身份认证实现,从依赖到拦截器

登录认证用JWT + 拦截器是常规方案,这里把完整实现路径写清楚。

第一步是引入依赖,我用的是jjwt库:

<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>

第二步是JwtUtil工具类,负责生成Token和解析Token。生成时把userId放进去,同时设置过期时间(我设成7天,音乐App打开频率高,太短容易反复登录)。

第三步是拦截器实现。用户请求除了注册、登录、歌曲列表这些公开接口外,都带着Token,拦截器校验Token合法性并从Redis里查一下用户会话状态,如果Redis里没有说明Token被注销过,直接返回401。

第四步是解决跨域问题。前后端分离部署,前端跑在5173端口,后端跑在8080端口,跨域是必然的。我在后端配置了一个WebMvcConfigurer的CorsFilter,允许前端域名或IP:端口,放行所有请求头。

4.3 MyBatis-Plus分页查询与动态SQL

歌曲列表分页用的是MyBatis-Plus内置的分页插件。配置分页拦截器后,直接写一个Page 对象作为查询参数,MyBatis-Plus自动拼接LIMIT语句。

动态条件查询这块,MyBatis-Plus的LambdaQueryWrapper非常好用。按风格筛选就eq(Song::getStyle, style),按年代范围就between(Song::getPublishDate, start, end),多个条件组合起来就是链式调用,代码可读性比拼接SQL高太多。

唯一要提醒的是分页插件版本兼容问题。如果你用SpringBoot 3.x,就要用MyBatis-Plus 3.5.3以上版本,低版本的分页插件会直接报错。这个坑我在热搜词里看到好多人遇到了,属于版本匹配的经典问题。

4.4 Redis缓存策略:热门榜与推荐结果的双写一致性

Redis在这个项目中主要用于缓存,具体缓存策略如下:

热门歌曲榜:热门榜基于play_count字段排序,这个字段每次播放都会更新MySQL,如果每次都去数据库查Top100非常浪费。我的方案是Redis维护一个ZSet,key为hot_song_rank,每次有播放行为时,ZINCRBY命令把play_count加1。前端要热门榜数据时,直接从ZSet查Top100返回,效率极高。

推荐结果缓存:离线推荐任务跑完后,把每个用户的推荐结果序列化成JSON字符串存入Redis的String类型key。这里注意TTL设置,我设置的24小时,第二天凌晨定时任务跑完会覆盖。

双写一致性问题:因为Redis里有推荐结果,管理员手动上架新歌后,缓存中的推荐可能没有新歌。我的解决方案是不做实时更新,而是接受24小时延迟。这在实际使用中可以接受,还能少了很多麻烦。如果确实需要实时性,可以在歌曲修改接口里通过Redis的keys pattern模糊匹配后批量删除推荐缓存,触发下次请求时重建缓存。

4.5 并发与性能优化:接口响应时间实测

我把项目部署到2核4G的服务器上,用JMeter压测了核心接口。单机环境下,歌曲列表接口QPS约800,推荐接口(走Redis)QPS约1200,登录接口因为要做BCrypt校验和JWT发放在300左右。对于个人项目来说完全够用。

性能优化最大的收益来自于两个动作:一是把推荐接口的数据库查询全部换成Redis缓存,二是给MySQL慢查询加了优化。我开了MySQL的slow_query_log,发现最慢的是一条连表查询用户播放历史的SQL,耗时1.8秒。后来改成先查Redis再查单表,响应时间降到了80ms以内。

5. 前端Vue 3核心功能实现与踩坑记录

5.1 Vue 3项目搭建与目录结构设计

前端项目用Vite搭建,npm create vite@latest music-frontend -- --template vue创建。安装依赖后,我把src目录按模块组织:

src ├── api/ # 接口请求封装 │ ├── auth.js # 认证相关接口 │ ├── song.js # 歌曲相关接口 │ ├── behavior.js # 行为上报接口 │ └── recommend.js # 推荐接口 ├── assets/ # 静态资源 ├── components/ # 通用组件 │ ├── PlayerBar.vue # 底部播放器 │ ├── SongCard.vue # 歌曲卡片 │ └── SearchBox.vue # 搜索框 ├── router/ # 路由配置 ├── store/ # Pinia状态管理 │ ├── user.js # 用户状态 │ └── player.js # 播放器状态 ├── views/ # 页面组件 │ ├── Home.vue # 首页 │ ├── Search.vue # 搜索页 │ ├── Recommend.vue # 推荐页 │ ├── Playlist.vue # 歌单页 │ └── Login.vue # 登录页 └── utils/ # 工具函数 └── request.js # Axios封装

这个目录结构参考了企业级Vue项目的分层方式,把接口、页面、组件、状态管理分开,后面对着接口开发新页面会非常顺手。组件命名统一用大驼峰,页面组件放views,复用组件放components,这个习惯能让你在项目变大后少走很多弯路。

5.2 播放器核心实现:全局状态管理与Audio控制

播放器是整个前端最复杂的模块,因为不管用户切换到哪个视图,底部播放器都要保持播放状态,歌曲不能断。这意味着播放状态不能放在单个页面组件里,必须放在全局状态管理器中。

我用的状态管理工具是Pinia(Vue 3官方推荐),播放器store的核心设计如下:

export const usePlayerStore = defineStore('player', { state: () => ({ currentSong: null, playList: [], playIndex: -1, isPlaying: false, currentTime: 0, duration: 0, audio: new Audio() // 全局唯一的Audio实例 }), actions: { playSong(song) { ... }, togglePlay() { ... }, nextSong() { ... }, prevSong() { ... } } })

之所以只用new Audio()单例,是因为如果每个组件都创建自己的Audio标签,切歌时效率低,而且声音会混杂。全局单例Audio对象,配合Pinia响应式状态,任何组件都能通过store控制全局播放。

音频地址加载有个核心坑:歌曲URL来自后端接口,有些是mp3格式,有些是m3u8格式。m3u8是流媒体分片格式,原生Audio是播放不了的。我引入hls.js这个库来解析m3u8流,代码如下:

import Hls from 'hls.js' function loadAudio(url) { const audio = playerStore.audio if (url.endsWith('.m3u8') || url.includes('.m3u8')) { if (Hls.isSupported()) { audio.pause() if (hlsInstance) hlsInstance.destroy() hlsInstance = new Hls() hlsInstance.loadSource(url) hlsInstance.attachMedia(audio) hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => { audio.play().catch(err => console.warn('autoplay blocked:', err)) }) } else if (audio.canPlayType('application/vnd.apple.mpegurl')) { audio.src = url } } else { audio.src = url } }

hls.js的版本更新很快,网上有些教程还是老API,直接抄容易报错。我这里用的API是官方最新文档里的写法,实测可行。要注意的是,m3u8流媒体存在版权和带宽合规问题,项目中涉及引用第三方流媒体地址时,务必确认来源合法性,生产环境建议使用自己存储的音频文件或正规授权的CDN资源。

5.3 Axios封装与接口请求拦截

接口请求统一走Axios,我封装了一个request.js工具,核心逻辑是创建Axios实例,配置baseURL和超时时间,然后在请求拦截器里加Token,在响应拦截器里处理错误码。

import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code === 401) { localStorage.removeItem('token') router.push('/login') return Promise.reject(new Error('未登录')) } if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { ElMessage.error(error.message || '网络错误') return Promise.reject(error) } )

这里有个细节:前端开发环境用Vite的devServer代理把/api转发到后端,避免开发时跨域问题。生产环境通过Nginx反向代理,/api前缀的请求转发到后端容器。

5.4 推荐页瀑布流与虚拟滚动优化

推荐页是用户打开最多的页面,我设计成瀑布流卡片布局,每首歌曲一张卡片显示封面、歌名、歌手、播放次数。数据量大的时候直接渲染几百个卡片会很卡,我用虚拟滚动方案,只渲染可见区域内的卡片,滚动时动态更新。

Vue 3没有内置虚拟滚动组件,我用了第三方库vue-virtual-scroller。安装后把推荐列表包进 组件,性能提升非常明显。实测在1200首歌曲的列表里滚动,帧率从30fps以下提升到稳定60fps。

卡片点击播放的交互逻辑:点击卡片后调用playerStore.playSong(song),同时上报behavior/report接口。上报接口要做防抖处理,避免用户连续点击同一首歌产生大量重复记录。

5.5 前端踩过的坑:路由模式、跨域、样式隔离

这一节把我在前端实现过程中踩过的坑做个实录,很多坑搜索引擎里问的人非常多。

第一个坑是Vue Router的mode。部署到Nginx后,刷新二级页面出现404,这是因为走了history模式,需要Nginx做try_files配置,把所有路由重写到index.html。我用的配置是:

location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }

第二个坑是跨域CORS。前后端分离开发时,Axios请求后端接口会被拦截。解决方案:开发环境用Vite proxy代理,生产环境用Nginx代理,后端同时配置CORS白名单兜底。腾讯地图也好、其他第三方SDK也好,引入时都会有类似问题,统一这么处理。

第三个坑是样式污染。Vue组件的

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

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

立即咨询