這是我第一次開發小程序,開發的產品是音頻類的,在大佬的建議下采用了mpvue
,一週時間把功能都作出來,因爲不太熟悉mpvue和微信小程序,足足用了一週時間來改bug纔出來一個能用的版本,在這裏整理分享下我開發時遇到的一些問題和給出一些建議。
javascript
Linux
上開發小程序在公司電腦裝了雙系統,平常用的是Ubuntu
系統,Linux或Mac的開發環境對前端相對來講會友好一些。微信小程序官方的開發者工具只有Windows
和Mac
版本,因此這就尷尬了。css
不過還好,發現已經有大神在GitHub上作了Linux的支持,推薦給你們:Linux微信web開發者工具。
根據教程安裝使用便可,使用時就用./bin/wxdt
命令打開。不過用了幾天後面以爲不太方便,就索性切回Windows系統用官方最新的版本了。前端
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
小程序已經支持了npm安裝,但不太會弄。仍是按網上方法,將項目clone下來放進static目錄下。github
git clone https://github.com/youzan/vant-weapp.git
而後將vant-weapp
的dist
目錄拷貝到項目的static目錄下(儘量精簡,刪掉一些奇奇怪怪的如.github
的東西,因此直接使用dist目錄),更名爲vant
(也能夠不更名)。全局使用時,能夠在app.json
引入:web
"usingComponents": { "van-button": "/static/vant/button/index", "van-field": "/static/vant/field/index" },
注意:須要打開微信開發者工具中的ES6轉ES5功能
一開始覺得使用起來和web端的沒啥差異,但沒想到那麼麻煩。好比:在vue中是可使用v-model
的,但在mpvue中的小程序中不能使用,只能vuex
<van-field :value="password" type="password" @change="pwdChange" input-class="myClass" />
並且不能隨意靈活添加class修改組件的樣式,須要vant組件支持提供外部樣式纔可修改,好比上面的van-field
是經過input-class
來添加樣式控制的,很不方便。並且某些內部樣式因爲沒有外部樣式表,根本改不了。
綜上: 在微信小程序使用第三方組件庫不太方便,樣式修改比較麻煩,若是產品是有UI設計時,儘可能不使用,有時候本身實現樣式可能更快,並且項目體積更小。
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會將div
編譯爲小程序中的view
。一開始我不瞭解,覺得用了mpvue後就不能使用小程序原生支持的組件了,好比swiper
,scroll-view
等,小程序是支持的,能夠放心使用哈哈。
原本在開發環境正常的,而後準備發版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.vue
和pages/page2/index.vue
。
因此我將這兩個頁面當作子組件來引入了:import Page1 from '@/pages/page1'
,一開始沒有問題,等重啓項目,或者build後就發現樣式丟失了。
這多是mpvue打包機制的一個限制,即頁面不能將另外一個頁面當子組件來引用
,不然會致使樣式丟失。
項目中但願用戶退出小程序後依然能播放音頻,因此用到了背景音頻的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'
title
和src
賦值後會直接播放音頻,後面的幾個屬性建議也填上,由於播放背景音頻時微信是有個界面須要封面圖和歌手名稱等的。
若是想要獲取當前正在播放的音頻src,原本覺得經過this.audio.src
來獲取就能夠了可是有bug。
在開發者工具中是能夠正常獲取的,即開發時是沒問題的,但在真機上返回的是undefined
,所以不能用this.audio.src
來獲取當前播放的音頻url,得用一個變量來存這個數據。
currentTime用於顯示當前的播放進度,但我用在子組件中時常常更新不及時,打印是正常的,但試圖渲染不及時,有時候須要點擊一下才能從新渲染,這多是mpvue使用時纔會遇到。
因此建議仍是項目自身維護一套背景音頻的變量比較好一點,好比放在vuex
中。監聽BackgroundAudioManager.onTimeUpdate()
方法每次賦值到自身維護的變量中。
一開始我監聽在onCanplay
方法,將音頻的時長信息duration
賦值到vuex中存起來,但發現onCanplay
有時候是不會觸發的,好比從新賦值src播放下一首時,很尷尬。
因此不要太依賴onCanplay這個方法,還好目前直接使用audio.duration
好像不會出現像上面的currentTime
渲染不及時的問題,因此就這樣用着先。
正常來講,音頻播放結束後,音頻的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> <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中是不支持的,會被編譯成一個空字符串。
好比高斯模糊
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>
@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好,代碼量也不多。
爲了動畫更流暢,想盡辦法作優化,雖然不知道有沒效果,反正用了再說[手動滑稽]。
能夠用will-change和transform: translate3d(0,0,0)開啓硬件加速。我也不太會用,具體用法你們自行百度Google。
will-change: auto; transform: translate3d(0, 0, 0);
因爲小程序中不能設置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佈局自適應處理。
Page -> 父組件 -> 子組件
,在子組件click後$emit
一個事件出來,發現沒法觸發。
這個bug一開始沒有出現,但偶然npm run build
出現的,而後排查緣由,後面即便回滾全部版本再npm start也還會出現。好像不觸發則已,一發就不可收拾,這又是一個大坑,搜issue和加羣問人,當晚下班回家研究到1點多都沒有解決。
次日繼續研究,感受多是框架的緣由,最後嘗試升級一下mpvue版本,沒想到就正常了。直接使用quick-strat項目的mpvue
版本是 2.0.0,mpvue
和mpvue-template-compiler
升級到最新2.0.6
就解決了。
過後查看mpvue版本記錄,果真是框架自己緣由,而且找到了issue。
解決: 沒找到緣由,多是引入vant致使的,打包時丟失了部分文件。多build幾回,或者重啓下小程序開發者工具就正常了。
mpvue文檔中建議儘可能不要使用小程序的生命週期,這個應該是爲了讓項目更好地適應支付寶小程序和頭條小程序等,因此才這樣建議你們儘可能不要使用某一個小程序自身的api。
若是大家的小程序只是微信小程序(不考慮兼容其餘平臺小程序),我建議直接用小程序的生命週期,而不要用mpvue的生命週期,坑太多了。好比mpvue的created週期,初始化時全部的page都會執行,因此created這個週期是不能用了。
小程序中與日常web開發不一樣的是,它的頁面會被緩存。舉個例子:
page1
跳轉到page2
,再從page2
返回page1
,此時的page1
還沒銷燬,不會觸發onLoad
再從新渲染,而是直接使用以前的數據。從性能上來講,單純的返回不該該再請求api獲取數據從新渲染,這是對的,符合咱們的預期。page2
返回page1
時,咱們但願page1
是從新獲取數據渲染的。好比在page2
作了一個退出登陸的操做,此時再返回page1
時,仍是會看到以前的數據。實際上咱們的預期是:因爲已經退出登陸了,page1
的數據應該被銷燬了。在日常的web開發中,遇到上面的問題,咱們多是無論緩存,每次返回page1
都再次請求api渲染最新的數據,犧牲掉部分性能從而保證邏輯的正確性。
在mpvue中我也嘗試這樣幹了:想在page1
的onUnload()
生命週期中銷燬數據,可是沒有成功。即便在page2
退出登陸時,採用wx.reLaunch()
從新刷一遍,page1
的onUnload()
生命週期也沒有執行。因此onUnload()
是有可能不執行的,建議慎用。
最後仍是得想辦法作到在page2
控制page1
的數據銷燬或保留。想到這裏,vuex
就不自覺浮如今眼前了,若是page1的數據是經過vuex來控制的,那麼我在page2就能夠用vuex來靈活管理其餘頁面的數據了。
若是page2作退出登陸操做時,就讓page1的數據銷燬,若是是不退出登陸正常返回,page1的數據仍是正常,作到靈活控制。
我的平時web開發不多用vuex
,由於項目比較簡單不用那麼複雜的全局數據傳遞。但在小程序中,建議全局使用vuex
來控制全部數據(固然是得根據需求來用)。
第一次開發小程序就直接上了mpvue,可能有些坑已經不少同窗總結過了,有些坑多是不熟悉而致使的,但本身沒有去踩過一遍可能不夠深入。
有兩種坑會比較難啃:
遇到真機和開發者工具不一致的狀況,可按如下步驟排查:
wx.canIUse
打印一下this.audio.src
,能夠在真機打印調試一下而遇到mpvue框架的問題能夠:
mpvue
的issue看有沒相關解決辦法但願對你們有所幫助。