Howler.js 控制栏显隐配置、移动端适配与 useAudioPlayer 逻辑抽离
概述
主功能完成后,真正影响复用价值的是适配与可配置,而不是再加更多按钮。这一节做三件事收尾:补齐移动端控制栏(结构重排、样式重排、逻辑复用桌面端已完成的模式/倍率/快进快退);把控制栏显隐从写死模板改为 controls 数组配置驱动;把页面里越来越长的歌单与模式逻辑抽成 useAudioPlayer() 组合式函数。
这是把音频播放器从「课程示例」收束成「更接近真实组件库产物」的关键一步。
学习目标
- 理解移动端适配本质是结构/样式重排加逻辑复用,而非另写一套事件系统
- 用
controls数组让用户决定哪些控制项显示,借鉴Pagination的 layout 思路 - 用
hasName()把模板里散落的includes()判断收敛成统一入口 - 用
some()处理整组控制项的组合显隐,避免隐藏后留下空壳占位 - 用
withDefaults()的工厂函数内联默认数组,规避script setup编译提升报错 - 把歌单与模式逻辑抽成
useAudioPlayer(),让页面只保留列表数据与组件接线
一、收尾优化对应成熟组件的三个维度
这一节不是新增核心能力,而是明确三个优化方向:移动端控制栏适配、控制栏按钮显隐配置、外层歌单逻辑抽离。它们分别对应成熟组件的关键维度——能不能在不同终端正常使用、能不能按业务裁剪功能、能不能把页面逻辑沉淀成可复用能力。
播放器收尾优化 =
移动端可用
控制栏可配置
页面逻辑可抽离组件真正好不好用往往取决于这些收尾能力,而非主功能本身;「能跑」到「可复用」之间通常就差这一类适配与抽象工作。本节更偏工程化完善,而不是 API 堆叠。
二、移动端适配是结构重排而非另写逻辑
移动端适配重点不是重写一套逻辑,而是把桌面端已完成的模式切换、倍率切换、快进快退、时间显示搬到更紧凑的控制栏里继续可用。本质是结构重排、样式重排、逻辑复用——课程把桌面端按钮块按移动端布局复制过来,在移动端条件分支中复用同一套状态与事件方法。
这种做法虽不够「优雅抽象」,但在当前体量下务实。关键是移动端和桌面端逻辑应尽量复用,差异主要放在布局层;如果只复制 UI 结构却没复用事件方法,后续维护会很麻烦。课程最终验证的重点是移动端每个按钮都仍能驱动原有播放器逻辑。
三、用 controls 数组驱动控制栏显隐
这一节真正新增的组件能力是控制栏配置化。课程参考 Element Plus Pagination 的思路:不再写死哪些按钮一定出现,而是交给用户传入一个数组声明需要显示哪些控制项。不同业务对控制栏诉求差异很大——有的页面不需要快进快退,有的不需要音量,有的不需要倍率与模式。
课程整理出控制项枚举:preview / next / forward / rewind / volume / rate / mode,播放按钮和进度条保留为固定必显,其他按钮走配置数组判断。
type ControlsType =
| "preview"
| "next"
| "forward"
| "rewind"
| "volume"
| "rate"
| "mode"
interface AudioPlayerProps {
controls?: ControlsType[]
}播放按钮和进度条通常属于核心结构,不建议随意关闭;控制项应先有受控枚举再开放给用户传递。这一步做完,播放器就开始具备「不同业务页面复用同一组件」的能力。
四、用 hasName 收敛模板里的 includes 判断
课程没有在模板里反复写 props.controls?.includes("rate") 这类判断,而是先封装一个 hasName(name):
const controls = props.controls ?? []
function hasName(name: ControlsType) {
return controls.includes(name)
}模板因此更像「配置驱动显示」:v-if="hasName('preview')"、v-if="hasName('rate')"。这种小函数能让模板表达更清楚,后续控制栏规则变化时改 hasName() 比改一堆模板条件稳得多。对复杂组件来说,「小函数收口」是很重要的维护手段。
五、整组控制项隐藏时连带隐藏容器
接入 controls 配置后很快发现一个典型 UI 问题:某些控制项全部隐藏了,但外层 div 还在,页面留下一块空区域。这是配置化组件的常见副作用——单个元素显隐处理完,还要继续判断这一组是否还有任何可见子项。
<div v-if="props.controls?.some(item => ['volume', 'rate', 'mode'].includes(item))">
<button v-if="hasName('volume')" type="button">音量</button>
<button v-if="hasName('rate')" type="button">倍率</button>
<button v-if="hasName('mode')" type="button">模式</button>
</div>配置化显示时,除了单个按钮条件,还要考虑整组容器的存在条件,否则容易出现「按钮没了但间距还在」的空壳布局。这类逻辑尤其容易在播放器、工具栏、分页条里出现。
六、withDefaults 默认数组用内联工厂函数
课程想给 controls 一组默认值,直接把上面定义的常量塞进 withDefaults() 结果报错——这类问题本质上和 script setup 编译提升有关。最终务实处理是:不再引用局部常量,直接把默认数组字面量内联进工厂函数。
const props = withDefaults(defineProps<AudioPlayerProps>(), {
controls: () => ["preview", "next", "forward", "rewind", "volume", "rate", "mode"],
})数组和对象类型默认值建议都用工厂函数返回;script setup 下若默认值引用局部变量报错,直接内联通常最稳。这一步是组件配置化完整落地的重要补丁。
七、把歌单逻辑抽成 useAudioPlayer
页面里围绕歌单的逻辑(mode、oldIndex、getRandomIndex、handleNext、handlePreview、handleMode)越来越长,出现明显信号:逻辑长、和列表数据强耦合、又可独立复用。抽成组合式函数后,页面变得清晰——上面是 AudioPlayer,中间是 list,下面只保留接线。
export function useAudioPlayer(list: AudioItem[]) {
const current = ref(0)
const mode = ref(0)
const oldIndex = ref<number[]>([])
function getRandomIndex(current: number, length: number) {
let number = current
do {
number = Math.floor(Math.random() * length)
} while (number === current)
return number
}
function handleNext() {}
function handlePreview() {}
function handleMode(index: number) {}
return { current, mode, handleMode, handleNext, handlePreview }
}抽组合式函数的前提是逻辑自成体系且与页面模板解耦;useAudioPlayer() 当前接收的是列表值本身而非 ref,调用时要传 list.value,不要直接传整个响应式对象。
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 移动端样式适配了但逻辑没跟上 | 只复制布局没复用原有控制方法 | 移动端结构直接复用桌面端已完成模式/倍率/快进快退逻辑 |
| 某些按钮隐藏后右侧还留空白 | 子元素隐藏但父容器还在 | 给外层容器再加一层 some() 判断 |
controls 不传时页面什么都不显示 | 没给 controls 设默认值 | 用 withDefaults() 提供一组完整默认控制项 |
withDefaults() 里引用局部数组报错 | script setup 编译提升导致局部变量不可直接用作默认字面量 | 直接把数组字面量写进默认值工厂函数 |
| 页面歌单逻辑越来越乱 | handleNext、mode、随机策略都堆在页面 | 抽成 useAudioPlayer() 组合式函数 |
useAudioPlayer() 调用后逻辑不生效 | 传进去的是 list 的 ref 而函数按普通数组处理 | 当前写法传 list.value,不要直接传响应式对象 |