引言
歌词功能是音乐播放器的核心交互模块之一,良好的歌词体验能极大提升用户使用感。本文基于 Vue3 技术栈,从零实现一套完整的 LRC 格式歌词处理方案,包含 LRC 歌词解析、实时歌词匹配、进度条点击跳转、歌词平滑居中滚动 四大核心功能。所有代码均为工业级写法,逻辑严谨、容错完备,新手可直接复制复用。
前置知识
在实现功能前,需掌握两个核心 Vue3 知识点:
ref绑定 DOM :分为静态绑定 (绑定单个固定 DOM)和动态条件绑定(绑定循环列表中的指定 DOM),是操作 DOM 元素的基础。- 响应式核心 :
computed计算属性自动依赖响应式数据更新,watch监听器监听数据变化执行副作用,nextTick等待 DOM 异步更新完成。
一、核心功能模块实现
1. LRC 歌词解析:parseLyric 函数
作用:接收原始 LRC 歌词文本,提取每句歌词的时间戳和文本内容,返回一一对应的数组。
核心代码
javascript
const parseLyric = (raw = '') => {
if (!raw) return { times: [], texts: [] }
const times = []
const texts = []
raw.split('\n').forEach(line => {
line = line.trim()
if (!line) return
// 正则匹配 [mm:ss.xx] 格式时间戳
const match = line.match(/^\[(\d+):(\d+)(\.\d+)?\](.*)/)
if (match) {
const minutes = parseInt(match[1])
const seconds = parseInt(match[2])
// 转换为总秒数,方便后续时间比对
const time = minutes * 60 + seconds
const text = match[4] ? match[4].trim() : ''
// 过滤无歌词的空行
if (text) {
times.push(time)
texts.push(text)
}
}
})
return { times, texts }
}
关键解析
- 容错处理:入参默认值为空字符串,避免无参调用报错;过滤空行和无歌词行,保证数据纯净。
- 正则匹配 :精准提取分钟、秒数、毫秒(可选)和歌词文本,兼容
[mm:ss]和[mm:ss.xx]两种格式。 - 时间戳转换 :将
分:秒转换为总秒数,便于和播放器进度(秒数)直接比对。
2. 实时歌词匹配:currentLyricIndex 计算属性
作用:根据播放器当前播放进度,计算出此刻应显示的歌词索引,是歌词高亮和滚动的核心依据。
核心代码
javascript
import { computed, ref } from 'vue'
// 响应式数据:解析后的歌词时间数组、播放进度
const lyricTimes = ref([])
const currentTime = ref(0)
const currentLyricIndex = computed(() => {
if (!lyricTimes.value.length) return -1
// 核心逻辑:倒序遍历有序数组
for (let i = lyricTimes.value.length - 1; i >= 0; i--) {
if (currentTime.value >= lyricTimes.value[i]) {
return i
}
}
return -1
})
关键解析
- 倒序遍历的必要性 :
lyricTimes是严格递增的有序数组,倒序遍历能快速找到最后一个 满足当前进度 ≥ 歌词时间的索引,即当前歌词索引。若正序遍历会匹配到第一个满足条件的旧歌词,导致显示错位。 - 返回值约定 :返回
-1表示无歌词可显示(如播放进度未到第一句歌词),返回非负整数表示有效歌词索引。
3. 进度条点击跳转:handleProgressClick 函数
作用:监听进度条点击事件,计算点击位置对应的播放时间,实现音频跳转并同步歌词显示。
核心代码
javascript
const audioRef = ref(null) // 绑定音频 DOM
const duration = ref(0) // 音频总时长
const handleProgressClick = (event) => {
const audio = audioRef.value
const bar = event.currentTarget // 进度条 DOM(必须用 currentTarget)
const rect = bar.getBoundingClientRect()
// 计算点击位置占进度条的比例(0~1)
const ratio = (event.clientX - rect.left) / rect.width
const newTime = ratio * duration.value
if (audio) {
audio.currentTime = newTime // 音频跳转
currentTime.value = newTime // 同步响应式进度,触发歌词更新
}
}
关键解析
event.currentTargetvsevent.target:currentTarget永远指向绑定事件的进度条 DOM,target可能指向进度条内的子元素,因此必须用currentTarget保证尺寸计算准确。- 像素到时间的映射 :通过
getBoundingClientRect()获取进度条位置和尺寸,计算点击比例后乘以总时长,得到目标播放时间。 - 响应式同步 :修改
currentTime.value是关键,否则音频跳转后歌词不会同步更新。
4. 歌词平滑居中滚动:watch 监听器
作用:监听歌词索引变化,自动让当前歌词在容器中垂直居中显示,实现平滑滚动效果。
核心代码
javascript
import { watch, nextTick, ref } from 'vue'
// 绑定 DOM:歌词容器、当前高亮歌词
const lyricsContainerRef = ref(null)
const activeLyricRef = ref(null)
watch(currentLyricIndex, async (newIndex) => {
if (newIndex === -1) return
await nextTick() // 等待 DOM 更新完成,避坑关键!
if (activeLyricRef.value && lyricsContainerRef.value) {
const container = lyricsContainerRef.value
const activeElement = activeLyricRef.value
// 获取 DOM 尺寸和位置
const containerHeight = container.clientHeight
const elementOffset = activeElement.offsetTop
const elementHeight = activeElement.offsetHeight
// 核心公式:计算居中滚动位置
const scrollTo = elementOffset - (containerHeight / 2) + (elementHeight / 2)
// 平滑滚动
container.scrollTo({
top: scrollTo,
behavior: 'smooth'
})
}
})
关键解析
await nextTick()的必要性 :Vue DOM 更新是异步的,歌词索引变化后,activeLyricRef指向的 DOM 不会立即更新。nextTick等待 DOM 更新完成,确保获取到最新的高亮歌词 DOM。- 居中公式推导 :目标是让歌词中线与容器中线重合,公式为:
scrollTo=歌词顶部偏移−容器半高+歌词半高scrollTo = 歌词顶部偏移 - 容器半高 + 歌词半高scrollTo=歌词顶部偏移−容器半高+歌词半高 - 平滑滚动 API :使用原生
Element.scrollTo(),behavior: 'smooth'实现丝滑过渡,替代传统的scrollTop赋值。
二、关键 DOM 绑定:lyricsContainerRef & activeLyricRef
两个 ref 的绑定是 DOM 操作的基础,前者为静态绑定 ,后者为动态条件绑定。
模板代码
vue
<template>
<!-- 歌词滚动容器:静态绑定 -->
<div class="lyrics-container" ref="lyricsContainerRef">
<!-- 循环渲染歌词:动态条件绑定 -->
<div
class="lyric-item"
v-for="(text, index) in lyricTexts"
:key="index"
<!-- 只有当前索引的歌词才绑定 ref -->
:ref="index === currentLyricIndex ? 'activeLyricRef' : null"
<!-- 高亮样式绑定,与 ref 条件一致 -->
:class="{ active: index === currentLyricIndex }"
>
{{ text }}
</div>
</div>
</template>
<style scoped>
.lyrics-container {
height: 300px; /* 固定高度,开启滚动 */
overflow-y: auto;
text-align: center;
}
.lyric-item {
line-height: 40px;
color: #999;
}
/* 高亮样式 */
.lyric-item.active {
color: #27ae60;
font-weight: bold;
}
</style>
绑定规则
| 变量名 | 绑定方式 | 作用 | 值的变化 |
|---|---|---|---|
lyricsContainerRef |
静态绑定 ref="xxx" |
歌词滚动容器 DOM | 初始化后固定不变 |
activeLyricRef |
动态条件绑定 :ref="条件?xxx:null" |
当前高亮歌词 DOM | 随 currentLyricIndex 动态更新 |
三、完整整合代码
将上述模块整合,形成可直接运行的完整代码。
vue
<template>
<div class="player">
<audio
ref="audioRef"
@timeupdate="currentTime = $event.target.currentTime"
@loadedmetadata="duration = $event.target.duration"
src="你的音频地址.mp3"
controls
>
<div class="progress-bar" @click="handleProgressClick"></div>
<div class="lyrics-container" ref="lyricsContainerRef">
<div
class="lyric-item"
v-for="(text, index) in lyricTexts"
:key="index"
:ref="index === currentLyricIndex ? 'activeLyricRef' : null"
:class="{ active: index === currentLyricIndex }"
>
{{ text }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick } from 'vue'
// 1. 声明响应式数据
const audioRef = ref(null)
const lyricsContainerRef = ref(null)
const activeLyricRef = ref(null)
const lyricTimes = ref([])
const lyricTexts = ref([])
const currentTime = ref(0)
const duration = ref(0)
// 2. 歌词解析函数
const parseLyric = (raw = '') => {
if (!raw) return { times: [], texts: [] }
const times = []
const texts = []
raw.split('\n').forEach(line => {
line = line.trim()
if (!line) return
const match = line.match(/^\[(\d+):(\d+)(\.\d+)?\](.*)/)
if (match) {
const minutes = parseInt(match[1])
const seconds = parseInt(match[2])
const time = minutes * 60 + seconds
const text = match[4] ? match[4].trim() : ''
if (text) {
times.push(time)
texts.push(text)
}
}
})
return { times, texts }
}
// 3. 解析歌词示例
const lrcText = `
[00:02]窗前明月光
[00:05]疑是地上霜
[00:08]举头望明月
[00:11]低头思故乡
`
const { times, texts } = parseLyric(lrcText)
lyricTimes.value = times
lyricTexts.value = texts
// 4. 当前歌词索引计算属性
const currentLyricIndex = computed(() => {
if (!lyricTimes.value.length) return -1
for (let i = lyricTimes.value.length - 1; i >= 0; i--) {
if (currentTime.value >= lyricTimes.value[i]) {
return i
}
}
return -1
})
// 5. 进度条点击函数
const handleProgressClick = (event) => {
const audio = audioRef.value
const bar = event.currentTarget
const rect = bar.getBoundingClientRect()
const ratio = (event.clientX - rect.left) / rect.width
const newTime = ratio * duration.value
if (audio) {
audio.currentTime = newTime
currentTime.value = newTime
}
}
// 6. 歌词滚动监听
watch(currentLyricIndex, async (newIndex) => {
if (newIndex === -1) return
await nextTick()
if (activeLyricRef.value && lyricsContainerRef.value) {
const container = lyricsContainerRef.value
const activeElement = activeLyricRef.value
const containerHeight = container.clientHeight
const elementOffset = activeElement.offsetTop
const elementHeight = activeElement.offsetHeight
const scrollTo = elementOffset - (containerHeight / 2) + (elementHeight / 2)
// 优化:限制滚动范围
const safeScrollTo = Math.max(0, Math.min(scrollTo, container.scrollHeight - containerHeight))
container.scrollTo({ top: safeScrollTo, behavior: 'smooth' })
}
})
</script>
<style scoped>
.player {
width: 300px;
margin: 20px auto;
}
.progress-bar {
height: 6px;
background: #eee;
margin: 10px 0;
cursor: pointer;
}
.lyrics-container {
height: 300px;
overflow-y: auto;
text-align: center;
}
.lyric-item {
line-height: 40px;
color: #999;
transition: color 0.2s;
}
.lyric-item.active {
color: #27ae60;
font-weight: bold;
}
/* 隐藏滚动条 */
.lyrics-container::-webkit-scrollbar {
display: none;
}
</style>
四、新手避坑指南
- 动态 ref 不能绑定到 v-for 所有项 :若给循环的所有歌词绑定同一个
ref,ref.value只会指向最后一个 DOM 元素,必须加条件判断。 nextTick不可省略:省略后会获取到更新前的旧 DOM,导致滚动错位。- 倒序遍历不能改为正序:正序遍历会匹配到旧歌词,引发显示错误。
- 进度条跳转必须同步
currentTime:只修改audio.currentTime不会触发歌词更新。
五、功能扩展方向
- 歌词拖拽功能 :监听
mousedown/mousemove事件,实现歌词拖拽调整播放进度。 - 歌词字号/颜色定制:通过响应式数据绑定样式,支持用户自定义歌词显示效果。
- 歌词缓存:解析后的歌词数据存入 localStorage,避免重复解析。
总结
本文实现的歌词功能覆盖了从文本解析 到交互体验的全链路,核心依赖 Vue3 的响应式系统和原生 DOM API。代码逻辑严谨、容错完备,可直接集成到各类音乐播放器项目中。掌握本文的核心思想,不仅能实现歌词功能,更能举一反三,解决前端开发中类似的「数据驱动 DOM 交互」问题。