mpvue開發音訊類小程式踩坑和建議

fengxianqi發表於2019-03-11

前言

這是我第一次開發小程式,開發的產品是音訊類的,在大佬的建議下采用了mpvue,一週時間把功能都做出來,由於不太熟悉mpvue和微信小程式,足足用了一週時間來改bug才出來一個能用的版本,在這裡整理分享下我開發時遇到的一些問題和給出一些建議。

mpvue開發音訊類小程式踩坑和建議

Linux上開發小程式

在公司電腦裝了雙系統,日常用的是Ubuntu系統,Linux或Mac的開發環境對前端相對來說會友好一些。微信小程式官方的開發者工具只有WindowsMac版本,所以這就尷尬了。

不過還好,發現已經有大神在GitHub上做了Linux的支援,推薦給大家:Linux微信web開發者工具。 根據教程安裝使用即可,使用時就用./bin/wxdt命令開啟。不過用了幾天後面覺得不太方便,就索性切回Windows系統用官方最新的版本了。

封裝wx.request為Promise

wx.request用於發起http請求,但平時習慣了Promise的寫法,所以還是封裝一下這個方法為Promise的形式。 我看很多小程式會使用fly這個庫。

但個人覺得發起請求不需要那麼強大的功能,小程式本身就應該是一個輕量級的東西,引入一個庫可能會導致專案打包變大,可能讓小程式更卡,所以本著能自己寫就自己寫吧的心態,索性自己封裝一下算了。

src/utils,新建一個request.js:

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的就可以啦。

使用vant-weapp

小程式已經支援了npm安裝,但不太會弄。還是按網上方法,將專案clone下來放進static目錄下。

git clone https://github.com/youzan/vant-weapp.git
複製程式碼

然後將vant-weappdist目錄拷貝到專案的static目錄下(儘可能精簡,刪掉一些奇奇怪怪的如.github的東西,所以直接使用dist目錄),改名為vant(也可以不改名)。全域性使用時,可以在app.json引入:

  "usingComponents": {
    "van-button": "/static/vant/button/index",
    "van-field": "/static/vant/field/index"
  },
複製程式碼

注意:需要開啟微信開發者工具中的ES6轉ES5功能

一開始以為使用起來和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的資料夾,分別建以下檔案:

mpvue開發音訊類小程式踩坑和建議
專案不太複雜時不建議使用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. 儘量使用最新版本的框架,可能某些問題已經修復了的。實在解決不了的,建議想辦法繞過,換一種方法來實現。

希望對大家有所幫助。

相關文章