這個播放器的開發歷時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一波~