这个播放器的开发历时2个多月,并非说它有多复杂,相反它的功能还很是不完善,仅具雏形。之因此磨磨蹭蹭这么久,一是由于拖延,二也是实习公司项目太紧。8月底结束实习前写完了样式,以后在家空闲时间多了,集中精力就把JS部分作完了。这个播放器确实比当初构想的复杂,开始只打算作一个搜歌播放的功能。如今作出来的这个播放器,能够获取热门歌曲,能够搜歌,能够调整播放进度条,功能确实完善很多。css
此次完成这个项目也是收获颇丰,点了很多新的技能点,固然,这个简陋的小项目也挖了很多坑,不知道啥时候能填上……html
话很少说,看代码吧。vue
不记得在哪一个网站看到这个组件库的了,以为好酷炫,因而用起来~html5
这是官网:地址node
使用这个组件库的缘由除了漂亮,还由于这是基于Vue 2.0,无缝对接,方便。ios
使用方法跟以前的插件同样,npm安装:git
npm install --save muse-ui
安装好后,在main.js
中注册。github
import MuseUi from 'muse-ui' import 'muse-ui/dist/muse-ui.css' import 'muse-ui/dist/theme-light.css' Vue.use(MuseUi)
就能够在项目中使用了。
PS:Muse-ui的icon是基于谷歌的Material icons,你们能够根据本身的需求到官网找icon的代码。web
接着咱们就该搭建这个播放器的组件了。vuex
结构以下:
||-- player.vue // 主页面 | |-- playerBox.vue // 播放器组件 | |-- popular.vue // 热门歌曲页面 | |-- songList.vue // 歌曲列表页面 | |-- play.vue // 播放器页面 | |-- search.vue // 搜索页面
PS:热门歌曲、搜索页面都能进入歌曲列表页面,播放器组件playerBox.vue
是放<audio>
标签的组件,是功能性组件。
咱们来分别叙述:
直接看代码吧:
<template> <div class="player"> <!-- banner here--> <router-view></router-view> <!-- navbar here --> <mu-paper> <mu-bottom-nav :value="bottomNav" @change="handleChange"> <mu-bottom-nav-item value="popular" title="流行" icon="music_note" to="/popular"/> <mu-bottom-nav-item value="play" title="播放" icon="play_arrow" to="/play"/> <mu-bottom-nav-item value="search" title="搜索" icon="search" to="/search"/> </mu-bottom-nav> </mu-paper> <!-- html5 player here --> <playerBox></playerBox> </div> </template> <script> import playerBox from './playerBox.vue' export default { name: 'player', data(){ const pa=this.$route.path; const Pa=pa.slice(1); return{ bottomNav: Pa } }, components: { playerBox }, methods:{ handleChange (val) { this.bottomNav = val }, changebar(){ const va=this.$route.path; const Va=va.slice(1); this.bottomNav = Va } }, watch:{ "$route":"changebar" } } </script> <style lang="less" > .mu-bottom-nav{ position: fixed!important; bottom: 0px; background: #fafafa!important; z-index: 5; } </style>
解释一下:
npm install less less-loader --save
watch监视路由变化并触发一个method:changebar(),这个函数会获取当前的路由名,并把bottomNav的值设置为当前路由名——即高亮当前的路由页面
这是推荐歌单界面,这里用到了一个轮播图插件,是基于vue的,使用起来比较方便,直接用npm安装:
npm install vue-awesome-swiper --save
安装好后,一样在main.js
中注册:
import VueAwesomeSwiper from 'vue-awesome-swiper' Vue.use(VueAwesomeSwiper)
而后咱们来看页面的代码:
<template> <div class="popular"> <!-- navbar here --> <mu-appbar> <div class="logo"> iPlayer </div> </mu-appbar> <!-- banner here--> <mu-card> <swiper :options="swiperOption"> <swiper-slide v-for="(item,index) in banners" :key="index"> <mu-card-media> <img :src="item.pic"> </mu-card-media> </swiper-slide> <div class="swiper-pagination" slot="pagination"></div> </swiper> </mu-card> <div class="gridlist-demo-container" > <mu-grid-list class="gridlist-demo"> <mu-sub-header>热门歌单</mu-sub-header> <mu-grid-tile v-for="(item, index) in list" :key="index"> <img :src="item.coverImgUrl"/> <span slot="title">{{item.name}}</span> <mu-icon-button icon="play_arrow" slot="action" @click="getListDetail(item.id)"/> </mu-grid-tile> </mu-grid-list> </div> <div class="footer-rights"> <h4>版权归Godown Huang全部,请<a href="https://github.com/WE2008311">联系我</a>。</h4> </div> </div> </template> <script> import {swiper,swiperSlide} from 'vue-awesome-swiper' import axios from 'axios' export default { name: 'popular', data(){ return{ swiperOption: { pagination: '.swiper-pagination', paginationClickable: true, autoplay: 4000, loop:true }, banners:[], list: [] } }, components: { swiper, swiperSlide }, computed:{ }, created(){ this.initPopular() }, methods:{ initPopular(){ axios.get('http://localhost:3000/banner').then(res=> { this.banners=res.data.banners; }), axios.get('http://localhost:3000/top/playlist/highquality?limit=8').then(res=> { this.list=res.data.playlists; }) }, getListDetail(id){ this.$router.push({path: '/songsList'}) this.$store.commit('playlist',id); } } } </script> <style lang="css"> @media screen and (min-width: 960px){ .mu-card-media>img{ height: 400px!important; } .mu-grid-list>div:nth-child(n+2){ width:25%!important; } } .mu-grid-tile>img{ width: 100%; } .gridlist-demo-container{ display: flex; flex-wrap: wrap; justify-content: space-around; } .gridlist-demo{ width: 100%; overflow-y: auto; } .footer-rights>h4{ color: #e1e1e1; font-weight: 100; font-size:.056rem; height:90px; padding-top: 10px; text-align: center; } </style>
这里要说明一下,上面的这些组件除了
playerBox
以外都要在main.js中注册才能使用。注册方法忘记的了话,回头看看我以前写的todolist的项目是怎么注册的。
在store.js
中添加playList函数:
playlist(state,id){ const url='http://localhost:3000/playlist/detail?id='+id; axios.get(url).then(res=> { state.playlist=res.data.playlist; }) },
这里的页面mu
开头的基本都是用Muse-ui搭建起来的,Swiper
开头的则是轮播图插件。界面不复杂,主要是三个部分,上面的轮播图,中间的热门歌单推荐,底部的版权信息。样式基本是模板,这里作了一个简单的移动端适配:在PC端歌单会以每排4个分两排的形式排列,在移动端歌单则会以每排2个分四排的形式排列,适配的方法是媒体查询,经过改变歌单div
的宽度改变每行歌单的数目。
这里要注意的:
axios
的部分能够先不写,也能够写好先放着。methods
和created
里面的内容都涉及到axios的请求,因此能够先不写,不影响样式呈现。数据能够先用假数据代替。终于到了最核心的组件,之因此说它核心是由于这是播放界面,音频播放的长度、音频信息都会在这里被呈现,而播放器的核心功能——播放——也是在这里被操做(播放/暂停)。
看具体代码:
<template> <div class="play"> <!-- navbar here --> <mu-appbar> <mu-icon-button icon="navigate_before" slot="left" v-on:click="backpage"/> <div class="logo"> iPlayer </div> </mu-appbar> <!-- player here--> <div class="bgImg"> <img :src="audio.picUrl" /> <!-- 封面CD --> <mu-avatar slot="left" :size="300" :src="audio.picUrl"/> </div> <div class="controlBar"> <mu-content-block> {{audio.songName}} - {{audio.singer}} </mu-content-block> <div class="controlBarSlide"> <span class="slideTime">{{audio.currentTime}}</span> <mu-slider v-bind:value="progressPercent" @change="editprogress" class="demo-slider"/> <span class="slideTime">{{audio.duration}}</span> </div> </div> </div> </template> <script> export default { name: 'play', data(){ return{ } }, components: { }, computed:{ audio(){ return this.$store.getters.audio; }, progressPercent(){ return this.$store.getters.audio.progressPercent; } }, methods:{ backpage(){ window.history.go(-1); }, editprogress(value){ this.$store.commit('editProgress',value) } } } </script> <style lang="css"> @media screen and (max-width: 414px){ .bgImg .mu-avatar{ height: 260px!important; width: 260px!important; margin-left: -130px!important; } } .bgImg{ position:fixed; height:100%; width:100%; background: #fff; z-index:-1; } .bgImg>img{ width: 100%; filter:blur(15px); -webkit-filter: blur(15px); -moz-filter: blur(15px); -ms-filter: blur(15px); } .bgImg .mu-avatar{ position: absolute; left: 50%; margin-left: -150px; top: 30px; } .controlBar{ position: fixed; width: 100%; height: 180px; background: #fff; bottom: 0; z-index: 11; text-align:center; } .mu-slider{ width: 70%!important; display: inline-block!important; margin-bottom: -7px!important; } .slideTime{ width: 29px; display: inline-block; } .mu-content-block{ font-size: 18px; color: #777 } .mu-slider{ display: inline-block; margin:0 3px -7px; width: 70%; } </style>
store.js
添加代码:
play(state){ clearInterval(ctime); const playerBar=document.getElementById("playerBar"); const eve=$('.addPlus i')[0]; let currentTime=playerBar.currentTime; let currentMinute=Math.floor(currentTime/60)+":"+(currentTime%60/100).toFixed(2).slice(-2); let duraTime=playerBar.duration; let duraMinute=Math.floor(duraTime/60)+":"+(duraTime%60/100).toFixed(2).slice(-2); state.audio.progressPercent=((playerBar.currentTime/playerBar.duration)*100).toFixed(1); if(playerBar.paused){ playerBar.play(); eve.innerHTML="pause"; state.audio.duration=duraMinute; state.audio.currentTime=currentMinute; ctime=setInterval( function(){ currentTime++; currentMinute=Math.floor(currentTime/60)+":"+(currentTime%60/100).toFixed(2).slice(-2); state.audio.currentTime=currentMinute; state.audio.progressPercent=((playerBar.currentTime/playerBar.duration)*100).toFixed(1); },1000 ) }else { playerBar.pause(); eve.innerHTML="play_arrow"; clearInterval(ctime); } }, audioEnd(state){ const playerBar=document.getElementById("playerBar"); const eve=$('.addPlus i')[0]; eve.innerHTML="play_arrow"; clearInterval(ctime); playerBar.currentTime=0; let currentTime=playerBar.currentTime; let currentMinute=Math.floor(currentTime/60)+":"+(currentTime%60/100).toFixed(2).slice(-2); state.audio.currentTime=currentMinute; }, editProgress(state,progressValue){ const playerBar=document.getElementById("playerBar"); const eve=$('.addPlus i')[0]; let duraTime=playerBar.duration; let duraMinute=Math.floor(duraTime/60)+":"+(duraTime%60/100).toFixed(2).slice(-2); // console.log(progressValue); clearInterval(ctime); if(playerBar.paused){ playerBar.play(); eve.innerHTML="pause" state.audio.duration=duraMinute; } let currentTime=playerBar.duration*(progressValue/100); ctime=setInterval( function(){ currentTime++; currentMinute=Math.floor(currentTime/60)+":"+(currentTime%60/100).toFixed(2).slice(-2); state.audio.currentTime=currentMinute; state.audio.progressPercent=((playerBar.currentTime/playerBar.duration)*100).toFixed(1); },1000 ) playerBar.currentTime=currentTime; let currentMinute=Math.floor(currentTime/60)+":"+(currentTime%60/100).toFixed(2).slice(-2); state.audio.currentTime=currentMinute; },
icon button
,样式来自Muse-ui
绑定了一个点击事件backpage,点击后会回到上一个路由页面。这个须要配合以前的高亮底部导航icon,才能实现返回上一路由的同时高亮相对应的icon。Muse-ui
的Slider
。mouseup
事件,结果无效,后来才发现,其实已经自带了change
事件,还能够实现移动端的兼容。因此写代码的时候必定要多看看官网文档。store.js
里的方法,play
是播放/暂停,具体会根据当前音频文件的paused
(便是否暂停)来判断。总的原理是首先获取音频的持续时间,而后经过一个定时器,不断更新显示时间,播放完成时,计时器中止。play
方法里,则没法在audioEnd
方法里中止计时器,因此这里咱们须要在最外层先声明一个ctime
,而后再在play
方法里把定时器赋值给ctime
,这样咱们就能够随时中止计时器了。audioEnd
方法是播放中止时要作的事情,咱们会把中止按钮切换成播放,把显示时间修改掉,别忘了中止计时器。editProgress
方法是点击或拖动进度条时作的事情,咱们会改变当前音频的currentTime
,即当前时间,若是音频是暂停状态,咱们要让它继续播放。这也是一个比较核心的一个功能,毕竟推荐的歌单只有几个。看代码:
<template> <div class="search"> <!-- navbar here --> <mu-appbar> <mu-icon-button icon="navigate_before" slot="left" v-on:click="backpage"/> <div class="logo searchLogo"> iPlayer </div> <mu-text-field icon="search" class="appbar-search-field" slot="right" hintText="想听什么歌?" v-model="searchKey"/> <mu-flat-button color="white" label="搜索" slot="right" @click="getSearch(searchKey)"/> </mu-appbar> <!-- banner here--> <mu-list> <template v-for="(item,index) in result.songs"> <mu-list-item :title="item.name" @click="getSong(item.id,item.name,item.artists[0].name,item.album.name,item.artists[0].id)"> <mu-avatar slot="leftAvatar" backgroundColor="#fff" color="#bdbdbd">{{index+1}}</mu-avatar> <span slot="describe"> <span style="color: rgba(0, 0, 0, .87)">{{item.artists[0].name}} -</span> {{item.album.name}} </span> </mu-list-item> <mu-divider/> </template> </mu-list> <div class="footer-rights"> <h4>版权归Godown Huang全部,请<a href="https://github.com/WE2008311">联系我</a>。</h4> </div> </div> </template> <script> export default { name: 'search', data(){ return{ searchKey:'' } }, computed:{ result(){ return this.$store.getters.result; } }, components: { }, methods:{ backpage(){ window.history.go(-1); }, getSearch(value){ this.$store.commit('getSearch',value); }, getSong(id,name,singer,album,arid){ this.$store.commit('getSong',{id,name,singer,album,arid}); this.$store.commit('play'); } } } </script> <style lang="less"> @media screen and (max-width: 525px){ .searchLogo{ display: none; } .appbar-search-field{ width: 200px!important; } } .appbar-search-field { color: #FFF; margin-top: 10px; margin-bottom: 0; &.focus-state { color: #FFF; } .mu-icon { color: #FFF; } .mu-text-field-hint { color: fade(#FFF, 54%); } .mu-text-field-input { color: #FFF; } .mu-text-field-focus-line { background-color: #FFF; } } .footer-rights>h4{ color: #e1e1e1; font-weight: 100; font-size:.056rem; height:90px; padding-top: 10px; text-align: center; } </style>
在store.js
里添加:
getSearch(state,value){ const url='http://localhost:3000/search?keywords='+value+'?limit=30'; axios.get(url).then(res=>{ state.result=res.data.result; }) }, getSong(state,{id,name,singer,album,arid}){ const url="http://localhost:3000/music/url?id="+id; const imgUrl="http://localhost:3000/artist/album?id="+arid; const playerBar=document.getElementById("playerBar"); axios.get(url).then(res=>{ state.audio.location=res.data.data[0].url; state.audio.flag=res.data.data[0].flag; state.audio.songName=name; state.audio.singer=singer; state.audio.album=album; }) axios.get(imgUrl).then(res=>{ state.audio.picUrl=res.data.artist.picUrl; }) let currentTime=playerBar.currentTime; let currentMinute=Math.floor(currentTime/60)+":"+(currentTime%60/100).toFixed(2).slice(-2); let duraTime=playerBar.duration; let duraMinute=Math.floor(duraTime/60)+":"+(duraTime%60/100).toFixed(2).slice(-2); state.audio.duration=duraMinute; state.audio.currentTime=currentMinute; state.audio.progressPercent=((playerBar.currentTime/playerBar.duration)*100).toFixed(1); }
注意,在有须要使用axios
的组件必定要import
,npm下载安装不用多说了。
解释一下这个组件的两个方法:
getSearch
是获取搜索结果,它被绑定再搜索按钮上,初始页面是空白,经过传递关键字,用axios
从api获取搜索结果,再把结果显示在页面上。getSong
绑定在每个搜索的结果上,有两个步骤,第一是getSong
,会把点击的歌曲设置为要播放的歌曲,并把相关信息传递给play.vue
,让它显示在相应的地方;第二个步骤,会播放歌曲,也就是上面的play
方法,具体没必要再说。undefined
的状况,这时候咱们要把参数们写成{参数一,参数二,参数三}
的形式。这个组件主要是歌单详情页,基本的样式和搜索页同样,就是获取歌单的内容不一样,搜索页面的列表是根据关键词获取的,歌单详情页的列表是根据歌单id获取的,获取的方式都是经过axios。
<template> <div class="songsList"> <!-- navbar here --> <mu-appbar> <mu-icon-button icon="navigate_before" slot="left" v-on:click="backpage"/> <div class="logo"> iPlayer </div> </mu-appbar> <!-- banner here--> <div class="listBgImg"> <img :src="playlist.coverImgUrl" /> <!-- 封面CD --> <mu-avatar slot="left" :size="120" :src="playlist.coverImgUrl"/> </div> <mu-list> <mu-sub-header>{{playlist.name}}</mu-sub-header> <template v-for="(item,index) in playlist.tracks"> <mu-list-item :title="item.name" @click="getSong(item.id,item.name,item.ar[0].name,item.al.name,item.ar[0].id)"> <mu-avatar :src="item.al.picUrl" slot="leftAvatar"/> <span slot="describe"> <span style="color: rgba(0, 0, 0, .87)">{{item.ar[0].name}} -</span> {{item.al.name}} </span> </mu-list-item> <mu-divider/> </template> </mu-list> <div class="footer-rights"> <h4>版权归Godown Huang全部,请<a href="https://github.com/WE2008311">联系我</a>。</h4> </div> </div> </template> <script> export default { name: 'songsList', data(){ return{ } }, components: { }, computed:{ playlist(){ return this.$store.getters.playlist; } }, methods:{ backpage(){ window.history.go(-1); }, getSong(id,name,singer,album,arid){ this.$store.commit('getSong',{id,name,singer,album,arid}); this.$store.commit('play'); } } } </script> <style lang="css"> .listBgImg{ height:200px; width:100%; background: #fff; overflow: hidden; } .listBgImg>img{ width: 100%; filter:blur(30px); -webkit-filter: blur(30px); -moz-filter: blur(30px); -ms-filter: blur(30px); } .listBgImg .mu-avatar{ position: absolute; left: 50%; margin-left: -60px; top: 130px; } .mu-list .mu-sub-header{ /* position: absolute; */ top: 260px; font-size: 16px; /* text-align: center; */ } </style>
没什么须要解释的,注意咱们在getSong
里面传递的多个参数。
<template> <div class="playerBox"> <audio ref="myAudio" :src="audio.location" @ended="audioEnd" id="playerBar"></audio> <div class="controlBarBtn" v-show="judgement()"> <mu-icon-button icon="skip_previous"/> <mu-icon-button class="addPlus" icon="play_arrow" @click="play"/> <mu-icon-button icon="skip_next"/> </div> </div> </template> <script> export default { name: 'playerBox', data(){ return{ } }, components: { }, computed:{ audio(){ return this.$store.getters.audio; } }, methods:{ play(){ this.$store.commit('play'); }, audioEnd(event){ this.$store.commit('audioEnd',event); }, judgement(){ let path=this.$route.path; if(path=="/play"){ return true; }else{ return false; } } } } </script> <style lang="less" > .controlBarBtn{ position: absolute; z-index:12; width: 243px; margin-left: -121.5px; top: 83%; left: 50%; } .controlBarBtn i.mu-icon{ font-size: 36px; color: #03a9f4; left: 50%; margin-left: -18px; position: absolute; top: 10%; } .controlBarBtn .addPlus{ top: 16px; width: 80px!important; height: 80px!important; margin: 0 30px!important; } .controlBarBtn .addPlus i.mu-icon{ font-size: 60px; margin-left: -30px; top: 10%; } </style>
这个页面比较简单,播放器audio
标签,绑定了ended事件,即播放完成后执行。
这里有一个坑,解释一下:我把播放器按钮放在这里了,为何呢?以前我是放在play.vue
里的,可是我发现一个问题,就是经过点击歌单的歌曲播放时,没法改变播放/暂停按钮,为何呢?由于我改变按钮的方法是用innerHTML
改变,我为何要用这种方法呢?由于Muse-ui的icon通过渲染,是以标签的值的形式出现的。这就不得不获取DOM了,可是若是把按钮写在play.vue
里,在歌单页面时是获取不到指定DOM的,由于当前页面根本没有这个DOM!只有把按钮写在在主组件里的playerBox.vue
里,才能获取到指定DOM。
可是写在playBox.vue
里又有一个问题,按钮会出如今每个页面里,可是咱们只要它出如今播放页面就行了,因此咱们在这里要给按钮绑定一个v-show
,里面的内容就是判断是否是在指定路由,若是是播放页面,就显示按钮,不是,就隐藏按钮。
axios具体的配置我都在上面讲了,这里介绍一款网易云的api和使用方法。
介绍一下使用方法,进入git把它下下来,在命令行执行:
$ node app.js
在浏览器输入地址:
localhost:3000
看到弹出的页面就说明服务器启动成功了。而后咱们能够在文档里查到具体请求的数据,好比banner啊,歌单啊,搜索啊,都能请求。咱们看到前面写的axios请求里的地址,都是具体请求的地址。
这里要注意的是,这个api默认的是没有开启跨域的,看app.js
里有一段被隐藏的代码就是跨域的相关设置,解除隐藏便可。
目前还存在一个比较大的bug,就是在歌单点击播放时,点击第一次由于没办法获取个去的url,没法播放,只有再点击一次才能播放,这个bug暂时尚未时间解决,会尽快解决。
而后目前尚未实现的功能是播放列表,天然上一曲/下一曲按钮也没有用了,歌曲播放一遍也就中止了,这个功能不算难,抽空把它作出来。
这个app参考了一些技术文章,给了我很大的启发,附上连接。
用vue全家桶写一个“以假乱真”的网易云音乐
DIY 一个本身的音乐播放器 2.0 来袭
这个app前先后后,磨磨蹭蹭作了两个月,好歹总算是作完了。学习仍是得找项目来作,虽然这个项目还很简陋,可是仍是get到不少知识点,对于个人提升仍是蛮大的。
这种项目不算难,写过的人也多,因此百分之八十的问题都能百度出来,剩下的百分之二十,技术社区里提个问基本可以解决。项目仍是得本身写一遍,写的过程当中才能发现问题,也才能想办法找到解决办法,事情老是会比你想象的要简单一点。
项目不算大,但要一步步写下来总有可能有所遗漏,这里是个人GitHub,你们能够对照着看看有没有遗漏。若是你喜欢个人项目,也但愿star或者fork一波~