1. 程式人生 > >輕量級音樂播放器搭建 3

輕量級音樂播放器搭建 3

capi 初始 很多 error algo cep 項目 blob ref

輕量級音樂播放器搭建 3

接著之前的工作,現在想要對歌曲進行請求。首先應當啟動網易雲音樂的API的服務器,github地址如下。克隆這個項目後,在終端啟動:

  npm install
node app.js

現在這個服務器就啟動了,默認為3000端口。但是又有一個問題就是跨域,webpack沒有更改默認的配置的話實在本地服務器的8080端口,但是以上的網易雲API服務器的端口為本地服務器的3000端口,所以端口不一致,不符合同源策略,所以這裏就需要使用axios。

axios是什麽?鏈接中是中文說明。據說之前尤雨溪已經建議使用axios,VueResource不再進行維護。axios就是一個基於Promise的http客戶端。安裝axios:

  
cnpm install axios --save

下面就是在webpack的本地服務器中配置axios,回到項目的根目錄中,找到dev-server.js這個文件,這個文件就是開發環境的服務器。就在這個文件中進行路由與axios請求轉發的配置。

  ......
var axios = require(‘axios‘)
var app = express()
?
var apiRoutes = express.Router()
?
apiRoutes.get(‘/getSomething‘, function (req, res) {
var url = ‘https://anotherUrl.com/something‘
axios.get(url, {
headers: {
referer: ‘https://anotherUrl.com/‘,
host: ‘anotherUrl.com‘
},
params: req.query
}).then((response) => {
res.json(response.data)
}).catch((e) => {
console.log(e)
})
})
?
app.use(‘/api‘, apiRoutes)
......

由以上的代碼示例可以看出,使用axios步驟如下:

  1. 創建express.Router實例,用以獲得路由。

  2. 對express.Router的實例進行監聽各種請求,如get等。當匹配到相應的url的時候,執行回調函數。

  3. 編寫回調函數,回調函數用於對請求進行轉發,並獲取響應。所以函數中需要轉發的url,然後就可以調用axios模塊的get方法(這裏以用戶發起get請求為例)。axios的get方法有三個參數,第一個參數就是要進行轉發的url,這個必選;第二個參數是各種配置,包括headers,這個對於跨域的請求來說非常重要,因為他改變了請求頭部信息;汗包括一個參數就是params,就是請求所攜帶的參數。然後就是執行promise的then回調,將response的data傳回res的json。

  4. 最後,將express框架應用實例調用use方法來使用這個router實例,當請求的路徑匹配到第一個參數的路徑的時候,就路由到apiRoutes這個模塊進行處理。

所以在dev-server.js中添加如下代碼:

  
// axios
var apiRouter = express.Router()
apiRouter.get(‘/getDefaultMusicList‘, function (req, res) {
let url = ‘http://localhost:3000/personalized/newsong‘
axios.get(url, {
headers: {
referer: ‘http://localhost:3000/‘,
host: ‘localhost:3000‘
},
params: req.query
}).then((response) => {
console.log(response)
res.json(response.data)
}).catch((err) => {
console.log(err)
})
})
app.use(‘/api‘, apiRouter)

這樣的話,就可以在music-player.vue中進行歌單的請求。但是先不要著急,因為比較好的思路與框架是進行模塊化,於是在src的common/api目錄中創建這些請求的功能,然後在vue組件中去引入使用這些模塊。所以先創建getDefaultMusicList.js文件:

  
import axios from ‘axios‘
export default function getDefaultMusicList () {
return axios.get(‘/api/getDefaultMusicList‘)
.then((res) => {
return Promise.resolve(res.data)
})
}

這裏就是使用了一個axios的http請求功能,地址就是本地服務器的地址加上路由的api(與之前在apiRouter實例中的路由對應),返回這個函數的請求返回值。那麽這個請求得到數據之後又再一次的進行解析,將解析後的數據返回。現在可以在music-player.vue中使用了:

  
<template>
<div class="music-player">
<header-bar></header-bar>
<div class="mid">
<img src="../../assets/logo.png">
</div>
<music-controller></music-controller>
</div>
</template>
?
?
<script>
import HeaderBar from ‘components/header-bar/header-bar‘
import MusicController from ‘components/music-controller/music-controller‘
import getDefaultMusicList from ‘api/getDefaultMusicList‘
?
export default {
components: {
HeaderBar,
MusicController
},
created () {
console.log(‘MusicPlayer Created‘)
this._getDefaultMusicList()
},
methods: {
_getDefaultMusicList () {
getDefaultMusicList().then((res) => {
if (res.code === 200) {
this.defaultMusicList = res.result
console.log(this.defaultMusicList)
}
})
}
},
data () {
return {
defaultMusicList: [],
}
}
}
</script>

現在如果打開瀏覽器的控制臺,就會看到當前這個music-player組件中的defaultMusicList的數據,我們已經獲取到了,是一個長度為10的數組。

現在要想播放一首歌曲,改怎麽辦呢?查詢API的文檔,找到了獲取音樂 url的接口。如果將獲取到的歌單中任一id作為參數進行請求,在瀏覽器中會是如下的返回結果:

技術分享

然後呢,返回值中data數組只有一個對象,對象裏有一個url,打開這個url就可以聽到歌曲了。然而機智的我發現事情並不是那麽簡單,歌曲的時間長度在哪?歌曲的背景圖片在哪?歌曲的各種參數都在哪?我靠這個list中的元素展開之後十分復雜。各種參數都不知道是幹什麽的,以第一個元素為例,他的歌曲背景圖片的地址是在song->album->blurPicUrl之中。至於播放時間長度,這個好像是在song->bMusic/hMusic/lMusic/mMusic->playtime中。這幾個music我估計應該是音樂品質的區分吧。但是這個playtime怎麽解釋,這裏的數字是185696,臣妾想不通啊。計算得不到一個像是時間的結果。另外這個是在list中才有的屬性,如果在其他的地方可能就沒有這個屬性了。真讓人頭大。算了這些先不管了,先用這個數據吧,至於播放時間就先不用了,也就是進度條暫時也不寫了。

那麽下面就是對歌單中的歌曲進行播放,我想要對列表進行循環播放。因為列表的長度是有限的但是不能播放完默認的列表就停止了,所以應當進行循環的播放。所以獲取完歌單之後就進行自動的播放。所以在api目錄中新建一個播放歌曲的函數,播放歌曲是另外一個請求,所以還是使用axios來進行轉發:

所以創建playThisMusic.js文件:

  
import axios from ‘axios‘
?
export default function playThisMusic (music) {
let response = getMusicUrl(music.id)
//axios.get(‘‘)
}
?
function getMusicUrl(id) {
let url = `/api/getMusicUrl/url?id=${id}`
return axios.get(url).then((res) => {
console.log(res.data)
return Promise.resolve(res.data)
})
}

這裏就是說現在有一個在defaultMusicList中的元素music。有了這個元素,可以獲得一些關於這個歌曲的信息,但是獲得不了歌曲播放的地址。所以使用getMusicUrl方法來獲取歌曲播放的地址。那這個url地址怎麽獲得呢?還是要通過當前歌曲的id發送一個請求,然後再進行解析。但是還是老問題,就是獲得url又跨域了,所以還是要再服務器端使用axios來發送請求。

由於跨域發送的請求估計會有很多,所以我想把這個apiRouter做成一個模塊來引入到服務器端文件。所以再build目錄創建apiRouter文件,並進行修改與引用如下:

  
......
apiRouter.get(‘/getMusicUrl/url‘, function (req, res) {
let url = `http://localhost:3000/music/url`
axios.get(url, {
headers: {
referer: ‘http://localhost:3000/‘,
host: ‘localhost:3000‘
},
params: req.query
}).then((response) => {
res.json(response.data)
}).catch((err) => {
console.log(err)
})
})
?
module.exports = apiRouter

這裏有一點就是註意apiRouter所get的第一個url參數,參數必須要完全匹配才可以,如果只寫為‘/getMusicUrl‘則是匹配不到的。url部分就是一個請求的‘?‘之前的部分,之後為params部分。然後作為一個模塊,隨後應當使用module.exports來對apiRouter進行暴露出去。

現在再控制臺中就可以看到有了返回的信息。是一個對象,對象的data部分是一個長度為1的數組。數組中的url屬性就是我們需要的播放地址。所以返回playThisMusic.js文件繼續對playThisMusic函數進行修改:

  
import axios from ‘axios‘
?
export function playThisMusic (music) {
let url = ‘‘
getMusicUrl(music.id).then((res) => {
if (res.code === 200) {
url = res.data[0].url
} else {
console.log(‘未能獲取播放地址‘)
}
})
}
?
export function getMusicUrl(id) {
let url = `/api/getMusicUrl/url?id=${id}`
return axios.get(url).then((res) => {
console.log(res.data)
return Promise.resolve(res.data)
})
}

我一開始以為需要對這個播放的url進行請求才能播放音樂,結果發現不是這回事。在html5中,有專門的audio標簽來播放音頻文件。所以以上代碼中的playThisMusic方法就不需要了。修改music-player.vue文件:

  
<template>
<div class="music-player">
<header-bar></header-bar>
<div class="mid">
<audio :src="currentMusicUrl" autoplay></audio>
<img src="../../assets/logo.png">
</div>
<music-controller></music-controller>
</div>
</template>
?
?
<script>
import HeaderBar from ‘components/header-bar/header-bar‘
import MusicController from ‘components/music-controller/music-controller‘
import getDefaultMusicList from ‘api/getDefaultMusicList‘
import {getMusicUrl} from ‘api/playThisMusic‘
?
export default {
components: {
HeaderBar,
MusicController
},
created () {
console.log(‘MusicPlayer Created‘)
this._getDefaultMusicList()
},
methods: {
_getDefaultMusicList () {
getDefaultMusicList()
.then((res) => {
if (res.code === 200) {
this.defaultMusicList = res.result
console.log(this.defaultMusicList)
}
})
.then(() => {
this._playDefaultMusic(this.defaultMusicList.length - 1)
})
},
_playDefaultMusic (lastIndex) {
let currentIndex
if (lastIndex === this.defaultMusicList.length - 1) {
currentIndex = 0
} else {
currentIndex = lastIndex + 1
}
getMusicUrl(this.defaultMusicList[currentIndex].id)
.then((res) => {
this.currentMusicUrl = res.data[0].url
})
}
},
data () {
return {
defaultMusicList: [],
currentMusicUrl: ‘‘,
}
}
}
</script>

怎麽沒有聲音?我找了半天的錯誤,發現原因就是慢!音頻沒有緩沖好,但是耐心等待一會確實會聽到斷斷續續地播放的,至於為什麽這麽慢我也不知道,如果直接請求音樂播放的url的話可以瞬間打開,但是作為audio標簽的src就很慢,我也不知道是為什麽。欸,不行。現在直接請求的話直接沒有歌了,我猜可能是因為網易雲音樂那邊的限制。

接著往下進行,現在如果要讓歌曲要自動調到下一個歌曲。經過查閱W3C文檔,發現audio元素有許多有用的事件。

Event nameDispatched when...
loadstart The user agent begins looking for media data, as part of the resource selection algorithm.
progress The user agent is fetching media data.
suspend The user agent is intentionally not currently fetching media data, but does not have the entire media resource downloaded.
abort The user agent stops fetching the media data before it is completely downloaded, but not due to an error.
emptied A media element whose networkState was previously not in the NETWORK_EMPTY state has just switched to that state (either because of a fatal error during load that‘s about to be reported, or because the load() method was invoked while the resource selection algorithm was already running).
error An error occurs while fetching the media data.
stalled The user agent is trying to fetch media data, but data is unexpectedly not forthcoming.
play Playback has begun. Fired after the play() method has returned, or when the autoplay attribute has caused playback to begin.
pause Playback has been paused. Fired after the pause() method has returned.
loadedmetadata The user agent has just determined the duration and dimensions of the media resource
loadeddata The user agent can render the media data at the current playback position for the first time.
waiting Playback has stopped because the next frame is not available, but the user agent expects that frame to become available in due course.
playing Playback has started.
canplay The user agent can resume playback of the media data, but estimates that if playback were to be started now, the media resource could not be rendered at the current playback rate up to its end without having to stop for further buffering of content.
canplaythrough The user agent estimates that if playback were to be started now, the media resource could be rendered at the current playback rate all the way to its end without having to stop for further buffering.
seeking The seeking IDL attribute changed to true and the seek operation is taking long enough that the user agent has time to fire the event.
seeked The seeking IDL attribute changed to false.
timeupdate The current playback position changed as part of normal playback or in an especially interesting way, for example discontinuously.
ended Playback has stopped because the end of the media resource was reached.
ratechange Either the defaultPlaybackRate or the playbackRate attribute has just been updated.
durationchange The duration attribute has just been updated.
volumechange Either the volume attribute or the muted attribute has changed. Fired after the relevant attribute‘s setter has returned.

對於要切換歌曲,時機就在於一首歌的結束位置。所以可以使用ended事件來切換當前播放的音樂。由於要切換歌曲,綁定事件等。所以要對audio元素綁定ended事件,所觸發的函數為_playDefaultMusic,但是這個函數之前寫的需要傳遞一個當前的索引值。目前我想有兩種方案,一是在audio元素上綁定一個自定義特性index來表示索引;另一個是不用傳遞索引,函數改為無參數,索引值改為由data保存(後期修改為vuex控制索引狀態)。顯然無論從代碼簡潔、資源控制還是後期的擴展上都是第二種方式較好。

由於初始的時候是聽第一首歌(其實第幾首根本無所謂),所以currentMusicIndex初始化為個單列表的長度減一(這句話是後來補上的,不能初始化為length - 1,因為這個數組實際上在一開始的時候是沒有定義的)。然後我想切換播放歌曲不管是點擊下一首也好,還是左右滑動也好,還是自動播放下一首也好,本質上都是對currentMusicIndex進行修改。所以可以觀察currentMusicIndex這個變量的變化,如果有變化,那麽就切換資源並且播放音頻。修改代碼如下:

  
<template>
<div class="music-player">
<header-bar></header-bar>
<div class="mid">
<audio :src="currentMusicUrl" autoplay @ended="_playDefaultMusic(currentMusicIndex)" ref="audio"></audio>
<img src="../../assets/logo.png">
</div>
<music-controller></music-controller>
</div>
</template>
?
?
<script>
import HeaderBar from ‘components/header-bar/header-bar‘
import MusicController from ‘components/music-controller/music-controller‘
import getDefaultMusicList from ‘api/getDefaultMusicList‘
import {getMusicUrl} from ‘api/playThisMusic‘
?
export default {
data () {
return {
defaultMusicList: [],
currentMusicUrl: ‘‘,
currentMusicIndex: 0,
}
},
components: {
HeaderBar,
MusicController
},
created () {
console.log(‘MusicPlayer Created‘)
this._getDefaultMusicList()
},
methods: {
_getDefaultMusicList () {
getDefaultMusicList()
.then((res) => {
if (res.code === 200) {
this.defaultMusicList = res.result
console.log(this.defaultMusicList)
}
})
.then(() => {
this._playDefaultMusic()
})
},
_playDefaultMusic () {
if (this.currentMusicIndex === this.defaultMusicList.length - 1) {
this.currentMusicIndex = 0
} else {
this.currentMusicIndex = this.currentMusicIndex + 1
}
getMusicUrl(this.defaultMusicList[this.currentMusicIndex].id)
.then((res) => {
this.currentMusicUrl = res.data[0].url
})
}
},
watch: {
currentMusicIndex: function (newVal, oldVal) {
console.log(this.$refs.audio)
this.$refs.audio.play()
}
}
}
</script>

可以運行,但是報一個很詭異的錯誤:

  
Uncaught (in promise) DOMException: The element has no supported sources.

為什麽呢?我想是因為一開始的時候audio中的src綁定的變量是currentMusicUrl,但是這個data初始化為空字符串,然而我這裏play()方法調用的時機是在_playDefaultMusic中改變了currentMusicIndex,然後在修改的currentMusicUrl。所以會出現src沒有的情況。並且還由別的bug。這個錯誤只報一次是因為最開始的一次直接沒有src。把兩段代碼交換一下位置,有什麽事明天再說。太晚了,得回去。

參考鏈接:

  1. axios中文說明

  2. axios github

  3. express 文檔

  4. 網易雲API文檔

  5. Promise 介紹

  6. audio W3C介紹

  7. vue watch 文檔

輕量級音樂播放器搭建 3