Vue2.x+Axios+VueX+Vue-Router移动端音乐播放器开发案例
2018/08/14 10:22 分类: 技术交流 浏览:307
本文将讲解使用前后端分离开发思路,实现基于vue2.x的在线音乐播放器,使用的技术栈为vue全家桶系列(Vue2.x+Axios+VueX+Vue-Router)。
1. 项目技术选型 vue:项目中主要运用了 vue.js 框架,轻量,易上手。
vuex:vue 的状态管理插件,便于 vue 各组件之间的通信。
vue-router:vue 的路由插件。
vue-axios:项目中主要用来请求数据。
swiper:触屏焦点图、触屏Tab切换、触屏多图切换插件。
better-scroll:固定高度内滚动插件。
需求思维导图如下:
需求说明:
个人中心
个人中心这一块比较传统,就是一些登录,注册,忘记密码,修改密码,修改个人资料。
音乐
音乐这一模块主要有4个页面
个人收藏
个人收藏页面是一个个人收藏的音乐列表,点击歌曲可播放该歌曲。左划可移除收藏列表。
歌榜
歌榜页面首先是个歌榜列表,点击单个歌榜,可进入该歌榜的音乐列表。点击歌曲可播放该歌曲,左划可添加至收藏。下拉加载。
搜索
搜索页面可以输入关键字搜索相关音乐,并以列表的形式展现搜索结果。点击歌曲可播放该歌曲,左划可添加至收藏。下拉加载。
音乐播放
音乐播放页面是音乐播放时的详情页面。可以上一曲,下一曲,暂停播放,开始播放,修改循环模式,添加或取消收藏。
界面要求
头部
头部固定,作为导航菜单,个人中心,个人收藏,歌榜,搜索,以图表的形式显示。个人中心靠左,其余的菜单居中,点击个人中心时,个人信息页面从左侧滑出。
底部
底部固定,显示播放信息,可暂停播放,下一曲,点击进入播放详情。
内容
内容部分一般都是用来显示列表的。
播放详情
该页面要求全屏显示,覆盖头部,底部。
3. 搭建项目开发环境
全局安装vue-cli脚手架工具
npm i -g vue-cli
使用vue-cli脚手架工具在自己指定的目录创建项目chain-vue-musi
vue init webpack chain-vue-music
创建成功后,执行以下命令,安装项目所需依赖
cd chain-vue-music
npm install
各种依赖成功安装后,再根据我们的需求安装vue插件:
npm install vuex --save
npm install vue-router --save
npm install vue-axios --save
npm install better-scroll –save-dev
以上全部安装成功后,package.json配置如下:
"dependencies": {
"axios": "^0.16.1",
"jsonp": "^0.2.1",
"vue": "^2.2.6",
"vue-axios": "^2.0.2",
"vue-router": "^2.3.1",
"vuex": "^2.3.1",
"better-scroll": "^0.1.15"
},
"devDependencies": {
……
}
我们可以执行以下命令,看下我们最初的项目:
npm run dev
结果如下:
用编译器打开我们创建的项目,在src中创建以下目录:
pages : 用于存放我们的页面组件
router : 用于我们书写前端
components : 是已经存在的目录,我们把里面的Hello.vue文件删除,该目录用于存放公用组件
index.html
在根目录的index.html文件中引入font-awesome,我们的项目中将会使用部分的font-awesome的icon图标。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>music</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no">
<link href="http://cdn.bootcss.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
router
这里暂时先添加项目中的主要页面的路由
import Vue from 'vue';
import Router from 'vue-router';
import recommends from '../components/recommends/recommends';
import songs from '../components/songs/songs';
import radios from '../components/radios/radios';
import rankings from '../components/rankings/rankings';
Vue.use(Router);
export default new Router({
// mode: 'history',
routes: [{
path: '',
name: 'recommends',
component: recommends
}, {
path: '/recommends',
name: 'recommends',
component: recommends
}, {
path: '/songs',
name: 'songs',
component: songs
}, {
path: '/radios',
name: 'radios',
component: radios
}, {
path: '/rankings',
name: 'rankings',
component: rankings
});
store
之前的需求我们提到了我们的个人信息页面,是点击头部的相关位置从左侧滑动出来的,然后在个人页面中,再点击某个部位再滑出去。所以我们需要一个全局的状态来记录这个页面是否展示。我们在store的modules目录下创建personal.js 模块用来处理个人信息。
目前先处理页面是否展示状态,以及显示界面和隐藏界面的的mutation其他的后面有需求再补充。
main.js
在main.js中引用
import Vue from 'vue';
import App from './App';
import Axios from 'axios';
import VueAxios from 'vue-axios';
// 引入路由
import router from './router';
// 引入状态管理
import store from './vuex/store.js';
import './common/css/iconfont.css';
import '../static/css/reset.css';
Vue.config.productionTip = false;
// 使用 axios 进行 ajax 请求
Vue.use(VueAxios, Axios);
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App }
});
conpoments
之前的需求中提到,我们的页面都有个共用的,固定位置的头部和底部。所以我们在这个文件夹里,先创建两个组件Header, musicMenu,篇幅有限,其他组件不在罗列, 其代码如下:
Header.vue
<template>
<div class="header">
<div class="icon-wrapper">
<i class="icon-menu"></i>
<div class="icon-middle">
<i class="icon-music"></i>
<i class="icon-wangyi"></i>
<i class="icon-friend"></i>
</div>
<i class="icon-search"></i>
</div>
</div>
</template>
<script type="text/javascript">
</script>
<style lang="scss" type="text/css">
.header {
width: 100%;
.icon-wrapper {
padding: 0 10px;
height: 36px;
line-height: 36px;
color: #fff;
background: rgb(192, 12, 12);
text-align: center;
.icon-middle {
display: inline-flex;
width: 50%;
i {
flex: 1;
}
.icon-wangyi {
font-size: 18px;
}
}
.icon-menu {
display: inline-block;
float: left;
}
.icon-search {
float: right;
}
}
}
</style>
musicPlay.vue
<template>
<div class="fade">
<transition name="fade">
<div class="musicPlay" v-show="showFlag">
<div class="musicPlay-content">
<div class="background">
<div class="mask"></div>
<div class="image-wrapper">
<!-- <img class="image" :src="singleMusic.al.picUrl"> -->
<img class="image" :src="singleMusicUrl">
</div>
</div>
<div class="title">
<div class="icon" @click="back">
<i class="icon-back"></i>
</div>
<div class="musicInfo">
<span class="name">{{singleMusic.name}}</span>
<span class="singer">{{singleMusic.ar[0].name}}</span>
<i class="icon-share"></i>
</div>
</div>
<div class="jukebox">
<div class="record-wrapper">
<img :class="{'microphonePlay': microphonePlay}" class="microphone" src="../../../static/image/microphone.png">
<div class="record-content">
<div class="record"></div>
<img :class="{'cover-pause': coverPause, 'coverAfresh': coverAfresh}" class="cover" :src="singleMusicUrl">
</div>
</div>
</div>
<div class="play-wrapper">
<div class="icon-wrapper">
<i class="icon-enshrine"></i>
<i class="icon-download"></i>
<i class="icon-comment"></i>
<i class="icon-omit"></i>
</div>
<div class="progress">
<span class="time">{{currentTime}}</span>
<div @click="controlBar" class="line">
<span ref="totalBar" class="totalBar"></span>
<span ref="currentBar" class="currentBar"></span>
<span ref="dot" class="dot"></span>
</div>
<span class="time">{{totalTime}}</span>
</div>
<div class="control-wrapper">
<i class="icon-circulation"></i>
<div class="control">
<i @click="playPre" class="icon-pre icon"></i>
<i @click="play" :class="{'icon-play': togglePlay, 'icon-pause': !togglePlay}" class="icon"></i>
<i @click="playNext" class="icon-next icon"></i>
</div>
<i class="icon-list"></i>
</div>
</div>
</div>
</div>
</transition>
<audio ref="audio" @timeupdate="getTimes" :src="music.url" id="audio"></audio>
</div>
</template>
<script type="text/javascript">
import musicApi from '../../musicApi/index';
import {
mapGetters
} from 'vuex';
const TRUE = 1;
const TOGGLE = 0;
export default {
data: function() {
return {
showFlag: false,
microphonePlay: false,
coverPause: false,
coverAfresh: false,
music: '',
currentTime: '00:00'
};
},
computed: {
...mapGetters([
'singleMusic',
'musicList',
'currentIndex',
'togglePlay'
]),
totalTime() {
let time = Math.round((this.music.size * 8) / this.music.br);
let munite = this.addZero(Math.floor(time / 60));
let second = this.addZero(time % 60);
return munite + ':' + second;
},
singleMusicUrl() {
// 将 https 替换为 http
let picUrl = this.singleMusic.al.picUrl;
return picUrl.replace(/https/, 'http');
}
},
watch: {
currentTime: function() {
let currentTime = Math.round(this.$refs.audio.currentTime);
let totalTime = Math.round((this.music.size * 8) / this.music.br);
this.$refs.currentBar.style.width = ((currentTime / totalTime) * 100).toFixed(2) + '%';
this.$refs.dot.style.left = ((currentTime / totalTime) * 100).toFixed(2) + '%';
if (currentTime >= totalTime) {
this.playNext();
}
}
},
methods: {
show() {
this.showFlag = true;
this.togglePlay = true;
this.microphonePlay = true;
},
get(item) {
this.axios.get(musicApi.getSong(item.id)).then((res) => {
this.music = res.data.data[0];
this.$nextTick(() => {
this.$refs.audio.play();
this.$refs.audio.volume = 0.1;
});
});
},
back() {
this.showFlag = false;
},
play() {
if (!this.togglePlay) {
this.$refs.audio.play();
this.microphonePlay = true;
this.coverPause = false;
} else {
this.$refs.audio.pause();
this.microphonePlay = false;
this.coverPause = true;
}
this.$store.commit('setTogglePlay', TOGGLE);
},
playPre() {
// 更新当前歌曲位置
let currentIndex = (this.currentIndex + this.musicList.length - 1) % this.musicList.length;
this.$store.commit('setCurrentIndex', currentIndex);
// 更新歌曲为前一首
let item = this.musicList[this.currentIndex];
this.$store.commit('setSingleMusic', item);
this.get(item);
this.microphonePlay = true;
// this.togglePlay = true;
this.$store.commit('setTogglePlay', TRUE);
this.coverPause = false;
this.coverAfresh = !this.coverAfresh;
},
playNext() {
// 更新当前歌曲位置
let currentIndex = (this.currentIndex + 1) % this.musicList.length;
this.$store.commit('setCurrentIndex', currentIndex);
// 更新歌曲为下一首
let item = this.musicList[this.currentIndex];
this.$store.commit('setSingleMusic', item);
this.get(item);
this.microphonePlay = true;
this.$store.commit('setTogglePlay', TRUE);
this.coverPause = false;
this.coverAfresh = !this.coverAfresh;
},
getTimes() {
let time = Math.round(this.$refs.audio.currentTime);
let munite = this.addZero(Math.floor(time / 60));
let second = this.addZero(time % 60);
this.currentTime = munite + ':' + second;
},
controlBar(event) {
let left = this.$refs.totalBar.offsetLeft;
let current = this.$refs.totalBar;
let totalWidth = this.$refs.totalBar.clientWidth;
let barX = event.clientX;
let totalTime = Math.round((this.music.size * 8) / this.music.br);
while (current.offsetParent !== null) {
current = current.offsetParent;
left += current.offsetLeft;
}
let scale = ((barX - left) / totalWidth).toFixed(2);
let time = Math.round(scale * totalTime);
let munite = this.addZero(Math.floor(time / 60));
let second = this.addZero(time % 60);
this.$refs.audio.currentTime = time;
this.currentTime = munite + ':' + second;
},
addZero(value) {
if (!value) {
return '00';
}
return value > 9 ? value : '0' + value;
}
}
};
</script>
<style lang="scss" type="text/css">
……
</style>
MusicAPI
音乐资源所使用的主要api接口。
const _baseUrl = 'http://musicapi.duapp.com/api.php';
const _baseUrl2 = 'https://api.imjad.cn/cloudmusic/';
export default {
getPlayListByWhere (cat, order, offset, total, limit) {
return _baseUrl + '?type=topPlayList&cat=' + cat + '&offset=' + offset + '&limit=' + limit;
},
getLrc (id) {
return _baseUrl2 + '?type=lyric&id=' + id;
},
getSong (id) {
return _baseUrl + '?type=url&id=' + id;
},
getPlayListDetail (id) {
return _baseUrl2 + '?type=playlist&id=' + id;
},
getMv (id) {
return _baseUrl2 + '?type=mv&id=' + id;
},
search (words) {
return _baseUrl2 + '?type=search&s=' + words;
}
};
项目打包
npm run build
5. 项目部分功能截图介绍
感谢源码时代教学讲师提供此文章!
本文为原创文章,转载请注明出处!
|
赞 1