这个播放器的开发历时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>
解释一下:
- 由于Muse-ui有部分样式用到了less,所以在这里我们需要npm安装一个less的依赖,安装好后即可使用。
npm install less less-loader --save
- 这里我们加载了一个底部导航,muse-ui的,官网可以查到相关代码。这里要注意的是,为了让用户体验更好,我们需要让我们的底部导航随当前路由变化而高亮。具体是用了一段JS代码。
watch监视路由变化并触发一个method:changebar(),这个函数会获取当前的路由名,并把bottomNav的值设置为当前路由名——即高亮当前的路由页面
- 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
的宽度改变每行歌单的数目。
这里要注意的:
- 歌单的数据和轮播图都是用的网易云数据,所以没有开api是无法读取的,引入
axios
的部分可以先不写,也可以写好先放着。 - 这里
methods
和created
里面的内容都涉及到axios的请求,所以可以先不写,不影响样式呈现。数据可以先用假数据代替。 - 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;
},
- 如代码所示,我在顶部导航添加了一个
icon button
,样式来自Muse-ui
绑定了一个点击事件backpage,点击后会回到上一个路由页面。这个需要配合之前的高亮底部导航icon,才能实现返回上一路由的同时高亮相对应的icon。 - 还要注意的是,computed里有两个方法,第一个是获取vuex里面的当前曲目信息;第二个则是获取进度条的百分比信息,这个方法实现了数据的双向绑定,随着后台设定的计时器,不断地更新,从而实现播放时进度条的变化。同样,这里的样式也是来自
Muse-ui
的Slider
。 - 这里有一个需要注意的坑是,Muse-ui自带了许多的函数,第一次写的时候没有注意,在进度条上绑定了一个
mouseup
事件,结果无效,后来才发现,其实已经自带了change
事件,还可以实现移动端的兼容。所以写代码的时候一定要多看看官网文档。 - 关于
store.js
里的方法,play
是播放/暂停,具体会根据当前音频文件的paused
(即是否暂停)来判断。总的原理是首先获取音频的持续时间,然后通过一个定时器,不断更新显示时间,播放完成时,计时器停止。 - 计时器很关键,进度条和显示时间的更新都需要它。但是计时器有个坑,如果把计时器声明放在
play
方法里,则无法在audioEnd
方法里停止计时器,所以这里我们需要在最外层先声明一个ctime
,然后再在play
方法里把定时器赋值给ctime
,这样我们就可以随时停止计时器了。 -
audioEnd
方法是播放停止时要做的事情,我们会把停止按钮切换成播放,把显示时间修改掉,别忘了停止计时器。 -
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下载安装不用多说了。
解释一下这个组件的两个方法:
-
getSearch
是获取搜索结果,它被绑定再搜索按钮上,初始页面是空白,通过传递关键字,用axios
从api获取搜索结果,再把结果显示在页面上。 -
getSong
绑定在每一个搜索的结果上,有两个步骤,第一是getSong
,会把点击的歌曲设置为要播放的歌曲,并把相关信息传递给play.vue
,让它显示在相应的地方;第二个步骤,会播放歌曲,也就是上面的play
方法,具体不必再说。 - 这里有一个坑,我们可能需要通过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一波~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。