没朋友(mpvue),还有音乐陪你

炎炎夏日,剧烈的高温也抵挡不住对学习的坚持。对于学习,很多时候都会有松懈和逃避的心理,但看到身边近乎疯狂的考研党们,我还是选择了坚持。距离上次发文章已经有一个月之久了,期间学习了很多知识,越学习越发觉得自己的路还有很长,但既然选择了远方,便只顾风雨兼程了。

mpvue仿网易云音乐小程序


关于mpvue

mpvue 是一个使用 Vue.js 开发小程序的前端框架。框架基于 Vue.js 核心,mpvue 修改了 Vue.js 的 runtime 和 compiler 实现,使其可以运行在小程序环境中,从而为小程序开发引入了整套 Vue.js 开发体验。

项目使用的技术栈

  • 数据请求:Fly.js 一个基于Promise的、强大的、支持多种JavaScript运行时的http请求库. 有了它,您可以使用一份http请求代码在浏览器、微信小程序、Weex、Node中都能正常运行。同时可以方便配合 Vue家族的框架,最大可能的实现 Write Once Run Everywhere。
  • 模拟数据:easy-mock 可以快速生成模拟数据的持久化服务
  • css预编译器:stylus-基于Node.js的CSS的预处理框架
  • vuex:集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
  • 框架:mpvue

关于模拟数据这里提一个网易云音乐api,真的很强但由于小程序不支持本地域名的request请求,需要部署到服务器上变为小程序合法域名,由于精力有限这里只是截取了部分数据进行模拟。有兴趣的小伙伴可以自己玩玩啊。

上图



分享(采坑)记

写这个项目的时候,碰到了很多问题,BUG更是不少,在mpvue与原生小程序中疯狂纠结,由于时间精力有限,目前只完成了三个页面,重点把核心播放功能完成了大半。由于网上关于mpvue的资源很少,遇到问题都是在疯狂百度,尝试等等方法也算是解决了部分。这里分享我遇到的一些困难,希望能帮助到有需要的人。

1.关于数据请求封装

  • 安装flyio

npm install flyio

  • 在util下新建flyio.js
import Vue from 'vue'
var Fly = require('flyio/dist/npm/wx.js') //wx.js为flyio的微信小程序入口文件
var fly = new Fly();

fly.interceptors.request.use((config,promise)=>{
    config.headers["X-Tag"]="flyio";  //给所有请求添加自定义header
    return config;
})
//配置请求基地址
fly.config.baseURL="https://www.easy-mock.com/mock/5b372361808a747e8d04a1e3/"
Vue.prototype.$http=fly //将fly挂载在vue上供全局使用
export default fly
复制代码
  • 可以在根目录main.js目录下封装一个方法用到请求数据的页面直接调用这个方法即可。提高代码复用率,这里由于使用了vuex来管理数据即更加的方便操作使用数据。

2.vuex管理数据

  • vuex的准备 npm install vuex
  • vue-cli + vuex 在一般的vue-cli + vuex项目中,主函数 main.js 中会将 store 对象提供给 “store” 选项,这样可以把 store 对象的实例注入所有的子组件中,从而在子组件中可以用this.$store.state.xxx,this.$store.dispatch 等来访问或操纵数据仓库中的数据
new Vue({
el: '#app',
store,
router,
template: '<App/>',
components: { App }
})
复制代码
  • mpvue + vuex

注意了,在mpvue + vuex项目中,很遗憾不能通过上面那种方式来将store对象实例注入到每个子组件中(至少我尝试N种配置不行),也就是说,在子组件中不能使用this.$store.xxx,从而导致辅助函数不能正确使用。这个时候我们就需要换个思路去实现,要在每个子组件中能够访问this.$store才行。既然我们需要在子组件中用this.$store 访问store实例,那我们直接在vue的原型上添加$store属性指向store对象不就行啦,于是解决方案如下:

Vue.prototype.$store = store

src下新建一个store文件夹,目录结构如下

action.js  //提交mutation以达到委婉地修改state状态,可异步操作
getters.js  //获取store内的状态 
index.js //引入vuex,设置state状态数据,引入getter、mutation和action
mutation-type.js  //将方法与方法名分开便于查看
mutations.js //更改store中状态用的函数的存储之地
state.js //存放state
复制代码

这里展示下index.js内容

import Vue from 'vue'
import Vuex from 'vuex'
import * as actions from './actions'
import * as getters from './getters'
import state from './state'
import mutations from './mutations'

Vue.use(Vuex)

export default new Vuex.Store({
  actions,
  getters,
  state,
  mutations
})
复制代码
  • vuex的使用 在fly请求数据后我们可以将其数据存储到state里 方便其他页面或组件使用、修改,在本项目中请求数据可以数据存储下来,后续其它页面需要此数据时可以不用再次请求直接引入调用即可,大大减少了数据请求次数
 methods: {
    ...mapMutations({ //es6 展开运算符
      saveDetailState: 'SAVE_DETAIL_STATE',
      saveMidimg2: 'SAVE_MIDIMG2',
      saveMidimg3: 'SAVE_MIDIMG3'
    }),
    fly
      .get('music#!method=get')
      .then(res => {
          this.menu = res.data.data.menu;
          this.midimg = res.data.data.midimg;
          this.saveMidimg2(res.data.data.midimg2);  //子组件标题数据
          this.saveDetailState(res.data.data.footimg);//推荐歌单信息
          this.saveMidimg3(res.data.data.midimg3);//
          this.footimg2 = res.data.data.footimg2;
          this.footimg3 = res.data.data.footimg3;
      })
复制代码

3.小程序css前缀问题

由于本人开发项目使用的css预编译器stylus 他会自动补全前缀,而小程序又并不支持-moz- -webkit-等前缀那怎么办呢? 解决方案如下:

<style lang="stylus">
...
</style>
 <style> //不在stylus编译下
  @keyframes rotate {
  0%{
    transform: rotate(0);}
  100%  {
       transform: rotate(360deg);
  }
    }
 </style> 
复制代码

4.创建创建并返回内部 audio 上下文对象

  • api
    这里我使用的是wx.createInnerAudioContext()api 是wx.createAudioContext 升级版。 由于初次使用audio并不是很熟悉,碰到了一个坑。播放音乐后返回歌单播放另一首歌曲,上一首歌曲不会被替换,两首歌竟同时播放,创建audio对象后要结束这个实例需要进行销毁,注意这里我们需要将这个audio对象进行存储不然再次跳转播放页面找不到销毁的对象
mounted(){
        wx.showLoading({
            title: '加载中'
        })
        this.midshow = true
        let options = this.$root.$mp.query;
        const songid = options.songid;
        const id = options.id;
        if(this.audioCtx != null) { //第一次不用销毁
            if(this.song.id != songid){
                this.audioCtx.destroy()} //销毁实例
        }
        if(this.song.id != songid || this.song.id == '') //同一首歌不用新建
        {   this.audioCtx = wx.createInnerAudioContext() //新建实例
            }
        if(options.name == 'undefined' && this.song.id != songid){ // 同一首歌不用重新更新数据
        this.getSong(songid,id) //获取当前音乐信息
        const afterlyric = this.normalizeLyric(this.song.lyric)
        this.currentLyric = new Lyric(afterlyric)
        ...
复制代码
  • 小问题
    这里还碰到一个并不是很理解的地方,就是传入歌曲链接src 后获取总时长并没有获取 猜测是引入src获取歌曲信息需要时间? 这里用了很粗暴的方法解决了,应该是使用promise进行异步处理?期待大家能给个指导意见
 this.audioCtx.src = this.song.mp3url //设置src
 this.allmiao = this.audioCtx.duration //读取歌曲总时长
 
 const a = setInterval(()=>{   
    this.allmiao = this.audioCtx.duration
    },50)
    if(this.allmiao){
    clearInterval('a')
}
复制代码

5.音乐播放条

  • 小点和线条分别通过translate3d,和wdith进行向右的增加等,
copmputed: {
 width () {
            return 'width:'+(this.nowmiao/this.allmiao)*550+'rpx' //盒子宽度总为550rpx
        },
midwidth () {
            return 'transform:translate3d('+(this.nowmiao/this.allmiao)*529+'rpx,0px,0px);'
        }}
复制代码
  • 点击、滑动进度条事件
clClick (e) { //点击进度条事件
            const rect = wx.getSystemInfoSync().windowWidth //屏幕总宽
            this.offsetWidth = e.pageX - (rect-275)/2 //点击进度条上距离左侧宽度
            this.nowmiao = this.allmiao*(this.offsetWidth/275) //根据比例计算点击位置对应的播放时间
            const miao = Math.floor(this.nowmiao)
            this.audioCtx.seek(miao) // 跳转播放
        },
        // 滑动进度条 分为start、move、end事件 这个大家应该不陌生,思路都是根据距离计算比例 跳转时间点播放
         clstart (e) {
            this.touch.initiated = true
            const rect = wx.getSystemInfoSync().windowWidth
            this.touch.setWidth = (rect-275)/2
            this.touch.startX = e.touches[0].pageX 
            this.touch.time = this.nowmiao
        },
        clmove (e) { //这里要注意进度条临界点 滑动距离超出的问题
            if(!this.touch.initiated) return;
            const movex = e.touches[0].pageX - this.touch.startX
            if(e.touches[0].pageX>=(this.touch.setWidth+275)) {this.nowmiao =this.allmiao }
            else if(e.touches[0].pageX<=this.touch.setWidth) {this.nowmiao = 0;}
            else {this.nowmiao = this.touch.time+this.allmiao*movex/275}
        },
        clend (e) {
            this.touch.initiated = false
            const miao = Math.floor(this.nowmiao)
            this.audioCtx.seek(miao)
        },
复制代码

6.歌词滚屏

这里使用了黄轶老师写的一个 lyric-parser使用他对歌词数据进行处理等,通过sroll-view-into进行跟随播放进度的滚屏

  <scroll-view class="lyric-wrapper" :scroll-into-view="'line'+toLineNum" scroll-y scroll-with-animation>
    <view v-if="currentLyric">
        <view  :id="'line'+i" class="text" :class="[currentLineNum == i ? 'current': '' ]" v-for="(item,i) of currentLyric.lines" :key="i">
            {{item.txt}}
        </view>
    </view>
    <view v-if="!currentLyric">
        <view class="text current">暂无歌词</view>
    </view>
</scroll-view>
复制代码

将歌词进行处理后v-for输出 通过id、class分别进行滚屏与当前播放歌词行的高亮

normalizeLyric: function (lyric) {//将歌词数据进行拆分
    return lyric.replace(/&#58;/g, ':').replace(/&#10;/g, '\n').replace(/&#46;/g, '.').replace(/&#32;/g, ' ').replace(/&#45;/g, '-').replace(/&#40;/g, '(').replace(/&#41;/g, ')')
},
const afterlyric = this.normalizeLyric(this.song.lyric) 
this.currentLyric = new Lyric(afterlyric) //创建Lyric实例
this.audioCtx.onTimeUpdate(()=>{ //音频播放进度更新事件 
            this.nowmiao = this.audioCtx.currentTime //当前播放时间
            if (this.currentLyric) {
            this.handleLyric(this.nowmiao * 1000) //歌词行处理函数
        }
        })
 handleLyric (currentTime) { //控制歌词滚屏 随播放进度不断触发
            let lines = [{time: 0, txt: ''}], lyric = this.currentLyric, lineNum
            lines = lines.concat(lyric.lines) //进行歌词对应时间、内容
            //判断当前播放时间位置 进行歌词行的调整 使其当前歌词部分处于中间,若开头则从头部往下,尾部反之
            for (let i = 0; i < lines.length; i++) {
                if (i < lines.length - 1) {
                    let time1 = lines[i].time, time2 = lines[i + 1].time
                if (currentTime > time1 && currentTime < time2) { 
                    lineNum = i - 1
                    break;
                    }
                } else {
                lineNum = lines.length - 2
                }
            }
            this.currentLineNum = lineNum,
            this.currentText = lines[lineNum + 1] && lines[lineNum + 1].txt
            let toLineNum = lineNum - 5
            if (lineNum > 5 && toLineNum != this.toLineNum) {
                this.toLineNum = toLineNum
            }
        },        
复制代码

7.组件复用

在项目中有很多部分是非常相似的,将这部分进行组件封装通过v-if、class、style等进行添加修改组件部分,组件封装要易于维护,高性能,低耦合。这里展示下首页中部组件

<scroll-view scroll-y="true" enable-back-to-top="true" class="mid-top">
            <swiper-t :menu="menu"></swiper-t>
            <mid-t :midimg="midimg"></mid-t>
            <foot-t :footimg="footimg" :footname="footname1"></foot-t>
            <foot-t :footimg="footimg2" :footname="footname2"></foot-t>
            <foot-t :footimg="footimg3" :footname="footname3"></foot-t>
</scroll-view>
复制代码

写在最后

虽然只是写了三个页面,但也确实让我体会到了mpvue框架好用的地方,对于那些不太熟悉小程序对vue比较熟悉的人mpvue是一个好用的框架了,但还有很多地方需要完善。文章写到这里其实还有东西可写,比如切歌 播放模式等等,这里就不一一阐述了。写文章,志在分享,志在认识更多志同道合的盆友,也算是自己的一个总结梳理吧。这里附上我的项目地址,有兴趣的朋友可以看看玩玩,帮忙点个star~作为一个即将出去闯荡的大三学生,时间是真的很宝贵,对待学习也不敢懈怠,如果你有什么好的建议或者问题欢迎加我qq:1404827307,写文章不易,且赞且珍惜。前端路漫漫,稳步向前行!

猜你喜欢

转载自juejin.im/post/5b545cbfe51d45198651503c