阅读 506

[译]如何使用React hooks构建音频播放器

原文链接:Building an Audio Player With React Hooks

原文作者:Ryan Finni

译者:giriawsh (Giria)

个人翻译,如有错误,欢迎指正

前言

今天我们将使用HTMLAudioElement接口构建一个基本的React音频播放器组件。这个播放器拥有播放列表,可以暂停、滑动、跳转到上一首或下一首曲目。而且每首曲子都具有不同的动画背景颜色。

completed screenshot of audio player

项目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, imagecolor

const tracks = [
  {
    title: string,
    artist: string,
    audioSrc: string | import,
		image: string,
    color: string,
  },
  ...
  ...
];
复制代码

构建音频播放器组件

首先创建一个名为 AudioPlayer.jsx 的新文件并导入 useStateuseEffectuseRef hooks。

我们应该维护三个state值:

  1. trackIndex - 正在播放的曲目的索引。
  2. trackProgress - 音轨的当前进度。
  3. 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。

  1. audioRef - 通过 Audio 构造函数创建的音频元素。
  2. intervalRef - 对 setInterval 计时器的引用。
  3. 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>
	);
}
复制代码

player markup

它看起来没什么特别的,到目前为止一切也都没什么问题。我们还没有样式,所以来添加一些吧。

播放器样式

我们将在我们的样式中使用一些 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;
}
复制代码

audio player styles

现在我们需要播放器控件。

控件组件

音频控件组件将存储播放、暂停、上一首和下一首曲目按钮。我们将把它拆分成它自己的组件,并将一些功能移出 AudioPlayer主组件。

首先创建一个新文件AudioControls.jsx。我们需要一些props:我们需要知道音频是否正在播放,以便我们可以显示播放或暂停按钮。这是通过将 isPlaying 状态值作为props传递来完成的。我们还需要一些用于播放、暂停、上一个和下一个动作的点击处理程序。它们是 onPlayPauseClickonPrevClickonNextClick

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。如果这是你期望的,请将值转换为字符串”。我们正在按照警告的建议进行操作,并将其转为字符串,直到曲目开始播放并且实际持续时间值替换它为止。

我们还有两个函数要添加:onScrubEndonScrub。这些函数在这些交互操作上运行:onkeyuponChange 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 ( ... );
复制代码

这将创建一个覆盖范围输入的白色背景,以直观地显示轨道进度。

player progress indicator

然后将 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滑块保留为浏览器默认设置

playback progress complete

更改背景颜色

最后要做的是动态更改页面背景颜色。因为每首曲子都有一个关联的颜色值,所以我们需要做的就是更新 --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或其他一些音频源。

文章分类
前端
文章标签