基於mpvue的仿滴滴出行小程式

渣臻不想說話發表於2018-07-02

初衷

本人最近一直在研究小程式,一個偶然的機會發現了mpvue框架,剛好本人對vue十分喜愛,就決定入坑了。曾經在掘金看過一個仿舊版滴滴的小程式,感覺挺不錯的,但它是基於原生小程式的,所以就決定花了一段時間用mpvue進行重寫。下面就開始正餐了。


效果圖

基於mpvue的仿滴滴出行小程式

更多效果請檢視 github.com/QinZhen001/…



目錄結構

┣━ 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       //花費
}複製程式碼


功能詳情


頭部導航自動滑動

基於mpvue的仿滴滴出行小程式

為了讓頭部導航點選時能自動滑出,滑動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的仿滴滴出行小程式

選擇出發點


基於mpvue的仿滴滴出行小程式

在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


選擇目的地

基於mpvue的仿滴滴出行小程式

這裡首先獲取到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'
      })
}   複製程式碼


選擇城市

基於mpvue的仿滴滴出行小程式

這裡的樣式是按照現在的滴滴小程式實現,只要將選中的城市儲存在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 = store

const 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裡面的資料也會導致卡頓問題


解決方案:每次更新資料時使用髒檢查優化

github.com/Meituan-Dia…


但是個人覺得這種直接改原始碼的方式還是比較妖怪的,於是找到了另一種辦法

    <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資料,這裡其實相當於做了節流處理。

基於mpvue的仿滴滴出行小程式

小程式的一些坑

常規的坑就不提了,這裡說一下奇葩的坑。

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應該怎麼做?

基於mpvue的仿滴滴出行小程式

可以在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不可用問題

blog.csdn.net/wp_boom/art…





相關文章