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

来源:互联网 发布:腾讯视频 for mac官方 编辑:程序博客网 时间:2024/05/15 01:46

这个播放器的开发历时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
  2. 这里我们加载了一个底部导航,muse-ui的,官网可以查到相关代码。这里要注意的是,为了让用户体验更好,我们需要让我们的底部导航随当前路由变化而高亮。具体是用了一段JS代码。
    watch监视路由变化并触发一个method:changebar(),这个函数会获取当前的路由名,并把bottomNav的值设置为当前路由名——即高亮当前的路由页面
  3. 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一波~

原创粉丝点击