028-86261949

当前位置:首页 > 技术交流 > Vue2.x+Axios+VueX+Vue-Router移动端音乐播放器开发案例

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:固定高度内滚动插件。
 
2. 音乐播放器的需求分析
需求思维导图如下:
需求说明:
个人中心
个人中心这一块比较传统,就是一些登录,注册,忘记密码,修改密码,修改个人资料。
音乐
音乐这一模块主要有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
 
结果如下:
 
4. 项目介绍和主要实现代码
用编译器打开我们创建的项目,在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. 项目部分功能截图介绍
 

图片6.png (113.58 KB, 下载次数: 0)

下载附件

32 秒前 上传

 
 
   感谢源码时代教学讲师提供此文章!
   本文为原创文章,转载请注明出处!
#标签:Vue2.x,移动端音乐播放器开发案例