想自己试做一个小网站,于是边学边做,也有了一个可以用的成品,想着拿出来并简单聊聊。
决定先做一个音乐播放器。以下是我参考的文章,成品也是在这位作者的基础上改来的。
界面设计
功能预览
1. 播放模式
- 列表循环
- 单曲循环
- 随机播放
2. 加载行为
- 分页加载(也许?)
- 加载视觉效果
- 从上次退出时的位置播放
3. 用户交互
- 键盘操作
- 滚动列表更新
- 搜索
- 歌词(5.30更新)
具体实现
因为算是初学,写得不算好,请见谅。
注意,因为内容很多,在这里只会展示部分,如需源码请见文末. 同时,最新内容也仅在源码处展示
页面布局
HTML
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<link rel="shortcut icon" href="/static/img/ico.webp">
<link rel="stylesheet" href="/static/css/audio_player_5_6.css">
<title>Audio Player</title>
</head>
<body id="body">
<div class="base-alert-box"></div>
<div class="upper-container">
<!-- 音乐介绍 -->
<div class="introduction-container">
<div id="text-container" class="text-container">
<div id="music-title">音乐</div>
<div class="author-container">作曲家:
<span id="author-name">未知</span>
</div>
<div class="author-container">专辑:
<span id="album-name">未知</span>
</div>
</div>
</div>
</div>
<!-- 音乐播放器主要内容 -->
<div class="audio-box">
<div class="audio-container">
<audio id="audioTag"></audio>
<!-- 进度条 -->
<div class="a-progress">
<div class="pgs-total" id="progress-total">
<div class="pgs-loading" id="progress-loading"></div>
<div class="pgs-play" id="progress" style="width: 0;"></div>
</div>
</div>
<!-- 下排控制按钮 -->
<div class="a-controls">
<!-- 播放时长 -->
<div class="time-container">
<span class="played-time" id="playedTime">00:00</span> /
<span class="audio-time" id="audioTime">00:00</span>
</div>
<!-- 中间按钮 -->
<div class="center-button-container">
<!-- 播放模式 -->
<div id="playMode" class="center-icon mode"></div>
<!-- 上一首 -->
<div id="skipForward" class="center-icon s-right"></div>
<!-- 暂停按钮 -->
<div id="playPause" class="icon-play"></div>
<!-- 下一首 -->
<div id="skipBackward" class="center-icon s-left"></div>
<!-- 音量调节 -->
<div id="volume" class="center-icon volume"></div>
<!-- 音量悬浮窗 -->
<label for="volume-toggle"></label>
<input type="range" id="volume-toggle" name="change" value="50" min="0" max="100" step="1">
</div>
<!-- 后部按钮 -->
<div class="bottom-button-container">
<!-- 列表 -->
<div id="list" class="bottom-icon list"></div>
</div>
</div>
</div>
</div>
<!-- 音乐列表 -->
<div class="close-list" id="close-list"></div>
<div class="music-list" id="music-list">
<div class="music-list-container">
<div class="music-list-title">播放队列</div>
<hr class="line">
<ul class="all-list" id="all-list"></ul>
<img id="loading" src="/static/img/svg/loading.svg" width="20" alt="">
</div>
</div>
</body>
<script type="text/javascript" src="/static/js/audio_player/audio_player_var_5_11.js"></script>
<script type="text/javascript" src="/static/js/audio_player/audio_player_data-5_11.js"></script>
<script type="text/javascript" src="/static/js/audio_player/audio_player_listener_5_11.js"></script>
</html>
CSS
body {
padding: 0;
margin: 0;
box-sizing: border-box;
background-repeat: no-repeat;
backdrop-filter: blur(40px);
transition: 200ms;
/*CSS3去除手机浏览器按钮点击出现的高亮框*/
-webkit-tap-highlight-color: transparent;
-webkit-backdrop-filter: blur(40px);
}
html,
body {
height: 100%;
background-size: cover;
overflow: hidden;
}
.upper-container {
width: 80%;
height: 80%;
}
.introduction-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
float: left;
}
.text-container {
width: 80%;
transition: 200ms;
}
.scroll-wrap {
max-width: 100%;
}
.scroll-item {
animation: scroll linear 6s alternate infinite;
}
#music-title {
color: rgba(255, 255, 255, .888);
font-size: 4rem;
line-height: 6rem;
}
.author-container {
color: rgb(171, 171, 171);
font-size: medium;
}
/* 歌词 */
.lyric-title {
position: absolute;
top: 0;
mix-blend-mode: difference;
color: white;
pointer-events: none;
transition: 200ms;
}
.lyric-box {
display: none;
position: absolute;
top: 10%;
width: 90%;
height: 70%;
overflow: hidden;
font-size: 20px;
color: white;
pointer-events: none;
transition: 250ms ease-out;
}
/* 直接使用class定位到ul似乎会导致列表样式概率消失 */
.lyric-box ul {
padding: 0;
position: relative;
top: 40%;
list-style: none;
text-align: center;
user-select: none;
-webkit-user-select: none;
transition-duration: 600ms;
}
.lyric-box ul li {
height: 25px;
line-height: 25px;
margin-top: 25px;
transition: 200ms;
opacity: .4;
}
.lyric-box ul li span {
cursor: pointer;
}
.lyric-box ul li span span {
font-size: 12px;
pointer-events: none;
}
.lyric-box ul .highlight-line {
color: #17E883;
opacity: 1;
}
.lyric-box ul .near-line {
opacity: 1;
}
/* 歌词调整 */
.lyric-calibration {
position: absolute;
display: flex;
left: 0;
top: 40%;
flex-direction: column;
z-index: 10;
}
.lyric-calibration img {
margin: 5px 0;
cursor: pointer;
opacity: .4;
}
.lyric-calibration img:hover {
opacity: .6;
}
.lyric-calibration img:active {
opacity: 1;
}
#lyric-offset {
text-align: center;
font-size: 12px;
}
/* 播放器样式设置*/
.audio-box {
width: 100%;
height: 20%;
display: flex;
align-items: center;
justify-content: center;
/* 设置超出部分隐藏,方便改变图标颜色 */
overflow: hidden;
}
.audio-container {
width: 90%;
}
/* 进度条样式 */
.a-progress {
width: 100%;
color: #42b680;
background-color: transparent;
border-radius: 10px;
margin-bottom: 10px;
cursor: pointer;
}
.pgs-total {
width: 100%;
height: 16px;
background-color: transparent;
border-radius: 10px;
position: relative;
touch-action: none;
}
.a-progress .pgs-total:after,
.a-progress .pgs-total .pgs-loading,
.a-progress .pgs-total .pgs-play:before,
.a-progress .pgs-total .pgs-play:after {
content: '';
position: absolute;
left: 0;
right: 0;
top: 7px;
bottom: 7px;
}
.a-progress .pgs-total:after {
background-color: #bbbbbb;
border-radius: 10px;
z-index: -1;
}
.a-progress .pgs-total .pgs-loading {
display: none;
box-shadow: #1affb2 0 5px 5px;
animation: loadingProgressBar 1200ms ease-out alternate infinite;
}
.a-progress .pgs-total .pgs-play:before {
background-color: #42b680;
border-radius: 10px;
z-index: 1;
}
.a-progress .pgs-total .pgs-play {
height: 100%;
position: relative;
border-radius: 10px;
}
.a-progress .pgs-total .pgs-play:after {
top: 4px;
left: 100%;
height: 8px;
width: 8px;
border-radius: 50%;
background-color: #FFFFFFFF;
}
/* 下排控制按钮样式*/
.a-controls {
-webkit-user-select: none;
user-select: none;
width: 100%;
}
/* 时间样式 */
.time-container {
float: left;
color: white;
font-weight: 300;
line-height: 50px;
white-space: nowrap;
}
.played-time {
left: 15px;
text-align: left;
}
.audio-time {
right: 15px;
text-align: right;
}
/* 中间部分按钮 */
.center-button-container {
width: 35%;
float: left;
display: flex;
align-items: center;
justify-content: center;
}
.center-button-container > * {
cursor: pointer;
}
.center-icon {
float: left;
min-width: 30px;
min-height: 30px;
margin: 0 10px 0 10px;
outline: none;
}
.center-icon:hover {
opacity: .7;
}
.center-icon:active {
opacity: .9;
}
.icon-pause, .icon-play, .s-left, .lyric, .mode, .s-right, .volume {
background-size: 100% 100%;
background-repeat: no-repeat;
}
.s-left {
background-image: url("/static/img/audio/ico/next.svg");
}
.icon-play {
float: left;
min-width: 50px;
min-height: 50px;
background-image: url("/static/img/audio/ico/play.svg");
}
.icon-pause {
float: left;
min-width: 50px;
min-height: 50px;
background-image: url("/static/img/audio/ico/pause.svg");
}
.lyric {
background-image: url("/static/img/audio/ico/lyric.svg");
}
.s-right {
background-image: url("/static/img/audio/ico/per.svg");
}
.volume {
background-image: url("/static/img/audio/ico/volume.svg");
}
/* 尾部按钮 */
.bottom-button-container {
float: left;
margin: 15px 0;
pointer-events: none;
width: 30%;
}
.bottom-icon {
float: right;
width: 20px;
height: 20px;
cursor: pointer;
pointer-events: initial;
}
.bottom-icon:hover {
opacity: .7;
}
.bottom-icon:active {
opacity: .6;
}
.list {
background-image: url("/static/img/audio/ico/list.svg");
background-size: cover;
}
/* 音乐列表 */
.close-list {
display: none;
position: fixed;
height: 100%;
left: 0;
top: 0;
z-index: 100;
}
.music-list {
display: none;
position: fixed;
height: 100%;
right: 0;
top: 0;
background-color: rgba(153, 153, 153, .6);
}
.music-list-container {
width: 100%;
}
.search {
position: absolute;
padding: 10px;
right: 0;
width: 20px;
cursor: pointer;
}
.search:hover {
opacity: .7;
}
.search:active {
opacity: .5;
}
.search-bar {
position: fixed;
top: 20px;
left: 100%;
width: 0;
height: 0;
font-size: 15px;
border-radius: 4px;
background-color: rgba(255, 255, 255, .3);
backdrop-filter: blur(8px);
border: none;
transition: 200ms ease-out;
}
.music-list-title {
text-align: center;
color: white;
font-size: 2rem;
font-weight: 300;
padding: 10% 15% 0;
}
.line {
height: 1px;
width: 90%;
border: none;
border-top: 1px dashed #3d3d3d;
}
.all-list {
display: flex;
flex-direction: column;
align-items: center;
padding: 0;
max-height: 80%;
overflow-y: scroll;
scrollbar-width: none;
list-style-type: none;
}
::-webkit-scrollbar {
display: none;
}
.all-list li {
min-width: 80%;
max-width: 80%;
min-height: 50px;
color: white;
font-weight: 300;
margin-top: 10px;
padding: 0 5px 0 5px;
overflow: hidden;
white-space: nowrap;
transition: 150ms;
}
.all-list-li-activate,
.all-list li:hover {
min-width: 90%;
background-color: rgb(150, 182, 156);
cursor: pointer;
}
.list-card-show {
animation: showAni;
animation-duration: 1s;
animation-fill-mode: forwards;
-webkit-animation-fill-mode: forwards;
}
.list-card-hide {
animation: hideAni;
animation-duration: 1s;
animation-fill-mode: forwards;
-webkit-animation-fill-mode: forwards;
}
@media (min-width: 767px) {
.lyric-box ul .highlight-line {
font-size: 25px;
}
.time-container {
width: 35%;
}
.close-list {
width: 70%;
}
.music-list {
width: 30%;
}
}
@media (max-width: 767px) {
.lyric-box {
font-size: 15px;
}
.lyric-box ul .highlight-line {
font-size: 20px;
}
.time-container {
width: 25%;
visibility: hidden;
}
.played-time, .audio-time {
visibility: initial;
position: absolute;
bottom: 14%;
}
.close-list {
width: 25%;
}
.music-list {
width: 75%;
}
#volume, #volume-toggle, #lyric {
display: none;
}
}
@keyframes hideAni {
from {
transform: translateX(0%);
}
to {
transform: translateX(100%);
}
}
@keyframes showAni {
from {
transform: translateX(100%);
}
to {
transform: translateX(0%);
}
}
@keyframes rotateAni {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes loadingProgressBar {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/*横条样式*/
#volume-toggle {
width: 150px;
min-width: 50px;
height: 5px;
appearance: none;
background-color: #dcdcdc;
outline: none;
overflow: hidden;
border-radius: 15px;
box-shadow: inset 0 0 5px rgb(91, 91, 91);
}
/*拖动块的样式*/
#volume-toggle::-webkit-slider-thumb {
-webkit-appearance: none;
width: 1px;
height: 5px;
border-radius: 50%;
background: #42b680;
cursor: pointer;
border: 4px solid #33333300;
box-shadow: -407px 0 0 400px #42b680;
}
#loading {
display: none;
margin: 10px 50% 0 50%;
animation: rotateAni 1s infinite;
}
#audio-player {
display: none;
}
JS (最新的设计请查看GitHub)
更新音乐列表数据
从后端获取到的音乐列表将被储存到一个对象中,不知道多少条会出现明显的性能问题。对于内存占用,在一百条时程序共使用2.1MB左右。
然后,所有关于静态资源的地址都需要根据自己的项目地址更改。
/**
* 从后台获取音乐列表;
*
* 注意,不要随意调用此函数, 您应该尽可能使用switchAudio
* @param {IUpdateData,{}} opts
* */
async function updateData(opts = {}) {
const defaultOpts = {
init: false,
newIndex: 0,
...opts
}
// 检查
if ((!defaultOpts.init && defaultOpts.newIndex >= MaxAudioCount) || defaultOpts.newIndex < 0) {
AudioIndex = POST_DATA['audio_index'];
throw RangeError('超出最大获取范围');
}
// 发送的信息
POST_DATA['audio_index'] = defaultOpts.newIndex;
POST_DATA['search_string'] = v.SearchInputBar.value.toString().trim();
// 显示加载图标
v.LoadingIco.style.display = 'block';
// 后台获取
const response = await baseFetch('/audio/audio_lists', {
body: JSON.stringify(POST_DATA)
});
const json = await response.json();
if (!json) {
POST_DATA['audio_index'] = AudioIndex;
throw Error('Fetch fail');
}
// 隐藏加载图标
setTimeout(() => v.LoadingIco.style.display = 'none', 2000);
// 更新所有全局信息, 并启动后续任务
if (json['status'] !== 1009) {
createAlert(json['msg'], 'danger');
throw Error('Error fetching data');
}
Object.assign(AudioObject, json['audio_dict']);
MaxAudioCount = Number(json['item_counts']);
if (AudioIndexRange) {
const tempArray = Object.keys(AudioObject);
AudioIndexRange = tempArray.length === MaxAudioCount ? null : [Number(tempArray[0]), Number(tempArray.pop())];
if (AudioIndex >= MaxAudioCount) AudioIndex = AudioIndexRange[1];
}
createChildLi(AudioObject).catch();
}
展示列表更新
/**
* 创建音乐列表
* @param {Object} obj 标准音频列表格式
* */
async function createChildLi(obj) {
if (v.ListParentUl.children.length === MaxAudioCount) return;
if (isEmpty(obj)) {
v.ListParentUl.textContent = '无结果';
return;
}
const frag = document.createDocumentFragment();
const paragraph = (value) => {
const p = document.createElement('p');
p.textContent = `${value[0]} - ${value[1]} - ${value[2]}.${value[3]}`;
return p;
}
const entries = Object.entries(obj);
const len = entries.length;
let index = 0;
const updateBatch = () => {
// 每次更新20个元素
const batchSize = 20;
for (let i = 0; i < batchSize && index < len; i++, index++) {
const [key, value] = entries[index];
const liElement = document.createElement('li');
liElement.setAttribute('id', `li-${key}`);
liElement.appendChild(paragraph(value));
frag.appendChild(liElement);
}
if (index < len) {
requestAnimationFrame(updateBatch);
return;
}
v.ListParentUl.textContent = '';
v.ListParentUl.appendChild(frag);
highlightChosenSelection(false);
}
requestAnimationFrame(updateBatch);
}
初始化音乐及展示信息
在这里,背景图片以及音乐地址,音乐信息将被加载
// 加载音乐数据
function initAudio() {
// 本地保存当前Audio index
localStorage.setItem('audio_index', AudioIndex.toString());
// 加载图标
v.ProgressLoading.style.display = 'block';
// 设置背景图片
NewImgUrl = `/static/img/audio/webp/audio-${ImgNum}.webp`;
ImgNum = (ImgNum + 1) % AllImgCount;
// 预加载图片
TempImgEle.src = NewImgUrl;
// 设置音频信息
;[v.Author.textContent, v.AudioTitle.textContent, v.Album.textContent] = [...AudioObject[AudioIndex]];
v.LyricTitle.textContent = v.AudioTitle.textContent;
v.AudioTitle.removeAttribute('class');
if (v.AudioTitle.clientWidth >= v.TextContainer.clientWidth) v.AudioTitle.className = 'scroll-item';
// 更改URL
const href = new URL(window.location.href);
const params = new URLSearchParams(href.search);
params.set('audio_index', AudioIndex.toString());
params.set('search', v.SearchInputBar.value.toString());
href.search = params.toString();
history.replaceState(null, '', href.href);
// 设置音乐, 并在加载后播放
v.AudioEle.src = `/audio/play/${AudioObject[AudioIndex][4]}`;
v.AudioEle.load();
}
滚动列表更新
移动端和桌面端方法
export const slideToUpdateFn = debounce(() => {
if (!AudioIndexRange) return;
const scrollTop = v.ListParentUl.scrollTop;
const clientHeight = v.ListParentUl.offsetHeight;
const scrollHeight = v.ListParentUl.scrollHeight;
// 判断滚动方向
const directionDown = ScrollUConfig.beforeScrollTop <= scrollTop;
ScrollUConfig.beforeScrollTop = scrollTop;
const [first, last] = AudioIndexRange;
const shouldUpdateDown = directionDown &&
(scrollTop + clientHeight + ScrollUConfig.threshold >= scrollHeight) &&
(last + 1 < MaxAudioCount);
const shouldUpdateUp = scrollTop <= ScrollUConfig.threshold && first - 1 >= 0;
if (shouldUpdateDown) switchAudio(last + 1, {refresh: false, scroll: false});
else if (shouldUpdateUp) switchAudio(first - 1, {refresh: false, scroll: false});
}, 100);
export const wheelingToUpdateFn = debounce((event) => {
if (!AudioIndexRange) return;
const scrollTop = v.ListParentUl.scrollTop;
const clientHeight = v.ListParentUl.offsetHeight;
const scrollHeight = v.ListParentUl.scrollHeight;
const directionDown = event.deltaY > 0;
const [first, last] = AudioIndexRange;
const shouldUpdateDown = directionDown && last + 1 < MaxAudioCount && scrollTop + clientHeight + 20 >= scrollHeight;
const shouldUpdateUp = !directionDown && first - 1 >= 0 && scrollTop === 0;
if (shouldUpdateDown) switchAudio(last + 1, {refresh: false, scroll: false});
else if (shouldUpdateUp) switchAudio(first - 1, {refresh: false, scroll: false});
}, 200);
下载链接
全部代码