我如何构建一个音乐播放器来展示我自己的曲目🎵😍
不久前,我创建了自己的作品集,并编写了一个自定义音频播放器来展示我的曲目。从那时起,不少人对它的技术实现感兴趣。我一直回复说我可能会为它写一个完整的教程,但具体的截止日期一直没有确定。
2022年4月,我看到Hashnode宣布举办Writeathon,其中一个入口类别是Web应用程序。我知道这是一个绝佳的机会,可以参与其中,最终完成我的写作。我很高兴偶然发现了它,因为它真的激励了我。
本文不仅对您获得的实际结果有益,而且对于那些希望转行从事应用程序开发或探索 React 生态系统的人来说也具有教育意义,原因如下:
-
我将展示完整的应用程序创建周期,从功能规划、线框图和设计到创建组件、实现逻辑、添加响应能力和部署应用程序。
-
它将教你如何在 React 中思考,例如,从原生 JavaScript 转换过来,这是一个相当大的转变。你将学习如何设置和构建 React 应用程序,以及该库的一些最佳实践和思维模式。
以下是我的作品集中音乐播放器的部署预览和使用情况,可让您深入了解我们将在本教程中构建的内容:
音频播放器的源代码是开源的。我还用它制作了一个 NPM包,以便您可以轻松地在现有项目中进行设置。
规划功能
最基本的音频播放器通常带有一组最少的功能,例如播放/暂停按钮、音量或进度控制,如果您想播放单个曲目而不必将播放器与网站的设计相匹配,这可能是一个很好的解决方案。
不过,如果您关心一些额外的功能和最终用户的体验,您很可能会想要一些高级的解决方案。
在本教程中,我们将重点介绍一种更复杂的情况:您需要展示多个曲目,实现快速查找或筛选它们的方法,并控制播放顺序的行为。我们将实现的功能包括:
- 播放和暂停音频
- 下一曲目和上一曲目
- 重复曲目
- 随机播放曲目顺序
- 进度滑块
- 剩余时间/总时间
- 音量滑块
- 搜索轨迹
- 按类型过滤曲目
- 播放列表项目
创建线框
音频播放器将采用简洁直观的用户界面,并将不同的功能划分为独立的组件。这将使音频播放器更加直观,并提升用户的整体交互体验。
应用程序的整个线框将如下所示:
我们将使用Template
组件作为子元素的主要容器。如果子元素本身包含其他元素,则它们将被包裹在Box
组件中。
整个应用程序将被包装到包装器中,PageTemplate
其中将包括子组件:TagsTemplate
、、和Search
。PlayerTemplate
PlaylistTemplate
TagsTemplate
将进一步包括子项TagItem
,PlayerTemplate
将包括TitleAndTimeBox
,Progress
和ButtonsAndVolumeBox
,而PlaylistTemplate
将包括PlaylistItem
组件。
甚至TitleAndTimeBox
组件将包括Title
和Time
组件,而ButtonsAndVolumeBox
将包括ButtonsBox
和Volume
组件。
最后,ButtonsBox
将包括Button
用户控件的所有组件。
设计应用程序
音频播放器的设计将基于最大程度的可访问性,以便所有信息都易于阅读,并且所有操作按钮都易于与播放器的背景面板区分开来。
为了实现这一目标,将使用以下配色方案:
标签将采用紫色背景色,与音频播放器其他部分的主色调相呼应。这将清晰地告知用户曲目所属的流派。为了进一步提升用户体验,我们将在鼠标悬停事件发生时将背景色更改为绿色。
搜索框将采用深色背景,并在其上显示灰色的占位符文本。占位符文本的颜色会刻意淡化,以提醒用户输入的是预期值。输入完成后,文本将以白色显示。
播放器本身将采用深色背景,所有曲目、标题和时间的文字都将为白色,以提供最大对比度。此外,播放器中的所有图标也将为白色,以便在深色背景中脱颖而出。
对于进度条和音量滑块,当前进度将显示为白色,而左侧进度将显示为深色。滑块旋钮将使用与标签相同的背景颜色,以便通知用户可以与其进行交互。
最后,所有播放列表项也将采用深色背景。为了突出当前播放的曲目,它将采用白色,而播放列表中其余非活动曲目将采用与搜索占位符相同的颜色。
字体
音频播放器将使用三种不同的字体系列。下文我将描述哪些元素将使用哪种字体系列,并通过一些示例文本进行预览。
- 标签文本和当前/总时间部分将使用Varela 圆形字体。
- 曲目标题、搜索占位符值和活动播放列表项将使用Quicksand字体。
- 非活动播放列表项将使用Poppins字体。
如果您想使用其他字体系列,请随意在Google 字体中选择一些替代字体。有大量字体可供选择,只需确保在项目中将使用它们的样式表中替换它们即可。
设置 React 应用
为了开始使用样板,我们将使用Create React App,这是一个官方支持的 CLI 工具,可让您在一分钟或更短的时间内创建一个新的ReactJS项目。
打开终端并运行以下命令:npx create-react-app@latest audio-player
。等待几分钟,终端向导将完成安装项目所需的依赖项。
然后通过运行将当前工作目录更改为新创建的项目文件夹并cd audio-player
运行npm start
以启动开发服务器。
现在打开浏览器,导航到http://localhost:3000,你应该会看到 ReactJS 应用程序模板,它看起来像这样:
切换回项目并查看文件夹树。导航到该src
目录并删除当前目录中的所有文件,因为我们将从头开始创建所有内容。
设置应用程序的基础
我们将首先创建应用程序的根文件,它将呈现整个应用程序。
为此,请导航到该src
文件夹并创建一个新文件index.js
。确保包含以下代码:
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { Player } from "./App";
const tracks = [
{
url: "https://audioplayer.madza.dev/Madza-Chords_of_Life.mp3",
title: "Madza - Chords of Life",
tags: ["house"],
},
{
url: "https://audioplayer.madza.dev/Madza-Late_Night_Drive.mp3",
title: "Madza - Late Night Drive",
tags: ["dnb"],
},
{
url: "https://audioplayer.madza.dev/Madza-Persistence.mp3",
title: "Madza - Persistence",
tags: ["dubstep"],
},
];
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Player trackList={tracks} />
</React.StrictMode>
);
首先,我们导入了文件React
,ReactDOM
这样我们就可以在文件中创建一个渲染函数。我们还导入了样式表文件(我们将在创建完此文件后创建它),并且已经包含了Player
应用逻辑所在的组件。
对于每个曲目,我们都需要它的来源、标题和标签,因此我们已经创建了一个由三个示例曲目组成的对象数组,这些对象将Player
作为道具传递到组件中。
音频源来自我已部署的示例项目,因此您无需在线搜索音轨。或者,您可以将一些本地文件上传到项目中并链接到它们。
接下来,在文件夹中src
,创建一个新文件index.css
并包含以下样式规则:
@import url('https://fonts.googleapis.com/css2?family=Varela+Round&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@500&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap');
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
background-color: #151616;
}
:root {
--tagsBackground: #9440f3;
--tagsText: #ffffff;
--tagsBackgroundHoverActive: #2cc0a0;
--tagsTextHoverActive: #ffffff;
--searchBackground: #18191f;
--searchText: #ffffff;
--searchPlaceHolder: #575a77;
--playerBackground: #18191f;
--titleColor: #ffffff;
--timeColor: #ffffff;
--progressSlider: #9440f3;
--progressUsed: #ffffff;
--progressLeft: #151616;
--volumeSlider: #9440f3;
--volumeUsed: #ffffff;
--volumeLeft: #151616;
--playlistBackground: #18191f;
--playlistText: #575a77;
--playlistBackgroundHoverActive: #18191f;
--playlistTextHoverActive: #ffffff;
}
首先,我们从 Google 字体导入了 Varela Round、Quicksand 和 Poppins 字体。
然后,我们重置了应用中所有元素的规则,以确保所有元素在每个浏览器上看起来都一致。我们移除了 padding 和 margin,并配置了 box-sizing,使其在宽度和高度中包含 padding 和 margin。
最后,我们设置了 body 的背景颜色,并创建了一个将在整个应用中使用的全局配色方案。借助:root
选择器,之后可以通过 访问每种颜色var(--property)
。
下载图标
为了给音频控制带来出色的用户体验,我们将使用 .PNG 图标来播放、暂停、循环、随机播放列表顺序以及切换到上一首和下一首曲目。
为了跟踪循环和随机播放按钮的状态,白色图标将用于表示非活动状态,而灰色图标将用于表示活动状态。
我整理了一个包含所有图标的可下载包,您可以点击此处下载。请确保解压文件夹并将其包含在src
目录中。
或者,您可以从flaticon.com或icons8.com等网站下载您自己的图标。只需确保将它们重命名为与上面下载包中的名称相同的名称即可。
创建组件
在我们的音频播放器中,我们将使用 20 个组件。对于大多数组件,我们将创建单独的 JS 和 CSS 模块文件。您可以手动创建它们,但我建议您运行以下命令,它将在几秒钟内创建所需的一切:
mkdir components && cd components && touch PageTemplate.js TagsTemplate.js TagsTemplate.module.css TagItem.js TagItem.module.css Search.js Search.module.css PlayerTemplate.js PlayerTemplate.module.css TitleAndTimeBox.js TitleAndTimeBox.module.css Title.js Title.module.css Time.js Time.module.css Progress.js Progress.module.css ButtonsAndVolumeBox.js ButtonsAndVolumeBox.module.css ButtonsBox.js ButtonsBox.module.css Loop.js Loop.module.css Previous.js Previous.module.css Play.js Play.module.css Pause.js Pause.module.css Next.js Next.module.css Shuffle.js Shuffle.module.css Volume.js Volume.module.css PlaylistTemplate.js PlaylistTemplate.module.css PlaylistItem.js PlaylistItem.module.css
。
一旦创建了所有组件,我们就用代码和样式规则填充每个组件。
打开PageTemplate.js
并包含以下代码:
export const PageTemplate = ({ children }) => {
return <div>{children}</div>;
};
这是应用程序的主要包装组件,它将包含我们在接下来的步骤中创建的所有子组件。
打开TagsTemplate.js
并包含以下代码:
import styles from "./TagsTemplate.module.css";
export const TagsTemplate = ({ children }) => {
return <div className={styles.wrapper}>{children}</div>;
};
这将是我们将使用的所有标签的包装组件,并确保它们遵循正确的布局。
打开TagsTemplate.module.css
并包含以下样式规则:
.wrapper {
width: 100%;
margin: 20px auto;
height: auto;
color: var(--primaryText);
display: inline-block;
text-align: center;
}
我们首先设置宽度以占用包装器中所有可用的宽度,在顶部和底部添加一些边距,设置标签文本中要使用的颜色,将其与中心对齐,并确保标签将水平显示为内联元素。
打开TagItem.js
并包含以下代码:
import styles from "./TagItem.module.css";
export const TagItem = ({ status, onClick, tag }) => {
return (
<div
className={`${styles.tag} ${status === "active" ? styles.active : ""}`}
onClick={onClick}
>
{tag}
</div>
);
};
这些将是标签组件本身。每个组件都会接收一个status
prop,用于通过自定义样式规则控制哪个标签处于活动状态;一个onClick
prop,用于描述点击标签时发生的情况;以及一个tag
prop,用于为每个标签提供标题。
打开TagItem.module.css
并包含以下样式规则:
.tag {
background-color: var(--tagsBackground);
color: var(--tagsText);
height: 40px;
min-width: 100px;
display: inline-grid;
place-items: center;
margin: 5px 5px;
transition: transform 0.2s;
padding: 0 10px;
font-family: 'Varela Round', sans-serif;
border-radius: 10px;
font-size: 18px;
}
.active {
background-color: var(--tagsBackgroundHoverActive);
color: var(--tagsTextHoverActive);
}
.tag:hover {
background-color: var(--tagsBackgroundHoverActive);
color: var(--tagsTextHoverActive);
cursor: pointer;
transform: scale(1.1);
}
我们设置了背景和文本颜色,定义了高度和宽度,使内容居中,添加了一些边距和填充,设置了字体大小,并为播放列表项添加了一些圆角。
对于活动标签,我们设置了不同的背景和文本颜色。对于悬停标签,我们也设置了不同的背景和文本颜色,并添加了一些尺寸缩放,并将光标更改为指针。
打开Search.js
并包含以下代码:
import styles from "./Search.module.css";
export const Search = ({ onChange, value, placeholder }) => {
return (
<input
type="text"
className={styles.search}
onChange={onChange}
value={value}
placeholder={placeholder}
/>
);
};
搜索组件将具有一个onChange
prop,用于描述输入值改变时的行为,value
用于跟踪输入的值的 prop,以及placeholder
用于在搜索栏中没有输入时显示占位符文本的 prop。
打开Search.module.css
并包含以下样式规则:
.search {
font-family: 'Quicksand', sans-serif;
height: 40px;
border: none;
font-size: 18px;
width: 100%;
margin: 0 auto 10px auto;
background-color: var(--searchBackground);
color: var(--searchText);
padding-left: 20px;
border-radius: 10px;
}
.search::placeholder {
color: var(--searchPlaceHolder);
}
我们设置了文本的字体系列、字体大小和颜色,以及栏的具体高度,并确保它充分利用了父级的所有可用宽度。我们还在底部增加了一些边距,在左侧增加了一些填充,并移除了默认边框并设置了圆角。
对于占位符值,我们设置文本颜色。
打开PlayerTemplate.js
并包含以下代码:
import styles from "./PlayerTemplate.module.css";
export const PlayerTemplate = ({ children }) => {
return <div className={styles.wrapper}>{children}</div>;
};
这将是播放器组件的主要包装器,其中包括所有子组件和子子组件。
打开PlayerTemplate.module.css
并包含以下样式规则:
.wrapper {
border-radius: 10px;
padding: 0 40px;
background-color: var(--playerBackground);
overflow: auto;
font-family: 'Quicksand', sans-serif;
}
在样式规则中,我们确保包装器具有一些左右填充、深色背景颜色、特定的字体系列、漂亮的圆角和自动溢出行为。
打开TitleAndTimeBox.js
并包含以下代码:
import styles from "./TitleAndTimeBox.module.css";
export const TitleAndTimeBox = ({ children }) => {
return <div className={styles.wrapper}>{children}</div>;
};
这是播放器包装器的第一个子组件,包括标题和时间组件。
打开TitleAndTimeBox.module.css
并包含以下样式规则:
.wrapper {
display: grid;
grid-template-columns: auto 200px;
margin: 30px 0 20px 0;
}
我们确保包装器使用网格布局,将可用空间分成两列,其中左列的宽度是根据可用空间计算得出的,并从右列的宽度中减去,右列的宽度设置为200px
。我们还确保包装器有一定的上下边距。
打开Title.js
并包含以下代码:
import styles from "./Title.module.css";
export const Title = ({ title }) => {
return <h1 className={styles.title}>{title}</h1>;
};
标题组件将包含title
道具,它将显示曲目的名称。
打开Title.module.css
并包含以下样式规则:
.title {
color: var(--titleColor);
font-size: 28px;
}
我们为标题设置颜色并为其设置特定的字体大小。
打开Time.js
并包含以下代码:
import styles from "./Time.module.css";
export const Time = ({ time }) => {
return <h1 className={styles.time}>{time}</h1>;
};
时间组件将接收time
显示曲目的播放时间和总时间的道具。
打开Time.module.css
并包含以下样式规则:
.time {
font-family: 'Varela Round', sans-serif;
color: var(--timeColor);
text-align: right;
font-size: 30px;
}
我们设置了文本的字体类型、大小和颜色,并将其右对齐。
打开Progress.js
并包含以下代码:
import styles from "./Progress.module.css";
export const Progress = ({ value, onChange, onMouseUp, onTouchEnd }) => {
return (
<div className={styles.container}>
<input
type="range"
min="1"
max="100"
step="1"
value={value}
className={styles.slider}
id="myRange"
onChange={onChange}
onMouseUp={onMouseUp}
onTouchEnd={onTouchEnd}
style={{
background: `linear-gradient(90deg, var(--progressUsed) ${Math.floor(
value
)}%, var(--progressLeft) ${Math.floor(value)}%)`,
}}
/>
</div>
);
};
进度组件将接收value
获取范围当前值的道具、onChange
控制滑块旋钮拖动时的行为的道具、onMouseUp
用户释放鼠标按钮时传递事件的道具以及onTouchEnd
从触摸屏设备的触摸表面移除一个或多个触摸点时的事件的道具。
我们还将范围的最小值设置为1
,最大值设置为 ,100
增量为1
。为了使使用进度和剩余进度具有不同的颜色,我们设置了自定义样式,并添加了具有度角的线性渐变背景90
。
打开Progress.module.css
并包含以下样式规则:
.container {
display: grid;
place-items: center;
margin-bottom: 20px;
}
.slider {
-webkit-appearance: none;
width: 100%;
height: 4px;
border-radius: 5px;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 25px;
height: 25px;
border-radius: 50%;
background: var(--progressSlider);
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--progressSlider);
cursor: pointer;
}
我们包装了进度条组件并将其置于网格布局的中心,同时设置了一些底部边距以将进度条与下面的组件分开。
我们将滑动条本身设置为占用父级的所有可用宽度,设置其高度,删除默认样式,并在滑动条的两端添加一些边框半径。
对于滑块旋钮本身,我们删除了它的默认样式,将其背景颜色设置为与标签相同,添加了固定的宽度和高度,将旋钮做成圆形,并在与其交互时将光标设置为指针。
打开ButtonsAndVolumeBox.js
并包含以下代码:
import styles from "./ButtonsAndVolumeBox.module.css";
export const ButtonsAndVolumeBox = ({ children }) => {
return <div className={styles.wrapper}>{children}</div>;
};
这将是一个包含按钮框和音量条的包装组件。
打开ButtonsAndVolumeBox.module.css
并包含以下样式规则:
.wrapper {
display: grid;
grid-template-columns: auto 30%;
margin-bottom: 30px;
}
我们确保包装器使用网格布局,并将其分成两列,右侧一列显示30
百分比,左侧一列则占据剩余的可用空间。我们还在底部设置了一些边距,以便将其与下方的组件分隔开。
打开ButtonsBox.js
并包含以下代码:
import styles from "./ButtonsBox.module.css";
export const ButtonsBox = ({ children }) => {
return <div className={styles.wrapper}>{children}</div>;
};
该组件将包含所有音频控制按钮作为子按钮。
打开ButtonsBox.module.css
并包含以下样式规则:
.wrapper {
display: grid;
grid-template-columns: repeat(5, auto);
place-items: center;
}
我们确保使用网格布局,并将可用空间分成五列,每列宽度相等。我们还将项目居中放置在列中。
打开Loop.js
并包含以下代码:
import styles from "./Loop.module.css";
export const Loop = ({ src, onClick }) => {
return <img className={styles.loop} src={src} onClick={onClick} />;
};
循环组件用于在当前曲目播放结束后循环播放。它将接收提供src
循环图标来源的 prop,以及onClick
接收点击时动作函数的 prop。
打开Loop.module.css
并包含以下样式规则:
.loop {
width: 26px;
height: 26px;
transition: transform 0.2s;
}
.loop:hover {
cursor: pointer;
transform: scale(1.2);
}
我们设置了图标的具体宽度和高度,并添加了漂亮的过渡效果,以便当用户将鼠标悬停在图标上时,图标会稍微放大。此外,当用户将鼠标悬停在图标上时,光标会变为指针。
打开Previous.js
并包含以下代码:
import styles from "./Previous.module.css";
export const Previous = ({ src, onClick }) => {
return <img className={styles.previous} src={src} onClick={onClick} />;
};
此组件允许我们切换到上一个轨道。它将接收src
图标来源的 prop 以及onClick
点击时的操作的 prop。
打开Previous.module.css
并包含以下样式规则:
.previous {
width: 50px;
height: 50px;
transition: transform 0.2s;
}
.previous:hover {
cursor: pointer;
transform: scale(1.2);
}
我们设置了比循环组件更大的宽度和高度。我们还添加了鼠标悬停时的尺寸过渡以及光标的指针。
打开Play.js
并包含以下代码:
import styles from "./Play.module.css";
export const Play = ({ src, onClick }) => {
return <img className={styles.play} src={src} onClick={onClick} />;
};
play 组件允许我们播放曲目。它将接收src
图标来源的 prop 以及onClick
点击时的动作 prop。
打开Play.module.css
并包含以下样式规则:
.play {
width: 60px;
height: 60px;
transition: transform 0.2s;
}
.play:hover {
cursor: pointer;
transform: scale(1.2);
}
我们进一步增大了图标的宽度和高度,使其更加突出。与之前一样,我们添加了图标尺寸增大和鼠标悬停时光标变化的功能。
打开Pause.js
并包含以下代码:
import styles from "./Pause.module.css";
export const Pause = ({ src, onClick }) => {
return <img className={styles.pause} src={src} onClick={onClick} />;
};
暂停组件可以让我们停止音频。它将接收src
图标源的 prop 以及onClick
点击时的动作的 prop。
打开Pause.module.css
并包含以下样式规则:
.pause {
width: 60px;
height: 60px;
transition: transform 0.2s;
}
.pause:hover {
cursor: pointer;
transform: scale(1.2);
}
我们设置了与播放组件相同的宽度和高度,并包括悬停时光标的尺寸增加和指针。
打开Next.js
并包含以下代码:
import styles from "./Next.module.css";
export const Next = ({ src, onClick }) => {
return <img className={styles.next} src={src} onClick={onClick} />;
};
这个组件允许我们切换到下一首曲目。它将接收src
图标源的 prop 以及onClick
点击时的动作 prop。
打开Next.module.css
并包含以下样式规则:
.next {
width: 50px;
height: 50px;
transition: transform 0.2s;
}
.next:hover {
cursor: pointer;
transform: scale(1.2);
}
我们设置了与切换上一曲目组件相同的宽度和高度。此外,我们还添加了图标尺寸的增加以及鼠标悬停时光标的变化。
打开Shuffle.js
并包含以下代码:
import styles from "./Shuffle.module.css";
export const Shuffle = ({ src, onClick }) => {
return <img className={styles.shuffle} src={src} onClick={onClick} />;
};
最后一个按钮组件是 shuffle,它允许我们混合播放列表曲目的顺序。propsrc
是图标源,onClick
当它被点击时会收到一个动作。
打开Shuffle.module.css
并包含以下样式规则:
.shuffle {
width: 26px;
height: 26px;
transition: transform 0.2s;
}
.shuffle:hover {
cursor: pointer;
transform: scale(1.2);
}
我们将图标的宽度和高度设置为与循环组件相同。最后,我们添加了尺寸增大效果,并将光标更改为悬停时的指针。
打开Volume.js
并包含以下代码:
import styles from "./Volume.module.css";
export const Volume = ({ onChange, value }) => {
return (
<div className={styles.wrapper}>
<input
type="range"
min="1"
max="100"
defaultValue="80"
className={styles.slider}
id="myRange"
onChange={onChange}
style={{
background: `linear-gradient(90deg, var(--volumeUsed) ${
value * 100
}%, var(--volumeLeft) ${value * 100}%)`,
}}
/>
</div>
);
};
音量组件允许我们改变正在播放的音频的音量。它会接收一个onChange
prop,允许我们在滑块改变时传递动作,以及一个value
prop,允许我们跟踪滑块的当前值。
它将使用输入范围的最小值为1
和最大值为 ,100
步长为 的增加和减少1
。与之前的进度组件类似,为了以不同的颜色显示范围的已使用部分和剩余部分,我们使用了线性渐变。
打开Volume.module.css
并包含以下样式规则:
.wrapper {
display: grid;
place-items: center;
min-height: 60px;
}
.slider {
-webkit-appearance: none;
width: 70%;
height: 3px;
border-radius: 5px;
background: var(--volumeSlider);
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--volumeSlider);
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--volumeSlider);
cursor: pointer;
}
我们用网格布局将音量条包裹到容器中,并使其居中。我们还设置了它的高度,以适应父布局。
对于滑块本身,我们首先移除了默认样式,然后将其设置为使用70
可用空间的百分比并设置具体的高度。我们还为滑块的圆角添加了 border-radius 属性,并设置了背景颜色。
对于滑块旋钮,我们删除了自定义样式,并设置了与进度组件相同的背景。我们也将其设计成圆形,但比进度组件中的要小一些。最后,我们将为鼠标悬停时添加指针效果。
打开PlaylistTemplate.js
并包含以下代码:
import styles from "./PlaylistTemplate.module.css";
export const PlaylistTemplate = ({ children }) => {
return <div className={styles.wrapper}>{children}</div>;
};
该组件将成为所有播放列表项的包装器。
打开PlaylistTemplate.module.css
并包含以下样式规则:
.wrapper {
margin: 20px auto;
max-height: 425px;
min-height: 120px;
overflow-x: hidden;
padding-right: 10px;
font-family: "Quicksand", sans-serif;
}
.wrapper::-webkit-scrollbar {
width: 5px;
}
.wrapper::-webkit-scrollbar-track {
border-radius: 10px;
}
.wrapper::-webkit-scrollbar-thumb {
background: var(--primaryText);
border-radius: 10px;
}
我们确保在顶部和底部设置了一些边距,设置了高度,将 x 轴上的溢出设置为隐藏,在左侧添加了一些填充,并为包含的播放列表项的文本设置了字体系列。
如果某些播放列表项超出播放列表包装器的高度,则允许用户滚动。为此,我们创建了一个自定义滚动条。我们设置了它的宽度、边框半径和背景颜色。
打开PlaylistItem.js
并包含以下代码:
import styles from "./PlaylistItem.module.css";
export const PlaylistItem = ({ status, data_key, src, title, onClick }) => {
return (
<p
className={`${styles.item} ${status === "active" ? styles.active : ""}`}
data-key={data_key}
src={src}
title={title}
onClick={onClick}
>
{title}
</p>
);
};
这是实际的播放列表项,它将接收status
用于控制活动项的道具、data_key
用于我们稍后识别它的道具、src
用于音频源的道具、title
用于显示音频标题的道具和onClick
用于控制点击行为的道具。
打开PlaylistItem.module.css
并包含以下样式规则:
.item {
background-color: var(--playlistBackground);
color: var(--playlistText);
text-align: center;
margin: 5px 0;
padding: 3px 0;
border-radius: 5px;
font-size: 16px;
font-family: 'Poppins', sans-serif;
}
.active {
color: var(--playlistTextHoverActive);
font-family: 'Quicksand', sans-serif;
font-size: 18px;
}
.item:hover {
color: var(--playlistTextHoverActive);
cursor: pointer;
}
我们设置了自定义背景和文本颜色,将文本对齐显示在中心,设置了一些边距和填充,设置了字体大小和字体系列,并添加了一些圆角。
对于活动项目,我们更改了文本颜色、字体大小和字体系列。我们还为悬停的项目设置了不同的文本颜色,并将光标更改为指针。
逻辑整合
现在回到src
文件夹并创建一个文件,App.js
作为音乐播放器逻辑的主文件。包含以下代码:
import { useState, useEffect, useRef } from "react";
import { PageTemplate } from "./components/PageTemplate";
import { TagsTemplate } from "./components/TagsTemplate";
import { TagItem } from "./components/TagItem";
import { Search } from "./components/Search";
import { PlayerTemplate } from "./components/PlayerTemplate";
import { TitleAndTimeBox } from "./components/TitleAndTimeBox";
import { Title } from "./components/Title";
import { Time } from "./components/Time";
import { Progress } from "./components/Progress";
import { ButtonsAndVolumeBox } from "./components/ButtonsAndVolumeBox";
import { ButtonsBox } from "./components/ButtonsBox";
import { Loop } from "./components/Loop";
import { Previous } from "./components/Previous";
import { Play } from "./components/Play";
import { Pause } from "./components/Pause";
import { Next } from "./components/Next";
import { Shuffle } from "./components/Shuffle";
import { Volume } from "./components/Volume";
import { PlaylistTemplate } from "./components/PlaylistTemplate";
import { PlaylistItem } from "./components/PlaylistItem";
import loopCurrentBtn from "./icons/loop_current.png";
import loopNoneBtn from "./icons/loop_none.png";
import previousBtn from "./icons/previous.png";
import playBtn from "./icons/play.png";
import pauseBtn from "./icons/pause.png";
import nextBtn from "./icons/next.png";
import shuffleAllBtn from "./icons/shuffle_all.png";
import shuffleNoneBtn from "./icons/shuffle_none.png";
const fmtMSS = (s) => new Date(1000 * s).toISOString().substr(15, 4);
export const Player = ({ trackList }) => {
const [audio, setAudio] = useState(null);
const [isPlaying, setIsPlaying] = useState(false);
const [hasEnded, setHasEnded] = useState(false);
const [title, setTitle] = useState("");
const [length, setLength] = useState(0);
const [time, setTime] = useState(0);
const [slider, setSlider] = useState(1);
const [drag, setDrag] = useState(0);
const [volume, setVolume] = useState(0.8);
const [shuffled, setShuffled] = useState(false);
const [looped, setLooped] = useState(false);
let playlist = [];
const [filter, setFilter] = useState([]);
let [curTrack, setCurTrack] = useState(0);
const [query, updateQuery] = useState("");
const tags = [];
trackList.forEach((track) => {
track.tags.forEach((tag) => {
if (!tags.includes(tag)) {
tags.push(tag);
}
});
});
useEffect(() => {
const audio = new Audio(trackList[curTrack].url);
const setAudioData = () => {
setLength(audio.duration);
setTime(audio.currentTime);
};
const setAudioTime = () => {
const curTime = audio.currentTime;
setTime(curTime);
setSlider(curTime ? ((curTime * 100) / audio.duration).toFixed(1) : 0);
};
const setAudioVolume = () => setVolume(audio.volume);
const setAudioEnd = () => setHasEnded(!hasEnded);
audio.addEventListener("loadeddata", setAudioData);
audio.addEventListener("timeupdate", setAudioTime);
audio.addEventListener("volumechange", setAudioVolume);
audio.addEventListener("ended", setAudioEnd);
setAudio(audio);
setTitle(trackList[curTrack].title);
return () => {
audio.pause();
};
}, []);
useEffect(() => {
if (audio != null) {
audio.src = trackList[curTrack].url;
setTitle(trackList[curTrack].title);
play();
}
}, [curTrack]);
useEffect(() => {
if (audio != null) {
if (shuffled) {
playlist = shufflePlaylist(playlist);
}
!looped ? next() : play();
}
}, [hasEnded]);
useEffect(() => {
if (audio != null) {
audio.volume = volume;
}
}, [volume]);
useEffect(() => {
if (audio != null) {
pause();
const val = Math.round((drag * audio.duration) / 100);
audio.currentTime = val;
}
}, [drag]);
useEffect(() => {
if (!playlist.includes(curTrack)) {
setCurTrack((curTrack = playlist[0]));
}
}, [filter]);
const loop = () => {
setLooped(!looped);
};
const previous = () => {
const index = playlist.indexOf(curTrack);
index !== 0
? setCurTrack((curTrack = playlist[index - 1]))
: setCurTrack((curTrack = playlist[playlist.length - 1]));
};
const play = () => {
setIsPlaying(true);
audio.play();
};
const pause = () => {
setIsPlaying(false);
audio.pause();
};
const next = () => {
const index = playlist.indexOf(curTrack);
index !== playlist.length - 1
? setCurTrack((curTrack = playlist[index + 1]))
: setCurTrack((curTrack = playlist[0]));
};
const shuffle = () => {
setShuffled(!shuffled);
};
const shufflePlaylist = (arr) => {
if (arr.length === 1) return arr;
const rand = Math.floor(Math.random() * arr.length);
return [arr[rand], ...shufflePlaylist(arr.filter((_, i) => i !== rand))];
};
const tagClickHandler = (e) => {
const tag = e.currentTarget.innerHTML;
if (!filter.includes(tag)) {
setFilter([...filter, tag]);
} else {
const filteredArray = filter.filter((item) => item !== tag);
setFilter([...filteredArray]);
}
};
const playlistItemClickHandler = (e) => {
const num = Number(e.currentTarget.getAttribute("data-key"));
const index = playlist.indexOf(num);
setCurTrack((curTrack = playlist[index]));
play();
};
return (
<PageTemplate>
<TagsTemplate>
{tags.map((tag, index) => {
return (
<TagItem
key={index}
status={
filter.length !== 0 && filter.includes(tag) ? "active" : ""
}
tag={tag}
onClick={tagClickHandler}
/>
);
})}
</TagsTemplate>
<Search
value={query}
onChange={(e) => updateQuery(e.target.value.toLowerCase())}
placeholder={`Search ${trackList.length} tracks...`}
/>
<PlayerTemplate>
<TitleAndTimeBox>
<Title title={title} />
<Time
time={`${!time ? "0:00" : fmtMSS(time)}/${
!length ? "0:00" : fmtMSS(length)
}`}
/>
</TitleAndTimeBox>
<Progress
value={slider}
onChange={(e) => {
setSlider(e.target.value);
setDrag(e.target.value);
}}
onMouseUp={play}
onTouchEnd={play}
/>
<ButtonsAndVolumeBox>
<ButtonsBox>
<Loop src={looped ? loopCurrentBtn : loopNoneBtn} onClick={loop} />
<Previous src={previousBtn} onClick={previous} />
{isPlaying ? (
<Pause src={pauseBtn} onClick={pause} />
) : (
<Play src={playBtn} onClick={play} />
)}
<Next src={nextBtn} onClick={next} />
<Shuffle
src={shuffled ? shuffleAllBtn : shuffleNoneBtn}
onClick={shuffle}
/>
</ButtonsBox>
<Volume
value={volume}
onChange={(e) => {
setVolume(e.target.value / 100);
}}
/>
</ButtonsAndVolumeBox>
</PlayerTemplate>
<PlaylistTemplate>
{trackList
.sort((a, b) => (a.title > b.title ? 1 : -1))
.map((el, index) => {
if (
filter.length === 0 ||
filter.some((filter) => el.tags.includes(filter))
) {
if (el.title.toLowerCase().includes(query.toLowerCase())) {
playlist.push(index);
return (
<PlaylistItem
status={curTrack === index ? "active" : ""}
key={index}
data_key={index}
title={el.title}
src={el.url}
onClick={playlistItemClickHandler}
/>
);
}
}
})}
</PlaylistTemplate>
</PageTemplate>
);
};
首先,我们导入了useState、useEffect和useRef钩子,我们将使用它们来跟踪状态并对某些操作执行副作用。
接下来,我们导入了在教程上一步中创建的所有组件,还导入了您下载的图标,以便我们可以在组件中将它们用作源文件。
音乐播放器将使用该M:SS
格式来显示曲目的当前时间和总时间,因此我们为时间组件创建了转换函数。
然后,我们设置应用中使用的所有变量的状态。我们还循环遍历了从playlist
接收到的对象的所有标签index.js
,并将它们推送到一个数组中,以便将它们显示在播放器的顶部。
在初始加载时,我们创建了一个新的音频对象并为loadeddata
、timeupdate
和设置了事件监听器volumechange
,ended
以便当其中任何一个发生时,都会触发特定的功能。
我们还使用副作用来设置活动轨道更改时的源,配置当前轨道结束时是否循环播放轨道或随机播放列表,并在拖动进度和音量旋钮时设置轨道进度和音量级别,以及在选择任何标签时过滤轨道。
接下来,我们为循环、上一首、播放、暂停、下一首和随机播放图标的点击事件创建了单独的函数。这些函数都很简单,通过函数名就能直观地了解其功能。
最后,我们将所有导入的组件按照与线框中设计的顺序放入返回块中,并传入单独创建每个组件时所需的所有道具。
增加响应能力
最后一步是添加响应式功能。我们将为以下组件创建一些 CSS 媒体规则:PlayerTemplate
、TitleAndTimeBox
、Title
、Time
、Progress
、ButtonsAndVolumeBox
、ButtonsBox
和Loop
。Shuffle
媒体规则通常添加在样式表的底部,因此我们将浏览样式文件并在我们之前编写的现有规则下添加以下规则:
打开PlayerTemplate.module.css
并包含以下样式规则:
@media only screen and (max-width: 600px) {
.wrapper {
padding: 0 20px;
}
}
我们确保播放器在移动设备上使用时侧面有一定的填充。
打开TitleAndTimeBox.module.css
并包含以下样式规则:
@media only screen and (max-width: 800px) {
.wrapper {
grid-template-columns: 1fr;
}
}
我们将标题和时间组件设置为在小于 的设备上直接显示在彼此上方800px
。
打开Title.module.css
并包含以下样式规则:
@media only screen and (max-width: 600px) {
.title {
width: 100%;
text-align: center;
}
}
我们将标题设置为占用所有可用空间并位于移动设备的中心。
打开Time.module.css
并包含以下样式规则:
@media only screen and (max-width: 600px) {
.time {
text-align: center;
}
}
我们将时间组件的文本置于移动设备的中心。
打开Progress.module.css
并包含以下样式规则:
@media only screen and (max-width: 600px) {
.container {
margin: 40px 0;
}
}
我们为移动设备上的进度组件设置了顶部和底部边距。
打开ButtonsAndVolumeBox.module.css
并包含以下样式规则:
@media only screen and (max-width: 800px) {
.wrapper {
grid-template-columns: 1fr;
}
}
我们将底部框和体积组件设置为在小于的屏幕上直接显示在彼此下方800px
。
打开ButtonsBox.module.css
并包含以下样式规则:
@media only screen and (max-width: 600px) {
.wrapper {
grid-template-columns: repeat(3, auto);
}
}
我们确保按钮框针对移动设备采用等宽的三列布局。
打开Loop.module.css
并包含以下样式规则:
@media only screen and (max-width: 600px) {
.loop {
display: none;
}
}
我们隐藏了移动设备上的循环按钮以简化用户界面。
打开Shuffle.module.css
并包含以下样式规则:
@media only screen and (max-width: 600px) {
.shuffle {
display: none;
}
}
我们隐藏了移动设备上的随机播放按钮以简化用户界面。
在添加媒体规则之后,我们添加了音频播放器应该全权负责。
要测试它,请查看您的开发服务器是否仍在终端中运行(如果它没有再次运行),然后在端口http://localhost:3000npm start
上打开浏览器并按 F12 打开开发工具。
尝试调整活动视图的大小以查看播放器如何适应不同的屏幕宽度:
应用程序部署
为了让我们的应用程序向公众开放,首先,我们需要将所有代码推送到GitHub。
首先,创建一个新的 GitHub 帐户(如果您还没有),然后登录。
从菜单中选择创建一个新的存储库,选择一个存储库名称(可以是“音频播放器”或您想要的任何其他名称),然后单击“创建存储库”。
要将应用程序推送到新创建的存储库,请切换回终端/代码编辑器并运行以下命令(替换<username>
为您的 GitHub 用户名和<reponame>
存储库的名称):
git remote add origin https://github.com/<username>/<reponame>.git
git branch -M main
git push -u origin main
然后切换回你的GitHub,检查你的项目文件是否出现在你创建的仓库中。如果是,则表示你已成功提交代码。
最后一步是将应用程序部署到线上。为此,我们将使用Vercel。
然后创建一个新项目。您需要安装 Vercel for GitHub(访问权限),以便Vercel可以查看您的Github存储库。
现在从“导入 Git 存储库”面板导入您的项目。
Vercel会自动检测项目名称、构建命令和 root 权限,因此您无需担心。构建过程应该不会超过一分钟。
现在返回到Overview
项目选项卡并单击“访问”按钮,这将打开项目的实际 URL。
恭喜,您已成功部署您的音乐播放器!
从现在开始,每次您将更新推送到GitHub时,它都会自动重新部署到Vercel上,这意味着您的音频播放器将与GitHub上的代码同步。
结论
在本教程中,我们首先明确了音频播放器的理念和功能。然后,我们创建了一个线框图,并将所有功能融入到 UI 中。设计的最后一步是选择合适的配色方案,并找到合适的字体,确保文本看起来美观。
然后,我们开始准备构建应用的基础。首先,我们设置了 React 应用。然后,我们创建了一些自定义基础文件,以便正确渲染播放器。最后,我们导入了所有用于控制音频播放的图标。
在播放器的技术实现中,我们首先编写了所有单独的组件。然后,我们创建了播放器的主应用文件,导入了所有组件,并编写了音频播放器的逻辑。为了改进用户界面,我们还添加了一些媒体规则,使播放器在移动设备上也能呈现出色的效果。
最后,我们将所有代码推送到 Github,并从那里部署到 Vercel,以便可以从任何具有互联网连接的设备访问它。
在此过程中,我希望你能够深入了解 React 应用的构建方式,以及构建文件结构的一些方法。下次你需要在网站上展示一些音轨时,你就会知道如何操作了。
写作一直是我的热情所在,能够帮助和激励他人让我感到快乐。如有任何疑问,欢迎随时联系我们!
请访问我的博客以获取更多类似文章。
文章来源:https://dev.to/madza/how-i-built-a-music-player-to-showcase-my-own-tracks-3268