前言
這是我第一次開發(fā)小程序产阱,開發(fā)的產(chǎn)品是音頻類的渠退,在大佬的建議下采用了mpvue
,一周時間把功能都做出來,由于不太熟悉mpvue和微信小程序设预,足足用了一周時間來改bug才出來一個能用的版本徙歼,在這里整理分享下我開發(fā)時遇到的一些問題和給出一些建議。
在Linux
上開發(fā)小程序
在公司電腦裝了雙系統(tǒng)鳖枕,日常用的是Ubuntu
系統(tǒng)魄梯,Linux或Mac的開發(fā)環(huán)境對前端相對來說會友好一些。微信小程序官方的開發(fā)者工具只有Windows
和Mac
版本宾符,所以這就尷尬了酿秸。
不過還好,發(fā)現(xiàn)已經(jīng)有大神在GitHub上做了Linux的支持魏烫,推薦給大家:Linux微信web開發(fā)者工具辣苏。
根據(jù)教程安裝使用即可,使用時就用./bin/wxdt
命令打開哄褒。不過用了幾天后面覺得不太方便稀蟋,就索性切回Windows系統(tǒng)用官方最新的版本了。
封裝wx.request為Promise
wx.request
用于發(fā)起http請求读处,但平時習(xí)慣了Promise的寫法糊治,所以還是封裝一下這個方法為Promise的形式。
我看很多小程序會使用fly這個庫罚舱。
但個人覺得發(fā)起請求不需要那么強大的功能井辜,小程序本身就應(yīng)該是一個輕量級的東西,引入一個庫可能會導(dǎo)致項目打包變大管闷,可能讓小程序更卡粥脚,所以本著能自己寫就自己寫吧的心態(tài),索性自己封裝一下算了包个。
在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' // 默認(rèn)值
},
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
當(dāng)然這是個簡化版的刷允,我實際項目中還會在初始化時加入一些token
之類的參數(shù),大家能看明白是這樣封裝成Promise的就可以啦碧囊。
使用vant-weapp
小程序已經(jīng)支持了npm安裝树灶,但不太會弄。還是按網(wǎng)上方法糯而,將項目clone下來放進(jìn)static目錄下天通。
git clone https://github.com/youzan/vant-weapp.git
然后將vant-weapp
的dist
目錄拷貝到項目的static目錄下(盡可能精簡,刪掉一些奇奇怪怪的如.github
的東西熄驼,所以直接使用dist目錄)像寒,改名為vant
(也可以不改名)烘豹。全局使用時,可以在app.json
引入:
"usingComponents": {
"van-button": "/static/vant/button/index",
"van-field": "/static/vant/field/index"
},
注意:需要打開微信開發(fā)者工具中的ES6轉(zhuǎn)ES5功能
一開始以為使用起來和web端的沒啥差別诺祸,但沒想到那么麻煩携悯。比如:在vue中是可以使用v-model
的,但在mpvue中的小程序中不能使用筷笨,只能
<van-field :value="password" type="password" @change="pwdChange" input-class="myClass" />
而且不能隨意靈活添加class修改組件的樣式憔鬼,需要vant組件支持提供外部樣式才可修改,比如上面的van-field
是通過input-class
來添加樣式控制的奥秆,很不方便逊彭。而且某些內(nèi)部樣式由于沒有外部樣式表咸灿,根本改不了构订。
綜上: 在微信小程序使用第三方組件庫不太方便,樣式修改比較麻煩避矢,如果產(chǎn)品是有UI設(shè)計時悼瘾,盡量不使用,有時候自己實現(xiàn)樣式可能更快审胸,而且項目體積更小亥宿。
使用vuex
mpvue官方的快速模板中是將vuex放在counter
這個page目錄下,可能習(xí)慣了vue官方寫法的很多同學(xué)(包括我)不太喜歡砂沛,所以最好就改為vuex官方的寫法烫扼。
在src目錄下建一個store
的文件夾,分別建以下文件:
項目不太復(fù)雜時不建議使用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在開發(fā)環(huán)境可以自動打印日志的工具堰氓,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() //調(diào)用vuex中的方法
}
}
</script>
踩坑指南
其實大多數(shù)坑可能是mpvue的得问,很多情況也是自己不熟悉小程序生命周期導(dǎo)致的一些奇奇怪怪的bug囤攀。
mpvue是支持小程序原生組件的
mpvue會將div
編譯為小程序中的view
。一開始我不了解宫纬,以為用了mpvue后就不能使用小程序原生支持的組件了焚挠,比如swiper
,scroll-view
等,小程序是支持的哪怔,可以放心使用哈哈宣蔚。
npm run build后樣式丟失
本來在開發(fā)環(huán)境正常的向抢,然后準(zhǔn)備發(fā)版npm run build
后發(fā)現(xiàn)樣式丟失了。然后重新npm start
排查問題胚委,樣式還是丟失的挟鸠。內(nèi)心此時是mmp的:npm run build丟失就算了,我沒改什么東西重新npm start后為什么還是丟失亩冬,之前還是正常的呀艘希?
剛開始懷疑是緩存什么的問題,刪掉的dist目錄硅急,重啟開發(fā)者工具覆享,甚至重啟電腦都試了一下,這是我遇到的超級詭異的問題之一营袜。
冷靜下來想到:之前的版本是正常的撒顿,一定是新版本引入了什么導(dǎo)致了打包樣式的丟失。于是回滾版本一個個build排查問題荚板,最后找到了原因:在一個page中引入了其他page凤壁,即在頁面中import另一個頁面。
在我這里的具體例子是:我在pages/index/index.vue
中想做底部共用一個tabbar跪另,頁面根據(jù)tabbar的值來顯示對應(yīng)的子級頁面:pages/page1/index.vue
和pages/page2/index.vue
拧抖。
所以我將這兩個頁面當(dāng)做子組件來引入了:import Page1 from '@/pages/page1'
,一開始沒有問題免绿,等重啟項目唧席,或者build后就發(fā)現(xiàn)樣式丟失了。
這可能是mpvue打包機制的一個限制嘲驾,即頁面不能將另一個頁面當(dāng)子組件來引用
淌哟,否則會導(dǎo)致樣式丟失。
背景音頻的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'
title
和src
賦值后會直接播放音頻,后面的幾個屬性建議也填上榕暇,因為播放背景音頻時微信是有個界面需要封面圖和歌手名稱等的蓬衡。
如果想要獲取當(dāng)前正在播放的音頻src,本來以為通過this.audio.src
來獲取就可以了但是有bug彤枢。
在開發(fā)者工具中是可以正常獲取的狰晚,即開發(fā)時是沒問題的,但在真機上返回的是undefined
缴啡,因此不能用this.audio.src
來獲取當(dāng)前播放的音頻url壁晒,得用一個變量來存這個數(shù)據(jù)。
直接使用音頻的currentTime可能渲染不及時
currentTime用于顯示當(dāng)前的播放進(jìn)度业栅,但我用在子組件中時經(jīng)常更新不及時秒咐,打印是正常的谬晕,但試圖渲染不及時,有時候需要點擊一下才能重新渲染携取,這可能是mpvue使用時才會遇到攒钳。
所以建議還是項目自身維護(hù)一套背景音頻的變量比較好一點,比如放在vuex
中雷滋。監(jiān)聽BackgroundAudioManager.onTimeUpdate()
方法每次賦值到自身維護(hù)的變量中不撑。
音頻的onCanplay方法不一定每個音頻都會觸發(fā)
一開始我監(jiān)聽在onCanplay
方法,將音頻的時長信息duration
賦值到vuex中存起來晤斩,但發(fā)現(xiàn)onCanplay
有時候是不會觸發(fā)的焕檬,比如重新賦值src播放下一首時,很尷尬澳泵。
所以不要太依賴onCanplay這個方法实愚,還好目前直接使用audio.duration
好像不會出現(xiàn)像上面的currentTime
渲染不及時的問題,所以就這樣用著先烹俗。
音頻播放結(jié)束爆侣,即onStop后萍程,不能再通過audio.play()的方法重新播放幢妄,得重新賦值src
正常來說,音頻播放結(jié)束后茫负,音頻的src是不變的蕉鸳,再次play()
應(yīng)該是可以的。但在小程序中偏偏不行忍法,得重新賦值src才能重新播放潮尝,這應(yīng)該是小程序的一個bug。饿序。勉失。
所以需要判斷一下暫停和停止的情況,用不同的辦法播放原探。正常來說乱凿,音頻暫停時currentTime
是不為0的,而結(jié)束時currentTime
會為0咽弦。
所以可以通過currentTime
(最好是自己維護(hù)的變量)來判斷暫停和停止的情況:如果currentTime不為0徒蟆,表示是暫停的情況,可以用play()
,如果小于等于0型型,則重新賦值src播放:
if (currentTime) {
this.audio.play()
} else {
this.audio.src = 'xx.mp3'
}
mpvue不支持直接在template上直接綁定函數(shù)
這個是mpvue文檔上有寫的段审,不過一開始并不是很理解,也踩坑了闹蒜,所以在這里提一下寺枉,避免不知道的同學(xué)踩坑找半天抑淫。
<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>
上面的代碼應(yīng)該是日常vue中比較常用的,就是將數(shù)據(jù)傳參給方法做一些處理姥闪,這個在mpvue中是不支持的丈冬,會被編譯成一個空字符串。
小程序中可放心使用css3的一些特性
比如高斯模糊
filter: blur(50px);
如果要使用動畫甘畅,盡量用css
動畫代替wx.createAnimation
在實際使用時埂蕊,wx.createAnimation
做動畫其實很卡,性能很差疏唾,所以在需要使用動畫時蓄氧,建議盡量使用css做動畫。
在小程序中是支持css動畫的槐脏,transition
,animation
,@keyframes
這些特性都支持喉童。
比如做一個div一直旋轉(zhuǎn)的動畫,大家可以對比一下兩個版本:
-
wx.createAnimation
版本
原理:通過setInterval()不斷更新div的旋轉(zhuǎn)位置
<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毫秒旋轉(zhuǎn)10度
this.animation.rotate(10 * n).step()
this.animationData = this.animation.export()
}
},
onShow () {
// 頁面從隱藏到顯示時才執(zhí)行
if (!this.animation) {
this.animation = wx.createAnimation()
this.startRotate()
}
},
onReady () {
// 第一次初始化時會執(zhí)行
if (!this.animation) {
this.animation = wx.createAnimation()
this.starRotate()
}
},
onHide () {
// 頁面隱藏時會執(zhí)行顿天,避免頻繁的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
做旋轉(zhuǎn)動畫
<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的間隔時間和旋轉(zhuǎn)角度牌废,比較難調(diào)咽白。而用css寫動畫很簡單,性能比js好鸟缕,代碼量也很少晶框。
使用css動畫時建議開啟硬件加速
為了動畫更流暢,想盡辦法做優(yōu)化懂从,雖然不知道有沒效果授段,反正用了再說[手動滑稽]。
可以用will-change和transform: translate3d(0,0,0)開啟硬件加速番甩。我也不太會用侵贵,具體用法大家自行百度Google。
will-change: auto;
transform: translate3d(0, 0, 0);
iPhoneX需要底部導(dǎo)航條預(yù)留34px(68rpx)的高度缘薛。
由于小程序中不能設(shè)置viewport-fit=cover
窍育,所以也就沒有web中的安全區(qū)域說法,目前主流的做法是通過wx.getSystemInfoSync()
判斷是否是ipx掩宜,若是則給頁面底部撐高34px蔫骂。
const res = wx.getSystemInfoSync()
if (res.model.indexOf('iPhone X') >= 0) {
this.isIpx = true
}
注意是用res.model.indexOf('iPhone X')
,在開發(fā)者工具的iPhone X中,model是全等于iPhone X
的牺汤,但在真機中往往拿到的值是iPhone X GZxxx
辽旋,即后面可能會帶一串東西,所以用indexOf
才是比較穩(wěn)的,而且對iPhone XR
等機型也適用补胚。
由于還有其他安卓機的全面屏码耐,不太可能一一判斷,而且某些安卓全面屏是沒有用iPhone底部的工具條的(不存在沖突的情況)溶其,所以我們只判斷iPhone X的情況就可以了骚腥,其他全面屏就不需要給底部預(yù)留了。
至于全面屏布局的適配瓶逃,需要用flex
布局或者獲取屏幕寬高來慢慢調(diào)了束铭,建議最好用flex布局自適應(yīng)處理。
for循環(huán)中的子組件click事件無法觸發(fā)
Page -> 父組件 -> 子組件
厢绝,在子組件click后$emit
一個事件出來契沫,發(fā)現(xiàn)無法觸發(fā)。
這個bug一開始沒有出現(xiàn)昔汉,但偶然npm run build
出現(xiàn)的懈万,然后排查原因,后面即使回滾所有版本再npm start也還會出現(xiàn)靶病。好像不觸發(fā)則已会通,一發(fā)就不可收拾,這又是一個大坑娄周,搜issue和加群問人涕侈,當(dāng)晚下班回家研究到1點多都沒有解決。
第二天繼續(xù)研究昆咽,感覺可能是框架的原因驾凶,最后嘗試升級一下mpvue版本,沒想到就正常了掷酗。直接使用quick-strat項目的mpvue
版本是 2.0.0,mpvue
和mpvue-template-compiler
升級到最新2.0.6
就解決了窟哺。
事后查看mpvue版本記錄泻轰,果然是框架本身原因,并且找到了issue且轨。
npm run build后代碼報錯浮声,再build一次可能報另一些錯
解決: 沒找到原因,可能是引入vant導(dǎo)致的旋奢,打包時丟失了部分文件泳挥。多build幾次,或者重啟下小程序開發(fā)者工具就正常了至朗。
mpvue中created() 鉤子會在頁面初始化時全部一起觸發(fā)屉符,盡量不要用
小程序生命周期的理解
- 進(jìn)入已銷毀的page組件時依次觸發(fā): onLoad,onShow,onReady,beforeMount,mounted
- 第一次進(jìn)入已銷毀的子組件時依次觸發(fā): onLoad,onReady,beforeMount,mounted
- 第二次進(jìn)入已銷毀的子組件時依次觸發(fā): onLoad,onShow,onReady
- 再次進(jìn)入 未被銷毀的page組件、子組件時只觸發(fā): onShow
mpvue文檔中建議盡量不要使用小程序的生命周期,這個應(yīng)該是為了讓項目更好地適應(yīng)支付寶小程序和頭條小程序等矗钟,所以才這樣建議大家盡量不要使用某一個小程序自身的api唆香。
如果你們的小程序只是微信小程序(不考慮兼容其他平臺小程序),我建議直接用小程序的生命周期吨艇,而不要用mpvue的生命周期躬它,坑太多了。比如mpvue的created周期东涡,初始化時所有的page都會執(zhí)行冯吓,所以created這個周期是不能用了。
onUnload不觸發(fā)
小程序中與平常web開發(fā)不同的是疮跑,它的頁面會被緩存桑谍。舉個例子:
- 從
page1
跳轉(zhuǎn)到page2
,再從page2
返回page1
,此時的page1
還沒銷毀祸挪,不會觸發(fā)onLoad
再重新渲染锣披,而是直接使用之前的數(shù)據(jù)。從性能上來說贿条,單純的返回不應(yīng)該再請求api獲取數(shù)據(jù)重新渲染雹仿,這是對的,符合我們的預(yù)期整以。 - 而有時候胧辽,從
page2
返回page1
時,我們希望page1
是重新獲取數(shù)據(jù)渲染的公黑。比如在page2
做了一個退出登錄的操作邑商,此時再返回page1
時,還是會看到之前的數(shù)據(jù)凡蚜。實際上我們的預(yù)期是:由于已經(jīng)退出登錄了人断,page1
的數(shù)據(jù)應(yīng)該被銷毀了。
在平常的web開發(fā)中朝蜘,遇到上面的問題恶迈,我們可能是不管緩存,每次返回page1
都再次請求api渲染最新的數(shù)據(jù)谱醇,犧牲掉部分性能從而保證邏輯的正確性暇仲。
在mpvue中我也嘗試這樣干了:想在page1
的onUnload()
生命周期中銷毀數(shù)據(jù),但是沒有成功副渴。即使在page2
退出登錄時奈附,采用wx.reLaunch()
重新刷一遍,page1
的onUnload()
生命周期也沒有執(zhí)行。所以onUnload()
是有可能不執(zhí)行的,建議慎用。
最后還是得想辦法做到在page2
控制page1
的數(shù)據(jù)銷毀或保留儒溉。想到這里中跌,vuex
就不自覺浮現(xiàn)在眼前了咨堤,如果page1的數(shù)據(jù)是通過vuex來控制的,那么我在page2就可以用vuex來靈活管理其他頁面的數(shù)據(jù)了漩符。
如果page2做退出登錄操作時一喘,就讓page1的數(shù)據(jù)銷毀,如果是不退出登錄正常返回嗜暴,page1的數(shù)據(jù)還是正常凸克,做到靈活控制。
個人平時web開發(fā)很少用vuex
闷沥,因為項目比較簡單不用那么復(fù)雜的全局?jǐn)?shù)據(jù)傳遞萎战。但在小程序中,建議全局使用vuex
來控制所有數(shù)據(jù)(當(dāng)然是得根據(jù)需求來用)舆逃。
總結(jié)
第一次開發(fā)小程序就直接上了mpvue蚂维,可能有些坑已經(jīng)很多同學(xué)總結(jié)過了,有些坑可能是不熟悉而導(dǎo)致的路狮,但自己沒有去踩過一遍可能不夠深刻虫啥。
有兩種坑會比較難啃:
- 框架本身的問題,如mpvue2.0.0出現(xiàn)的子組件無法觸發(fā)事件的問題奄妨。
- 開發(fā)者工具和真機運行環(huán)境不一致導(dǎo)致的坑涂籽。
遇到真機和開發(fā)者工具不一致的情況,可按以下步驟排查:
- 有可能是緩存砸抛,可以殺掉之前的版本再跑起來
- 手機微信版本太低评雌,可能api不支持,用
wx.canIUse
打印一下 - 手機端某些屬性不支持讀取直焙,比如上面的
this.audio.src
景东,可以在真機打印調(diào)試一下 - 代碼在手機端運行有報錯,可以在手機端開啟調(diào)試箕般,看一下log
- 微信設(shè)計上的坑耐薯,百度下是否有相關(guān)的案例和解決辦法
而遇到mpvue框架的問題可以:
- 去搜一下
mpvue
的issue看有沒相關(guān)解決辦法 - 盡量使用最新版本的框架,可能某些問題已經(jīng)修復(fù)了的丝里。實在解決不了的,建議想辦法繞過体谒,換一種方法來實現(xiàn)杯聚。
希望對大家有所幫助。