尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Vue.js音乐播放网站开发实战:从播放内核到状态管理
简介这是一份基于 Vue.js 与 JavaScript 构建的音乐播放网站项目源码面向具备前端基础、希望练习组件化开发与音频交互的开发者可用于课程设计、个人作品集或二次开发。项目围绕播放器、歌曲列表、搜索过滤、播放模式切换等模块展开涉及 Vue 数据绑定、组件封装、HTML5 Audio API 事件监听、axios 请求与 JSON 数据渲染以及 CSS3 过渡动画和 Flexbox 响应式布局等知识点。压缩包为 zip 格式整体约 84.53MB文件类型明细上游暂未提供但按项目性质应包含 Vue 单文件组件、JavaScript 脚本与样式表等前端资源。目前已有 2046 人学习下载适合想通过完整案例理解音乐播放类应用实现思路的读者参考可从中梳理组件划分、播放控制与界面美化的具体做法。1. 从零搭一个 vue / js 音乐播放网站为什么我劝你先别急着写播放器很多人第一次做 vue / js 音乐播放网站第一反应是去找一个现成的播放器组件塞进去能响就行。我当年也这么干过结果上线第二天就翻车切歌时上一首的音频还在后台偷偷播进度条和实际播放位置对不上移动端锁屏后直接失声。后来才明白音乐播放网站真正的难点从来不是「放出声」而是把播放状态、进度同步、播放列表、切歌时序这几件事管住。这个标题背后要解决的核心问题是用 Vue 的响应式数据去驱动一个原生 audio 元素让 UI 永远和真实播放状态一致。它适合两类人一类是想拿一个完整小项目练 Vue 组件通信和状态管理的开发者另一类是手上有音频资源、想做个自用播放页面的从业者。下面我按自己实际搭过的一套方案从结构、播放内核、列表管理一路讲到踩坑能照着复现。2. 先把工程骨架和播放内核立起来audio 元素到底该由谁管2.1 为什么我不建议用第三方播放器组件起步市面上能搜到的播放器组件大多把 audio 元素封装在内部对外只暴露 play、pause 这类方法。问题在于一旦你要做自定义进度条拖拽、倍速、播放列表联动就得反复和组件的内部状态打架。我的做法是自己持有 audio 元素用 Vue 的 ref 拿到 DOM所有播放行为都通过这个原生元素触发组件只负责渲染。这样状态来源只有一个不会出现「组件以为在播、audio 其实停了」的玄学问题。工程用 Vite 起依赖只装 vue 和 vue-router播放逻辑不引任何音频库。目录上把播放内核单独抽成一个 composableUI 组件只消费它暴露出来的状态和方法。这样做的直接好处是播放逻辑可以脱离组件单独测试后面加功能也不用动模板。# 初始化工程选 vue 模板 npm create vitelatest music-player -- --template vue cd music-player npm install npm install vue-router4这段命令建的是一个最小 Vue 工程路由后面用来区分「播放页」和「列表页」。注意模板选 vue 而不是 vue-ts除非你团队已经在用 TS否则初期类型定义会拖慢节奏。装 vue-router 是因为播放列表和详情页通常要分路由提前留好位置。2.2 用 composable 封装 audio一个 useAudio 的完整实现播放内核我放在src/composables/useAudio.js。核心思路是audio 元素只创建一次用new Audio()而不是绑在模板上这样切路由、切组件都不会重建元素避免播放中断。所有对外状态用 ref 暴露播放事件通过监听 audio 的 timeupdate、ended、error 回写。// src/composables/useAudio.js import { ref, shallowRef, onUnmounted } from vue export function useAudio() { // shallowRefaudio 是 DOM 对象不需要深层响应式 const audio shallowRef(null) const isPlaying ref(false) const currentTime ref(0) const duration ref(0) const volume ref(1) // 懒初始化避免 SSR 或多次调用重复创建 function ensureAudio() { if (audio.value) return audio.value const el new Audio() el.preload metadata // 只预加载元数据省流量 el.volume volume.value // 关键把原生事件回写到响应式状态 el.addEventListener(timeupdate, () { currentTime.value el.currentTime }) el.addEventListener(loadedmetadata, () { duration.value el.duration }) el.addEventListener(play, () { isPlaying.value true }) el.addEventListener(pause, () { isPlaying.value false }) el.addEventListener(ended, () { isPlaying.value false }) audio.value el return el } async function load(src) { const el ensureAudio() el.src src el.load() // 换源后必须 load否则部分浏览器不更新 duration currentTime.value 0 duration.value 0 } async function play() { const el ensureAudio() try { await el.play() } catch (e) { // 浏览器自动播放策略会 reject需要用户手势触发 console.warn(play rejected:, e.name) } } function pause() { audio.value?.pause() } function seek(sec) { if (!audio.value) return audio.value.currentTime sec currentTime.value sec } function setVolume(v) { volume.value v if (audio.value) audio.value.volume v } onUnmounted(() { // 组件卸载时暂停并释放防止后台继续播放 audio.value?.pause() audio.value null }) return { isPlaying, currentTime, duration, volume, load, play, pause, seek, setVolume } }逻辑上要盯住三点。第一shallowRef而不是ref因为 audio 是复杂 DOM 对象用深层响应式会带来无谓的性能开销甚至在某些浏览器里触发代理异常。第二load(src)里换源后必须调el.load()这是血泪经验只改 src 不调 loadChrome 下 duration 有时不刷新进度条会显示上一首的长度。第三play()用 try/catch 包住因为浏览器的自动播放策略会直接 reject不处理的话控制台一片红用户点了没反应还找不到原因。参数上preload设成 metadata 是权衡结果设 none 会导致点播放后要等一会才出声设 auto 又会在列表页把每首歌都拉一遍流量吃不消。volume默认 1实际项目里我会从 localStorage 读上次的值这里为了简洁先留默认。2.3 播放状态和 UI 的绑定进度条为什么不能直接双向绑定进度条是最容易翻车的地方。很多人图省事把input[typerange]的 v-model 直接绑到 currentTime结果拖动时 audio 的 timeupdate 事件和用户拖动互相打架进度条疯狂抖动。正确做法是分两个状态一个是 audio 回写的真实进度一个是用户拖动时的临时值。拖动中用临时值渲染松手才 seek。// 进度条组件里的关键逻辑 const dragging ref(false) const dragValue ref(0) function onInput(e) { dragging.value true dragValue.value Number(e.target.value) } function onChange(e) { // 松手才真正 seek避免拖动过程中频繁触发 seek(Number(e.target.value)) dragging.value false } // 模板里显示的进度拖动时用临时值否则用真实值 const displayTime computed(() dragging.value ? dragValue.value : currentTime.value )这里onInput和onChange的分工是关键。input 事件在拖动过程中高频触发只更新临时值change 事件在松手时触发一次才调用 seek。如果两个都调 seekaudio 会被反复设置 currentTime移动端上直接卡成幻灯片。displayTime用 computed 做切换模板里只认它逻辑就干净了。3. 播放列表与切歌时序为什么「下一首」比想象中难写3.1 播放列表的数据结构怎么定列表看着简单但数据结构定错后面切歌、随机、单曲循环全要返工。我一般用一个数组存曲目每项至少包含 id、title、artist、src、cover 五个字段再用一个 currentIndex 记录当前播放位置。不要用「当前歌曲对象」当唯一状态因为切歌时需要知道「下一首是谁」只有 index 才能算出来。// src/store/playlist.js用 reactive 做一个轻量 store import { reactive, computed } from vue export const playlistStore reactive({ tracks: [], // 曲目数组 currentIndex: -1, // 当前播放索引-1 表示没在播 mode: order, // order 顺序 / loop 单曲 / shuffle 随机 }) export const currentTrack computed(() playlistStore.currentIndex 0 ? playlistStore.tracks[playlistStore.currentIndex] : null )currentIndex用 -1 表示空状态比用 null 更省心因为数组下标运算不会因为 null 报错。mode用字符串枚举后面切歌逻辑里直接 switch比用数字可读。曲目数据我一般从public/tracks.json拉字段和上面保持一致方便替换成接口。3.2 切歌的三种模式顺序、单曲、随机怎么统一处理切歌最容易出的 bug 是「随机模式下点下一首结果又放回刚播过的那首」。根因是随机逻辑没排除当前索引。我的做法是写一个getNextIndex()把三种模式的分支都收进去切歌只调这一个函数。function getNextIndex(direction 1) { const { tracks, currentIndex, mode } playlistStore const len tracks.length if (len 0) return -1 if (mode loop) return currentIndex // 单曲循环索引不变 if (mode shuffle) { if (len 1) return 0 let next currentIndex // 循环直到随机到不同的索引避免连续重复 while (next currentIndex) { next Math.floor(Math.random() * len) } return next } // order 模式到尾部回到头部direction 支持上一首 return (currentIndex direction len) % len }(currentIndex direction len) % len这个写法同时处理了「下一首越界」和「上一首越界」两种情况direction 传 1 或 -1 即可不用写两个 if。shuffle 模式里的 while 循环在 len 很小时比如只有 2 首会多转几次但可接受如果列表特别大可以改成「维护一个已播队列」不过那是进阶优化初期没必要。切歌的完整流程是算出 nextIndex → 更新 currentIndex → 调 load(新 src) → 调 play()。注意顺序不能反必须先 load 再 play否则 play 的还是旧源。另外 load 之后 duration 是异步更新的UI 上要允许它短暂为 0别在此时做除法算百分比否则会得到 NaN。3.3 用 watch 监听 currentIndex 驱动播放把「索引变化」和「实际播放」解耦是这套方案最舒服的地方。组件里只需要改 currentIndex播放行为由 watch 统一处理。import { watch } from vue import { playlistStore, currentTrack } from /store/playlist import { useAudio } from /composables/useAudio const { load, play } useAudio() watch( () playlistStore.currentIndex, async (idx) { const track currentTrack.value if (!track) return await load(track.src) await play() } )这样无论切歌是来自「下一首」按钮、列表点击还是播放结束的 ended 事件都只改 currentIndex播放逻辑只有一份。ended 事件里调playlistStore.currentIndex getNextIndex(1)就行不用重复写 load 和 play。这种「单一数据源驱动副作用」的写法是 Vue 里管播放状态最不容易出错的方式。4. 避坑与排查这几个问题我几乎每个项目都会遇到4.1 切歌后上一首还在后台播现象是点了下一首新歌响了但旧歌的声音还叠在里面。原因是每次切歌都new Audio()创建了新元素旧的没停。解决就是第 2 章说的audio 元素全局只创建一次切歌只改 src。如果你已经写成了多元素排查方法是在控制台打印document.querySelectorAll(audio).length大于 1 就说明漏了。4.2 移动端锁屏或切后台后播放中断现象是手机锁屏后音乐停了解锁回来进度条还在走但没声音。原因是部分移动浏览器在页面不可见时会挂起 audio。解决是给 audio 元素设置el.setAttribute(playsinline, )并在页面 visibilitychange 时检查audio.paused如果被系统暂停了就尝试恢复。注意恢复播放可能被自动播放策略拦截需要用户再次交互所以更稳的做法是提示用户点击继续而不是硬恢复。4.3 进度条拖动后位置回跳现象是拖到中间松手进度条先跳到目标位置又弹回拖动前的位置。原因是 seek 之后 audio 的 timeupdate 还没触发currentTime 仍是旧值UI 读到了旧值。解决是 seek 函数里同步把 currentTime 这个 ref 也设成目标值第 2 章代码里已经这么做了让 UI 立刻拿到新值等 timeupdate 来了再覆盖成精确值。4.4 duration 显示为 NaN 或 Infinity现象是进度条总时长显示 NaN或者百分比算出来是 Infinity。原因是音频元数据还没加载完就读取了 duration此时它是 NaN。解决是在 loadedmetadata 事件里才更新 durationUI 上对 duration 为 0 或 NaN 的情况做兜底显示--:--并且算百分比前先判断duration 0。另外流式音频的 duration 可能是 Infinity这种源要特殊处理普通 mp3 不会遇到。4.5 自动播放被浏览器拦截用户点了没反应现象是页面加载后调 play() 没声音控制台报NotAllowedError。这是浏览器的自动播放策略不是 bug。解决是不要在页面加载时自动 play把首次播放绑定到用户的点击事件上。如果业务确实需要自动播放只能静音自动播放再引导用户点一下取消静音。我的习惯是首次进入页面只 load 不 play等用户点第一下再开始这样最省心。5. 进阶技巧把播放体验做扎实的几个细节到这一步基本能跑了但离「好用」还差几个细节。第一个是预加载下一首。顺序播放时可以在当前歌播到一半时用第二个 audio 元素静默 load 下一首的 src这样切歌几乎无延迟。注意这个预加载元素不要 play只 load且要在切歌后销毁或复用别越积越多。第二个是进度条的缓冲显示。audio 的 buffered 属性是个 TimeRanges 对象可以读出已缓冲区间在进度条上画一条浅色底条表示「已加载」。实现上监听 progress 事件取audio.buffered.end(audio.buffered.length - 1)作为缓冲终点除以 duration 得到百分比。这个细节对长音频体验提升明显用户知道还要等多久。第三个是键盘快捷键。空格播放暂停、左右箭头快退快进 5 秒、上下箭头调音量是音乐播放器的标配。实现时注意在输入框聚焦时不要拦截按键判断e.target.tagName是不是 INPUT 或 TEXTAREA是就放行。快捷键绑定在 document 上组件卸载时记得 removeEventListener否则切路由后旧监听还在按空格会触发两个播放器。第四个是播放失败的重试。网络抖动时 audio 会触发 error 事件此时不要直接跳过可以先重试一次再失败才提示用户。重试逻辑放在 error 监听里用一个计数器限制最多重试 2 次避免死循环。let retry 0 el.addEventListener(error, () { if (retry 2) { retry setTimeout(() el.load(), 800) // 延迟重试给网络恢复时间 } else { console.warn(音频加载失败请检查源地址) } })最后说个我自己的习惯每加一个播放相关功能我都会在真机上把「切歌、拖动、锁屏、切后台、断网」这五个动作各走一遍。桌面浏览器一切正常不代表移动端没问题很多坑只有真机才暴露。这套 vue / js 音乐播放网站的方案不复杂难的是把状态管住、把边界想到。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

卫宁电子病历表结构解析:从数据库设计到医疗信息化落地

卫宁电子病历表结构解析:从数据库设计到医疗信息化落地

简介:《卫宁电子病历表结构》是上海金仕达卫宁软件临床信息系统数据库结构设计说明书,面向医院信息化实施、EMR 二次开发及数据对接人员,用于厘清电子病历系统各表的关系与字段含义。文档覆盖系统框架、基础代码库、财务收费、医疗信息、数据…

📅 2026/10/14 2:39:59
smol developer 全程序生成实战:从 shared_deps.md 共享依赖契约解读 AI 生成的 Pong 游戏架构

smol developer 全程序生成实战:从 shared_deps.md 共享依赖契约解读 AI 生成的 Pong 游戏架构

AI Agent代码智能体 【免费下载链接】developer the first library to let you embed a developer agent in your own app! 项目地址: https://gitcode.com/gh_mirrors/de/developer 点击查看 免费下载 shared_deps.md 是 smol developer(当前仓库即其 …

📅 2026/10/14 2:34:59
Apache OpenDAL 接入 MongoDB GridFS:从配置到源码级的完整实战指南

Apache OpenDAL 接入 MongoDB GridFS:从配置到源码级的完整实战指南

数据存储后端 【免费下载链接】opendal Apache OpenDAL: One Layer, All Storage. 项目地址: https://gitcode.com/gh_mirrors/op/opendal 点击查看 免费下载 Apache OpenDAL 通过 opendal-service-gridfs 服务,为基于 OpenDAL 的应用提供对 MongoDB Gr…

📅 2026/10/14 2:34:59
MORE NEWS

更多资讯

📰

airllm逐层加载实战:4GB显存如何跑70B大模型

1. 当显存告急时,我们还能怎么跑大模型显存不够这件事,几乎每个在本地折腾过大模型的人都遇到过。你兴冲冲下载了一个 70B 参数的模型权重,打开推理脚本,结果第一行就给你甩回来一个CUDA out of memory。显卡就那么大,…

📰

YOLO26算法城市街道背包手提包纸袋行人目标检测+训练好的模型+9130张数据集+pyqt可视化界面

YOLO26算法城市街道背包手提包纸袋行人目标检测训练好的模型9130张数据集pyqt可视化界面 这套数据共 9130 张图。用 YOLO26 训了 100 轮,mAP50 0.9158,mAP50-95 0.7920。Precision 0.9433、Recall 0.8620。下面按数据构成 → 训练曲线 → 预测效果的顺序…

📰

动态多Agent工作流公测体验:架构拆解与压测实战

最近只要在搞AI应用,多Agent编排就绕不开。单Agent能力不够用,静态编排又总在各种场景里卡壳,所以动态多Agent工作流这个概念一出来,我第一时间申请了公测。前前后后跑了两周多,压过几轮高负载任务,也塞了几…

📰

线束工厂100问|端子采购需要核对哪些关键参数,避免来料不匹配?

1. 引言端子是线束连接的核心元件,来料不匹配往往会导致压接不良、拉脱力不足甚至批量返工。结合上海红同电子有限公司(www.hongtongsh.com)近30年电子高低压线束的生产经验,本文梳理端子采购时需要重点核对的关键参数&#xff0c…

📰

小说一键成片:开源AI工具如何用大模型重构短剧生产链

要说最近技术圈里什么方向最热,AI 视频生成绝对算一个。不过比起那种输入一句话生成几秒特效片段的花活,我更关注的是另一类工具——直接能把一本百万字小说变成一部可以看的短剧。这个细分赛道里有个开源项目最近表现很猛,GitHub 上狂揽了 1…

📰

NSGAII多目标优化在综合能源系统调度中的Matlab实现与调参实践

做综合能源优化调度的人,多半都遇到过这种尴尬:目标函数一多,单目标优化那套权重法就不好使了——权重拍脑袋定,结果跑出来要么成本低得离谱但碳排放高得吓人,要么反过来,折腾半天压根看不到整体权衡。所以…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬