mpvue开发音频类小程序踩坑和建议

前言

这是我第一次开发小程序,开发的产品是音频类的,在大佬的建议下采用了mpvue,一周时间把功能都作出来,因为不太熟悉mpvue和微信小程序,足足用了一周时间来改bug才出来一个能用的版本,在这里整理分享下我开发时遇到的一些问题和给出一些建议。 javascript

Linux上开发小程序

在公司电脑装了双系统,平常用的是Ubuntu系统,Linux或Mac的开发环境对前端相对来讲会友好一些。微信小程序官方的开发者工具只有WindowsMac版本,因此这就尴尬了。css

不过还好,发现已经有大神在GitHub上作了Linux的支持,推荐给你们:Linux微信web开发者工具。 根据教程安装使用便可,使用时就用./bin/wxdt命令打开。不过用了几天后面以为不太方便,就索性切回Windows系统用官方最新的版本了。前端

封装wx.request为Promise

wx.request用于发起http请求,但平时习惯了Promise的写法,因此仍是封装一下这个方法为Promise的形式。 我看不少小程序会使用fly这个库。vue

但我的以为发起请求不须要那么强大的功能,小程序自己就应该是一个轻量级的东西,引入一个库可能会致使项目打包变大,可能让小程序更卡,因此本着能本身写就本身写吧的心态,索性本身封装一下算了。java

src/utils,新建一个request.js:css3

const apiUrl = 'https://your server.com/api/'
const request = (apiName, reqData, isShowLoading = true) => {
  // 某些请求可能不须要显示loading
  if (isShowLoading) {
    wx.showLoading({
      title: '正在努力加载中',
      mask: true
    })
  }

  return new Promise(function (resolve, reject) {
    wx.request({
      url: apiUrl + apiName,
      method: 'POST',
      data: reqData,
      header: {
        'content-type': 'application/json' // 默认值
      },
      success (res) {
        if (res.data.code === 0) {
          // 与后端约定code=0时才是正常的
          resolve(res)
        } else {
          reject(res)
        }
      },
      fail (err) {
        reject(err)
      },
      complete (res) {
        wx.hideLoading()
      }
    })
  })
}

export default request
复制代码

固然这是个简化版的,我实际项目中还会在初始化时加入一些token之类的参数,你们能看明白是这样封装成Promise的就能够啦。git

使用vant-weapp

小程序已经支持了npm安装,但不太会弄。仍是按网上方法,将项目clone下来放进static目录下。github

git clone https://github.com/youzan/vant-weapp.git
复制代码

而后将vant-weappdist目录拷贝到项目的static目录下(尽量精简,删掉一些奇奇怪怪的如.github的东西,因此直接使用dist目录),更名为vant(也能够不更名)。全局使用时,能够在app.json引入:web

"usingComponents": {
    "van-button": "/static/vant/button/index",
    "van-field": "/static/vant/field/index"
  },
复制代码

注意:须要打开微信开发者工具中的ES6转ES5功能vuex

一开始觉得使用起来和web端的没啥差异,但没想到那么麻烦。好比:在vue中是可使用v-model的,但在mpvue中的小程序中不能使用,只能

<van-field :value="password" type="password" @change="pwdChange" input-class="myClass" />
复制代码

并且不能随意灵活添加class修改组件的样式,须要vant组件支持提供外部样式才可修改,好比上面的van-field是经过input-class来添加样式控制的,很不方便。并且某些内部样式因为没有外部样式表,根本改不了。

综上: 在微信小程序使用第三方组件库不太方便,样式修改比较麻烦,若是产品是有UI设计时,尽可能不使用,有时候本身实现样式可能更快,并且项目体积更小。

使用vuex

mpvue官方的快速模板中是将vuex放在counter 这个page目录下,可能习惯了vue官方写法的不少同窗(包括我)不太喜欢,因此最好就改成vuex官方的写法。

在src目录下建一个store的文件夹,分别建如下文件:

项目不太复杂时不建议使用modules,使用起来比较麻烦。

贴一下index.js的代码,其余的actions.js,getters.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'
import createLogger from 'vuex/dist/logger'

Vue.use(Vuex)

const debug = process.env.NODE_ENV !== 'production'

export default new Vuex.Store({
  actions,
  getters,
  state,
  mutations,
  strict: debug,
  plugins: debug ? [createLogger()] : []
})

复制代码

vuex/dist/logger是vuex在开发环境能够自动打印日志的工具,debug比较方便,建议使用。 而后在src/main.js引入:

import Vue from 'vue'
import App from './App'
import store from '@/store'

Vue.config.productionTip = false
App.mpType = 'app'

Vue.prototype.$store = store

const app = new Vue({
  store
})
app.$mount()
复制代码

这样就能够在项目中正常使用啦,彻底支持mapState,mapActions,mapGetters的写法,好比在pages/index/index.vue中使用:

<script>
import { mapState, mapActions } from 'vuex'
export default {
  computed: {
    ...mapState(['myAudio'])
  },
  methods: {
    ...mapActions(['myActions'])
  },
  created () {
    this.myActions() //调用vuex中的方法
  }
}
</script>
复制代码

踩坑指南

其实大多数坑多是mpvue的,不少状况也是本身不熟悉小程序生命周期致使的一些奇奇怪怪的bug。

mpvue是支持小程序原生组件的

mpvue会将div编译为小程序中的view。一开始我不了解,觉得用了mpvue后就不能使用小程序原生支持的组件了,好比swiper,scroll-view等,小程序是支持的,能够放心使用哈哈。

npm run build后样式丢失

原本在开发环境正常的,而后准备发版npm run build后发现样式丢失了。而后从新npm start排查问题,样式仍是丢失的。心里此时是mmp的:npm run build丢失就算了,我没改什么东西从新npm start后为何仍是丢失,以前仍是正常的呀?

刚开始怀疑是缓存什么的问题,删掉的dist目录,重启开发者工具,甚至重启电脑都试了一下,这是我遇到的超级诡异的问题之一。

冷静下来想到:以前的版本是正常的,必定是新版本引入了什么致使了打包样式的丢失。因而回滚版本一个个build排查问题,最后找到了缘由:在一个page中引入了其余page,即在页面中import另外一个页面。

在我这里的具体例子是:我在pages/index/index.vue 中想作底部共用一个tabbar,页面根据tabbar的值来显示对应的子级页面:pages/page1/index.vuepages/page2/index.vue

因此我将这两个页面当作子组件来引入了:import Page1 from '@/pages/page1',一开始没有问题,等重启项目,或者build后就发现样式丢失了。

这多是mpvue打包机制的一个限制,即页面不能将另外一个页面当子组件来引用,不然会致使样式丢失。

背景音频的src没法读取

项目中但愿用户退出小程序后依然能播放音频,因此用到了背景音频的api: wx.getBackgroundAudioManager()。

this.audio = wx.getBackgroundAudioManager()
this.audio.src = 'http://ws.stream.qqmusic.qq.com/M500001VfvsJ21xFqb.mp3?guid=ffffffff82def4af4b12b3cd9337d5e7&uin=346897220&vkey=6292F51E1E384E061FF02C31F716658E5C81F5594D561F2E88B854E81CAAB7806D5E4F103E55D33C16F3FAC506D1AB172DE8600B37E43FAD&fromtag=46' 
this.audio.title = '此时此刻' //注意必填
this.audio.epname = '此时此刻'
this.audio.singer = '许巍'
this.audio.coverImgUrl = 'http://y.gtimg.cn/music/photo_new/T002R300x300M000003rsKF44GyaSk.jpg?max_age=2592000'
复制代码

titlesrc赋值后会直接播放音频,后面的几个属性建议也填上,由于播放背景音频时微信是有个界面须要封面图和歌手名称等的。

若是想要获取当前正在播放的音频src,原本觉得经过this.audio.src来获取就能够了可是有bug。

在开发者工具中是能够正常获取的,即开发时是没问题的,但在真机上返回的是undefined,所以不能用this.audio.src来获取当前播放的音频url,得用一个变量来存这个数据。

直接使用音频的currentTime可能渲染不及时

currentTime用于显示当前的播放进度,但我用在子组件中时常常更新不及时,打印是正常的,但试图渲染不及时,有时候须要点击一下才能从新渲染,这多是mpvue使用时才会遇到。

因此建议仍是项目自身维护一套背景音频的变量比较好一点,好比放在vuex中。监听BackgroundAudioManager.onTimeUpdate()方法每次赋值到自身维护的变量中。

音频的onCanplay方法不必定每一个音频都会触发

一开始我监听在onCanplay方法,将音频的时长信息duration赋值到vuex中存起来,但发现onCanplay有时候是不会触发的,好比从新赋值src播放下一首时,很尴尬。

因此不要太依赖onCanplay这个方法,还好目前直接使用audio.duration好像不会出现像上面的currentTime渲染不及时的问题,因此就这样用着先。

音频播放结束,即onStop后,不能再经过audio.play()的方法从新播放,得从新赋值src

正常来讲,音频播放结束后,音频的src是不变的,再次play()应该是能够的。但在小程序中恰恰不行,得从新赋值src才能从新播放,这应该是小程序的一个bug。。。

因此须要判断一下暂停中止的状况,用不一样的办法播放。正常来讲,音频暂停时currentTime是不为0的,而结束时currentTime会为0。

因此能够经过currentTime(最好是本身维护的变量)来判断暂停和中止的状况:若是currentTime不为0,表示是暂停的状况,能够用play(),若是小于等于0,则从新赋值src播放

if (currentTime) {
  this.audio.play()
} else {
  this.audio.src = 'xx.mp3'
}

复制代码

mpvue不支持直接在template上直接绑定函数

这个是mpvue文档上有写的,不过一开始并非很理解,也踩坑了,因此在这里提一下,避免不知道的同窗踩坑找半天。

<template>
  <div v-for="(item, index) in list" :key="index">{{ formatItem(item) }}</div>
</template>
 
<script>
export default {
  data () {
    return{
      list: [1, 2, 3]
    }
  },
  methods: {
    formatItem (item) {
      return `我是${item}`
    }
  }
}
</script>
复制代码

上面的代码应该是平常vue中比较经常使用的,就是将数据传参给方法作一些处理,这个在mpvue中是不支持的,会被编译成一个空字符串。

小程序中可放心使用css3的一些特性

好比高斯模糊

filter: blur(50px);
复制代码

若是要使用动画,尽可能用css动画代替wx.createAnimation

在实际使用时,wx.createAnimation作动画其实很卡,性能不好,因此在须要使用动画时,建议尽可能使用css作动画。

在小程序中是支持css动画的,transition,animation,@keyframes这些特性都支持。

好比作一个div一直旋转的动画,你们能够对比一下两个版本:

  • wx.createAnimation版本

原理:经过setInterval()不断更新div的旋转位置

<template>
  <div class="cover" :animation="animationData"></div>
</template>

<script>
export default {
  data () {
    return {
      animationData: '',
      animation: '',
      rotateCount: 0,
      timer: ''
    }
  },
  components: {

  },
  methods: {
     startRotate () {
       this.timer = setInterval(() => {
         this.rotateAni(++this.rotateCount)
       }, 100)
     },
     rotateAni (n) {
       if (!this.animation) {
         return
       }
       // 每100毫秒旋转10度
       this.animation.rotate(10 * n).step()
       this.animationData = this.animation.export()
     }
  },
  onShow () {
     // 页面从隐藏到显示时才执行
     if (!this.animation) {
       this.animation = wx.createAnimation()
       this.startRotate()
     }
  },
  onReady () {
     // 第一次初始化时会执行
     if (!this.animation) {
       this.animation = wx.createAnimation()
       this.starRotate()
     }
  },
  onHide () {
    // 页面隐藏时会执行,避免频繁的setData操做,将定时器停掉
    this.timer && clearInterval(this.timer)
  },
  beforeDestroy () {
    // 页面卸载,也停掉定时器
    this.timer && clearInterval(this.timer)
  }
}
</script>

<style scoped lang="scss">
  .cover {
    left: 20px;
    bottom: 70px;
    border-radius: 50%;
    background: #fff;
    position: absolute;
    width: 50px;
    height: 50px;
    background: rgba(0, 0, 0, 0.2);
    box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.2);
    border: 1px solid rgba(255, 255, 255, 0.5);
    overflow: hidden;
    z-index: 10000;
  }
</style>
复制代码
  • 使用css的@keyframes作旋转动画
<template>
  <div class="cover" :style="coverStyle"></div>
</template>

<script>
export default {
}
</script>

<style scoped lang="scss">
  // 定义一个动画名为 rotate
  @keyframes rotate {
    0%,
    100% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(360deg);
    }
  }
  .cover {
    left: 20px;
    bottom: 70px;
    border-radius: 50%;
    background: #fff;
    position: absolute;
    width: 50px;
    height: 50px;
    background: rgba(0, 0, 0, 0.2);
    box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.2);
    border: 1px solid rgba(255, 255, 255, 0.5);
    overflow: hidden;
    z-index: 10000;
    // 使用动画
    animation: rotate 4s linear infinite;
  }
</style>
复制代码

用js写的动画须要控制好setInterval的间隔时间和旋转角度,比较难调。而用css写动画很简单,性能比js好,代码量也不多。

使用css动画时建议开启硬件加速

为了动画更流畅,想尽办法作优化,虽然不知道有没效果,反正用了再说[手动滑稽]。

能够用will-changetransform: translate3d(0,0,0)开启硬件加速。我也不太会用,具体用法你们自行百度Google。

will-change: auto;
transform: translate3d(0, 0, 0);
复制代码

iPhoneX须要底部导航条预留34px(68rpx)的高度。

因为小程序中不能设置viewport-fit=cover,因此也就没有web中的安全区域说法,目前主流的作法是经过wx.getSystemInfoSync()判断是不是ipx,如果则给页面底部撑高34px。

const res = wx.getSystemInfoSync()
if (res.model.indexOf('iPhone X') >= 0) {
  this.isIpx = true
}
复制代码

注意是用res.model.indexOf('iPhone X'),在开发者工具的iPhone X中,model是全等于iPhone X的,但在真机中每每拿到的值是iPhone X GZxxx,即后面可能会带一串东西,因此用indexOf才是比较稳的,并且对iPhone XR等机型也适用。

因为还有其余安卓机的全面屏,不太可能一一判断,并且某些安卓全面屏是没有用iPhone底部的工具条的(不存在冲突的状况),因此咱们只判断iPhone X的状况就能够了,其余全面屏就不须要给底部预留了。

至于全面屏布局的适配,须要用flex布局或者获取屏幕宽高来慢慢调了,建议最好用flex布局自适应处理。

for循环中的子组件click事件没法触发

Page -> 父组件 -> 子组件,在子组件click后$emit一个事件出来,发现没法触发。

这个bug一开始没有出现,但偶然npm run build出现的,而后排查缘由,后面即便回滚全部版本再npm start也还会出现。好像不触发则已,一发就不可收拾,这又是一个大坑,搜issue和加群问人,当晚下班回家研究到1点多都没有解决。

次日继续研究,感受多是框架的缘由,最后尝试升级一下mpvue版本,没想到就正常了。直接使用quick-strat项目的mpvue版本是 2.0.0,mpvuempvue-template-compiler升级到最新2.0.6就解决了。

过后查看mpvue版本记录,果真是框架自己缘由,而且找到了issue

npm run build后代码报错,再build一次可能报另外一些错

解决: 没找到缘由,多是引入vant致使的,打包时丢失了部分文件。多build几回,或者重启下小程序开发者工具就正常了。

mpvue中created() 钩子会在页面初始化时所有一块儿触发,尽可能不要用

小程序生命周期的理解

  1. 进入已销毁的page组件时依次触发: onLoad,onShow,onReady,beforeMount,mounted
  2. 第一次进入已销毁的子组件时依次触发: onLoad,onReady,beforeMount,mounted
  3. 第二次进入已销毁的子组件时依次触发: onLoad,onShow,onReady
  4. 再次进入 未被销毁的page组件、子组件时只触发: onShow

mpvue文档中建议尽可能不要使用小程序的生命周期,这个应该是为了让项目更好地适应支付宝小程序和头条小程序等,因此才这样建议你们尽可能不要使用某一个小程序自身的api。

若是大家的小程序只是微信小程序(不考虑兼容其余平台小程序),我建议直接用小程序的生命周期,而不要用mpvue的生命周期,坑太多了。好比mpvue的created周期,初始化时全部的page都会执行,因此created这个周期是不能用了。

onUnload不触发

小程序中与日常web开发不一样的是,它的页面会被缓存。举个例子:

  1. page1跳转到page2,再从page2返回page1,此时的page1还没销毁,不会触发onLoad再从新渲染,而是直接使用以前的数据。从性能上来讲,单纯的返回不该该再请求api获取数据从新渲染,这是对的,符合咱们的预期。
  2. 而有时候,从page2返回page1时,咱们但愿page1是从新获取数据渲染的。好比在page2作了一个退出登陆的操做,此时再返回page1时,仍是会看到以前的数据。实际上咱们的预期是:因为已经退出登陆了,page1的数据应该被销毁了。

在日常的web开发中,遇到上面的问题,咱们多是无论缓存,每次返回page1都再次请求api渲染最新的数据,牺牲掉部分性能从而保证逻辑的正确性。

在mpvue中我也尝试这样干了:想在page1onUnload()生命周期中销毁数据,可是没有成功。即便在page2退出登陆时,采用wx.reLaunch()从新刷一遍,page1onUnload()生命周期也没有执行。因此onUnload()是有可能不执行的,建议慎用。

最后仍是得想办法作到page2控制page1的数据销毁或保留。想到这里,vuex就不自觉浮如今眼前了,若是page1的数据是经过vuex来控制的,那么我在page2就能够用vuex来灵活管理其余页面的数据了。

若是page2作退出登陆操做时,就让page1的数据销毁,若是是不退出登陆正常返回,page1的数据仍是正常,作到灵活控制。

我的平时web开发不多用vuex,由于项目比较简单不用那么复杂的全局数据传递。但在小程序中,建议全局使用vuex来控制全部数据(固然是得根据需求来用)。

总结

第一次开发小程序就直接上了mpvue,可能有些坑已经不少同窗总结过了,有些坑多是不熟悉而致使的,但本身没有去踩过一遍可能不够深入。

有两种坑会比较难啃:

  1. 框架自己的问题,如mpvue2.0.0出现的子组件没法触发事件的问题。
  2. 开发者工具和真机运行环境不一致致使的坑。

遇到真机和开发者工具不一致的状况,可按如下步骤排查:

  1. 有多是缓存,能够杀掉以前的版本再跑起来
  2. 手机微信版本过低,可能api不支持,用wx.canIUse打印一下
  3. 手机端某些属性不支持读取,好比上面的this.audio.src,能够在真机打印调试一下
  4. 代码在手机端运行有报错,能够在手机端开启调试,看一下log
  5. 微信设计上的坑,百度下是否有相关的案例和解决办法

而遇到mpvue框架的问题能够:

  1. 去搜一下mpvue的issue看有没相关解决办法
  2. 尽可能使用最新版本的框架,可能某些问题已经修复了的。实在解决不了的,建议想办法绕过,换一种方法来实现。

但愿对你们有所帮助。

相关文章
相关标签/搜索