mpvue開發小程式總結
前期準備
1.框架選型
原生小程式開發方式與vue有些類似,所以用過vue的前端er會很容易上手。但是原生的開發體驗實在糟糕,在前端元件化的今天用原生開發元件顯得很無力。對於習慣vue開發方式的前端er來說mpvue再合適不過了。mpvue可以將H5程式碼打包成小程式程式碼,目前mpvue還做不到一套程式碼多端執行(畢竟各個端有自己的差異性,小程式沒有document和window,所以那些第三方移動端元件庫並不能適用於小程式),但是已經大大減少了開發的工作量。
2.專案的搭建
3.專案結構
project └───build └───config └───dist └───node_modules └───server//mock伺服器 └───src └───assets └───sass |common.scss// 全域性樣式 └───components └───pages └───plugins //存放封裝的外掛 └───services |Api.js// 封裝請求 |WxApi.js// 對小程式api二次封裝 └───store └───modules // vuex模組資料夾 |index.js // vuex處理檔案 |action.js |state.js |mutations.js |type.js複製程式碼
|App.vue |config.js//配置資訊,如請求地址等 |main.js └───static//靜態資源 └───images//圖片 └───font//字型圖示 │README.md │package.json │package-lock.json 複製程式碼
3.其他
小程式網路請求只能是https協議且不能有埠號,可以使用natapp代理,然後在開發中可在開發者工具中設定不校驗域名
登入流程
1.關於OpenId和UnionId
OpenId
是一個使用者對於一個小程式/公眾號的標識,開發者可以通過這個標識識別出使用者。
UnionId
是一個使用者對於同主體微信小程式/公眾號/APP的標識,開發者需要在微信開放平臺下繫結相同賬號的主體。開發者可通過 UnionId
,實現多個小程式、公眾號、甚至APP 之間的資料互通了。
同一個使用者的這兩個 ID 對於同一個小程式來說是永久不變的,就算使用者刪了小程式,下次使用者進入小程式,開發者依舊可以通過後臺的記錄標識出來。
2.官方給出的最佳實踐
新版的小程式對獲取使用者資訊授權進行了改動,使用者在小程式中需要點選元件後,才可以觸發登入授權彈窗、授權自己的暱稱頭像等資料。因此官方給出的最佳實踐是
1.呼叫 wx.login
獲取 code
,然後從微信後端換取到 session_key
,用於解密 getUserInfo
返回的敏感資料。
2.使用 wx.getSetting
獲取使用者的授權情況
1) 如果使用者已經授權,直接呼叫 API wx.getUserInfo
獲取使用者最新的資訊;
2) 使用者未授權,在介面中顯示一個按鈕提示使用者登入,當用戶點選並授權後就獲取到使用者的最新資訊。
3.獲取到使用者資料後可以進行展示或者傳送給自己的後端。
3.實現
- 製作一個登入頁面
- 在App.vue裡的onLaunch生命週期中判斷Storage中是否存在,如不存在跳轉到登入頁並把當前頁面的路由當作引數傳遞過去,如存在再呼叫wx.checkSession()檢查session_key 的有效性否則跳到登入頁並把當前頁面的路由當作引數傳遞過去
- 在ajax請求response攔截器裡判斷狀態碼為401表示token已過期,跳轉到登入頁重新登陸並把當前頁面的路由當作引數傳遞過去
- 登入按鈕加上open-type="getUserInfo"屬性,並監聽getuserinfo事件,使用者點選後會返回加密後的使用者資訊,此時執行wx.login()獲取到code,將code和使用者資訊傳送到後臺換取token,並把token儲存到Storage
附官方的最佳實踐 ofollow,noindex">mp.weixin.qq.com/s/JBdC-G9Mw…
vuex模組化
在開發時有時會遇到一些變數需要跨兩三個頁面傳遞公用,所以引入vuex是比較好的解決方案。當一個專案比較大時,所有的狀態都集中在一起會得到一個比較大的物件,進而顯得臃腫,難以維護。為了解決這個問題,Vuex允許我們將store分割成模組(module),每個module有自己的state,mutation,action,getter。新建以下目錄
store └───modules // vuex模組資料夾 └───kx //一級模組 └───jkdbb//二級模組 | index.js ... |index.js // vuex處理檔案 |action.js |state.js |mutations.js |type.js複製程式碼
action,state,mutations,type作為公共。
在index.js中設定namespaced為true
export default { namespaced: true, state: { ywmkid: '1', formData:{} }, mutations:{ SET_YWMKID (state,ywmkid) { state.ywmkid = ywmkid }, SET_FORMDATA (state,formData) { state.formData = formData } } }複製程式碼
在store下的index.js
import Vue from 'vue' import Vuex from 'vuex' import state from './state' import mutations from './mutations' import actions from './actions' import jkdbb from './module/kx/jkdbb/index' Vue.use(Vuex) const isDebug = process.env.NODE_ENV !== 'production' export default new Vuex.Store({ state, mutations, actions, modules:{ jkdbb }, strict: isDebug }) 複製程式碼
訪問模組裡的state
this.$store.state.jkdbb.ywmkid複製程式碼
修改模組裡的state
this.$store.commit('jkdbb/SET_YWMKID',this.$route.query.id)複製程式碼
mpvue-router-patch
vue-router不相容小程式,所以為了和h5程式碼保持一致選用mpvue-router-patch,其和vue-router類似的使用方式,預設對應小程式的navigateTo。附小程式的三種跳轉方式的區別
wx.navigateTo:跳轉到某個頁面。會產生一個新的頁面
wx.redirectTo:重定向到某個頁面。替換掉當前頁面
wx.switchTab:切換tab
wx.reLaunch:重新啟動。清空之前的所有頁面
在小程式中有層級的限制,只能開啟十個頁面,因此要合理利用跳轉的方式。
在需要重啟應用時只需要設定reLaunch為true即可
this.$router.push({path:'/pages/index/main',reLaunch: true}) 複製程式碼
在需要切換tab時只需要設定switchTab為true即可
this.$router.push({path:'/pages/index/main',switchTab: true}) 複製程式碼
附完整api地址 github.com/F-loat/mpvu…
flyio
axios不相容小程式,flyio相容Node.js 、 微信小程式 、 Weex 、 React Native 、Quick App 和瀏覽器。以下的程式碼是對flyio的使用進行封裝
import Config from '@/config' import {CacheUtil} from '@/services/WxApi' let Fly=require("flyio/dist/npm/wx") let fly=new Fly; class Response { constructor (res) { this.rawData = res this.code = res.code this.messages = res.messages this.data = res.data this.wwRequestEntity = res.wwRequestEntity } resolve () { if (this.isSuccess()) { return Promise.resolve(this.rawData) } if (this.isError()) { let message = Config.defErrorMessage } return Promise.reject(this.messages) } isSuccess () { return this.code === 1 } isError () { return this.code === 0 } } class ApiManager { constructor (apiPrefix) { fly.config.baseURL = apiPrefix || Config.apiPrefix; fly.config.timeout = 120000 fly.interceptors.request.use(request => { const token = CacheUtil.getStorage('token'); if (token) {// 判斷是否存在token,如果存在的話,則每個http header都加上token request.headers.Authorization = `Bearer ${token}`; } return request }) fly.interceptors.response.use(res => { if (res.status >= 200 && res.status < 300) { let response = new Response(res.data) return response.resolve() } return Promise.reject(res) }, error => { const { response } = error; if (!response) return Promise.reject(error) if (response.status === 401) { //沒有許可權跳轉到授權頁面 return Promise.reject(null) } return Promise.reject(error) }) } post(uri, data, config){ return fly.post(uri, data, config); } get(uri, data){ return fly.get(uri, data); } put (uri, data) { return fly.put(uri, data) } delete (uri, data) { return fly.delete(uri, data) } } export function httpManager(baseURL){ return new ApiManager(baseURL) } export let apiManager = httpManager()複製程式碼
在main.js引用並掛載
Vue.prototype.apiManager = apiManager; 複製程式碼
之後在頁面中就可以這樣使用
this.apiManager.post複製程式碼
二次封裝Storage
小程式的Storage沒有設定過期時間的引數,我們可以對他進行二次封裝
export const CacheUtil = { getStorage(key){ let data = wx.getStorageSync(key); if (!data) { return null; }else{ let now = new Date().getTime() if (now >= parseInt(data.expired)) { wx.removeStorageSync(key) return null; }else{ return data.data; } } return }, setStorage(key,data,expired){ let time = new Date().getTime() + expired; wx.setStorageSync(key,{data:data,expired:time}) }, removeStorage(key){ wx.removeStorageSync(key) } } 複製程式碼
表單驗證
小程式的表單驗證似乎不多,常用的就是 WxValidate 。但是我覺得不是那麼好用,提示判斷還要自己寫。所以採用 we-validator ,它支援微信小程式、支付寶小程式、瀏覽器以及Nodejs端使用。我們可以在plugins資料夾中新建一個js用於新增自定義的校驗規則
import WeValidator from 'we-validator' WeValidator.addRule('Mobile', function(value, param){ const reg = 11 && /^(((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\d{8})$/; return reg.test(value) }) export default{ WeValidator } 複製程式碼
然後再main.js中引用並掛載
Vue.prototype.$WeValidator = WeValidator.WeValidator; 複製程式碼
在頁面中
mounted(){ this.oValidator = new this.$WeValidator({ rules: { sbmc: { required: true }, sbxh: { required: true }, fx: { required: true }, yxqk: { required: true }, azrq: { required: true }, ssqx: { required: true }, azdz: { required: true } }, messages: { sbmc: { required: '請選擇裝置名稱' }, fx: { required: '請選擇方向' }, sbmc: { required: '請選擇裝置名稱' }, yxqk: { required: '請選擇執行情況' }, azrq: { required: '請選擇安裝日期' }, ssqx: { required: '請選擇所屬區縣' }, azdz: { required: '請輸入安裝地址' } } }) }, methods:{ submit(){ if(this.oValidator.checkData(this.form)){ } } }複製程式碼
獲取當前位置
小程式的api獲取當前位置返回的資訊比較少,如有需要獲取到地址需要引入地圖的地址解析。我用的是騰訊地圖,在引用的時候會報錯,是因為它不是用export匯出的,所以可以在最後修改為export default QQMapWX。
import QQMapWX from '../plugins/qqmap-wx-jssdk.js' let qqmapsdk = new QQMapWX({ key: config.txMapKey }); export function getPosition(){ return new Promise((resolve,reject)=>{ wx.showLoading({ title:'正在定位', mask:true }) wx.getLocation({ success:function(res){ qqmapsdk.reverseGeocoder({ coord_type:0, location: { latitude: res.latitude, longitude: res.longitude }, success: function(data) { resolve({ latitude: res.latitude, longitude: res.longitude, adcode: data.result.ad_info.adcode, addr:data.result.address }) }, fail: function(err) { reject(err); }, complete: function(err) { wx.hideLoading() } }); }, fail:function(err){ reject(err) } }) }) }複製程式碼
封裝input元件
我們可以將input做成一個元件,可以去配置輸入框的標題,清空輸入框等功能。isShowArea如下一條所說的作用。在向父頁面傳遞值時需要一個settimeout,如果不新增會有閃爍的bug。
<template> <div class="yg-input"> <label :placeholder="placeholder":style="{width:labelWidth}">{{label}}</label> <div class="yg-input-control yg-textarea-control" v-if="type=='textarea'"> <textarea v-if="isShowArea" :maxlength="maxlength" v-bind="$attrs" :cursor-spacing="100" :placeholder="placeholder" :disabled="disabled||readonly" :value="currentVal" @input="updateVal($event.target.value)"></textarea> <div v-if="!isShowArea">{{currentVal}}</div> </div> <div class="yg-input-control" v-else> <input :placeholder="placeholder" :maxlength="maxlength" :cursor-spacing="100" :disabled="disabled||readonly" :value="currentVal" v-bind="$attrs" @input="updateVal($event.target.value)" :type="type" /> <i v-if="value && $attrs.readonly!==!!'true'" @click="clear" class="iconfont iconfont-close"></i> </div> </div> </template> <script> export default { inheritAttrs: false, data() { return { currentVal:this.value } }, computed:{ isShowArea(){ return this.$store.state.mask; } }, methods:{ clear(){ this.currentVal = ''; this.$emit('input', ''); }, updateVal(val) { this.currentVal = val; setTimeout(()=>{ this.$emit('input', val); },0) } }, watch:{ value: { handler(newValue, oldValue){ this.currentVal = newValue; } } }, props: { label:{ default:'', type:String }, labelWidth:{ default:'75px', type:String }, type:{ default:'text', type:String }, value:{ default:'', type:String }, placeholder:{ default:'', type:String }, maxlength:{ default:'-1', type:String }, disabled:{ default:false, type:Boolean }, readonly:{ default:false, type:Boolean } } } </script> <style lang="scss"> </style> 複製程式碼
可以將它定義為全域性元件,在main.js中
Vue.component('yg-input',YgInput) 複製程式碼
在父頁面使用
<yg-input :readonly="status==3" v-model="form.sbxh" label="裝置型號" placeholder="請輸入裝置型號"></yg-input> 複製程式碼
textarea的bug
textarea是原生元件因此層級最高,因此在頁面上自定義的彈窗會被textarea的內容覆蓋。因此我們可以在彈窗是將它隱藏,在關閉彈窗時顯示。可以在vuex中定義一個變數控制,然後在彈窗元件中對這個變數賦值。
<textarea v-if="isShowArea"></textarea> <div v-if="!isShowArea">{{currentVal}}</div>//在textarea隱藏時顯示textarea填寫的內容複製程式碼
還有一個問題是textarea在ios真機上會有一個預設的padding導致和其他表單元素不對齊。這個官方還是沒修復。
封裝select元件
原生的picker不能自定義樣式,而卻選項的文字多了會顯示...,專案中還要顯示中英文所以自己寫了一個
<template> <div class="yg-input yg-select"> <label :style="{width:labelWidth}">{{label}}</label> <div class="yg-input-control" @click="showPicker"> <input :value="selectVal" :placeholder="placeholder" v-bind="$attrs" :disabled="true"/> <i class="iconfont iconfont-arrow-r"></i> </div> <div class="popup-mark" v-if="toggle"> <ul class="yg-popup"> <li class="yg-popup-head"> <span @click.stop="cancel">取消</span> <span @click.stop="confirm">確定</span> </li> <li v-for="(item,$index) in selectOption" :class="{'active':item.active}" :key="index" @click.stop="itemClick(item)">{{item.text}}</li> </ul> </div> </div> </template> <script> export default { inheritAttrs: false, data() { return { selectVal:'', toggle:false, cache_selectVal:'', cache_selectTxt:'', isFirstEnter:true, selectOption:[] } }, methods:{ cancel(){ this.selectOption = [...this.setActive(this.value,this.option)]; this.hide(); }, confirm(){ if(this.selectVal != this.cache_selectTxt&&this.cache_selectTxt){ this.selectVal = this.cache_selectTxt; this.$emit('input', this.cache_selectVal); this.$emit('change', this.cache_selectVal,this.cache_selectTxt); } this.hide(); }, itemClick(item){ this.cache_selectVal = item.value; this.cache_selectTxt = item.text; this.selectOption = [...this.setActive(item.value,this.option)]; }, setActive(value,arr){ for(let item of arr){ if(value==item.value){ item.active=true; }else{ item.active=false; } } return arr; }, hide(){ this.$store.commit('SET_MARK',true); this.toggle = false; }, showPicker(){ if(this.disabled||this.readonly){ return false; } if(!this.option.length){ this.$message('沒有相關選項'); return false; } this.$store.commit('SET_MARK',false); this.toggle = true; } }, props: { label:{ default:'', type:String }, disabled:{ default:false, type:Boolean }, readonly:{ default:false, type:Boolean }, labelWidth:{ default:'75px', type:String }, value:{ default:'', type:String }, placeholder:{ default:'', type:String }, option:{// default:function(){ return [] }, type:Array }, defaultFirst:{ default:false, type:Boolean } }, watch:{ value: { handler(newValue, oldValue){ this.selectOption = this.selectOption.map((item)=>{ if(this.value==item.value){ item.active=true; this.selectVal=item.text; }else{ item.active=false; } return item; }) }, immediate: true }, option: { handler(newValue, oldValue){ this.selectOption = [...newValue]; let _value = ''; for(let item of newValue){ if(this.value==item.value){ _value = item.value; break; } } if(_value){ this.$emit('input', _value); } if(this.option.length!==0&&!_value&&this.defaultFirst){ this.$emit('input', this.option[0].value); } if(this.option.length==0&&!_value&&!this.isFirstEnter){ this.$emit('input', ''); this.isFirstEnter = false; } }, deep: true, immediate: true } } } </script> <style lang="scss"> @keyframes popup{ 0%{ transform:translate(0,100%); } 100%{ transform:translate(0,0) } } .popup-mark{ position:fixed; z-index:9999; background:rgba(0,0,0,0.4); top:0; left:0; right:0; bottom:0; display:flex; align-items:flex-end; .yg-popup{ animation:popup .3s; flex:1; max-height:800px; min-height:500px; overflow:auto; background:#fff; li{ &.yg-popup-head{ background:#fbf9fe; color:#999; line-height: 1; padding:0; overflow:hidden; padding:30px; span:nth-of-type(1){ float: left; } span:nth-of-type(2){ float: right; color:#2b79fb; } } background:#fff; border-bottom:1px solid #eee; line-height: 1; padding:30px 50px 30px 30px; font-size:32px;/*no*/ color:#666; position:relative; &.active{ color:#2b79fb; &:before{ content:''; position:absolute; width:8PX; height:8PX; border-radius:50%; top:50%; background:#2b79fb; margin-top: -4PX; right: 40px; } } } } } </style>複製程式碼
下拉重新整理上拉載入
1.上拉載入更多
在列表的下面新增以下節點,loadTip預設為空,用於載入時的提示
<div class="page-list-loading">{{loadTip}}</div> 複製程式碼
定義maxPage表示最後一頁
在onReachBottom鉤子里加載下一頁
onReachBottom(){ this.loadTip = '正在載入中'; this.form.page++; this.loadData(); }, 複製程式碼
在loadData的回撥裡
this.list = this.list.concat(res.data); this.loadTip = ''; this.maxPage = Math.ceil(res.total/this.form.size);//算出最後一頁 複製程式碼
2.下拉重新整理
在需要下拉重新整理的頁面同級新建main.json
{ "enablePullDownRefresh": true } 複製程式碼
在onPullDownRefresh鉤子里加載資料
this.maxPage = null; this.form.page = 1; this.list = []; this.loadTip = ''; wx.showNavigationBarLoading();//顯示標題欄的載入動畫 //請求成功後 wx.hideNavigationBarLoading(); wx.stopPullDownRefresh();//停止下拉複製程式碼
截至2018-10-15,下拉重新整理還存在bug,ios上頭部head設定position為fixed時會擋住載入動畫,在安卓上頭部head又會跟隨下滑
粘性導航
在h5中可以方便地利用window物件獲取滾動距離,元素距離頂部距離等。但是在小程式中並沒有window物件只能使用小程式的api獲取相關資料。
在export default上面新增以下方法,該方法返回導航的第幾個為高亮狀態
function getNavActiveIndex(num,arr){ if(num===0){ return 0; } for(var i=0;i<arr.length;i++){ if(num>=arr[i]){ if (!arr[i + 1]) { return arr.length - 1; } else if (num < arr[i + 1]) { return i; } }else if (num < arr[i]) { if (!arr[i - 1]) { return 0; } else if (num >= arr[arr - 1]) { return i-1; } } } } 複製程式碼
在onPageScroll鉤子裡,affix為導航的最外層,isfixed變數控制是否新增position:fixed屬性,navActive控制導航的第幾個為高亮狀態,boundingClientRect獲取affix相對於顯示區域的距離
onPageScroll(e){ const query = wx.createSelectorQuery(); query.select('.affix').boundingClientRect((res)=>{ if(res){ if(res.top<0){ this.isfixed = true; }else{ this.isfixed = false; this.navActive = 0; } } }).exec(); this.navActive = getNavActiveIndex(e.scrollTop,this.offsetTopArr); }複製程式碼
在請求完資料後獲取每一個距離頁面頂部的距離,45是減去導航本身的高度
mounted () { this.$showLoading(); Promise.all([this.apiManager.post('/home/indexLists'),this.apiManager.post('/home/frequentlyUsed'),this.apiManager.post('/gaywtz/wrapNotice')]).then(res=>{ this.affixArr = res[0].data[0].indexLabelLists this.indexDataLists=res[0].data[0].indexDataLists; this.hotAppList = res[1].data; this.imgUrls = res[2].data[0]; this.newsList = res[2].data[1]; this.colorCtrl(); setTimeout(()=>{ const query = wx.createSelectorQuery(); query.selectAll('.affix-pane').boundingClientRect((res)=>{ res.map((item)=>{ this.offsetTopArr.push(item.top-45) }) }).exec(); this.$hideLoading(); }, 200) }) }複製程式碼
點選導航時
changeSt(index){ this.navActive = index; wx.pageScrollTo({ scrollTop: this.offsetTopArr[index] }) }, 複製程式碼
px2rpx
rpx 是微信小程式解決自適應螢幕尺寸的尺寸單位。以iPhone6為例,螢幕寬度為375px,把它分為750rpx後, 1rpx = 0.5px。px2rpx可以很方便地把px轉成rpx。文件中說和px2rem使用一樣,但是當某個屬性並不需要將px轉成rpx時,設定/*no*/並沒有用,解決辦法是把px改為PX。
mpvue-wxparse
後臺返回的富文字編輯器中的內容是html節點,這在小程式中是解析不出來的。因此需要mpvue-wxparse去轉換
js import wxParse from 'mpvue-wxparse' components:{ wxParse } html <wxParse :content="content" @preview="preview" @navigate="navigate" /> 複製程式碼
所有頁面的created鉤子函式在小程式開啟時就全部執行
改用mouted鉤子
重新進入頁面會保留之前的資料
假設有三個頁面:列表頁A,表單頁一B,表單頁二C。當填寫完表單頁C後跳轉到列表頁,這時如果再從列表頁進入表單頁B會發現B還是之前填寫的資料。這是因為mpvue在離開頁面時並沒有呼叫destroyed鉤子,因此目前的解決方案是在小程式的onUnload中重置data函式裡的資料。可以將它封裝成一個外掛
const resetPageData = { install: function (Vue) { Vue.mixin({ onUnload() { if (this.$options.data) { Object.assign(this.$data, this.$options.data()) } } }) } } export default resetPageData; 複製程式碼
然後在main.js中使用
Vue.use(resetPageData); 複製程式碼
其他需要注意的
1.background不支援本地路徑
2.不能使用v-show需替換成v-if
3.在map中使用cover-view需要直接使用cover-view,如使用div會有問題,文件中寫到目前cover-view支援動畫,開發者工具中有效實際在真機無效,且不支援單邊border,rotate等
4.solt不支援動態渲染,在封裝業務元件時很是蛋疼
5.不支援自定義指令
最後吐槽
小程式還很年輕,還存在很多不足(bug超多),開發體驗說實話目前為止感覺不太好。但是什麼技術都不可能一出來就做得很完美,作為前端只能跟著潮流掌握這些新技術了。