那天夜里,我戴上耳机准备听首老歌,却猛然发现自己手机里找不到一个既好看又好用的小型音乐播放器 App。主流播放器不是太臃肿,就是界面审美老旧。突然就冒出一个念头——干脆自己写一个简约现代、功能完善的小型音乐播放器吧。
我决定从 UI 到交互逻辑都由我自己操刀,实现一个拥有基础功能(播放/暂停、切歌、进度条拖动)的播放器,再逐步加上音频可视化(用 Canvas 绘制频谱条)、播放列表管理、歌词同步,甚至是滤镜特效。我想看看,在浏览器中,能否做出一个既炫酷又实用的音乐播放界面。
在开始编码之前,我先拿出纸笔,把我要实现的所有功能梳理了一遍。大致划分成几个功能区:
<audio>
元素控制播放、暂停、跳转;于是我画了一张系统结构流程图,帮助我理清模块之间的依赖关系:
这张图是我开发过程中的第一份系统视图,从“用户”操作出发,到每个子组件的触发关系。我明确了组件职责后,接下来的开发就像拼积木一样,目标明确许多。
我希望这个播放器界面符合当下的设计趋势,比如玻璃拟态、微妙的渐变色背景、圆角卡片、细腻的光影效果等。因此我先用 Figma 画了一份界面草图,再用 HTML + CSS 重构出来,最终的页面主要分为三块:
在 UI 层,我用了一些现代化的 CSS 技巧,比如:
.container {
background: linear-gradient(145deg, #1e1e2f, #2e2e3e);
border-radius: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
padding: 20px;
color: white;
font-family: "Segoe UI", sans-serif;
}
通过 backdrop-filter: blur(10px);
实现玻璃拟态的感觉,加上线性渐变与卡片投影,整个播放器看起来非常通透现代。
音频播放的实现并不复杂,HTML5 的 <audio>
元素已经提供了极为丰富的 API。以下是播放器的核心 HTML 和绑定的 JavaScript 控制逻辑:
<audio id="audio" src="./songs/song1.mp3"></audio>
const audio = document.getElementById("audio");
const playBtn = document.getElementById("play");
playBtn.addEventListener("click", () => {
if (audio.paused) {
audio.play();
playBtn.innerHTML = "⏸️";
} else {
audio.pause();
playBtn.innerHTML = "▶️";
}
});
我还添加了 audio.ontimeupdate
事件,在每次播放位置变动时实时更新进度条。
这个模块最初困扰了我一会,因为要实现两件事:
实现方式是使用 <input type="range">
控件,并用 JS 绑定 input
和 timeupdate
事件:
<input type="range" id="progress" min="0" max="100" value="0" />
const progress = document.getElementById("progress");
audio.ontimeupdate = () => {
const percent = (audio.currentTime / audio.duration) * 100;
progress.value = percent;
};
progress.addEventListener("input", () => {
audio.currentTime = (progress.value / 100) * audio.duration;
});
这样,播放和拖动就都能同步控制了。为了让滑块更美观,我还加上了自定义样式:
input[type="range"]::-webkit-slider-thumb {
background-color: #00ffe1;
border-radius: 50%;
height: 14px;
width: 14px;
box-shadow: 0 0 8px #00ffe1;
}
可视化频谱是我整个播放器中最炫的一部分。为此我查阅了不少资料,主要用到的是 Web Audio API 中的 AnalyserNode
,它可以提供音频的频域数据。我用 Canvas 绘制出一个动态的柱状频谱图,每根柱子随着音量变化而跳动。
首先,需要连接 AudioContext
并获取频域数据:
const audioCtx = new AudioContext();
const source = audioCtx.createMediaElementSource(audio);
const analyser = audioCtx.createAnalyser();
source.connect(analyser);
analyser.connect(audioCtx.destination);
analyser.fftSize = 256;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
接下来是 Canvas 绘图部分,我使用一个循环动画函数 draw()
,每帧读取一次频域数据,并绘制出来:
const canvas = document.getElementById("visualizer");
const ctx = canvas.getContext("2d");
canvas.width = 800;
canvas.height = 200;
function draw() {
requestAnimationFrame(draw);
analyser.getByteFrequencyData(dataArray);
ctx.clearRect(0, 0, canvas.width, canvas.height);
const barWidth = canvas.width / bufferLength;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = dataArray[i];
const hue = i * 2;
ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
}
draw();
每个频率条的颜色通过 HSL 色调变换形成渐变彩虹效果,同时频谱跳动与真实音频同步,这种“视觉+听觉”的融合体验极大提升了播放器的现代感。
实现播放列表功能的第一步是设计其数据结构。我使用了一个数组来存储每首歌的信息:
const playlist = [
{
title: "夜空中最亮的星",
artist: "逃跑计划",
src: "./songs/star.mp3",
cover: "./covers/star.jpg",
lyrics: "./lyrics/star.lrc"
},
{
title: "晴天",
artist: "周杰伦",
src: "./songs/sunny.mp3",
cover: "./covers/sunny.jpg",
lyrics: "./lyrics/sunny.lrc"
},
// 更多歌曲...
];
然后渲染出一个浮动的侧边列表,通过点击实现切歌:
const listContainer = document.getElementById("playlist");
playlist.forEach((song, index) => {
const li = document.createElement("li");
li.textContent = `${song.title} - ${song.artist}`;
li.addEventListener("click", () => {
loadSong(index);
audio.play();
});
listContainer.appendChild(li);
});
而 loadSong()
函数则负责切换封面图、音频路径、标题信息等:
function loadSong(index) {
const song = playlist[index];
audio.src = song.src;
document.getElementById("title").textContent = song.title;
document.getElementById("artist").textContent = song.artist;
document.getElementById("cover").src = song.cover;
currentLyricFile = song.lyrics;
loadLyrics(currentLyricFile);
}
为了让当前播放的歌曲在列表中高亮,我加了简单的 class 切换逻辑:
const items = document.querySelectorAll("#playlist li");
items.forEach((item, idx) => {
item.classList.toggle("active", idx === index);
});
在 CSS 中添加一层发光边框效果:
#playlist li.active {
background: rgba(255, 255, 255, 0.1);
border-left: 3px solid #00ffe1;
}
整个播放列表就这样完成了,不仅逻辑清晰,而且 UI 也更具层次感。
歌词同步是一个颇具挑战的环节,因为需要解析 .lrc
文件格式,并精确对应时间点显示歌词。我通过异步加载 .lrc
文件,并构建一个歌词数组:
let lyrics = [];
async function loadLyrics(path) {
const res = await fetch(path);
const text = await res.text();
lyrics = parseLRC(text);
}
.lrc
解析函数如下:
function parseLRC(lrc) {
const lines = lrc.split("\n");
const result = [];
const timeReg = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/;
for (const line of lines) {
const match = line.match(timeReg);
if (match) {
const min = parseInt(match[1]);
const sec = parseInt(match[2]);
const ms = parseInt(match[3].padEnd(3, "0"));
const time = min * 60 + sec + ms / 1000;
const text = line.replace(timeReg, "").trim();
result.push({ time, text });
}
}
return result;
}
然后在 audio.ontimeupdate
中判断当前播放时间,并更新当前歌词行:
audio.ontimeupdate = () => {
const currentTime = audio.currentTime;
for (let i = 0; i < lyrics.length; i++) {
if (currentTime < lyrics[i].time) {
document.getElementById("lyric").textContent = lyrics[i - 1]?.text || "";
break;
}
}
};
为了让歌词逐行滚动,我使用了一个小的过渡容器,并控制其 translateY
:
// 更新偏移量
function updateLyricScroll(index) {
const lineHeight = 24;
const offset = lineHeight * (index - 3);
document.getElementById("lyric-container").style.transform = `translateY(-${offset}px)`;
}
视觉上,歌词跟着节奏平稳移动,有一种沉浸式的感受。
最后一个环节,是为播放器增加高级滤镜特效。我主要实现了以下几种视觉层次:
backdrop-filter
;box-shadow
;text-shadow
。比如播放按钮使用的样式:
.play-btn {
background: transparent;
border: none;
font-size: 28px;
color: #00ffe1;
text-shadow: 0 0 5px #00ffe1, 0 0 10px #00ffe1;
}
整体 UI 风格接近赛博朋克风格,非常符合我的初衷:一个迷你但酷炫的音乐播放器。
写着写着我意识到,如果不进行合理的文件组织,项目很快就会变得杂乱无章。于是我把所有相关功能按照逻辑分成了几类,每类一个文件夹,主目录结构如下:
/music-player/
│
├── index.html
├── style/
│ └── main.css
├── js/
│ ├── player.js # 音频控制与进度逻辑
│ ├── visualizer.js # Canvas 可视化频谱绘制
│ ├── playlist.js # 播放列表逻辑
│ ├── lyrics.js # 歌词加载与同步
│ └── ui-effects.js # 视觉滤镜控制
├── songs/
│ └── *.mp3
├── covers/
│ └── *.jpg
├── lyrics/
│ └── *.lrc
└── assets/
└── icon.svg / bg.jpg / font.ttf
我还特意将公共样式和核心脚本分离,便于维护,比如 main.css
负责全局主题,而播放进度、按钮发光、歌词滚动样式则集中在 ui-effects.css
中。
为了确保在页面加载时所有资源顺序正确,我使用了 defer
标签引入 JavaScript 文件:
<script src="./js/player.js" defer></script>
<script src="./js/visualizer.js" defer></script>
<script src="./js/lyrics.js" defer></script>
每一个模块中的函数都尽量保持纯粹,依赖尽可能通过参数传入,避免形成数据耦合,提升整体可维护性和复用性。
播放器初次加载时需要处理音乐、歌词、封面图等多个资源,因此我增加了一些懒加载和预处理机制。
最有效的是对音频资源使用 preload="metadata"
属性,避免一开始就加载全部:
<audio id="audio" preload="metadata"></audio>
此外,为了改善歌词和封面图的加载速度,我在用户选择某首歌后才触发歌词加载,而封面图使用了 loading="lazy"
属性:
<img id="cover" loading="lazy" />
同时,我加入了一个简洁的 loading 动画,在首次加载完成前展示,提升了感知速度:
.loader {
width: 40px;
height: 40px;
border: 5px solid rgba(0,0,0,0.1);
border-top-color: #00ffe1;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
音乐播放开始的那一刻,loading 会淡出,播放控制器才出现,这种节奏上的“先藏后现”给了我意外的细腻感。
播放器虽然最初是为桌面浏览器设计的,但我希望它也能在手机上完美运行。于是我添加了响应式设计:
@media (max-width: 600px) {
.container {
padding: 10px;
border-radius: 0;
box-shadow: none;
}
#visualizer {
height: 100px;
}
.controls {
flex-direction: column;
}
}
此外,我将按钮间距、字体大小等也做了动态调整,在小屏上变得更大更易点。为了防止 iOS Safari 默认的 <audio>
控件干扰,我还用 CSS 隐藏了它的控件条,只使用自定义控制。
在开发完成后,我打算将它部署到 GitHub Pages 或 Vercel 上,让更多人可以在线体验。我将所有文件打包后上传,使用 vite
做了一个最基础的构建:
npm create vite@latest music-player
我将资源拷贝进去,并修改 vite.config.js
:
export default defineConfig({
base: './',
build: {
outDir: 'dist',
assetsDir: 'assets'
}
});
构建完成后,dist/
文件夹就是最终可部署内容,我选择 Vercel,只需一键部署即可访问。
这个音乐播放器项目花了我差不多一个周末的时间,从最初的功能构思,到 UI 设计,再到代码实现,每一个小细节都让我沉浸其中。尤其是 Canvas 可视化和歌词同步部分,虽然不难,但需要耐心和调试,过程中的每一次小改动都带来了肉眼可见的提升。
在这次实践中,我对以下几个方面有了更深的理解:
比起造一个工业级播放器,我更享受在这过程中与代码的对话。它不只是一个音乐工具,更像是我与声音之间的“界面”。而我,也从中获得了构建完整 Web 应用的全流程实践经验。
也许这只是一个“简单播放器”,但它承载了我的审美、技术探索与创造欲。希望你在读完这篇文章后,也能打开编辑器,打造属于自己的 Web 小作品,不为炫技,只为把心中想象变成现实。
愿代码与音乐,都与你我长存。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。