用Vue搭建一个应用盒子(三):音乐播放器

2018-01-31 大树

这个播放器的开发历时2个多月,并不是说它有多复杂,相反它的功能还非常不完善,仅具雏形。之所以磨磨蹭蹭这么久,一是因为拖延,二也是实习公司项目太紧。8月底结束实习前写完了样式,之后在家空闲时间多了,集中精力就把JS部分做完了。

这个播放器确实比当初构想的复杂,开始只打算做一个搜歌播放的功能。现在做出来的这个播放器,可以获取热门歌曲,可以搜歌,可以调整播放进度条,功能确实完善不少。

这次完成这个项目也是收获颇丰,点了不少新的技能点,当然,这个简陋的小项目也挖了不少坑,不知道啥时候能填上……

话不多说,看代码吧。

Muse-ui

不记得在哪个网站看到这个组件库的了,觉得好酷炫,于是用起来~

这是官网: 地址

使用这个组件库的原因除了漂亮,还因为这是基于Vue 2.0,无缝对接,方便。

使用方法跟之前的插件一样,npm安装:

npm install --save muse-ui

安装好后,在 main.js 中注册。

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的代码。

组件结构

接着我们就该搭建这个播放器的组件了。

结构如下:

||-- player.vue       // 主页面
|    |-- playerBox.vue   // 播放器组件
|    |-- popular.vue    // 热门歌曲页面
|        |-- songList.vue     // 歌曲列表页面 
|    |-- play.vue    // 播放器页面
|    |-- search.vue    // 搜索页面

PS:热门歌曲、搜索页面都能进入歌曲列表页面,播放器组件 playerBox.vue 是放 <audio> 标签的组件,是功能性组件。

我们来分别叙述:

1.player.vue

直接看代码吧:

<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>

解释一下:

  1. 由于Muse-ui有部分样式用到了less,所以在这里我们需要npm安装一个less的依赖,安装好后即可使用。

npm install less less-loader --save

  1. 这里我们加载了一个底部导航,muse-ui的,官网可以查到相关代码。这里要注意的是,为了让用户体验更好,我们需要让我们的底部导航随当前路由变化而高亮。具体是用了一段JS代码。

watch监视路由变化并触发一个method:changebar(),这个函数会获取当前的路由名,并把bottomNav的值设置为当前路由名——即高亮当前的路由页面

  1. playerBox.vue组件之所以放在主组件里,就是为了音乐在每一个子页面都能播放,而不会因为跳转路由而停止播放。

###2.popular.vue

这是推荐歌单界面,这里用到了一个轮播图插件,是基于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 的宽度改变每行歌单的数目。

这里要注意的:

  1. 歌单的数据和轮播图都是用的网易云数据,所以没有开api是无法读取的,引入 axios 的部分可以先不写,也可以写好先放着。
  2. 这里 methodscreated 里面的内容都涉及到axios的请求,所以可以先不写,不影响样式呈现。数据可以先用假数据代替。
  3. playList的目的是点击歌单的时候,进入歌单详情页,同时根据传递进去的歌单id获取歌单的具体数据,axios的地址是api的地址,需要加载api插件才能使用。

3.play.vue

终于到了最核心的组件,之所以说它核心是因为这是播放界面,音频播放的长度、音频信息都会在这里被呈现,而播放器的核心功能——播放——也是在这里被操作(播放/暂停)。

看具体代码:

<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;
    },
  1. 如代码所示,我在顶部导航添加了一个 icon button ,样式来自 Muse-ui 绑定了一个点击事件backpage,点击后会回到上一个路由页面。这个需要配合之前的高亮底部导航icon,才能实现返回上一路由的同时高亮相对应的icon。
  2. 还要注意的是,computed里有两个方法,第一个是获取vuex里面的当前曲目信息;第二个则是获取进度条的百分比信息,这个方法实现了数据的双向绑定,随着后台设定的计时器,不断地更新,从而实现播放时进度条的变化。同样,这里的样式也是来自 Muse-uiSlider
  3. 这里有一个需要注意的坑是,Muse-ui自带了许多的函数,第一次写的时候没有注意,在进度条上绑定了一个 mouseup 事件,结果无效,后来才发现,其实已经自带了 change 事件,还可以实现移动端的兼容。所以写代码的时候一定要多看看官网文档。
  4. 关于 store.js 里的方法, play 是播放/暂停,具体会根据当前音频文件的 paused (即是否暂停)来判断。总的原理是首先获取音频的持续时间,然后通过一个定时器,不断更新显示时间,播放完成时,计时器停止。
  5. 计时器很关键,进度条和显示时间的更新都需要它。但是计时器有个坑,如果把计时器声明放在 play 方法里,则无法在 audioEnd 方法里停止计时器,所以这里我们需要在最外层先声明一个 ctime ,然后再在 play 方法里把定时器赋值给 ctime ,这样我们就可以随时停止计时器了。
  6. audioEnd 方法是播放停止时要做的事情,我们会把停止按钮切换成播放,把显示时间修改掉,别忘了停止计时器。
  7. editProgress 方法是点击或拖动进度条时做的事情,我们会改变当前音频的 currentTime ,即当前时间,如果音频是暂停状态,我们要让它继续播放。

4.search.vue

这也是一个比较核心的一个功能,毕竟推荐的歌单只有几个。看代码:

<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下载安装不用多说了。

解释一下这个组件的两个方法:

  1. getSearch 是获取搜索结果,它被绑定再搜索按钮上,初始页面是空白,通过传递关键字,用 axios 从api获取搜索结果,再把结果显示在页面上。
  2. getSong 绑定在每一个搜索的结果上,有两个步骤,第一是 getSong ,会把点击的歌曲设置为要播放的歌曲,并把相关信息传递给 play.vue ,让它显示在相应的地方;第二个步骤,会播放歌曲,也就是上面的 play 方法,具体不必再说。
  3. 这里有一个坑,我们可能需要通过vuex传递参数,但是有时候传递多个参数会出现 undefined 的情况,这时候我们要把参数们写成 {参数一,参数二,参数三} 的形式。

5.songList

这个组件主要是歌单详情页,基本的样式和搜索页一样,就是获取歌单的内容不同,搜索页面的列表是根据关键词获取的,歌单详情页的列表是根据歌单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 里面传递的多个参数。

6.playerBox.vue

<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

axios具体的配置我都在上面讲了,这里介绍一款网易云的api和使用方法。

文档在此

介绍一下使用方法,进入git把它下下来,在命令行执行:

$ node app.js

在浏览器输入地址:

localhost:3000

看到弹出的页面就说明服务器启动成功了。然后我们可以在文档里查到具体请求的数据,比如banner啊,歌单啊,搜索啊,都能请求。我们看到前面写的axios请求里的地址,都是具体请求的地址。

这里要注意的是,这个api默认的是没有开启跨域的,看 app.js 里有一段被隐藏的代码就是跨域的相关设置,解除隐藏即可。

bug和未实现功能

目前还存在一个比较大的bug,就是在歌单点击播放时,点击第一次因为没办法获取个去的url,无法播放,只有再点击一次才能播放,这个bug暂时还没有时间解决,会尽快解决。

然后目前还没有实现的功能是播放列表,自然上一曲/下一曲按钮也没有用了,歌曲播放一遍也就停止了,这个功能不算难,抽空把它做出来。

参考资料

这个app参考了一些技术文章,给了我很大的启发,附上链接。

用vue全家桶写一个“以假乱真”的网易云音乐

DIY 一个自己的音乐播放器 2.0 来袭

结语

这个app前前后后,磨磨蹭蹭做了两个月,好歹总算是做完了。学习还是得找项目来做,虽然这个项目还很简陋,但是还是get到很多知识点,对于我的提高还是蛮大的。

这种项目不算难,写过的人也多,所以百分之八十的问题都能百度出来,剩下的百分之二十,技术社区里提个问基本能够解决。项目还是得自己写一遍,写的过程中才能发现问题,也才能想办法找到解决办法,事情总是会比你想象的要简单一点。

项目不算大,但要一步步写下来总有可能有所遗漏,这里是我的 GitHub ,大家可以对照着看看有没有遗漏。如果你喜欢我的项目,也希望star或者fork一波~