金沙官网线上轻量级音乐播放器搭建 3

在微信开发中,写过的一个简单的音乐播放组件,记录下。

轻量级音乐播放器搭建 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 name Dispatched 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控制索引状态)。显然无论从代码简洁、资源控制还是后期的扩展上都是第二种方式较好。

music

音乐播放组件。

由于初始的时候是听第一首歌(其实第几首根本无所谓),所以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 文档

属性

属性名 类型 默认值 说明
music String   传入的音乐资源地址
musicStyle String (随便写了个) 音乐组件的样式
rotate Boolean true 播放时是否有旋转效果
iconOn String (随便写了个) 音乐播放时的icon地址
iconOff String (随便写了个) 音乐暂停时的icon地址

代码

properties: {
    // 音乐路径
    music: {
      type: String,
      value: '',
      observer: function (newVal) {
        this._initMusic(newVal)
      }
    },
    // 样式
    musicStyle: {
      type: String,
      value: 'position: absolute; right: 20rpx; top: 20rpx; width: 100rpx; height: 100rpx;'
    },
    // 播放时是否有旋转效果
    rotate: {
      type: Boolean,
      value: true
    },
    // 播放时的icon路径
    iconOn: {
      type: String,
      value: '/resources/img/music-on.png' // 请填写默认的图片地址
    },
    // 暂停时的icon路径
    iconOff: {
      type: String,
      value: '/resources/img/music-off.png' // 请填写默认的图片地址
    }
  }

初始化音乐

首先,在properties中接收页面传来的音乐文件地址,

music: {
  type: String,
  value: '',
  observer: function (newVal) {
    this._initMusic(newVal)
  }
}

这里的处理是,一旦接收到页面传来的 music 地址,就初始化音乐:

_initMusic: function (newVal) {
  // 当页面传来新的music时,先销毁之前的audioCtx,否则页面会很嗨
  if (this.data.audioCtx) {
    this.data.audioCtx.destroy()
  }
  if (newVal) {
    var audioCtx = wx.createInnerAudioContext()
    this.setData({
        audioCtx: audioCtx
    })
    if (this.data.audioStatus == '1') {
        audioCtx.autoplay = true
    }
    audioCtx.loop = true
    audioCtx.src = newVal
  }
}

 audioStatus 用来记录音乐播放状态,在data中默认设置为1:

data: {
    icon: '',
    audioStatus: 1,
    audioCtx: '',
    musicClass: 'music-on'
}

wxml文件里,只用一个 <image> 标签:

<image class='music {{ rotate && musicClass }}'  
        style="{{ musicStyle }}"  
        src="{{ icon }}"  
        bindtap='_switch'  
        wx:if="{{ music }}"></image>

其中, icon 在组件ready()时赋值成播放状态的icon:

ready() {
    this.setData({
      icon: this.data.iconOn
    })
}

本文由金沙官网线上发布于Web前端,转载请注明出处:金沙官网线上轻量级音乐播放器搭建 3

您可能还会对下面的文章感兴趣: