初衷
本人最近一直在研究小程式,一個偶然的機會發現了mpvue框架,剛好本人對vue十分喜愛,就決定入坑了。曾經在掘金看過一個仿舊版滴滴的小程式,感覺挺不錯的,但它是基於原生小程式的,所以就決定花了一段時間用mpvue進行重寫。下面就開始正餐了。
效果圖
目錄結構
┣━ api # 存放網路請求相關┣━ common ● ┣━ constant //常量 ┣━ css //weui.css ┣━ less //通用less樣式與變數 ┗━ lib //第三方庫 qqmap-wx-jssdk.js┣━ components ● 抽取出來的元件 ┣━ addressList.vue ┣━ common-footer.vue ┣━ driver-header.vue ┣━ loading-sprinner.vue ┣━ search-bar.vue ┗━ star.vue ┣━ pages ● 頁面 ┣━ cars //選擇車 ┣━ cityChoose //選擇城市 ┣━ destination //選擇目的地 ┣━ evaluation //評價 ┣━ index //主頁面 ┣━ login //登入 ┣━ orderCancel //訂單取消 ┣━ orderClose //訂單關閉 ┣━ orderService //訂單服務 ┣━ orderWhy //詢問原因 ┣━ starting //選擇出發地點 ┗━ wait //等待┣━ store ● 存放vuex相關 ┣━ index.js ┣━ mutation-types.js ┣━ mutations.js ┗━ state.js ┣━ utils 工具類┣━ App.vue┣━ main.js┗━ static # 靜態資源,存放圖片複製程式碼
vuex資料
const state = {
curNavIndex: 0, //當前頭部導航索引 phone: '', //登入號碼 curCity: '', //當前所在的城市 startPlace: '出發地', //出發地 startFormattedPlace: '', //更具人性化的描述的出發地 startPosition: [], //包含startLatitude和startLongitude destination: '你要去哪兒', //目的地 endPosition: [], //包含endLatitude和endLongitude driver: {
}, //司機資訊 包含Cartnumber,cart,id,name,stars cost: 0 //花費
}複製程式碼
功能詳情
頭部導航自動滑動
為了讓頭部導航點選時能自動滑出,滑動swiper的同時頭部導航跟著滑動,在cars頁面選中車時回退到index頁面時頭部導航自動滑動,我在vuex中維護了一個索引值curNavIndex。根據不同的curNavIndex對scroll-view設定不同的scroll-left值。
那麼如何設定準確的scroll-left值呢?
微信小程式無法進行Dom操作,所以無法動態拿到元素寬度。所以我根據頭部導航每項的寬度維護了一個陣列navOffsetArr
//兩個字寬度+2*margin 也就是 32+10*2 = 52 const NAV_SMALL_WIDTH = 52;
//三個字寬度+2*margin 也就是 48+10*2 = 68 const NAV_BIG_WIDTH = 68;
this.navOffsetArr = [ 0, 0, NAV_SMALL_WIDTH, NAV_SMALL_WIDTH * 2, NAV_SMALL_WIDTH * 2 + NAV_BIG_WIDTH, NAV_SMALL_WIDTH * 2 + NAV_BIG_WIDTH * 2, NAV_SMALL_WIDTH * 3 + NAV_BIG_WIDTH * 2, NAV_SMALL_WIDTH * 4 + NAV_BIG_WIDTH * 2 ]複製程式碼
獲取索引值
computed: {
...mapState([ 'curNavIndex' ])
}複製程式碼
watch裡監聽索引值,當curNavIndex改變時,拿到不同的navScrollLeft值
watch: {
curNavIndex(newIndex){
this.navScrollLeft = this.navOffsetArr[newIndex]
}
}複製程式碼
最後將scroll-left與navScrollLeft繫結,從而實現自動滑動
<
scroll-view class="nav" scroll-x="true" scroll-with-animation="true" :scroll-left="navScrollLeft">
...... ...... <
/scroll-view>
複製程式碼
首頁自動儲存位置資訊
在進入index首頁的時候,就會自動將當前城市,當前經緯度,當前地址存入state中作為出發點資訊。這裡接入了騰訊地圖api,還是比較方便的。
wx.getLocation({
type: 'gcj02', success: (res) =>
{
reverseGeocoder(qqmapsdk, res).then(res =>
{
this.saveStartPlace(res.result.address) this.saveFormattedStartPlace(res.result.formatted_addresses.recommend) this.saveCurCity(res.result.address_component.city)
}) this.saveStartPosition([res.latitude, res.longitude])
}
})複製程式碼
mapMutations
methods: {
...mapMutations({
saveCurNavIndex: 'SET_CUR_NAV_INDEX', saveStartPlace: 'SET_START_PLACE', saveFormattedStartPlace: 'SET_FORMATTED_START_PLACE', saveCurCity: 'SET_CUR_CITY', saveStartPosition: 'SET_START_POSITION', saveCost: 'SET_COST'
})
} 複製程式碼
其中reverseGeocoder()就是一個位置轉換為地址的函式,是對qqmapsdk.reverseGeocoder()進行了一次封裝
function reverseGeocoder(qqmapsdk, {latitude, longitude
}) {
return new Promise((resolve, reject) =>
{
qqmapsdk.reverseGeocoder({
location: {
latitude: latitude, longitude: longitude,
}, success: (res) =>
resolve(res), fail: (res) =>
reject(res)
})
})
}複製程式碼
這樣當我們進入index首頁的時,就可以在Console中就看到資料成功儲存到vuex裡
選擇出發點
在mpvue中使用map元件時會有一些坑,這裡先緩一緩,坑稍後再說。
地圖map
<
map class="map-didi" id="map-didi" :latitude="latitude" :longitude="longitude" :markers="markers" @regionchange="regionChange" @begin="begin" @end="end" show-location >
...<
/map>
複製程式碼
初始化地圖時將地圖中心移動至startPosition,如果startPosition不存在,就將地圖中心移動至wx.getLocation()獲取的當前位置座標
initLocation(){
if (this.startPosition.length) {
this.latitude = this.startPosition[0] this.longitude = this.startPosition[1]
} else {
wx.getLocation({
type: "gcj02", success: (res) =>
{
this.longitude = res.longitude this.latitude = res.latitude
}
})
}
}複製程式碼
採用隨機資料模擬附近的車,然後新增到this.markers中,車的圖示根據curNavIndex動態設定,這樣就可以在選擇不同的服務時展示不同的車圖示
this.markers = [] const carNum = getRandomNum(3, 8) for (let i = 1;
i <
= carNum;
i++) {
// 定義一個車物件 let car = {
id: 0, iconPath: "/static/img/car/cart1.png", latitude: 0, longitude: 0, width: 35, height: 15
} //隨機值 const lon_dis = (Math.ceil(Math.random() * 99)) * 0.00012;
const lat_dis = (Math.ceil(Math.random() * 99)) * 0.00012;
car.id = 2 + i car.latitude = this.latitude + lat_dis car.longitude = this.longitude + lon_dis car.iconPath = `/static/img/car/cart${this.curNavIndex + 1
}.png` this.markers.push(car)
}複製程式碼
地圖中心的紅色定點陣圖標以及接駕時間的文字是用cover-view包裹cover-image實現
<
cover-view class="center-marker">
<
cover-view class="text-center">
最快{{minutes
}
}分鐘接駕<
/cover-view>
<
cover-image class="inverted-triangle" src="/static/img/triangle-down.png">
<
/cover-image>
<
cover-image class="img-center" src="/static/img/marker2.png">
<
/cover-image>
<
/cover-view>
複製程式碼
其中inverted-triangle是一個倒三角形圖片,因為cover-view無法實現複雜css樣式,所以底部的倒三角形效果只能用圖片實現。
map這裡不推薦使用controls,官方也說明 controls即將廢棄,請使用 cover-view
選擇目的地
這裡首先獲取到state中的curCity,利用qqmapsdk.getSuggestion(),並將其引數region設定為curCity, 就可以進行地址模糊檢索。選中地址時,利用qqmapsdk.geocoder()進行地址解析,得到目的地的相關資料,再將資料通過mapMutations存入state中
computed: {
...mapState([ 'curCity' ])
}複製程式碼
模糊檢索
qqmapsdk.getSuggestion({
keyword: value, region: this.curCity, success: (res) =>
{
this.addresses = res.data
}
})複製程式碼
點選地址時,解析地址儲存資料
choosePlace(item){
//item.address詳細地址 //item.title簡短語義化地址 console.log(item) qqmapsdk.geocoder({
address: item.address, success: (res) =>
{
this.saveEndPosition([res.result.location.lat, res.result.location.lng]) this.saveDestination(item.title) this.goBack()
}, fail: (err) =>
{
console.log(err)
}
})
}複製程式碼
mapMutations
methods: {
...mapMutations({
saveDestination: 'SET_DESTINATION', saveEndPosition: 'SET_END_POSITION'
})
} 複製程式碼
選擇城市
這裡的樣式是按照現在的滴滴小程式實現,只要將選中的城市儲存在state中的curCity就好了,搜尋功能暫未開發。獲取城市列表資料用到了騰訊地圖的api中的qqmapsdk.getCityList()。這裡其實就是資料的過濾與處理,先初始化了一個空物件temp_citys,然後根據城市的拼音的首字母的大寫建立key,對應value為一個陣列,陣列裡麵包含所有以這個拼音字母開頭的城市,最後將temp_citys賦值給this.cityList
qqmapsdk.getCityList({
success: (res) =>
{
const result = res.result[1] let temp_citys = {
} //使用temp_citys 避免頻繁改動data裡面的資料 for (let i = 0;
i <
result.length;
i++) {
let key = result[i].pinyin[0].charAt(0).toLocaleUpperCase() if (!temp_citys[key]) {
temp_citys[key] = []
} temp_citys[key].push(result[i].fullname)
} this.cityList = temp_citys
}
})複製程式碼
其他的一些頁面就不提了,感興趣的小夥伴可以去看下原始碼
使用mpvue的一些好處
可以使用vuex
使用vuex進行狀態管理,可以更方便地構建複雜應用。這裡講一個除錯小技巧,使用createLogger(),使用之後就可以在Console中清楚地看到state的變化
在store下的index.js
import Vue from 'vue'import Vuex from 'vuex'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({
state, mutations, strict: debug, plugins: debug ? [createLogger()] : []
})複製程式碼
使用vuex時要記得在對應頁面的main.js引入store,並將store賦給Vue.prototype.$store
例如:
import Vue from 'vue'import App from './wait.vue'import store from '../../store/index'Vue.prototype.$store = storeconst app = new Vue(App)app.$mount()複製程式碼
元件化開發
使用mpvue元件化開發更加方便,也方便將元件移植到其他專案中,完整的Vue開發體驗,提高了程式碼複用性。
例如 這裡的search-bar:
<
template>
<
div class="search-bar">
<
div class="text-location" @click.stop="chooseCity">
{{curCity
}
}<
/div>
<
input type="text" v-model="search" class="input-location" placeholder="你在哪兒上車" placeholder-style="color:#cccccc">
<
div class="cancel-location" @click.stop="cancel">
取消<
/div>
<
/div>
<
/template>
<
script type="text/ecmascript-6">
import {debounce
} from '../utils/index' export default{
props: {
curCity: {
type: String, default: '暫無'
}
}, data(){
return {
search: ''
}
}, methods: {
cancel(){
this.$emit('cancel')
}, clear(){
this.search = ''
}, chooseCity(){
this.$emit('chooseCity')
}
}, watch: {
search(newVal){
debounce(() =>
{
this.$emit('search', newVal)
}, 500)()
}
}
}<
/script>
複製程式碼
這裡為了節流處理,引入了debounce()函式
可以使用Async/await
原生小程式已經支援Promise,但對於async/await還不支援,利用mpvue框架我們可以封裝一些非同步函式,避免回撥地獄。
例如:網路請求
export function request(url, method = 'GET', data, header = {
}) {
return new Promise((resolve, reject) =>
{
wx.showLoading({title: '玩命載入中...'
}) wx.request({
url: baseUrl + url, method, data, header: {'Content-Type': 'json'
}, success: function (res) {
if (res.statusCode === 200) {
resolve(res.data)
} else {
showToast('發生未知錯誤!') reject(res.data)
}
}, fail: function () {
showToast('獲取資料失敗!')
}, complete:function () {
wx.hideLoading()
}
})
})
}複製程式碼
async getInitData(){
const res = await request('/comments') ...
}複製程式碼
使用mpvue的一些坑
年輕人比較衝動,愣頭青,說多了都是眼淚,官方文件一定要好好看,首先提一下常規的一些坑。
巢狀列表渲染
只是需要注意一點,巢狀列表渲染,必須指定不同的索引!
示例:
<
!-- 在這種巢狀迴圈的時候, index 和 itemIndex 這種索引是必須指定,且別名不能相同,正確的寫法如下 -->
<
template>
<
ul v-for="(card, index) in list">
<
li v-for="(item, itemIndex) in card">
{{item.value
}
} <
/li>
<
/ul>
<
/template>
複製程式碼
regionchange
bindregionchange 事件直接在 dom 上將bind改為@regionchange,同時這個事件也非常特殊,它的 event type 有 begin 和 end 兩個,導致我們無法在handleProxy 中區分到底是什麼事件,所以你在監聽此類事件的時候要同時監聽事件名和事件型別
<
map @regionchange="functionName" @end="functionName" @begin="functionName">
<
map>
複製程式碼
事件觸發問題
舉個簡單的例子,slider元件有一個bindchange屬性,它是完成一次拖動後觸發的事件,那麼如果我們想取對應的值該怎麼操作。
在小程式中我們使用:event.detail
但在 mpvue中要這樣寫:event.mp.detail
map閃動
動態更新markers時,地圖會閃動,導致無法移動地圖,這個可是一個大坑
地圖元件bindregionchange的bug:
https://github.com/Meituan-Dianping/mpvue/issues/401
原因:mpvue在更新某個屬性時都會更新整個data, 在資料量比較大的情況下效率低下,而且頻繁改動data裡面的資料也會導致卡頓問題
解決方案:每次更新資料時使用髒檢查優化
但是個人覺得這種直接改原始碼的方式還是比較妖怪的,於是找到了另一種辦法
<
map class="map-didi" id="map-didi" @regionchange="regionChange" @begin="begin" @end="end" >
<
/map>
複製程式碼
let touchTimeStamp = 0 regionChange(){
}, begin({timeStamp
}){
touchTimeStamp = timeStamp
}, end({timeStamp
}){// 加入時間判斷 if (timeStamp - touchTimeStamp >
50) {
this.mapCtx.getCenterLocation({
success: (res) =>
{
reverseGeocoder(qqmapsdk, res).then(res =>
{
this.saveStartPlace(res.result.address) this.saveFormattedStartPlace(res.result.formatted_addresses.recommend)
}) const lon_distance = res.longitude - this.longitude const lat_distance = res.latitude - this.latitude // 更新當前位置座標 this.longitude = res.longitude this.latitude = res.latitude //判斷螢幕移動的距離,如果超過閥值 if (Math.abs(lon_distance) >
= 0.0022 || Math.abs(lat_distance) >
= 0.0022) {
//重新整理附近的車 this.updateCars() //重新整理等待時間 this.minutes = getRandomNum(3, 12)
}
}
})
}
}複製程式碼
為了防止map不斷地觸發begin,end事件導致data頻繁更新,這裡做了雙重判斷,當end事件的觸發時間減去start事件的觸發時間超過一個設定的時間,當中心點移動的距離超過一個閥值,我們才去更新data資料,這裡其實相當於做了節流處理。
小程式的一些坑
常規的坑就不提了,這裡說一下奇葩的坑。
cover-view的坑
cover-view覆蓋在原生元件之上的文字檢視,可覆蓋的原生元件包括map、video、canvas、camera、live-player、live-pusher,只支援巢狀cover-view、cover-image。
只支援基本的定位、佈局、文字樣式。不支援設定單邊的border、background-image、shadow、overflow: visible等
那如果我們想在cover-view裡實現單邊的border應該怎麼做?
可以在cover-view裡再增加一個寬度1px的cover-view來模擬單邊border
<
cover-view class="footer-bar">
<
cover-view class="text" @click.stop="cancel">
取消訂單 <
/cover-view>
<
cover-view class="right-border">
<
/cover-view>
<
cover-view class="text" @click.stop="endTrip">
結束行程 <
/cover-view>
<
cover-view class="right-border">
<
/cover-view>
<
cover-view class="text">
下載滴滴APP<
/cover-view>
<
/cover-view>
複製程式碼
.footer-bar {
padding: 0 12px;
display: flex;
align-items: center;
height: 44px;
color: rgba(0, 0, 0, .7);
background: #fff;
font-size: 0;
.text {
flex: 1 1 auto;
display: inline-block;
height: 22px;
line-height: 22px;
text-align: center;
font-size: 18px;
} .right-border {
flex: 0 0 1px;
height: 22px;
width: 1px;
background-color: #d9d9d9;
}
}複製程式碼
map元件的層級最高,如何在map元件上做出陰影效果呢?
實現方式其實也是類似,利用cover-image新增一張能覆蓋在map元件之上的圖片來模擬陰影
具體實現請看這篇文章: juejin.im/post/5b1a10…
專案地址
歡迎小夥伴來一起交流學習,喜歡專案的話請給顆小星星
總結
學習之路漫漫,不必急於求成。技術日新月異,掌握不變的核心才是王道。不斷打磨作品的感覺也挺好的,如果以後有機會的話再慢慢完善。
另外本人目前大三,準備暑假後找實習,有沒有廣州的大大願意收留下我。。。
友情連結
滴滴一夏, 小程式專車來了
https://juejin.im/post/5b15ce94f265da6e29010554
網路請求request的坑
www.cnblogs.com/huangenai/p…
mpvue + vuex 開發微信小程式 mapState、mapGetters不可用問題