尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

SSM450播放器前端实战:Vue3+Web Audio API实现歌词滚动与频谱可视化

SSM450播放器前端实战:Vue3+Web Audio API实现歌词滚动与频谱可视化 1. 为什么我会做一个叫SSM450的播放器前端先交代一下背景。我手上有一批音频设备准确说是一块型号为SSM450的嵌入式音频解码模块支持SD卡、U盘直读也能走串口和I2S输出。模块本身不带界面只有最基本的按键和一块小屏交互逻辑非常简单。项目交付时客户提了一句能不能做一个好看的界面让用户在网页上直接控制播放、切歌、看歌词、调音量于是就有了这个SSM450音乐播放器的Vue前端项目。关于命名SSM450是设备代号我沿用了它作为项目名。整个前端是用Vue 3 Vite搭的音频播放能力没有走原生Audio标签而是基于模块通过串口/WebSocket把状态透传到前端再由浏览器侧的Web Audio API负责解码渲染。也就是说我做的其实是一个设备控制系统可视化播放器的混合体而不只是把MP3文件丢进浏览器让它响。如果你也有类似的硬件播放器设备或者单纯想做一个高自由度的网页音乐播放器这篇内容应该能给你一些可复用的思路。文章中涉及的具体代码我尽量用简化版展示重点讲清楚为什么这么做以及哪些地方容易踩坑。SSM450这个项目的完整技术链路大致是这样的前端框架Vue 3 Composition API Vite状态管理Pinia音频处理Web Audio API播放、频谱、增益控制设备通信WebSocket接收设备状态Web Serial作为备选通道歌词与封面本地解析LRC优先从音频文件内置封面读取播放列表前端维护索引设备端按索引播放这个架构选择不是拍脑袋定的。当时我对比过另外两套方案一是完全用浏览器播放本地音频文件二是完全由设备解码、前端只做遥控。最终选择设备解码前端增强的混合模式是因为SSM450模块本身只支持解码WAV和MP3如果完全靠前端播放FLAC、APE这些格式就得额外处理而如果完全靠设备UI交互又太死板没法做歌词滚动、频谱可视化、拖拽进度条这些体验层的东西。2. 核心功能拆解播放控制、歌词滚动与频谱可视化2.1 播放控制状态机的设计音乐播放器最核心的其实是状态管理而不是UI。开始做之前我先列了一下播放器必须有的状态当前播放的曲目播放/暂停状态当前播放进度秒总时长音量播放模式顺序、循环、单曲循环、随机歌词当前行索引设备连接状态这些状态放在Pinia里统一管理组件只负责消费状态、触发action。这样做的直接好处是频谱组件、歌词组件、进度条组件各自从store里取数据不会因为组件层级深导致props层层传递。播放控制的核心逻辑其实是事件驱动的。设备端每100ms上报一次播放进度前端收到后更新store里的currentTime。如果是本地文件播放模式则用requestAnimationFrame去拉取audioContext.currentTime。两种来源我都保留了一个timeOffset变量用来做时钟对齐避免拖拽进度后时间跳动。播放模式的切换我用了一个循环队列来实现type PlayMode sequence | loop | single | shuffle; function switchMode(mode: PlayMode): PlayMode { const order: PlayMode[] [sequence, loop, single, shuffle]; const currentIndex order.indexOf(mode); return order[(currentIndex 1) % order.length]; }这块简单归简单但有个细节需要注意单曲循环和顺序播放的差异在当前曲目自然结束时才体现。如果只是切歌按钮触发下一首这两者没有区别。我最初把逻辑写反了导致单曲循环模式下点击下一首仍然会跳到同一首排查了半天才发现是判断条件的边界没覆盖到按钮点击和自然结束两种触发源。2.2 歌词滚动LRC解析与平滑位移SSM450项目里歌词是一大亮点也是我花时间最多的地方。设备端的SD卡根目录下有同名的.lrc文件前端通过串口/HTTP接口读取文本内容然后解析成带时间戳的歌词数组。LRC格式本身很简单形如[00:12.34]第一句歌词但我解析的时候顺手支持了多时间标签也就是同一句歌词前面可能挂了多个时间戳比如循环副歌部分。解析函数核心逻辑function parseLRC(lrcText: string): Array{ time: number; text: string } { const lines lrcText.split(\n); const result []; const timeTagPattern /\[(\d{2}):(\d{2})\.(\d{2,3})\]/g; for (const line of lines) { const tags []; let match; while ((match timeTagPattern.exec(line)) ! null) { const minutes parseInt(match[1]); const seconds parseInt(match[2]); const milliseconds match[3].length 2 ? parseInt(match[3]) * 10 : parseInt(match[3]); tags.push(minutes * 60 seconds milliseconds / 1000); } if (tags.length 0) continue; const pureText line.replace(timeTagPattern, ).trim(); if (!pureText) continue; for (const t of tags) { result.push({ time: t, text: pureText }); } } return result.sort((a, b) a.time - b.time); }解析之后歌词滚动组件需要根据currentTime找到当前行。最简单粗暴的做法是遍历数组找第一条time大于currentTime的记录它的前一条就是当前行。但如果歌词文件很大、歌曲很长每次currentTime更新约250ms一次都做一次遍历有点浪费。我在实际项目里做了一个小优化歌词索引不从头找而是记录一个lastIndex缓存。每次查找时先判断当前时间是否在lastIndex歌词的起止区间内如果在就直接返回否则从lastIndex向后或向前二分查找。这样歌词滚动非常顺滑CPU占用几乎可以忽略。歌词滚动的动画效果是核心体验。我的是这样处理的歌词区域高度固定比如360px每行歌词高度30px当前句居中显示。activeIndex变化时用CSS transform: translateY实现整体位移。不要把每行歌词都放一个独立动画那样滚动会很凌乱。.lyrics-viewport { height: 360px; overflow: hidden; position: relative; } .lyrics-track { transition: transform 0.4s ease; will-change: transform; }2.3 频谱可视化Web Audio API的AnalyserNode频谱组件是播放器里最有视觉冲击力的部分。虽然SSM450设备自带解码但我还是把音频流接进了浏览器做分析。具体做法是如果播放源是设备通过网络推流就通过Web Audio API的decodeAudioData解码后播放同时创建一个AnalyserNode挂在AudioContext上。如果你播放的是本地文件就简单了直接元素或者AudioBufferSourceNode都行。关键代码const audioContext new AudioContext(); const analyser audioContext.createAnalyser(); analyser.fftSize 256; analyser.smoothingTimeConstant 0.82; const source audioContext.createMediaElementSource(audioElement); source.connect(analyser); analyser.connect(audioContext.destination);fftSize取值需要注意。fftSize是FFT窗口大小必须是2的幂。取256时频域数据有128个频段画起来比较好看取值太小频谱颗粒感太重取值太大比如2048低频响应虽然更细腻但渲染开销也上去了。移动端我建议用128到256之间桌面上可以开到512。画频谱的方式我用Canvas 2D而非WebGL理由很简单128个柱状条用fillRect就能跑满60帧完全没必要上WebGL。网上很多教程喜欢用3D旋转的频谱视觉效果确实炫但代码复杂度高而且对SSM450播放器这种工具属性强的产品来说清晰看到高中低频的能量分布才是核心需求。我的绘制代码大致是function drawSpectrum() { const bufferLength analyser.frequencyBinCount; const dataArray new Uint8Array(bufferLength); analyser.getByteFrequencyData(dataArray); canvasContext.clearRect(0, 0, canvas.width, canvas.height); const barWidth canvas.width / bufferLength * 0.7; for (let i 0; i bufferLength; i) { const value dataArray[i] / 255; const barHeight value * canvas.height; const x i * (barWidth 2); const y canvas.height - barHeight; const gradient canvasContext.createLinearGradient(0, y, 0, canvas.height); gradient.addColorStop(0, #00d4ff); gradient.addColorStop(1, #7b2ff7); canvasContext.fillStyle gradient; canvasContext.fillRect(x, y, barWidth, barHeight); } requestAnimationFrame(drawSpectrum); }有一个很关键的体验问题直接画原始频域数据频谱跳动会非常“碎”视觉上不够柔和。smoothingTimeConstant我设成0.82后柱子变化就有了自然的阻尼感。很多播放器频谱看起来舒服不是因为数据好而是平滑系数调得好。3. 设备通信选型WebSocket为主、Web Serial为辅3.1 WebSocket双向指令协议SSM450模块支持以太网口能通过TCP对外暴露一个简单的指令服务。我在固件端模拟了一套JSON指令协议前端通过WebSocket发送命令设备实时返回状态。协议格式长得像这样{ cmd: play, params: { index: 3 } }设备返回{ type: status, data: { playing: true, index: 3, currentTime: 82.5, duration: 245.3, volume: 60 } }这个协议看似简单但有几个细节成败攸关第一是状态是全量推送还是增量推送。我用的是全量推送设备每500ms上报一次完整状态。虽然带宽开销略大JSON大概几百字节但胜在前端逻辑简单不需要合并增量状态也不会因为漏了一条增量消息导致状态不一致。在局域网环境下这完全没问题。第二是音量变化和播放进度更新的频率。进度用秒为单位浮点精度保留一位即可不需要毫秒级。毫秒级数据会让前端频繁刷新导致抖动。如果歌词滚动需要更精确的时间就再用一个timeShift字段做校正。第三是断线重连。Wi-Fi环境也好、有线网络也好场景里总有短暂断网的情况。我实现了指数退避重连机制初次重连延迟1秒之后翻倍最大延迟30秒。同时前端UI上给出连接状态指示灯设备断开超过10秒自动切换为灰色。3.2 Web Serial作为调试备选说到Web Serial我是抱着锦上添花的心态加进去的。SSM450本身也可以走USB串口来控制而Web Serial API在Chromium内核浏览器里可以让我们直接用浏览器读写串口不需要安装驱动程序。使用前需要用户主动点击连接设备然后从弹出列表里选择对应的COM口这是Chrome的安全策略限制。关键代码async function connectSerial() { const port await navigator.serial.requestPort(); await port.open({ baudRate: 115200 }); const reader port.readable.getReader(); while (true) { const { value, done } await reader.read(); if (done) break; handleDeviceResponse(new TextDecoder().decode(value)); } }这个功能实际用下来发现一个问题Chrome的串口读写如果不加流控遇到设备端一次返回大量状态数据时容易丢失。解决办法是在设备端做分包输出每条指令响应结束后追加换行符前端用readLine的方式逐行解析。再不行就给每条消息加个序号前端检测到序号不连续就主动请求一次全量状态同步。我个人的建议是如果你的设备同时有网络口和串口优先用网络口做WebSocket功能稳定、调试方便串口只留给那些实在没有网络口的场景。Web Serial目前Edge和Chrome支持最好Firefox上Gecko还在推进Safari别想了。4. 播放列表与本地文件管理拖拽上传、索引同步还有那个烦人的格式兼容4.1 前端文件拖拽上传因为设备本身带有SD卡槽很多使用场景其实是用户提前把歌拷进SD卡前端播放器只是遥控器。但我也做了一个本地文件拖拽上传的功能方便在没有SD卡的情况下快速试听歌曲。实现方式是用HTML5的Drag and Drop File API拖入文件后读取FileList然后创建对象URLfunction handleDrop(event: DragEvent) { const files Array.from(event.dataTransfer.files) .filter(file /\.(mp3|wav|flac|ape)$/i.test(file.name)); const playlist files.map(file ({ title: file.name.replace(/\.(mp3|wav|flac|ape)$/i, ), url: URL.createObjectURL(file), size: file.size, duration: 0, })); pinnedStore.addSongsToPlaylist(playlist); }这里有个容易忽略的问题用URL.createObjectURL生成的blob URL占内存是实打实的歌曲数量多的时候几十个blob URL就能吃掉上百MB内存。所以我在切歌或移除歌曲时及时调用URL.revokeObjectURL释放掉不再使用的URL。4.2 格式兼容与编码检测格式兼容说多了都是泪。SSM450模块本身只能硬解MP3和WAVFLAC、APE这类无损格式是前端处理的。前端对FLAC的解码能力取决于浏览器是否支持audio/mpeg、audio/flac这些MIME类型。实测下来Chrome和Edge原生支持FLAC播放Safari支持FLAC但部分老版本有问题Firefox播放FLAC没问题但有些FLAC内嵌封面图无法显示APE格式就麻烦得多。我查过一圈浏览器原生不支持APE解码也没有靠谱的纯JS解码库。最终方案是不主动支持APE但也不禁止用户拖入——拖入后前端尝试用AudioContext.decodeAudioData解码失败则提示暂不支持该格式。不要在这里死磕不值得。标题、歌手、专辑这些元数据可以从音频文件里读。前端最靠谱的方案是解析ID3v2标签。mp3-parser库或者music-metadata库都行前者轻量一些后者功能全但体积大。Vite项目我用的是动态import按需加载避免主bundle体积膨胀。async function readTags(arrayBuffer: ArrayBuffer) { const metadata await import(music-metadata); const result await metadata.parseBlob(new Blob([arrayBuffer])); return { title: result.common.title || 未知标题, artist: result.common.artist || 未知艺术家, album: result.common.album || , cover: extractCover(result), }; }读取封面图的思路是先看common.picture数组取到image数据转成Blob URL如果没有再尝试从同目录下的cover.jpg读取。这招对没有内嵌封面的老歌非常实用。5. 组件架构一个解耦播放器核心状态与视图层的最佳实践5.1 组件树是怎么拆的我把整个播放器页面拆成了下面这些组件PlayerContainer布局容器负责整体排版PlaylistPanel播放列表面板支持拖拽排序、双击播放AudioVisualizer波形/频谱显示组件LyricDisplay歌词滚动组件ProgressBar进度条支持点击/拖拽seekControlButtons播放/暂停/上一首/下一首/播放模式按钮组VolumeSlider音量滑杆DeviceStatusBar设备连接状态指示这是一个非常传统的拆分方式好处是每个组件职责单一、可复用。如果你做的是纯前端播放器这套拆法完全够用。如果你还要做多端适配可以考虑把状态逻辑抽到composables里方便跨端复用。我实际项目中把播放器状态和UI做了彻底分离。播放器相关状态全部集中在usePlayerStore这个Pinia store里组件只通过storeToRefs拿数据const playerStore usePlayerStore(); const { currentTrack, isPlaying, currentTime } storeToRefs(playerStore); const { play, pause, next, previous } playerStore;这样Spectrum组件、歌词组件、进度条组件都各自独立订阅currentTime数据源统一不会出现进度条显示的时间和歌词滚动的时间对不上这种低级bug。5.2 进度条拖拽与时间同步进度条看似简单实际拖拽的时候有很多细节。基础逻辑是点击事件根据点击位置计算目标时间调用seek指令拖拽事件按下时记住开始位置移动时更新预览时间松开后发送seek实现上有两个坑一个是拖拽过程中不能直接改播放器真实进度。我用了两个变量displayTime展示时间和actualTime真实播放时间。拖拽中展示的是鼠标位置对应的时间拖拽结束才跳出actualTime并发送seek指令。另一个是设备网络延迟。从点击到设备真正播放到对应位置需要几百毫秒所以进度条拖拽后不要立刻把进度条强制对齐到actualTime而是留一个短暂的缓冲期。我这边设置的是300ms后如果状态还没到位再同步。不然会出现肉眼可见的回跳画面非常掉价。进度条组件的核心模板大概是div classprogress-bar refbarRef mousedownstartDrag div classprogress-track div classprogress-fill :style{ width: progress % }/div div classprogress-thumb :style{ left: progress % }/div /div /div5.3 深色主题与内容可读性播放器的UI主题我做了深色玻璃拟态风。深色底的优点是频谱、歌词的高亮色特别突出视觉重心会集中在正在进行的播放上。但UI要实用不能为了好看牺牲可读性。歌词区域我加了半透明渐变遮罩歌词滚动时当前行高亮描边翻页区域用mask-image做淡出效果。字体、间距、圆角这些细节我是按照远距离可读的标准做的。因为这个播放器可能被投屏到电视或者挂在墙上的触屏一体机上字号就不能按手机端的标准来。歌词字号我做到了20px以上按钮最小触控面积44x44px进度条高度从默认的4px加宽到8px。6. SSR视角与移动端适配响应式布局的实战经验很多音乐播放器在PC上是横屏布局左边封面、中间歌词、下面控制栏。但一旦塞进手机里这种布局就要完全重排。我这次特意为SSM450做了移动端适配踩了几个印象深刻的坑。第一个是频谱显示不全的问题。PC端我用Canvas宽度800px画128根柱子手机端Canvas宽度只有360px柱子挤成一团根本看不清。解决办法不是减少柱子数量而是让Canvas根据设备像素比缩放canvas.width container.offsetWidth * window.devicePixelRatio; canvas.height container.offsetHeight * window.devicePixelRatio; canvasContext.scale(window.devicePixelRatio, window.devicePixelRatio);同时根据容器宽度动态计算柱宽和间距const barWidth (canvas.width / bufferLength) * 0.55; const gap 4;PC上gap可以是8手机上只能压到4否则柱子太细视觉上稀疏得很难看。第二个坑是移动端的触摸事件和点击事件冲突。在手机浏览器上click事件在touchend之后大约300ms才会触发如果拖拽进度条时绑定了click事件会触发两次seek。解决方式是在拖拽开始时标记isDragging结束时拦截click事件。加一个0延迟的pointerup处理用PointerEvent替代MouseEvent问题就绕开了。第三个是Web Audio API在移动端的自动播放限制。浏览器不允许在没有用户手势的情况下播放音频这是safari和Chrome移动端的明文规定。我的处理方法是在用户点击连接设备或加载播放列表的按钮时提前调用一次audioContext.resume()让AudioContext处于运行态。否则用户切歌后音频不响排查半天才发现是浏览器策略搞的鬼。移动端布局我还特意做了播放页和列表页的切换式设计而不是把列表直接放在旁边。底部固定控制栏不变上方区域通过Vue Router或者条件渲染切换视图。这样单手操作时不会被误触。7. 性能优化与线上部署经验从首屏加载到长列表渲染7.1 首屏加载优化与分包策略这个播放器前端的首屏加载指标我一直盯着。Vue3 Vite项目常见的问题是把所有组件打包到一个JS包里代码可能1MB以上。我做了几层优化第一是路由懒加载。播放器虽然只有一个页面但文件拖拽、音乐元数据解析这些功能做了动态导入用户没拖文件进来的时候连music-metadata都不会被加载。const PlaylistPanel defineAsyncComponent(() import(/components/PlaylistPanel.vue));第二是UI组件库按需引入。我用了Element Plus的少量组件主要是Slider、Icon用按需引入插件把组件和样式裁剪到最小省掉了全部引入时的几百KB。第三是CDN缓存。生产环境的构建产物上传到CDN后把index.html的cache-control设置成no-cache把静态资源设置成immutable。这样版本更新时用户能拿到新页面老资源又能充分利用缓存不会每次刷新都重新下载所有文件。7.2 长列表渲染10万首歌曲的播放列表怎么办有测试同事一次性导入了几万首歌播放列表渲染卡到不行。我优化前用的是最简单的v-for几千条就开始卡顿别说几万条了。换用虚拟滚动列表之后问题彻底解决。我选用了vue-virtual-scroller只渲染可视区域内的歌曲条目。具体配置virtual-scroller :itemsfilteredPlaylist :item-height48 v-slot{ item } !-- 歌曲条目 -- /virtual-scroller使用虚拟滚动要注意item-height必须是固定值不能有动态高度。我的歌曲条目信息固定一行所以高度48px没问题。如果你需要多行信息需要自己计算高度或者用动态测量方案复杂度和性能都会打折扣。还有一个容易被忽视的点播放列表搜索。歌曲多的时候用户一定会用搜索。我的搜索框做了300ms防抖然后基于标题、歌手做fuzzy匹配。模糊搜索我用到了fuse.js效果挺好就是库体积有点大。后来我自己写了一个简单的子串匹配关键词高亮效果也够用体积几乎为零。7.3 设备推流性能音频缓冲区与延迟控制设备端把音频数据通过WebSocket不断推给前端前端接收后写入Web Audio的缓冲区。这里最需要关注的是缓冲策略。如果前端消费速度跟不上推流速度声音会卡顿如果缓冲太多声音延迟会变大切歌和拖拽进度时明显感觉反应慢。我采用的方案是前端维护一个环形缓冲区目标缓冲时长200ms。当缓冲数据低于100ms时暂停播放等待高于400ms时丢弃部分数据。这样切歌后最快能保证声音几乎无感衔接。这里需要说明一下Web Audio API本身不是流式的AudioBufferSourceNode不支持流式追加数据。真要流式播放要么用AudioWorklet自定义播放节点要么转用Media Source ExtensionsMSE。当时为了快速跑通我选了AudioWorklet。如果你想做真正的流式音频建议先从AudioWorklet入手文档虽然少但可控性比MSE强很多。8. 上线实测与踩坑记录三个让我印象深刻的bug排查过程8.1 Bug 1进度条拖拽回跳问题现象很直观用户拖拽进度条到第90秒松手后进度条先显示90秒半秒后跳回拖动前的时间比如60秒再隔一秒才慢慢走到90秒。排查后发现是两个问题叠加导致的。第一个是WebSocket消息延迟。拖动时前端显示displayTime为90秒但设备端的播放进度还停在60秒之前上报的status消息在队列里积压到达前端后直接覆盖了displayTime。修复方案是给所有status消息加一个reqId只有最新的请求结果才能更新进度条。第二个是设备端在接收到seek指令后没有立刻停止上报旧状态所以前端连续收到几条旧数据。我在设备固件里加了一行seek后立即上报一次新状态的逻辑问题就解决了。这个bug教会我一个通用原则任何需要交互控制的硬件设备状态上报必须考虑用户操作瞬间的状态跃迁。8.2 Bug 2频谱在播放一段时间后突然静止这个bug是测试过程中发现的。歌曲播到2分多钟时频谱条突然全部拉直成一条线。浏览器控制台也没有报错。检查代码发现AudioContext的sampleRate和设备的采样率不一致时AnalyserNode可能因为数据量不足导致getByteFrequencyData返回全零。我的解决方式是在AudioContext初始化时显式设置采样率const audioContext new AudioContext({ sampleRate: 44100 });然后检测currentTime是否正常推进如果发现时钟停滞重建整个AudioContext。重构代码时要注意把旧context的引用置null再创建新实例避免内存泄漏。8.3 Bug 3歌词和音频不同步越往后差越多歌词一开始对齐播放到后段就偏差好几秒。一开始以为是设备时钟漂移后来发现是LRC解析时毫秒单位错了。我解析LRC时每首歌词时间标注是[01:05.29]这个29是92毫秒还是290毫秒绝大多数LRC文件约定是百分之一秒即29表示290毫秒。而我最初解析时当成了29毫秒导致歌词时间标偏短越到后面累积误差越大。这个bug很隐蔽因为歌曲前30秒误差只有几十毫秒人能感知出来的偏差要到一两百毫秒才明显刚好在30秒以后才暴露。修改解析逻辑后用几首已知歌词时间轴精确到毫秒的歌曲做回归测试终于完全对齐。歌词同步测试我还借鉴了一个技巧直接对比设备上报的currentTime和歌词解析出的nextLine.time画一条偏差曲线。偏差稳定在±100ms以内就算合格超过200ms就得查时钟同步问题。9. 后续扩展与核心经验复盘SSM450这个项目做下来播放器功能已经稳定但我个人觉得还有两块值得继续折腾。第一块是音频指纹与自动匹配歌词。现在歌词依赖本地同名LRC文件如果没有就空白。可以试试把MusicBrainz或者网易云开放接口的歌词匹配功能接进来根据音乐的标题和时长做近似匹配。但注意这里要处理版权和合规问题我建议只在本地解析展示不做云端存储。第二块是把播放器的核心能力抽成一个可复用的npm包。毕竟播放列表管理、歌词解析、频谱绘制、WebSocket状态同步这套逻辑换一个硬件设备或者另一个前端框架都可以复用。把store、composables、组件全拆开做成一个headless的核心包加一套默认皮肤组件会是更好的工程架构。再分享一个做硬件联动播放器的通用心得尽量把硬件相关状态抽象成统一接口。比如音量、播放状态、进度这三个字段无论底层是WebSocket、串口还是BLE上层的播放器UI不应该感知差异。我在项目里定义了一个DeviceAdapter接口WebSocket适配器和Serial适配器都实现它后续如果要再接一个蓝牙音箱只需要新增一个BLEAdapterUI层完全不需要改动。如果你也正在折腾这类设备播放器建议在动手写组件之前先把状态图、协议格式、异常处理这三件事想清楚。状态图决定交互的边界协议格式决定前后端联调的效率异常处理决定产品在弱网环境下的存活率。这三件事想清楚了开发一个月就能跑通要是边写边定大概率后期推翻重来。SSM450播放器的Vue前端开发到这里告一段落。如果你有类似的设备控制项目或者准备在网页里做一个更自由的音乐播放器希望这份经验复盘对你有帮助。有问题可以给我留言看到会回。
返回列表