原文链接:Building an Audio Player With React Hooks
原文作者:Ryan Finni
个人翻译,如有错误,欢迎指正
前言
今天我们将使用HTMLAudioElement接口构建一个基本的React音频播放器组件。这个播放器拥有播放列表,可以暂停、滑动、跳转到上一首或下一首曲目。而且每首曲子都具有不同的动画背景颜色。
项目Demo地址:react-audio-player-demo - CodeSandbox
该音频播放器的设计由Dribbble shot提供,演示的音乐文件由Pixabay提供。
HTMLAudioElement概述
现存技术下,有几种不同的方式来处理web音频。最常见的就是通过HTML <audio>标签,或者使用Web Audio API做更为底层的控制。本教程采用的方法是HTMLAudioElement接口的中间位置(?)
HTMLAudioElement接口提供对<audio>元素的属性访问及一系列操控它的方法,它基于并从HTMLMediaElement元素的属性访问及一系列操控它的方法. - MDN文档
使用方式非常简单:
const audioElement = new Audio(audio source);
上面的Audio()构造函数返回一个音频audio元素,其中包含一些关于source的方法和数据。
audioElement.play();
audioElement.pause();
audioElement.currentTime;
audioElement.ended;
audioElement.duration;
我们很快就会用到这些,但首先,我们应该定义音频组件props.
定义 Props
我们的组件需要的唯一prop就是它可以播放的曲目列表。我们将为它提供一组对象,每个对象都包括title, artist, audioSrc, image和color
const tracks = [
{
title: string,
artist: string,
audioSrc: string | import,
image: string,
color: string,
},
...
...
];
构建音频播放器组件
首先创建一个名为 AudioPlayer.jsx 的新文件并导入 useState、useEffect 和 useRef hooks。
我们应该维护三个state值:
trackIndex- 正在播放的曲目的索引。trackProgress- 音轨的当前进度。isPlaying- 曲目是否正在播放。
import React, { useState, useEffect, useRef } from 'react';
const AudioPlayer = ({ tracks }) => {
// State
const [trackIndex, setTrackIndex] = useState(0);
const [trackProgress, setTrackProgress] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
return ( ... );
}
export default AudioPlayer;
除了状态之外,还需要三个 refs。
audioRef- 通过 Audio 构造函数创建的音频元素。intervalRef- 对 setInterval 计时器的引用。isReady- 一个布尔值,用于确定何时准备好运行某些操作。
const AudioPlayer = () => {
// State
...
// Destructure for conciseness
const { title, artist, color, image, audioSrc } = tracks[trackIndex];
// Refs
const audioRef = useRef(new Audio(audioSrc));
const intervalRef = useRef();
const isReady = useRef(false);
// Destructure for conciseness
const { duration } = audioRef.current;
return ( ... );
}
接下来我们先添加两个函数占位。我们将在后面的部分跟进并完成它们。
一个函数 toPrevTrack 将处理上一首轨道按钮单击,另一个函数 toNextTrack 处理下一首按钮单击。
const AudioPlayer = () => {
// State
...
// Refs
...
const toPrevTrack = () => {
console.log('TODO go to prev');
}
const toNextTrack = () => {
console.log('TODO go to next');
}
return ( ... );
}
最后,完成播放器的主要部分。它显示曲目图像、标题和艺术家,默认为列表中的第一首曲目。
const AudioPlayer = () => {
...
return (
<div className="audio-player">
<div className="track-info">
<img
className="artwork"
src={image}
alt={`track artwork for ${title} by ${artist}`}
/>
<h2 className="title">{title}</h2>
<h3 className="artist">{artist}</h3>
</div>
</div>
);
}
它看起来没什么特别的,到目前为止一切也都没什么问题。我们还没有样式,所以来添加一些吧。
播放器样式
我们将在我们的样式中使用一些 CSS 变量,但它们不会太复杂。
注意
--active-color变量。稍后将使用它来将活动轨道颜色设置为背景颜色。
:root {
--white: #fff;
--active-color: #00aeb0;
}
* {
box-sizing: border-box;
}
html {
font-family: Arial, Helvetica, sans-serif;
height: 100%;
background: var(--active-color);
transition: background 0.4s ease;
}
button {
background: none;
border: none;
cursor: pointer;
}
接下来对音频播放器添加一些特定样式。
.audio-player {
max-width: 350px;
border-radius: 20px;
padding: 24px;
box-shadow: 0 28px 28px rgba(0, 0, 0, 0.2);
margin: auto;
color: var(--white);
}
.artwork {
border-radius: 120px;
display: block;
margin: auto;
height: 200px;
width: 200px;
}
.track-info {
text-align: center;
z-index: 1;
position: relative;
}
.title {
font-weight: 700;
margin-bottom: 4px;
}
.artist {
font-weight: 300;
margin-top: 0;
}
现在我们需要播放器控件。
控件组件
音频控件组件将存储播放、暂停、上一首和下一首曲目按钮。我们将把它拆分成它自己的组件,并将一些功能移出 AudioPlayer主组件。
首先创建一个新文件AudioControls.jsx。我们需要一些props:我们需要知道音频是否正在播放,以便我们可以显示播放或暂停按钮。这是通过将 isPlaying 状态值作为props传递来完成的。我们还需要一些用于播放、暂停、上一个和下一个动作的点击处理程序。它们是 onPlayPauseClick、onPrevClick 和 onNextClick。
const AudioControls = ({
isPlaying,
onPlayPauseClick,
onPrevClick,
onNextClick,
}) => ( ... )
export default AudioControls;
您如何使用 SVG 将取决于您的环境设置。对于像 Create React App 这样的库,默认情况下导入和使用它们应该可以正常工作,但在其他情况下,您可能需要一些 Webpack 工具才能使其工作。
import React from 'react';
import { ReactComponent as Play } from './assets/play.svg';
import { ReactComponent as Pause } from './assets/pause.svg';
import { ReactComponent as Next } from './assets/next.svg';
import { ReactComponent as Prev } from './assets/prev.svg';
const AudioControls = ({ ... }) => (
<div className="audio-controls">
<button
type="button"
className="prev"
aria-label="Previous"
onClick={onPrevClick}
>
<Prev />
</button>
{isPlaying ? (
<button
type="button"
className="pause"
onClick={() => onPlayPauseClick(false)}
aria-label="Pause"
>
<Pause />
</button>
) : (
<button
type="button"
className="play"
onClick={() => onPlayPauseClick(true)}
aria-label="Play"
>
<Play />
</button>
)}
<button
type="button"
className="next"
aria-label="Next"
onClick={onNextClick}
>
<Next />
</button>
</div>
);
最后,我们为控制按钮和间距添加样式。
.audio-controls {
display: flex;
justify-content: space-between;
width: 75%;
margin: 0 auto 15px;
}
.audio-controls .prev svg,
.audio-controls .next svg {
width: 35px;
height: 35px;
}
.audio-controls .play svg,
.audio-controls .pause svg {
height: 40px;
width: 40px;
}
.audio-controls path {
fill: var(--white);
}
AudioControls组件就完事儿了。将它添加到AudioPlayer主组件,将上面提到的那些props传递给它。
import AudioControls from './AudioControls';
const AudioPlayer = () => {
...
...
return (
<div className="audio-player">
<div className="track-info">
<img
className="artwork"
src={image}
alt={`track artwork for ${title} by ${artist}`}
/>
<h2>{title}</h2>
<h3>{artist}</h3>
<AudioControls
isPlaying={isPlaying}
onPrevClick={toPrevTrack}
onNextClick={toNextTrack}
onPlayPauseClick={setIsPlaying}
/>
</div>
</div>
);
}
写完了AudioControls组件,我们下一步就让播放器工作起来吧!
播放器操作函数
回到 AudioPlayer 组件,我们需要完成我们之前添加的 toPrevTrack 和 toNextTrack 函数。
单击下一步按钮应转到列表中的下一首曲目,或返回到第一首曲目。如果点击上一个按钮,则相反。
const toPrevTrack = () => {
if (trackIndex - 1 < 0) {
setTrackIndex(tracks.length - 1);
} else {
setTrackIndex(trackIndex - 1);
}
}
const toNextTrack = () => {
if (trackIndex < tracks.length - 1) {
setTrackIndex(trackIndex + 1);
} else {
setTrackIndex(0);
}
}
通过这些更改,你现在可以在曲目列表中进行切换。
下一步要添加一些useEffect hooks.
第一个用于在单击播放或暂停按钮时,使音频开始播放或暂停播放。
useEffect(() => {
if (isPlaying) {
audioRef.current.play();
} else {
audioRef.current.pause();
}
}, [isPlaying]);
每当 isPlaying 状态发生变化时,我们都会根据其值调用 audioRef 上的 play() 或pause()方法。
下一个 useEffect 钩子将在组件卸载时进行一些清理。卸载时,我们要确保音乐暂停并清除可能正在运行的所有 setInterval 计时器。下一节将详细介绍计时器!
useEffect(() => {
// Pause and clean up on unmount
return () => {
audioRef.current.pause();
clearInterval(intervalRef.current);
}
}, []);
最后一个 useEffect钩子运行在trackIndex 状态改变时。它允许我们暂停当前播放的曲目,将 audioRef 的值更新为新的源,重置进度状态,并设置新曲目播放。
// Handle setup when changing tracks
useEffect(() => {
audioRef.current.pause();
audioRef.current = new Audio(audioSrc);
setTrackProgress(audioRef.current.currentTime);
if (isReady.current) {
audioRef.current.play();
setIsPlaying(true);
startTimer();
} else {
// Set the isReady ref as true for the next pass
isReady.current = true;
}
}, [trackIndex]);
我们还在第一遍(初始安装)时在此处设置 isReady ref 的值。这是为了防止当这个 useEffect 钩子第一次运行时音频自动播放,这是我们不想要的。第二次和随后的时间运行(在 trackIndex 更改时),我们才希望播放逻辑发生。
注意这里必须要先暂停再回收。
「如果所有使用
Audio()构造函数创建的 audio 元素被删除,根据 JavaScript 垃圾回收机制,如果播放正在进行,内存中的 audio 元素不会被移除。相反,音频将会继续播放并且它的对象会保留在内存中,直到播放结束或是被暂停(例如调用pause())。在那个时候,这个对象才会成为垃圾回收的目标。」 -MDN doc
如果您测试音频播放器控件,它们现在应该可以正常工作了。
播放进度和选取
接下来,我们需要显示曲目播放进度并添加对音频不同部分进行选取的功能。
首先在 AudioPlayer 组件中定义一个名为 startTimer 的新函数。该函数负责在曲目开始播放时启动一个新的 setInterval 计时器。
const AudioPlayer = () => {
...
...
const startTimer = () => {
// Clear any timers already running
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
if (audioRef.current.ended) {
toNextTrack();
} else {
setTrackProgress(audioRef.current.currentTime);
}
}, [1000]);
}
}
每一秒,我们都会检查音频是否已经结束/完成。如果是,则转到下一首曲子,否则更新trackProgress状态。计时器 ID 存储在 intervalRef 中,以便我们可以在组件其他部分中计清除它。
startTimer 函数作为我们在上一节中添加的 useEffect 钩子的一部分被调用。首先,当 isPlaying 状态改变并且为 true 时添加它。
useEffect(() => {
if (isPlaying) {
audioRef.current.play();
startTimer();
} else {
clearInterval(intervalRef.current);
audioRef.current.pause();
}
}, [isPlaying]);
startTimer 函数也需要在 trackIndex 值发生变化时运行。
useEffect(() => {
audioRef.current.pause();
audioRef.current = new Audio(audioSrc);
setTrackProgress(audioRef.current.currentTime);
if (isReady.current) {
audioRef.current.play();
setIsPlaying(true);
startTimer();// 其实我觉得这个好像不需要?因为isPlaying变化后必定触发上面的useEffect hooks
}
}, [trackIndex]);
现在我们可以来写进度指示器了。
为了让我们的 UI 易于访问,我们将使用原生 HTML range input作为我们的播放进度指示器。这使我们可以自由使用鼠标或键盘进行音轨操作,并且可以使用大量事件进行处理。
return (
<div className="audio-player">
<div className="track-info">
...
<AudioControls ... />
<input
type="range"
value={trackProgress}
step="1"
min="0"
max={duration ? duration : `${duration}`}
className="progress"
onChange={(e) => onScrub(e.target.value)}
onMouseUp={onScrubEnd}
onKeyUp={onScrubEnd}
/>
</div>
</div>
);
持续时间值最初为
NaN。 React 会对此发出警告:“收到了max属性的NaN。如果这是你期望的,请将值转换为字符串”。我们正在按照警告的建议进行操作,并将其转为字符串,直到曲目开始播放并且实际持续时间值替换它为止。
我们还有两个函数要添加:onScrubEnd 和 onScrub。这些函数在这些交互操作上运行:onkeyup、onChange 和 onMouseUp。
const onScrub = (value) => {
// Clear any timers already running
clearInterval(intervalRef.current);
audioRef.current.currentTime = value;
setTrackProgress(audioRef.current.currentTime);
}
const onScrubEnd = () => {
// If not already playing, start
if (!isPlaying) {
setIsPlaying(true);
}
startTimer();
}
现在为播放进度指示器本身设置样式。
可能还有其他方法,但看起来没有一个很好的跨浏览器标准来设置range input的背景样式。下面的解决方案使用 webkit-gradient 解决方法,适用于 Chrome、Firefox 和 Safari,但尚未测试其他浏览器。
创建一个常量,用于保存曲目播放的当前百分比。我们在 webkit-gradient 中使用这个百分比值来更新range input的背景样式。
const currentPercentage = duration ? `${(trackProgress / duration) * 100}%` : '0%';
const trackStyling = `
-webkit-gradient(linear, 0% 0%, 100% 0%, color-stop(${currentPercentage}, #fff), color-stop(${currentPercentage}, #777))
`;
return ( ... );
这将创建一个覆盖范围输入的白色背景,以直观地显示轨道进度。
然后将 trackStyling 常量作为样式属性应用于input。
return (
...
...
<input
type="range"
value={trackProgress}
step="1"
min="0"
max={duration ? duration : `${duration}`}
className="progress"
onChange={(e) => onScrub(e.target.value)}
onMouseUp={onScrubEnd}
onKeyUp={onScrubEnd}
style={{ background: trackStyling }}
/>
);
现在为播放进度写CSS
input[type=range] {
height: 5px;
-webkit-appearance: none;
width: 100%;
margin-bottom: 10px
border-radius: 8px;
background: #3b7677;
transition: background 0.2s ease;
cursor: pointer;
}
需要注意的一件事。我发现如果您尝试在 CSS 中隐藏range input的滑块,请使用左右箭头键在 Safari 中来回选取。因此,我选择将range input滑块保留为浏览器默认设置
更改背景颜色
最后要做的是动态更改页面背景颜色。因为每首曲子都有一个关联的颜色值,所以我们需要做的就是更新 --active-color CSS 变量。之前我们设置了 HTML 背景以使用此变量,通过更新它,我们将在循环歌曲时看到颜色变化。
首先创建一个名为 Backdrop.jsx 的新组件。在 useEffect 钩子中,setProperty 方法将在 trackIndex 更改时更新 CSS 变量值。
import React, { useEffect } from 'react';
const Backdrop = ({
activeColor,
trackIndex,
isPlaying,
}) => {
useEffect(() => {
document.documentElement.style.setProperty('--active-color', activeColor);
}, [trackIndex]);
return (
<div className={`color-backdrop ${isPlaying ? 'playing' : 'idle'}`} />
);
};
export default Backdrop;
返回 AudioPlayer 组件,添加相邻的背景组件。
现在来增加一些动画。使用相同的 --active-color 变量添加线性渐变背景,并将其定位到屏幕的整个高度和宽度。
.color-backdrop {
background: linear-gradient(45deg, var(--active-color) 20%, transparent 100%) no-repeat;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
}
.color-backdrop.playing {
animation: colorChange 20s alternate infinite;
}
当音频正在播放并且背景具有playing类时展示这个动画。它是使用色调旋转hue-rotate过滤器功能完成的。
hue-rotate()CSS 函数旋转元素及其内容的色调。 - MDN Docs
Hue-rotate 接收一个角度作为参数。在动画关键帧内,我们所做的就是设置为最多旋转 360 度。
@keyframes colorChange {
from {
filter: hue-rotate(0deg);
}
to {
filter: hue-rotate(360deg);
}
}
请注意,Internet Explorer 不支持
Hue-rotate。
如果您要在生产场景中使用这种类型的动画,您应该考虑将动画样式包装在首选减少动画(
prefers-reduced-motion)的媒体查询中。这样,想要避免看到它的用户就可以不看。请参阅我写的有关该主题的帖子以获取更多信息。
总结
感谢您看到这里:我们至此涵盖了很多东西!到现在为止,您应该对如何处理音频有了一个很好的了解,我希望这能激发您构建自己的炫酷音频项目。
由于我们只介绍了音频播放器的基本操作,因此您当然可以添加更多内容。添加音量控制、播放速度调整和使用 localStorage 保存播放进度等,都是您可以探索的一些可行选项。您还可以连接该组件以使用Spotify API或其他一些音频源。