QQ音乐webapp是基于
vue2
,vuex
,vue-router
,axios
和flexible
box布局与及css3的transform
,animation
,transition
开发。实现了歌曲搜索、歌词检索、歌曲播放等功能。项目利用QQ音乐线上真实接口,在数据接口跨域请求中使用了NodeJS
的express
做请求转发。
运行效果
开发目录
开发目录是通过vue-cli
脚手架生成的,其中webpack
构建工具是3.10版本,下面是开发目录的结构,其中红色标注是比较主要的文件,其他都是脚手架自动生成的,vuejs开发逻辑与及思路主要放在src目录进行讲解:
├─main.js(程序入口)
├─App.js(挂载顶层组件)
├─common
│ ├─musicOperate.js(音乐播放函数)
│
├─components
│ ├─AudioPane.vue(底部播放组件)
│ ├─globalList.vue
│ ├─history.vue(搜索记录列表)
│ ├─index.vue(推荐音乐卡片列表)
│ ├─indexlist.vue(推荐音乐列表)
│ ├─List.vue(音乐详情页播放列表)
│ ├─loading.vue(加载组件)
│ ├─MusicDetail.vue(音乐详情组件)
│ ├─my-footer.vue(footer)
│ ├─my-header.vue(头部)
│ ├─Search.vue(搜索组件)
│ ├─toplist.vue
│
├─router
│ ├─index.js(路由文件)
│
└─store
├─store.js(vuex全局store)
复制代码
src目录结构如下所示,common
是公用组件或函数文件夹components
主要放置开发的组件,router
是vue-router
的路由文件,store是vuex状态管理器文件夹,asset
是脚手架生成的静态文件夹。vuejs
开发单页面应用,采取的单一入口文件main.js
(其中挂载需要在根组件挂载的资源),App.vue
是默认的根组件。
入口文件main.js
分析
main.js
入口文件引入了vue-router
路由,axios
的http
请求模块,vuex
全局状态管理,vue-awesome-swiper
轮播组件,fastclick
(避免移动端点击延迟300ms的效果)以及vue-lazyload
懒加载模块。
import Vue from 'vue'
import App from './App'
import router from './router'
import axios from 'axios'
import Vuex from 'vuex'
import store from './store/store.js'
import VueAwesomeSwiper from 'vue-awesome-swiper'
import VueLazyload from 'vue-lazyload'
import FastClick from 'fastclick'
FastClick.attach(document.body)
Vue.use(Vuex)
Vue.use(VueAwesomeSwiper)
Vue.use(VueLazyload,{
preLoad:1.3,
error:"../static/images/load.png",
loading:"../static/images/load1.png",
attempt:1
})
Vue.config.productionTip = false
Vue.prototype.$http=axios
new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App }
})
复制代码
项目路由定义
项目路由定义,在router/index.js
文件下引入官方推荐路由类库vue-router
,对项目所使用路由进行统一定义:
import Vue from 'vue'
import Router from 'vue-router'
import Index from "@/components/index"
import List from "@/components/List"
import Search from "@/components/Search"
import Toplist from "@/components/toplist"
import IndexList from '@/components/indexlist'
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path:'/',
redirect:'/recomment'
},
{
path: '/recomment',
name: 'Index',
component: Index
},{
path: '/list',
name: 'List',
component: List
},{
path: '/search',
name: 'Search',
component: Search
},{
path:"/toplist",
component:Toplist
},{
path:'/indexlist',
component:IndexList
},{
path:"*",
redirect:"/recomment"
}]
})
复制代码
全局数据管理store
在今天开发前端SPA时,不一定需要使用到全局数据管理.vuex
类似于react
中的redux
,采取的都是flux
单向数据流的思想。记得redux
项目的作者说过,如果你不知道你是否需要redux,那你肯定是不需要
。在这个单页SPA项目中,需要在多个不同层级的组件中,共享播放列表数据。所以这里使用vuex
保存音乐播放列表简直是游鱼得水,利用数组模拟队列进行播放歌曲的保存,搜索历史的保存等关键逻辑。
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store =new Vuex.Store({
//定义状态
state:{
toggle:false,
songPlayList:[{
mid:'4962990',
singerAvartar:"https://y.gtimg.cn/music/photo_new/T006R300x300M00000333So02drvak.jpg?max_age=2592000",
songName:"七里香",
singer:"周杰伦",
songSrc:"http://dl.stream.qqmusic.qq.com/C400002ZKnKQ34rbZu.m4a?guid=957707267&vkey=A31FA931F2D4AE48FC9262A169955162673D2882402CF3D4E5FE40F32CD0035C97A18C3BEEA3AE0864D62ED9A8E0DDDC0853D532C1B055AB&uin=&fromtag=999"
}],
prevPlayList:[],
searchHistory:[],
Mkey:"guid=957707267&vkey=5C23405C26338354E28E61E4679EFE31BCE634C3BC1F4141131DA3C42E45F9C2F20CEA658F41B279C328AA7EB7219CB2B889FCC73918580D&uin=&fromtag=999"
},
mutations:{
//加入搜索历史记录
addSearchWord(state,word){
state.searchHistory.push(word);
},
//清除搜索记录
clearHistory(state){
for(var i=0;i<state.searchHistory.length;i++){
state.searchHistory.pop();
}
},
//显示音乐详情
changeTog(state,toggle){
state.toggle=toggle;
},
//播放下一首歌曲
nextSong(state,num){
var prevSong=state.songPlayList.shift();
state.prevPlayList.push(prevSong);
},
//播放上一首歌曲
prevSong(state,num){
var stackTop=state.prevPlayList.pop();
state.songPlayList.unshift(stackTop);
},
changeList(state,index){
var song=state.songPlayList.splice(index,1);
state.songPlayList.unshift(song[0]);
},
//添加歌曲
addSong(state,data){
for(var i=0;i<data.length;i++){
var singleSong={
singerAvartar:data[i].singerAvartar,
songName:data[i].title,
singer:data[i].singer,
songSrc:data[i].songSrc,
mid:data[i].mid
};
state.songPlayList.push(singleSong);
}
}
}
})
export default store
复制代码
App.vue
根组件
App.vue
组件是整个项目的根目录组件,在这里挂载其他所有的组件。Mheader是QQ音乐的导航部分,包括tab
部分和状态栏部分。router-view
是将点击tab单项匹配到路由组件加载到这块内容。而musicDetail
组件是可以查看音乐详情的组件,采用在App.vue
根组件挂载是为了其他加载跳转操作不至于中断音乐的播放。audio
就是音乐控制面板,当音乐详情关闭,则就以小化的面板控制音乐。
<template>
<div id="app">
<Mheader></Mheader>//头部
<router-view></router-view>//动态加载路由匹配的内容将生成这里
<musicDetail></musicDetail>//为保证整个项目运行过程中,音乐播放器可以不中断,把他挂载在App.vue
<audioPane></audioPane>
<audio id="play" :src="songPlayList[0].songSrc" @ended="nextSong" @error="nextSong" preload="auto"></audio>//播放器audio
</div>
</template>
<script type="text/ecmascript-6">
import Mheader from '@/components/my-header';
import audioPane from'@/components/AudioPane';
import musicDetail from '@/components/MusicDetail'
export default {
components: {
Mheader,
audioPane,
musicDetail,
},
methods: {
nextSong(){
if(this.$store.state.songPlayList){
this.$store.commit("nextSong",1);
var audio=document.getElementById('play');
audio.addEventListener("canplaythrough", function(){
audio.play();
},false);
if(!this.toggle){
var state=!this.toggle;
this.$store.commit('changeTog',state);
}
}else{
console.log("not found this song");
}
}
},
computed:{
songPlayList(){
return this.$store.state.songPlayList;
}
},
mounted() {
//
this.$http.get("http://localhost:3000/songList").then((response)=>{
var data=response.data.cdlist[0].songlist;
this.$store.commit('addSong',data);
}).catch((e)=>{
console.log(e);
});
}
}
</script>
<style scoped>
body{
margin:0;
overflow: hidden !important;
}
</style>
复制代码
歌词数据解析
整个单页应用,比较难的部分是获取QQ音乐相应歌曲的歌词部分。这一部分是通过express
获取线上真实歌词接口数据。返回的数据需要在前端页面进行处理。这一部分可以在musicDetail
中获取,代码如下:
lyric() {
let globalArr = [];
this.$http
.get("http://localhost:3000/lyrics?id=" + this.songPlayList[0].mid)
.then(response => {
let lyric = response.data.lyric;
let tmpLyric = this.formatLyric(lyric);
let Arr = tmpLyric.split("\n");
for (let i = 0; i < Arr.length; i++) {
let row = Arr[i].split("]");
let obj = {};
if (row[2]) {
let time = row[1].split("[")[1];
let second = Math.floor(time.split(":")[1]);
let minute = parseInt(time.split(":")[0]);
time = minute * 60 + second;
obj[time] = row[2];
globalArr.push(obj);
} else if (!row[1]) {
continue;
} else {
let time = row[0].split("[")[1];
let second = Math.floor(time.split(":")[1]);
let minute = parseInt(time.split(":")[0]);
time = second + minute * 60;
obj[time] = row[1];
globalArr.push(obj);
}
}
})
.catch(e => {
console.log(e);
});
this.downloaded = true;
return globalArr;
}
复制代码
开发思路
在App.vue
挂载tab主要路由,通过<router-view>
动态加载其他的组件,而musicDetail
组件则内嵌在/recomment
路由里面;list
组件和toplist
组件则以动态路由的防水进行加载之后,通过绝对定位的方式,定位在当前路由组件之上(通过z-index)完成。如果不想组件直接加载在App.vue
的路由匹配区域,可以通过当前路由的子路由方式进行组件的加载。
本项目是在17年大学实习期间开发。可能QQ音乐线上接口发生更改,如果接口数据无法使用。可以通过线上抓取接口信息进行相应的更改。
如果觉得有帮助,可以给个star哟~
Github源码地址: github.com/Harhao/QQMu…