SpringBoot+Vue实现个性化音乐推荐系统:协同过滤与工程化实践

发布时间:2026/9/24 19:32:22
SpringBoot+Vue实现个性化音乐推荐系统:协同过滤与工程化实践 做音乐推荐系统之前我以为最麻烦的部分是推荐算法。真正把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老版本的SpringBoot2.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、passwordBCrypt加密存储、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_typeplay、collect、download、skip、create_time。这张表是推荐算法的数据基础也是数据量最大的表。必须在user_id和song_id上建联合索引behavior_type建议用int类型枚举而不是varchar查询更快存储更省。recommend_result表推荐结果快照表字段包括id、user_id、song_idsJSON字符串、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列表同步写入Rediskey的格式是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: MapsongId, 行为得分 */ private MapLong, MapLong, Double userItemPref; /** * 物品相似度矩阵 * key: songId_i, value: MapsongId_j, 相似度 */ private MapLong, MapLong, Double itemSimMatrix; public void fit(ListUserBehavior behaviors) { // 第一步统计数据集中不同行为类型的权重 // behaviorType: play1.0, collect3.0, download5.0, skip-1.0 MapLong, MapLong, 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; // 第二步计算物品共现矩阵 MapLong, MapLong, Integer cooccurrence new HashMap(); MapLong, Integer itemUserCount new HashMap(); for (MapLong, Double itemPrefs : pref.values()) { ListLong 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.EntryLong, MapLong, Integer entry : cooccurrence.entrySet()) { Long itemI entry.getKey(); double denominator Math.sqrt(itemUserCount.getOrDefault(itemI, 1)); for (Map.EntryLong, 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 ListLong recommend(Long userId, int topN) { MapLong, Double pref userItemPref.getOrDefault(userId, Collections.emptyMap()); MapLong, Double scores new HashMap(); // 对用户行为过的每个物品找出相似物品累加得分 for (Map.EntryLong, Double prefEntry : pref.entrySet()) { Long itemI prefEntry.getKey(); double prefScore prefEntry.getValue(); MapLong, Double simItems itemSimMatrix.getOrDefault(itemI, Collections.emptyMap()); for (Map.EntryLong, 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, DoublecomparingByValue().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?keywordxxx 歌曲搜索POST /api/behavior/report 上报用户行为GET /api/recommend/list 获取个性化推荐GET /api/playlist/my 我的歌单4.2 JWT身份认证实现从依赖到拦截器登录认证用JWT 拦截器是常规方案这里把完整实现路径写清楚。第一步是引入依赖我用的是jjwt库dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/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维护一个ZSetkey为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推荐接口走RedisQPS约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 vitelatest 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控制播放器是整个前端最复杂的模块因为不管用户切换到哪个视图底部播放器都要保持播放状态歌曲不能断。这意味着播放状态不能放在单个页面组件里必须放在全局状态管理器中。我用的状态管理工具是PiniaVue 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组件的