文章目录
展示
音乐播放器
功能
- 3个互相跳转页面
- 点击歌单歌曲或播放列表切换歌曲
- 上一首,下一首切换歌曲
- 顺序,循环,随机播放
- 进度条和音量拖拽,点击改变
- 歌词滚动,高亮
具体实现过程
1. 轮播图
- 轮播图的思路是将多张图片排列成一行,只显示第一张图片,其他图片隐藏,通过改变父盒子的位置,让下一张出现。
- 小圆点颜色改变是通过记录当前显示的是第几张图片,把所有小圆点样式清空,给对应的第几个小圆点添加样式。
- 点击圆点切换图片,是利用循环判断是第几个小圆点,并跳转到第几个图片。
const container=document.getElementById('center-container')
const imgList=container.getElementsByClassName('imgs')
const dots=document.getElementById('dot')
const dot=dots.getElementsByTagName('span')
const imgWidth=600
let currentPage=0
let timer
const td=1000
const btl=document.getElementById('btn-left')
const btr=document.getElementById('btn-right')
function changePic(){
if(currentPage==imgList.length){
currentPage=0
}
if(currentPage==-1){
currentPage=imgList.length-1
}
container.style.left="-"+imgWidth*currentPage+"px"
for(let i=0;i<dot.length;i++){
dot[i].style.backgroundColor='#B0AFBA'
}
dot[currentPage].style.backgroundColor='#B20A0A'
}
// 上一张
function forward(){
changePic()
currentPage++
}
// 下一张
function back(){
changePic()
currentPage--
}
// 轮播图开始
function start(){
timer=setInterval(forward,td)
}
// 轮播图停止
function stop(){
clearInterval(timer)
}
//点击圆点切换图片
for(let i=0;i<dot.length;i++){
dot[i].addEventListener('click',()=>{
currentPage=i
changePic();
})
}
btl.addEventListener('click',back)
btr.addEventListener('click',forward)
container.addEventListener('mouseover',stop)
container.addEventListener('mouseout',start)
//设置初始页面呈现的图片
container.style.left=imgWidth*currentPage+"px"
//设置第一个小圆点的颜色为红色
dot[0].style.backgroundColor='#B20A0A'
//开始轮播
start()
2. 页面跳转
思路是利用url中的hash来实现页面的跳转,#和#后面的字符串就是url的hash值,可以通过这个hash值来实现页面的跳转。
- 如何获取hash值
window.location.hash //#center
- 如何修改hash值
window.location.hash='#words'
- 如何检测hash值的改变
利用hashchange事件,监听页面hash的改变
let header=document.getElementById('header')
let center=document.getElementById('center')
let songs=document.getElementById('songsList')
let words=document.getElementById('songsWords')
let inforImg=document.getElementById('infor-img')
let infora=inforImg.getElementsByTagName('a')
songs.classList.add('hidden')
words.classList.add('hidden')
// 页面切换
window.addEventListener('hashchange',()=>{
let hash=window.location.hash.slice(1)
change(hash);
})
// 页面显示
function change(hash){
if(hash=='songsList'){
songs.classList.remove('hidden')
center.classList.add('hidden')
words.classList.add('hidden')
stop()
header.innerHTML='歌单'
}
else if(hash=='center'){
center.classList.remove('hidden')
songs.classList.add('hidden')
words.classList.add('hidden')
start()
header.innerHTML=`<div class="header-img"></div>`
}
else if(hash=='words'){
words.classList.remove('hidden')
center.classList.add('hidden')
songs.classList.add('hidden')
header.innerHTML='播放器'
stop()
}
}
// 页面二和页面三的跳转
var count=0
inforImg.addEventListener('click',()=>{
if(window.location.hash=='#words'){
count++
change('songsList')
if(count%2){
change('words')
}
}
})
3. 点击歌单或播放列表切换歌曲
点击歌单切换歌曲的思路是利用for循环,为每一行歌曲信息添加事件监听,当点击某一行时,将歌曲信息添加到集合中,防止重复添加,同时获取所点击的歌曲内容:歌曲名,歌手,在所有的audio中,寻找与所点击歌曲名字一样的audio,播放对应的audio,并把audio添加到song集合中,以免添加歌曲重复,同时同步底部歌曲信息。 点击播放列表切换歌曲也是同理。
//添加到songMap集合的内容
let addContent
//歌单内容的集合
const songMap =new Map()
//歌曲音频的集合
const songs=new Set()
// 当前播放的音频在arr数组中的索引值
let position
export let arr=[]
//当前播放的音频
export let audioMax=audio1
// 点击歌单向播放列表添加歌曲
for(let songsListUl of songsListUls){
songsListUl.addEventListener('click',()=>{
//点击歌单的内容
addContent=songsListUl.innerHTML.substring(110)
// 向播放列表中添加歌曲信息
if(!songMap.has(addContent)){
songMap.set(addContent)
dirList.innerHTML+=`<ul>${addContent}</ul>`
}
// 获取所点击歌曲内容:歌曲名,歌手
let clickSong=songsListUl.querySelector('.name').textContent
let clickAuthor=songsListUl.querySelector('.author').textContent
// 对所有的audio标签进行遍历,寻找与所点击歌曲名字一样的audio,播放对应的audio,
// 并把audio添加到song集合中,以免添加歌曲重复
au.forEach((a)=>{
if(addContent.includes(a.getAttribute('data-name'))){
audioMax=a
clearAudio()
a.currentTime=0
// 同步底部歌曲信息
inforName.textContent=clickSong
inforAuthor.textContent=clickAuthor
throttle(playNoError(a))
btnArr[1].innerHTML=''
songs.add(a)
}
})
})
}
// 点击播放列表切换歌曲
for(let dd of dirList.querySelectorAll('ul')){
dd.addEventListener('click',()=>{
let clickSong=dd.querySelector('.name').textContent
let clickAuthor=dd.querySelector('.author').textContent
au.forEach((a)=>{
if(a.getAttribute('data-name')==clickSong){
audioMax=a
clearAudio()
a.currentTime=0
// 同步底部歌曲信息
inforName.textContent=clickSong
inforAuthor.textContent=clickAuthor
throttle(playNoError(a))
btnArr[1].innerHTML=''
songs.add(a)
}
})
})
}
arr=Array.from(songs)
//切换歌曲时清除其他播放歌曲
export function clearAudio(){
for(let au of audioAll){
au.pause()
}
}
//方法一:节流
function throttle(fn,delay=2000) {
let timer=null
return function(...args){
if(!timer){
timer=setTimeout(()=>{
fn.apply(this,args)
timer=null
},delay)
}
}
}
function playNoError(a){
setTimeout(() => {
a.play()
}, 1000);
}
4. 上一首、下一首
想进行上一首,下一首的切换首先要知道当前所播放的音频所在的位置,可以利用数组的索引,因此将歌曲音频的集合songs转换为数组arr,并计算当前所播放的音频的索引值,通过索引值的加减实现歌曲的切换。
let btns=document.getElementById('footer-btns')
let btnArr=btns.getElementsByTagName('i')
// 播放按钮
btnArr[0].addEventListener('click',pre)
btnArr[1].addEventListener('click',playAudio)
btnArr[2].addEventListener('click',next)
// 上一首
function pre() {
position=arr.indexOf(audioMax)>=0?arr.indexOf(audioMax):0
if (position == 0) {
position = arr.length - 1;
}
else{
position=--position%arr.length;
}
audioPlay()
}
// 播放暂停和开始
function playAudio(){
if(audioMax.paused){
clearAudio()
throttle(playNoError(audioMax))
btnArr[1].innerHTML=''
}
else if(!audioMax.paused){
audioMax.pause()
btnArr[1].innerHTML=''
}
if(songMap.size==0){
addContent=songsListUls[0].innerHTML.substring(110)
dirList.innerHTML+=`<ul>${addContent}</ul>`
songMap.set(addContent)
songs.add(audio1)
}
}
// 下一首
export function next() {
position=arr.indexOf(audioMax)>=0?arr.indexOf(audioMax):0
position=++position%arr.length;
audioPlay()
}
5. 不同播放模式
- 顺序播放
当当前所播放的音频结束时,播放下一首,可实现顺序播放。 - 循环播放
将所有音频的循环属性设为false,只将当前所播放的音频属性设为true,可实现循环模式下,当前播放的歌曲循环播放 - 随机播放
获取播放列表中存放音频的数组arr的长度,利用random函数实现。
// 播放模式,播放列表
let footerList=document.getElementById('footer-list')
let musicMode=footerList.querySelector('.list-icon')
let mode=0
// 通过点击切换播放模式
musicMode.addEventListener('click',()=>{
mode=++mode%3
if(mode==0){
// 顺序播放
musicMode.innerHTML=''
}
else if(mode==1){
// 循环播放
musicMode.innerHTML=''
}
else if(mode==2){
// 随机播放
musicMode.innerHTML=''
}
})
function changeMode(){
if(mode==0){
// 顺序播放
clearLoop()
if(audioMax.ended){
next()
}
}
else if(mode==1){
// 循环播放
clearLoop()
audioMax.loop=true
}
else if(mode==2){
// 随机播放
clearLoop()
if(audioMax.ended){
randomChange()
}
}
}
// 清除所有歌曲的循环播放
function clearLoop(){
au.forEach((a)=>{
a.loop=false
})
}
// 只有一首歌时,所有的模式都相当于循环播放
function onlyOneSong(){
if(arr.length<=0){
}
else if(arr.length==1){
audioMax.loop=true
}
else{
changeMode()
}
}
// 随机播放
export function randomChange(){
position=Math.round(Math.random()*arr.length)
audioPlay()
}
6. 进度条和音量拖拽,点击改变
- 进度条的长度=音乐当前播放的时长/音乐总时长*进度条的总长度
进度条的长度和音乐当前播放的时长可以相互推算出来 - .如何获取改变后进度条的长度
- 进度条最左端的位置proPosition
- 为进度条添加事件监听,鼠标点击时获取当前点击的位置e.clientX
- 改变后的进度条的长度=e.clientX-proPosition
- 需要注意拖拽的范围不能超过进度条的宽度范围,需要给进度条设置最大宽度。
- 需要注意音量的大小在[0,1]之间,超出范围会报错
// 进度条拖拽,点击
function proChange(e){
// 获取进度条的位置
let proPosition=Math.floor(progressBar.getBoundingClientRect().left);
// 点击进度条后进度条的宽度
let proWidth=e.clientX-proPosition;
// 如果进度回退,通过进度条的宽度反向计算目前歌曲播放时间
if(proWidth<=progressBar.clientWidth){
proBar.style.width=proWidth+'px'
proDot.style.left=proWidth-2+'px'
audioMax.currentTime=audioMax.duration*proWidth/(progressBar.clientWidth)
}
}
progressBar.addEventListener('click',(e)=> {
proChange(e);
})
progressBar.addEventListener('mousedown',()=> {
document.onmousemove = (e)=>{
if(e.clientX>=progressBar.getBoundingClientRect().left){
proChange(e);
}
}
})
progressBar.addEventListener('mouseup', () => {
document.onmousemove = null
})
// 音量拖拽,点击
var volWidth
function vulChange(e){
let volPosition=Math.floor(volumeBar.getBoundingClientRect().left);
volWidth=e.clientX-volPosition;
if(volWidth<=volumeBar.clientWidth){
volBar.style.width=volWidth+'px'
volDot.style.left=volWidth-2+'px'
audioMax.volume=volWidth/(volumeBar.clientWidth)>=0?volWidth/(volumeBar.clientWidth):0
}
}
volumeBar.addEventListener('click',(e)=> {
vulChange(e);
})
volumeBar.addEventListener('mousedown',()=> {
document.onmousemove = (e)=>{
if(e.clientX>=volumeBar.getBoundingClientRect().left){
vulChange(e);
}
}
})
volumeBar.addEventListener('mouseup', () => {
document.onmousemove = null
})
// 字符格式化
function format(number){
return number.toString().padStart(2,'0')
}
7. 歌词滚动,高亮
思路是当播放时间大于自定义属性data-time时,清除所有样式,给对应的标签添加高亮样式。
根据高亮歌词的位置,改变滚动条位置,实现歌词滚动。
export const songsP=document.getElementsByClassName('words-flag')
export const songsRig=document.getElementById('songsWords-right')
const songsLef=document.getElementById('songsWords-left')
export const songsPage=songsLef.querySelector('.songsWords-page')
export function wordsScroll(){
// 样式清空
if(!audio1.paused){
let i=0
for (let p of songsP){
let time = p.getAttribute("data-time")
if(audio1.currentTime>=time) i++
p.classList.remove('words-light')
}
//歌词高亮
songsP[i-1].classList.add('words-light')
// 滚动条滚动
let lightTop=document.querySelector('.words-light').offsetTop
songsRig.scrollTop=lightTop-170
//封面旋转
songsPage.style.animationPlayState='running'
}
else{
songsPage.style.animationPlayState='paused'
}
}
遇到的问题以及如何解决
问题1:
原因:浏览器安全机制导致的,不支持跨域访问,而http, data, chrome, chrome-extension, https这些协议是支持跨域请求的。
解决方法:使用live server打开。
问题2:
原因:执行了play()方法以后立即执行pause()方法,频繁切换音频播放。
解决方法:
- 使用异步
audio.load()
let playPromise = audio.play()
if (playPromise !== undefined) {
playPromise.then(() => {
audio.play()
}).catch(()=> {
})
}
- 设置定时器,延时一段时间再切换
this.timer = setTimeout(() => {
video.play();
},500);
- 节流
function throttle(fn,delay=2000) {
let timer=null
return function(...args){
if(!timer){
timer=setTimeout(()=>{
fn.apply(this,args)
timer=null
},delay)
}
}
}
function playNoError(a){
setTimeout(() => {
a.play()
}, 1000);
}
throttle(playNoError(a))