Web前端之原生实现本地音乐播放器、随机获取下标与十六进制颜色、元素始终处于可视区域、音乐播放器细节处理、自定义音频播放器、可视化渲染引擎、文件系统访问、键盘按下监听_vue 全文检索
MENU
- 前言
- 效果图
-
- 升级版(私信可获取完整代码)
- 基础版(文章末尾有完整代码)
- 知识点
-
- 核心领域
- 编程范式
- 功能点
-
- 核心功能模块
- 用户体验增强
- 技术点
-
- 核心技术栈
- 关键技术实现
- 性能优化策略
- 架构设计解析
-
- 模块化结构设计
- 核心组件交互
- 关键技术实现详解
-
- 文件系统访问
- 音频处理流水线
- 可视化渲染引擎
- 用户体验优化设计
-
- 播放控制优化
- 视觉引导设计(动态反馈系统)
- 性能与兼容性考量
-
- 资源管理策略(关键优化点)
- 浏览器兼容方案
- 扩展性设计
-
- 架构扩展点(可插拔模块设计)
- 数据持久化方案(存储扩展选项)
- 总结与展望
- 基础版完整代码
-
- html
- JavaScript
- Style(scss)
- 最后的话
前言
概述1
在Web技术日新月异的今天,浏览器已从简单的文档查看器进化为强大的应用平台。本项目展示的本地音乐播放器正是这一进化的典型案例。通过HTML5和现代JavaScript API,实现了传统上需要桌面应用才能完成的功能到直接访问本地文件系统并播放音乐,同时提供了媲美专业软件的音频可视化效果。
概述2
该音乐播放器完整实现了从文件管理到音频播放的全流程功能,核心技术点包括现代浏览器API的深度整合、实时音频处理算法、响应式UI架构以及跨设备交互支持。系统通过模块化设计将文件处理、音频引擎和可视化渲染解耦,确保了功能扩展性和性能稳定性。
优势
1、零部署成本:无需安装,打开浏览器即可使用
2、跨平台兼容:在Windows、macOS、Linux等系统上表现一致
3、数据隐私性:所有音乐文件仅在本地处理,不上传至任何服务器
效果图
升级版(私信可获取完整代码)
基础版(文章末尾有完整代码)
知识点
核心领域
现代Web API集成
1、File System Access API:实现本地文件系统交互
2、Web Audio API:处理音频分析和可视化
3、Canvas API:动态渲染音频可视化效果
前端架构设计
1、状态管理:播放模式/当前曲目/音量等状态维护
2、事件驱动架构:键盘/鼠标/音频事件协同工作
3、响应式UI:播放状态与UI元素实时同步
性能优化
1、动画帧管理:requestAnimationFrame/cancelAnimationFrame
2、节流控制:防止高频操作(如颜色生成)
3、对象URL:高效加载本地音频文件
编程范式
函数式编程
1、高阶函数(getThrottledRandomColor)
2、纯函数(时间格式化)
3、闭包应用(状态封装)
异步编程
1、async/await处理文件读取
2、Promise链式调用
3、事件监听器管理
功能点
核心功能模块
文件管理
1、本地文件夹选择
2、MP3文件自动扫描
3、动态播放列表生成
播放控制
1、播放/暂停切换
2、进度条拖拽定位
3、三种播放模式(单曲/列表/随机循环)
4、上一曲/下一曲导航
音频处理
1、实时频谱可视化
2、音量精确控制(滚轮/按键)
3、时间格式化显示
用户体验增强
交互设计
1、键盘快捷键(空格播放/方向键控制)
2、进度条悬停时间预览
3、播放列表自动滚动定位
视觉反馈
1、当前歌曲高亮显示
2、播放状态颜色变化
3、动态音频可视化效果
状态持久
1、音量记忆功能
2、播放模式保持
3、当前曲目自动恢复
技术点
核心技术栈
#mermaid-svg-8g8Ys6YfbjfYFHGH {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-8g8Ys6YfbjfYFHGH .error-icon{fill:#552222;}#mermaid-svg-8g8Ys6YfbjfYFHGH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8g8Ys6YfbjfYFHGH .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-8g8Ys6YfbjfYFHGH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8g8Ys6YfbjfYFHGH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8g8Ys6YfbjfYFHGH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8g8Ys6YfbjfYFHGH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8g8Ys6YfbjfYFHGH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8g8Ys6YfbjfYFHGH .marker.cross{stroke:#333333;}#mermaid-svg-8g8Ys6YfbjfYFHGH svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8g8Ys6YfbjfYFHGH .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8g8Ys6YfbjfYFHGH .cluster-label text{fill:#333;}#mermaid-svg-8g8Ys6YfbjfYFHGH .cluster-label span{color:#333;}#mermaid-svg-8g8Ys6YfbjfYFHGH .label text,#mermaid-svg-8g8Ys6YfbjfYFHGH span{fill:#333;color:#333;}#mermaid-svg-8g8Ys6YfbjfYFHGH .node rect,#mermaid-svg-8g8Ys6YfbjfYFHGH .node circle,#mermaid-svg-8g8Ys6YfbjfYFHGH .node ellipse,#mermaid-svg-8g8Ys6YfbjfYFHGH .node polygon,#mermaid-svg-8g8Ys6YfbjfYFHGH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8g8Ys6YfbjfYFHGH .node .label{text-align:center;}#mermaid-svg-8g8Ys6YfbjfYFHGH .node.clickable{cursor:pointer;}#mermaid-svg-8g8Ys6YfbjfYFHGH .arrowheadPath{fill:#333333;}#mermaid-svg-8g8Ys6YfbjfYFHGH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8g8Ys6YfbjfYFHGH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8g8Ys6YfbjfYFHGH .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-8g8Ys6YfbjfYFHGH .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-8g8Ys6YfbjfYFHGH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8g8Ys6YfbjfYFHGH .cluster text{fill:#333;}#mermaid-svg-8g8Ys6YfbjfYFHGH .cluster span{color:#333;}#mermaid-svg-8g8Ys6YfbjfYFHGH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8g8Ys6YfbjfYFHGH :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 核心系统 文件系统 音频引擎 渲染系统 目录选择器 MP3扫描 Web Audio API 播放控制逻辑 Canvas可视化 DOM动态更新
关键技术实现
音频可视化引擎
1、基于AnalyserNode的频率分析
2、双声道对称频谱渲染
3、节流随机颜色生成算法
4、动画帧精准控制
播放状态机
1、多模式切换机制(单曲/列表/随机)
2、播放结束自动续播逻辑
3、异常状态处理(未加载/错误)
高效DOM管理
1、事件委托处理动态列表
2、CSS变量驱动UI变化
3、滚动定位优化算法
输入处理系统
1、统一事件处理中心
2、键盘/鼠标/触摸事件归一化
3、防止默认行为冲突
性能优化策略
资源管理
1、对象URL内存回收
2、动画帧生命周期控制
3、大数据集分批处理
渲染优化
1、Canvas局部刷新
2、CSS硬件加速
3、离屏计算策略
代码优化
1、函数节流/防抖
2、惰性加载机制
3、事件监听器复用
架构设计解析
模块化结构设计
层级 文件 职责 视图层 index.html 定义UI结构和元素布局 表现层 index.scss 控制视觉样式和响应式布局 逻辑层 index.js 实现业务逻辑和交互控制
核心组件交互
#mermaid-svg-76UqMFSNnCTpvLrg {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-76UqMFSNnCTpvLrg .error-icon{fill:#552222;}#mermaid-svg-76UqMFSNnCTpvLrg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-76UqMFSNnCTpvLrg .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-76UqMFSNnCTpvLrg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-76UqMFSNnCTpvLrg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-76UqMFSNnCTpvLrg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-76UqMFSNnCTpvLrg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-76UqMFSNnCTpvLrg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-76UqMFSNnCTpvLrg .marker.cross{stroke:#333333;}#mermaid-svg-76UqMFSNnCTpvLrg svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-76UqMFSNnCTpvLrg .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-76UqMFSNnCTpvLrg .cluster-label text{fill:#333;}#mermaid-svg-76UqMFSNnCTpvLrg .cluster-label span{color:#333;}#mermaid-svg-76UqMFSNnCTpvLrg .label text,#mermaid-svg-76UqMFSNnCTpvLrg span{fill:#333;color:#333;}#mermaid-svg-76UqMFSNnCTpvLrg .node rect,#mermaid-svg-76UqMFSNnCTpvLrg .node circle,#mermaid-svg-76UqMFSNnCTpvLrg .node ellipse,#mermaid-svg-76UqMFSNnCTpvLrg .node polygon,#mermaid-svg-76UqMFSNnCTpvLrg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-76UqMFSNnCTpvLrg .node .label{text-align:center;}#mermaid-svg-76UqMFSNnCTpvLrg .node.clickable{cursor:pointer;}#mermaid-svg-76UqMFSNnCTpvLrg .arrowheadPath{fill:#333333;}#mermaid-svg-76UqMFSNnCTpvLrg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-76UqMFSNnCTpvLrg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-76UqMFSNnCTpvLrg .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-76UqMFSNnCTpvLrg .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-76UqMFSNnCTpvLrg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-76UqMFSNnCTpvLrg .cluster text{fill:#333;}#mermaid-svg-76UqMFSNnCTpvLrg .cluster span{color:#333;}#mermaid-svg-76UqMFSNnCTpvLrg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-76UqMFSNnCTpvLrg :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 文件夹选择 播放控制 可视化 用户操作 操作类型 File System API Audio Element Web Audio API 音乐列表更新 播放状态管理 Canvas绘制 DOM渲染 UI状态同步 频谱动画
关键技术实现详解
文件系统访问
概述
使用showDirectoryPicker()实现安全沙箱内的文件访问
代码
async function chooseMusicFolder() { try { const directoryHandle = await window.showDirectoryPicker(); for await (const entry of directoryHandle.values()) { if (entry.kind === \'file\' && entry.name.toLowerCase().endsWith(\'.mp3\')) { // 处理MP3文件... } } } catch (error) { if (error.name !== \'AbortError\') alert(\'选择目录出错: \' + error.message); }}
安全机制
1、必须由用户手势触发(如点击事件)
2、权限仅限当前会话有效
3、无法获取完整文件系统路径
音频处理流水线
音频处理流程
1、通过
代码
const audioContext = new AudioContext();const sourceNode = audioContext.createMediaElementSource(idAudio);analyserNode = audioContext.createAnalyser();analyserNode.fftSize = 512; // 频率分辨率sourceNode.connect(analyserNode);analyserNode.connect(audioContext.destination);
可视化渲染引擎
性能优化策略
1、节流颜色变化:getThrottledRandomColor(300)
2、动态FFT大小:analyserNode.fftSize = 512
3、智能帧率控制
代码
function draw() { animationFrameId = requestAnimationFrame(draw); // 仅当音频播放时进行绘制 if (!idAudio.paused) { analyserNode.getByteFrequencyData(frequencyData); // 绘制逻辑... }}
绘制算法特点
1、左右对称柱状图布局
2、动态高度映射(0-255 => 0-canvasHeight)
3、智能条宽计算:width / (frequencyData.length/8) / 2
用户体验优化设计
播放控制优化
多操作途径整合
1、点击列表项:播放/暂停切换
2、按钮控制:上一曲/下一曲
3、键盘快捷键
3.1、空格:播放/暂停
3.2、方向键:切歌
状态同步机制
function setActive(index) { const { domAll, domItem } = getDomItem(index); domAll.forEach(el => el.classList.remove(\'active\')); domItem?.classList.add(\'active\'); idSongTitle.textContent = domItem.textContent; activeSongIndex = index;}
视觉引导设计(动态反馈系统)
1、当前歌曲高亮(.active类)
2、自动滚动居中domItem.scrollIntoView({ block: \"center\", behavior: \'smooth\'});
3、播放模式视觉提示
性能与兼容性考量
资源管理策略(关键优化点)
1、对象URL回收:URL.revokeObjectURL()
2、动画帧管理:clearAnimationFrame()
3、内存控制:限制频率数据数组大小
浏览器兼容方案
特性检测策略
if (!window.showDirectoryPicker) { alert(\'您的浏览器不支持文件系统访问API\');}
降级方案
1、传统文件输入
2、拖放API作为备选
3、基础音频播放保底
扩展性设计
架构扩展点(可插拔模块设计)
1、歌词解析模块
2、音效处理模块(通过Web Audio API)
3、皮肤主题系统
数据持久化方案(存储扩展选项)
// 使用IndexedDB存储播放记录const db = indexedDB.open(\'musicPlayerDB\');// 或使用localStorage缓存最近播放localStorage.setItem(\'lastPlayed\', JSON.stringify({ index: activeSongIndex, time: idAudio.currentTime}));
总结与展望
概述
这个本地音乐播放器项目生动展示了现代Web技术如何突破浏览器传统边界,实现接近原生应用的体验。
其技术实现的突出价值
1、API创新应用:将File System Access、Web Audio、Canvas等前沿API有机结合
2、性能优化典范:展示了Web应用如何高效处理媒体数据和实时渲染
3、架构设计示范:清晰的模块划分和状态管理机制
未来演进方向
1、支持更多音频格式(FLAC、AAC等)
2、增加音频处理效果(均衡器、混响等)
3、实现PWA离线应用特性
4、开发浏览器插件版本
结束语
本项目不仅是功能完善的音乐播放器,更是学习现代Web开发的优质范例,值得开发者深入研究其技术实现细节。
基础版完整代码
html
<!DOCTYPE html><html lang=\"en\"><head> <meta charset=\"UTF-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> <title>本地音乐播放器</title> <link rel=\"stylesheet\" href=\"./style/index.css\"></head><body> <div class=\"box\"> <div class=\"musicPlayerHeader\"> <div class=\"musicPlayerTitle\">本地音乐播放器</div> <div class=\"selectFolderBtn\" onclick=\"chooseMusicFolder()\">选择音乐文件夹</div> </div> <div class=\"musicList\" id=\"idMusicList\" onclick=\"selectSong(event)\"></div> <div class=\"musicPlayer\"> <div class=\"playerControls\"> <div class=\"songTitle\" id=\"idSongTitle\"></div> <div class=\"btnPrevModeNext\"> <svg class=\"btnPrev\" width=\"30\" height=\"30\"> <polygon points=\"0,15 30,0 30,30\" onclick=\"prevSong()\" /> </svg> <div class=\"btnPlayMode\" id=\"idPlayMode\" onclick=\"togglePlayMode(1)\"></div> <svg class=\"btnNext\" width=\"30\" height=\"30\"> <polygon points=\"0,0 30,15 0,30\" onclick=\"nextSong()\" /> </svg> </div> <div class=\"timeDisplay\"> <span id=\"idCurrentTime\"></span> <span> / </span> <span id=\"idTotalTime\"></span> </div> </div> <div class=\"progressBar\"> <audio id=\"idAudio\" controls controlsList=\"nodownload noplaybackrate\"> </audio> </div> </div> <canvas id=\"idCanvas\"></canvas> </div> <script src=\"./index.js\"></script></body></html>
JavaScript
// 音乐列表数组,用来存放所有歌曲的信息let musicList = [];// 音乐列表的长度,表示当前有多少首歌曲let musicListLen = 0;// 当前选中的歌曲索引,默认是第0首let activeSongIndex = 0;// 播放模式列表,包含不同的播放方式及其标识和名称const playModeList = [ // 播放完一首歌曲后重复播放当前歌曲 { id: \'id1\', title: \'单曲循环\' }, // 播放完列表后从头开始循环播放 { id: \'id2\', title: \'列表循环\' }, // 随机顺序播放列表中的歌曲 { id: \'id3\', title: \'随机播放\' }];// 播放模式列表长度,方便后续根据长度计算索引等const playModeLen = playModeList.length;// 当前播放模式索引,默认设置为最后一个(随机播放)let playModeIndex = playModeLen - 1;// 获取画布的2D绘图上下文,用于绘制音频可视化效果const ctx = idCanvas.getContext(\'2d\');// 用于存储频率数据的数组,后续会被音频分析器填充let frequencyData = undefined;// 音频分析器节点,负责从音频源获取频率数据let analyserNode = undefined;// 动画帧ID,用于管理和取消动画循环let animationFrameId = undefined;// 通过调用getThrottledRandomColor函数,创建一个节流版本的随机颜色生成器// 参数300表示函数调用间隔最少为300毫秒,避免颜色变化过快const getColor = getThrottledRandomColor(300);// ---------初始化方法(start)---------/** * 初始化 */function init() { // 设置歌曲标题显示为“请选择歌曲”,提示用户当前未选中任何歌曲 idSongTitle.textContent = \'请选择歌曲\'; // 将当前播放时间显示设为“00:00”,表示还未开始播放 idCurrentTime.textContent = \'00:00\'; // 将总时长显示设为“00:00”,表示歌曲时长未知或未加载 idTotalTime.textContent = \'00:00\'; // 设置音频播放音量为0.1,音量范围是0到1,这里设置较低音量 idAudio.volume = 0.1; // 切换播放模式为索引0的模式,通常是“单曲循环”或默认播放模式 togglePlayMode(0);}/** * 初始化音频 */function initAudio() { // 创建一个音频上下文对象,用于音频处理 const audioContext = new AudioContext(); // 使用音频元素创建一个音频源节点 const sourceNode = audioContext.createMediaElementSource(idAudio); // 创建一个音频分析器节点,用于获取音频的频率和时域数据 analyserNode = audioContext.createAnalyser(); // 设置FFT的大小为512,决定频率数据的分辨率(频率条数为fftSize/2) analyserNode.fftSize = 512; // 创建一个无符号8位整型数组,用于存储频率数据 frequencyData = new Uint8Array(analyserNode.frequencyBinCount); // 将音频源连接到分析器节点,供其分析音频数据 sourceNode.connect(analyserNode); // 将分析器节点连接到音频输出(扬声器) analyserNode.connect(audioContext.destination);}// ---------初始化方法(end)---------// ---------公共方法(start)---------/** * 获取随机下标 * @param {*} array * @returns */function getRandomIndex(array = []) { // 返回一个随机整数,范围是0到数组长度-1,用作数组的随机索引 return Math.floor(Math.random() * array.length);}/** * 获取节流后的随机颜色 * @returns */function getThrottledRandomColor(delay = 1000 * 0.5) { // 初始化上一次调用的时间,起始为0 let lastCall = 0; // 初始化上一次生成的颜色,起始为null let lastColor = null; // 返回一个函数,这个函数用来生成节流后的随机颜色 return function () { // 获取当前时间的时间戳(毫秒) const now = new Date().getTime(); // 判断距离上一次调用是否已经超过设定的延迟时间delay(单位毫秒) if (now - lastCall >= delay) { // 生成一个随机整数,范围是0到0xFFFFFF(16777215),即24位颜色值 const hex = Math.random() * 0x1000000 | 0; // 更新上一次调用时间为当前时间 lastCall = now; // 将随机整数转换为6位十六进制字符串,并加上#号,构成标准的颜色字符串 // padStart用于保证字符串长度为6位,前面补0 lastColor = `#${hex.toString(16).padStart(6, \'0\')}`; // 返回新生成的颜色字符串 return lastColor; } // 如果距离上次调用还没超过延迟时间,则返回之前生成的颜色,避免频繁变化 return lastColor; };}/** * 时间格式化 * @param {*} seconds * @returns */function formatTime(seconds) { // 将总秒数转换成分钟数,向下取整(整数分钟) const minutes = Math.floor(seconds / 60); // 计算剩余秒数,向下取整(整数秒数) const remainingSeconds = Math.floor(seconds % 60); // 将分钟数转换为字符串,如果长度不足2位,则在前面补0,比如5=>\"05\" const mins = minutes.toString().padStart(2, \'0\'); // 将秒数转换为字符串,如果长度不足2位,则在前面补0,比如9=>\"09\" const secs = remainingSeconds.toString().padStart(2, \'0\'); // 返回格式化后的时间字符串,格式为\"MM:SS\",例如\"03:07\" return `${mins}:${secs}`;}// ---------公共方法(end)---------// ---------页面元素方法(start)---------/** * 选择音乐文件夹 */async function chooseMusicFolder() { try { // 弹出目录选择器,让用户选择一个文件夹(目录) const directoryHandle = await window.showDirectoryPicker(); // 先清空音乐列表数组,准备重新加载 musicList = []; // 异步遍历所选目录中的每个条目(文件或子目录) for await (const entry of directoryHandle.values()) { // 判断当前条目是否是文件且扩展名为.mp3(不区分大小写) const isMp3 = entry.kind === \'file\' && entry.name.toLowerCase().endsWith(\'.mp3\'); if (isMp3) { // 去掉文件名最后4个字符(.mp3)作为歌曲标题文本 const text = entry.name.slice(0, -4); // 将符合条件的mp3文件的信息加入音乐列表 musicList.push({ // 歌曲名称(不含扩展名) text, // 文件完整名称 name: entry.name, // 文件句柄(方便后续读取文件) handle: entry }); } } // 记录音乐列表长度 musicListLen = musicList.length; // 调用函数,根据新的musicList创建页面元素(如列表显示) createElems(musicList); } catch (error) { // 如果捕获到异常,且不是用户取消选择目录(AbortError)导致的,弹出错误提示 if (error.name !== \'AbortError\') alert(\'选择目录出错: \' + error.message); }}/** * 选择歌曲 * @param {*} param0 * @returns */async function selectSong({ target}) { // 找到触发事件元素最近的带有class=\"item\"的祖先元素 const dom = target.closest(\'.item\'); // 如果没找到符合条件的元素,则停止执行 if (!dom) return false; // 获取该元素的data-index属性值,并转换为数字类型,表示歌曲索引 const index = Number(dom.dataset.index); // 判断点击的歌曲索引是否为当前正在播放的歌曲索引 // 是:调用togglePlayPause()切换播放/暂停状态 // 否:调用setSong(index)切换并播放点击的歌曲 index === activeSongIndex ? togglePlayPause() : setSong(index);}/** * 上一曲 */function prevSong() { // 如果播放模式索引不等于0(即不是单曲循环模式) // 且音乐列表不为空,则切换到上一首歌曲(向上切换) if (playModeIndex !== 0 && musicListLen) switchSong(\'up\');}/** * 选择播放模式 * @param {*} type */function togglePlayMode(type = 0) { // 如果type不等于0,则切换到下一个播放模式,循环索引范围为0到playModeLen-1 if (type !== 0) playModeIndex = (playModeIndex + 1) % playModeLen; // 获取当前播放模式对象 const item = playModeList[playModeIndex]; // 更新界面显示当前播放模式名称 idPlayMode.textContent = item.title;}/** * 下一曲 */function nextSong() { // 如果播放模式索引不等于0(即不是单曲循环模式) // 且音乐列表不为空,则切换到下一首歌曲(向下切换) if (playModeIndex !== 0 && musicListLen) switchSong(\'down\');}// ---------页面元素方法(end)---------// ---------功能方法(start)---------/** * 创建元素标签 * @param {*} list */function createElems(list = []) { // 获取随机下标,假设getRandomIndex是一个返回list中随机索引的函数 const index = getRandomIndex(list); // 清空父元素idMusicList中的所有内容,准备重新渲染新的列表项 idMusicList.innerHTML = \'\'; // 遍历列表list,每个元素用item表示,i是当前元素的下标 list.forEach((item, i) => { // 创建一个新的div元素,用来表示列表中的一项 const div = document.createElement(\'div\'); // 给新创建的div添加类名\'item\',方便CSS样式控制 div.className = \'item\'; // 通过dataset自定义属性存储当前项的下标i,方便后续通过点击事件或其他操作获取对应索引 div.dataset.index = i; // 设置div的文本内容为当前项的文本(假设item对象有text属性) div.textContent = item.text; // 把新创建并设置好的div添加到父容器idMusicList中,页面上就能看到这个列表项 idMusicList.appendChild(div); }); // 根据刚才随机获得的下标index,调用setSong函数切换到对应的歌曲或项 setSong(index);}/** * 切换歌曲 * @param {*} type */function switchSong(type = \'up\') { // 根据当前播放模式playModeIndex选择不同的播放行为 switch (playModeIndex) { case 0: // 播放模式0:单曲循环,直接播放当前音频 idAudio.play(); break; case 1: // 播放模式1:顺序播放,根据type参数决定上一首或下一首 if (type === \'up\') { // 向上(上一首),索引减1,注意循环处理防止越界 activeSongIndex = (activeSongIndex - 1 + musicListLen) % musicListLen; } if ([\'down\', \'end\'].includes(type)) { // 向下(下一首),索引加1,循环播放 activeSongIndex = (activeSongIndex + 1) % musicListLen; } // 根据新的索引设置并播放对应歌曲 setSong(activeSongIndex); break; case 2: // 播放模式2:随机播放,调用getRandomIndex获取随机歌曲下标 const index = getRandomIndex(musicList); setSong(index); break; default: // 如果传入的播放模式不正确,输出错误提示 alert(\'出错啦!\'); break; }}/** * 设置并自动播放歌曲 * @param {*} index */async function setSong(index = 0) { // 从音乐列表中根据索引获取对应的文件对象(异步获取) const file = await musicList[index].handle.getFile(); // 为该文件创建一个本地的临时URL,用于音频播放器加载 const fileUrl = URL.createObjectURL(file); // 取消之前的动画帧,避免多次动画叠加 clearAnimationFrame(); // 更新音频播放器的音源为刚创建的文件 URL idAudio.src = fileUrl; // 开始播放当前音频 idAudio.play(); // 设置当前歌曲为激活状态(高亮显示等) setActive(index); // 滚动列表使当前歌曲项可见(通常是滚动到该元素) setScroll(index);}/** * 设置歌曲激活 * @param {*} index */function setActive(index = 0) { // 从getDomItem(index)中解构 // domAll:所有可选的item元素集合(用于清除激活状态) // domItem:当前选中的目标元素(根据传入的index) const { domAll, domItem } = getDomItem(index); // 遍历所有item元素,移除它们的\'active\'激活类,确保只有一个处于激活状态 for (let i = 0; i < domAll.length; i++) domAll[i].classList.remove(\'active\'); // 如果当前domItem不存在,直接返回false,防止后续操作报错 if (!domItem) return false; // 设置当前激活的歌曲下标为传入的index activeSongIndex = index; // 将当前选中元素的文本内容(歌曲标题)赋值到页面对应的标题显示区域 idSongTitle.textContent = domItem.textContent; // 给当前选中的元素添加\'active\'类名,用于样式高亮或标识状态 domItem.classList.add(\'active\');}/** * 设置滚动条 * @param {*} index * @returns */function setScroll(index = 0) { // 从getDomItem(index)中解构获取domItem,表示要滚动到的目标DOM元素 const { domItem } = getDomItem(index); // 让目标元素滚动到视口中,并垂直居中显示,滚动过程使用平滑动画 domItem.scrollIntoView({ block: \"center\", behavior: \'smooth\' });}/** * 获取dom元素 * @param {*} index * @returns */function getDomItem(index = 0) { // 根据父元素id获取所有子代元素(不包括后代) const domAll = idMusicList.children; // 根据下标获取对应子元素 const domItem = domAll[index]; return { domAll, domItem };}/** * 切换音频播放与暂停状态 */function togglePlayPause() { // 判断当前音频是否处于暂停状态 // 如果是暂停状态,则播放音频 // 如果正在播放,则暂停音频 idAudio.paused ? idAudio.play() : idAudio.pause();}/** * 画布实现(canvas) */function draw() { if (frequencyData && analyserNode) { // 请求浏览器在下一帧调用draw函数,并返回动画帧ID animationFrameId = requestAnimationFrame(draw); // 设置画布宽度为浏览器窗口宽度减18像素 idCanvas.width = window.innerWidth - 18; // 设置画布高度为浏览器窗口高度减18像素 idCanvas.height = window.innerHeight - 18; // 获取画布宽高 const { width, height } = idCanvas; // 取频率数据长度的八分之一作为绘制条数 const len = frequencyData.length / 8; // 计算每个条形柱的宽度(左右对称) const barWidth = width / len / 2; // 获取当前绘制颜色 const hex = getColor(); // 清空画布 ctx.clearRect(0, 0, width, height); // 获取频率数据填充到 frequencyData 数组 analyserNode.getByteFrequencyData(frequencyData); // 设置绘制颜色 ctx.fillStyle = hex; for (let i = 0; i < len; i++) { // 当前频率值(0-255) const data = frequencyData[i]; // 计算条形高度,比例映射到画布高度 const barHeight = data / 255 * height; // 右半部分条形起始x坐标 const x1 = i * barWidth + width / 2; // 左半部分条形起始x坐标 const x2 = width / 2 - (i + 1) * barWidth; // 条形起始y坐标(底部向上绘制) const y = height - barHeight; // 右半部分绘制条形柱 ctx.fillRect(x1, y, barWidth - 4, barHeight); // 左半部分绘制条形柱 ctx.fillRect(x2, y, barWidth - 4, barHeight); } } else { // 初始化音频 initAudio(); }}/** * 清除动画帧 */function clearAnimationFrame() { // 如果当前没有记录的动画帧id(即未启用动画),则直接返回false,表示无需清除 if (animationFrameId === undefined) return false; // 取消已注册的动画帧,以停止绘制或动画循环 cancelAnimationFrame(animationFrameId); // 将动画帧id重置为undefined,表示当前没有正在运行的动画 animationFrameId = undefined;}// ---------功能方法(end)---------// ---------监听(start)---------/** * 当前已播放的时间 */idAudio.addEventListener(\'timeupdate\', function () { // 将当前播放进度(单位为秒)格式化为mm:ss形式,并显示在当前时间元素上 idCurrentTime.textContent = formatTime(idAudio.currentTime);});/** * 当前歌曲总时长 */idAudio.addEventListener(\'loadedmetadata\', function () { // 将音频的总时长(单位为秒)格式化为mm:ss形式,并显示在总时长元素上 idTotalTime.textContent = formatTime(idAudio.duration);});/** * 播放回调 */idAudio.onplay = function () { // 如果频率数据frequencyData或分析器节点analyserNode不存在,则初始化音频分析功能 if (!frequencyData || !analyserNode) initAudio(); // 清除之前可能存在的动画帧,避免多个动画帧叠加导致性能问题或绘制异常 clearAnimationFrame(); // 执行绘制函数,用于在画布上绘制可视化内容(如音频频谱等) draw();};/** * 暂停回调 */idAudio.onpause = function () { // 获取画布的宽度和高度 const { width, height } = idCanvas; // 清空画布区域(从坐标0, 0开始,清除整个画布) ctx.clearRect(0, 0, width, height); // 取消之前通过requestAnimationFrame注册的动画帧(避免重复绘制或资源浪费) clearAnimationFrame();};/** * 歌曲播放结束 */idAudio.addEventListener(\'ended\', function () { // 执行歌曲切换方法 switchSong(\'end\');});/** * 监听键盘按下 */document.addEventListener(\'keydown\', function (event) { // 当按下键盘上的某个按键时触发以下判断 // 如果按下的是左方向键(←),且音乐列表不为空,则播放上一首歌曲 if (event.key === \'ArrowLeft\' && musicListLen) prevSong(); // 如果按下的是右方向键(→),且音乐列表不为空,则播放下一首歌曲 if (event.key === \'ArrowRight\' && musicListLen) nextSong(); // 如果按下的是空格键,且音乐列表不为空,则切换播放/暂停状态 if (event.code === \'Space\' && musicListLen) togglePlayPause(); // 如果按下的是上方向键(↑),则增加音量,每次加0.1,最大不超过1.0 if (event.key === \'ArrowUp\') idAudio.volume = Math.min(idAudio.volume + 0.1, 1); // 如果按下的是下方向键(↓),则降低音量,每次减0.1,最小不低于0 if (event.key === \'ArrowDown\') idAudio.volume = Math.max(idAudio.volume - 0.1, 0);});// ---------监听(end)---------// 执行初始化init();
Style(scss)
body,div,audio,canvas { margin: 0px; padding: 0px; box-sizing: border-box;}/* 隐藏原生时间显示 */audio::-webkit-media-controls-current-time-display,audio::-webkit-media-controls-time-remaining-display { display: none;}// 隐藏全局滚动条滑槽::-webkit-scrollbar { display: none;}.active { color: rgba(64, 158, 255, .9);}body { background-color: rgba(0, 0, 0, .8); .box { display: flex; justify-content: center; color: #ffffff; .musicPlayerHeader { width: 96%; position: fixed; top: 38px; left: 50%; transform: translateX(-50%); display: flex; justify-content: space-between; align-items: center; white-space: nowrap; .musicPlayerTitle { font-size: 38px; font-weight: bold; } .selectFolderBtn { padding: 6px 8px 6px 8px; font-size: 18px; background-color: rgba(64, 158, 255, .8); border-radius: 6px; cursor: pointer; } } .musicList { position: fixed; top: 50%; left: 2%; transform: translateY(-50%); max-height: 500px; overflow-y: auto; .item { cursor: pointer; } .item:not(:first-child) { margin-top: 18px; } } .musicPlayer { width: 96%; position: fixed; bottom: 38px; left: 50%; transform: translateX(-50%); white-space: nowrap; .playerControls { display: flex; justify-content: space-between; align-items: center; font-size: 28px; .songTitle { flex: 2; } .btnPrevModeNext { flex: 1; display: flex; justify-content: space-between; align-items: center; .btnPrev>polygon, .btnPlayMode, .btnNext>polygon { cursor: pointer; } .btnPrev>polygon, .btnNext>polygon { fill: rgba(255, 255, 255, .8); } .btnPlayMode { color: rgba(255, 255, 255, .8); font-weight: bold; } .btnPrev>polygon:hover, .btnPlayMode:hover, .btnNext>polygon:hover { fill: rgba(64, 158, 255, .9); color: rgba(64, 158, 255, .9); } } .timeDisplay { flex: 2; text-align: right; } } .progressBar { margin-top: 18px; audio { width: 100%; } } } }}
最后的话
感谢您的支持!!!
如有问题请联系:15289682517(微信同号)