![689e3604ef3777b424d9a2fb7b456ac4.png](https://img-blog.csdnimg.cn/img_convert/689e3604ef3777b424d9a2fb7b456ac4.png)
一直都想做一个音乐播放器,这次总算做了一个。
![398b1cc12e368f00c35e945a751285c1.png](https://img-blog.csdnimg.cn/img_convert/398b1cc12e368f00c35e945a751285c1.png)
关键词:JavaScript、jQuery、响应式布局 、MVC、eventHub(发布/订阅模式)、七牛&LeanCloud数据库、Swiper、移动端
描述:移动端播放歌曲、切换、暂停、搜索等功能,PC 端歌曲上传、删除、修改等功能。使用 jQuery、MVC,以及 LeanCloud、七牛等作为数据库实现。使用vConsole进行调试
源码链接:
ajing741472797/163-music-demogithub.com![26c51dc0e337acb47ad57c497aaa24af.png](https://img-blog.csdnimg.cn/img_convert/26c51dc0e337acb47ad57c497aaa24af.png)
预览链接:
我的云音乐ajing741472797.github.io![408999d45e1242d23e00db630e09f526.png](https://img-blog.csdnimg.cn/img_convert/408999d45e1242d23e00db630e09f526.png)
整理一下页面分析和解决bug的通用方法:
一、页面分析
1、网易云音乐logo+下载APP:
logo用SVG做,a标签做跳转
2、推荐音乐、热歌榜、搜索三个Tab键跳转
- 用ol>li>span标签包裹:推荐音乐、热歌榜、搜索
- 通过onclick事件点击任意一个Tab,将其余两个Tab页面隐藏,并用::after来显示红色下划线
- 对span使用inline-block,让红色下划线能够动态获取父元素的宽度
//index.html
<section id="tabs" class="globalTabs">
<ol class="tabs-nav">
<li class=active data-tab-name="page-1">
<div class="text"> 推荐音乐 </div>
</li>
<li data-tab-name="page-2">
<div class="text">热歌榜</div>
</li>
<li data-tab-name="page-3">
<div class="text">搜索</div>
</li>
</ol>
</section>
//tabs.js
bindEvents(){
this.view.$el.on('click','.tabs-nav > li',(e)=>{
let $li = $(e.currentTarget)
let pageName = $li.attr('data-tab-name')
$li.addClass('active')
.siblings().removeClass('active')
window.eventHub.emit('selectTab', pageName)
})
}
//page-1.js
//view
show(){
this.$el.addClass('active')
},
hide(){
this.$el.removeClass('active')
}
//controller
bindEventHub(){
window.eventHub.on('selectTab',(tabName)=>{
if(tabName === 'page-1'){
this.view.show()
}else{
this.view.hide()
}
})
}
3、在MVC中一个js模块引入多个js
![664043593c595628292c04688f1dc2a0.png](https://img-blog.csdnimg.cn/img_convert/664043593c595628292c04688f1dc2a0.png)
//page-1.js
loadModule1(){
let script1 = document.createElement('script')
script1.src = './js/index/page-1-1.js'
script1.onload = function(){
}
document.body.appendChild(script1)
},
loadModule2(){
let script2 = document.createElement('script')
script2.src = './js/index/page-1-2.js'
script2.onload = function(){
}
document.body.appendChild(script2)
}
4、推荐音乐页面-轮播
使用Swiper官网组件,引入min.css和min.js,自定修改样式
5、推荐音乐页面-推荐歌单
- 使用ol>li*6>a>img+p标签插入跳转、图片、文字
- 确定每个li元素的宽度和间隙
- 使用flex布局:flex-wrap:wrap、flex-direction:row、justify-content:space-between
- a标签的href=“跳转页面的地址+查询参数(?id=xxxxxx)”
6、推荐音乐页面-最新音乐
音乐歌曲、图片上传到七牛,地址、歌名、歌描述、歌词、歌的id保存到LeanCloud
从数据库批量获取所存音乐,并把获取的数据通过evenHub通知其他所需要数据的页面
find(){
var query = new AV.Query('Song');
return query.find().then((songs)=>{
this.data.songs = songs.map((song)=>{
return Object.assign({id: song.id}, song.attributes) // ...表示song.attributes有什么我就要什么
})
return songs
})
}
- 对获取音乐的数组进行map()动态生成音乐清单,播放图标使用iconfont,smybol引用
renderSongList(data){
let songs=data
songs.map((song)=>{
let name = song.attributes.name
let descript =song.attributes.descript
let url =song.attributes.url
let id = song.id
let $li = $(`
<li>
<h3>${name}</h3>
<div class='sq'></div>
<p>${descript}</p>
<a href="./song.html?id=${id}">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-play1"></use>
</svg>
</a>
</li>
`)
$(this.el).find('#lastestMusic').append($li)
})
},
![00cf87b7a561d20f7f5014d2f56356c0.png](https://img-blog.csdnimg.cn/img_convert/00cf87b7a561d20f7f5014d2f56356c0.png)
7、推荐音乐页面-底部logo
logo用SVG做,a标签做跳转
8、热歌榜-云音乐背景
![d5e56f36677ad8292526e2e35bea86ac.png](https://img-blog.csdnimg.cn/img_convert/d5e56f36677ad8292526e2e35bea86ac.png)
- 其中白色的字的图片是一个雪碧图
- 外面再套一个特定宽高的div使用overflow:hidden
- 对雪碧图进行移动
9、热歌榜-音乐清单
![57ac3b82e7394eb1c464ce4caadea3d9.png](https://img-blog.csdnimg.cn/img_convert/57ac3b82e7394eb1c464ce4caadea3d9.png)
- 同5从数据库动态获取所有的音乐
find(){
var query = new AV.Query('Song');
return query.find().then((songs)=>{
this.data.songs = songs.map((song)=>{
return Object.assign({id: song.id}, song.attributes) // ...表示song.attributes有什么我就要什么
})
return songs
})
}
- 1、2、3数字高亮
activeNumber(){
let $songsNumber = $('.hotMusicList').find('.songNumber')
for(let i=0;i<=2;i++){
$songsNumber.eq(i).addClass('active')
}
},
- 1、2、3变成01、02、03
<div class="songNumber">${this.makeNumber(n)}</div>
makeNumber(n){
if(n<10){
n=`0${n}`
return n
}
},
10、搜索-搜索框
![d38d7857bfd76c4a8a5a82c566346716.png](https://img-blog.csdnimg.cn/img_convert/d38d7857bfd76c4a8a5a82c566346716.png)
搜索框html代码如下
<div class="searchContainer">
<img class="searchIcon"src="xxx" alt="">
<input type="search" placeholder="搜索歌曲、歌手" >
<img class="deleteIcon"src="xxx" alt="">
</div>
- 对输入框的value.length进行监听,searchIcon默认显示,当用户输入内容时deleteIcon显示。
changeDelete(value){
if(value.length===0){
$('.deleteIcon').removeClass('active')
}else{
$('.deleteIcon').addClass('active')
}
}
- 当用户点击删除按钮,则令value=“”,并显示热门搜索
![4bec8c392f3b6b34239d4cec4d0ab89b.png](https://img-blog.csdnimg.cn/img_convert/4bec8c392f3b6b34239d4cec4d0ab89b.png)
11、搜索-从数据库动态显示用户输入的音乐
- 每400毫秒获取一次用户输入的value值
- 把value值传进一个函数,该函数从数据库获取所有音乐的信息,并找出包含value值的音乐,并返回数组
search(keyword){
return new Promise((resolve,reject)=>{
let database=this.model.data
let result = database.filter(function(item){
return item.name.indexOf(keyword)>=0
})
resolve(result)
})
}
- 获取该数组的音乐的信息,进行页面的渲染
renderSongList(result){
$('.searchSongList>a').remove()
result.map((item)=>{
$aTag=$(`
<a href="./song.html?id=${item.id}">
<img src="./img/search.png" alt="">
<p>${item.name}</p>
</a>`)
$('.searchSongList').append($aTag)
})
},
renderNoSong(){
$(this.el).find('.searchSongList>a>p').text('sorry,该歌曲暂未收入')
}
- 整段代码如下:
let timer
$(this.view.el).find('input[type="search"]').on('input',(e)=>{
let $input=$(e.currentTarget)
let value = $input.val().trim() //val()返回或设置被选元素的值,trim() 函数用于去除字符串两端的空白字符
if(timer){
clearTimeout(timer)
}
timer = setTimeout(()=>{
if(value.length===0){
this.view.changeDelete(value)
this.view.isHotSong()
}else{
this.view.changeDelete(value)
this.view.isSearch()
this.view.renderSearchText(value)
this.search(value).then((result)=>{
let songNumber = result.length
if(songNumber==0){
this.view.renderNoSong()
}else{
this.view.renderSongList(result)
}
})
}
},400)
})
12、播放音乐页面-光盘旋转
![2947697e02b767eb14c9537d3a1a33d3.png](https://img-blog.csdnimg.cn/img_convert/2947697e02b767eb14c9537d3a1a33d3.png)
- 三张图片进行旋转
- 根据播放和暂停来激活active 旋转
@keyframes circle{
0%{
transform: rotate(0);
}
100%{
transform: rotate(360deg);
}
}
.disc-container>.disc.active{
animation: circle 20s linear infinite;
}
13、获取封面和背景图
this.$el.css('background-image', `url(${song.background})`)
this.$el.find('img.cover').attr('src',song.cover)
14、播放音乐页面-音乐播放与暂停
- 对播放与暂停的icon进行onclick监听,并通过evenHub通知光盘模块进行切换旋转状态,以及icon切换
play(){
this.$el.find('audio')[0].play()//audio[0] audio中的dom元素
},
pause(){
this.$el.find('audio')[0].pause()
}
$(this.view.el).on('click','.icon-play',()=>{
this.model.data.status = 'playing'
this.view.render(this.model.data)
this.view.play()
})
$(this.view.el).on('click','.icon-pause',()=>{
this.model.data.status = 'pause'
this.view.render(this.model.data)
this.view.pause()
})
if(status === 'playing'){
this.$el.find('.disc-container').addClass('playing')
this.$el.find('.disc-container .pointer').removeClass('active')
}else{
this.$el.find('.disc-container').removeClass('playing')
this.$el.find('.disc-container .pointer').addClass('active')
}
15、播放音乐页面-歌词同步
原理:lyric是对应了时间和歌词的,然后我们获取当前歌词的时间,与歌曲播放时间轴一致,然后当前的与下次生成的歌词p之间就是我们要找的在播放的那句歌词,渲染高亮并使其歌词随时间移动。
- 获取歌词并处理,使其渲染的时间与时间轴一致
let {lyrics} = song
lyrics.split('n').map((string)=>{
let p = document.createElement('p')
let regex = /[([d:.]+)](.+)///正则
let matches = string.match(regex) //字符串匹配正则
if(matches){
p.textContent = matches[2]
let time = matches[1]
let parts = time.split(':')
let minutes = parts[0]
let seconds = parts[1]
let newTime = parseInt(minutes,10) * 60 + parseFloat(seconds,10)
p.setAttribute('data-time', newTime)//等于时间轴
}else{
p.textContent = ''
}
this.$el.find('.lyric>.lines').append(p)
})
- 调用
if(this.$el.find('audio').attr('src') !== song.url){//同一首歌不改src,这样暂停歌曲不会重置
let audio = this.$el.find('audio').attr('src',song.url).get(0)//get(0)就是audio
audio.onended = ()=>{
window.eventHub.emit('songEnd')//歌曲结束使旋转动画停止
}
audio.ontimeupdate = ()=>{ //ontimeupdate 歌曲或视频当前的播放位置发送改变时触发
this.showLyric(audio.currentTime)
}
}
- 渲染歌词&移动
showLyric(time){
let allP = this.$el.find('.lyric>.lines>p')
for(let i =0;i<allP.length;i++){
if(i === allP.length-1){//如果是最后一行就显示最后一行
console.log(allP[i])
break
}else{
let currentTime = allP.eq(i).attr('data-time')
let nextTime = allP.eq(i+1).attr('data-time')
if(currentTime <= time && time < nextTime){
let p = allP[i]
let pHeight = p.getBoundingClientRect().top//getBoundingClientRect DOM API 获取当前元素距离屏幕的位置
let linesHeight = this.$el.find('.lyric>.lines')[0].getBoundingClientRect().top
let height = pHeight - linesHeight
this.$el.find('.lyric>.lines').css({
transform: `translateY(${- (height - 25)}px)`
})
$(p).addClass('active').siblings('.active').removeClass('active')
break
}
}
}
},
二、bug解决方案
1、遇到报错
- 查StackOverFlow
- 查Google
- 查知乎
- 查组件官网
2、没有报错,但不出结果
- JS:console.log('xxx')、console.log(变量)
- CSS:border:1px solid red
3、移动端调试
- 使用onerror事件,移动端没有console.log(),改为alert()
<script>
window.onerror = function(message, file, row){
alert(message)
alert(file)
alert(row)
}// 移动端调试
</script>
- 使用vConsole
//安装
npm install vconsole
cp node_modules/vconsole/dist/vconsole.min.js vendors
<script src="../vendors/vconsole.min.js"></script>
<script>
var vConsole = new VConsole();
window.onerror = function(message, file, row){
console.log(message)
console.log(file)
console.log(row)
}// 移动端调试
</script>