據說2018將是小程序爆發的一年,也許是隨波追流吧,做爲一名前端學習者,我也開始玩起了小程序,從原先在掘金看別人寫的小程序項目,到現在不知不覺就本身倒騰了一個多月,也想作點東西練練手,因而有了這個小項目。css
今年,青桔單車登陸了我所在的城市,外形簡約時尚,反正是特別喜歡。恰好我又在研究小程序,因而就想仿寫一個青桔單車小程序的前端實現,大廠的項目仍是很牛逼的,有很多提高體驗的細節,值得學習,我也在其中注入了一些本身的想法,整體來講,我認爲共享單車類小程序,其實體驗還能夠作得更好。html
在這個項目過程當中,踩了挺多坑的,很值得記錄下來。因而行文,將我實現過程的種種,做爲分享,但願能幫助到一些同窗。前端
模擬單車刷新 git
重置定位 判斷距離最近單車 點擊單車自動路徑規劃 輸入框手機號自動分割 模擬登陸 模擬掃碼騎行 結束騎行並支付2018.6.13更新 修復了手機號顯示爲null的Buggithub
Github源碼地址:改良版青🍊單車 有須要的歡迎fork ,若是喜歡,請給個Starjson
接下來是具體內容介紹小程序
●
┣━ config # 存放僞造數據的mock
┣━ images # 圖片素材
┣━ libs # 引入的高德地圖SDK
┣━ pages ● 頁面
┣━ init //主界面
┣━ login //登陸界面
┣━ userCenter //我的中心
┣━ messageCenter //消息中心
┣━ unlock //解鎖
┣━ charge //計價
┣━ end //結束行程
┣━ repair //單車報修
┗━ record //行程記錄
┣━ utils
┣━ app.js
┣━ app.wxss
┣━ app.json
┗━ project.config.json
複製代碼
map組件堪稱小程序最複雜的一個組件,它是由客戶端建立的原生組件,而且它的層級是最高的,不能經過 z-index 控制層級。這句話意味着,普通組件,沒法覆蓋在它的上方。不過cover-view 和 cover-image 組件例外,接下來就要用到。後端
更多細節能夠查看map組件的官方文檔map組件微信小程序
實測發現,青桔單車的底部按鈕並不是button組件,而是使用view組件 不過是添加了一些樣式。底部按鈕的高度是固定的,使用相對單位rpx。在不一樣的設備上,map組件高度要能跟據設備的屏幕高度自動拉伸或者收窄,而不影響到顯示效果。使用彈性佈局是最佳的解決方案,只要設置下方按鈕flex:1,上方自適應便可。api
咱們可使用map組件得controls 作製做控件。也可使用cover-image來製做。
體驗了一下其餘的地圖類小程序,發現大部分的地圖控件使用了map組件的controls來製做,controls控件自帶按壓的交互效果,可是隻能使用圖片,沒法設置樣式,且它們的寬高大小單位默認是px,在不一樣設備上,實際體驗很詭異。
青桔單車使用cover-image是來製做覆蓋在地圖表面的控件,在樣式中經過rpx相對單位來設置控件大小,這能帶來使人舒心的效果,不只如此,這幾天忽然發現官方文檔更新了,cover-image之後將徹底代替controls
在開發地圖首頁的過程當中,最令我印象深入的莫過於此了,畢竟這深深折磨過我好長一段時間。隨意舉兩個例子,先來看看這些小程序中的效果。
有沒有陰影效果,對於實際使用來講,徹底沒有任何影響,但也許這就是前端吧,即便在map組件上實現起來各類坑,前端開發者都得去想辦法實現,正如雷布斯說過得那句話:由於咱們是工程師。哪怕它的實際體驗只能好百分之一,咱們都願意付出百分之九十九的努力。
前面有講到map組件的層級最高。這也是最坑的一點。起初我天真地覺得使用css box-shadow屬性就能搞定,坑爹的開發者工具中確實也會顯示出陰影效果,可是一到真機測試,全部的陰影都會被map組件覆蓋,嘗試了各類方法無果,而cover-view和cover-image可以支持的css樣式又只有簡單的幾種,想要在map組件上使用css作出陰影效果基本上是不可能的。
目前解決方案只有一個,就是使用cover-image,添加一張能覆蓋在map組件之上的圖片來模擬陰影。實際上,這些大廠都是這樣作的。
分析完了上述的問題,咱們就能順利作出這個主界面的效果,附上主頁的wxml你就會明白怎麼作了,樣式具體實現方法能夠查看個人源碼
<view class='map-box'>
<map id='myMap' latitude='{{latitude}}' longitude='{{longitude}}' markers='{{markers}}' polyline='{{polyline}}' scale='{{scale}}' bindcontroltap='controltap' bindregionchange='regionchange' bindmarkertap='toVisit' show-location>
<!-- 地圖上下陰影 -->
<cover-image class='map-shadow-top' src='/images/map-shadow-top.png'/>
<cover-image class='map-shadow-btm' src='/images/map-shadow-btm.png'/>
<!-- 頂部橫幅 -->
<cover-view class='top-tips'>
<cover-image class='top-icon' src='/images/top-tip.png'/>
<cover-view class='top-text'>{{topText}}</cover-view>
</cover-view>
<!-- 中心座標 -->
<cover-image class='map-icon_point' src='/images/point_in_map.png'/>
<!-- 控件 -->
<cover-image class='map-icon map-icon_msg' src='/images/icon-msg.png' bindtap='toMsg'/>
<cover-image class='map-icon map-icon_user' src='/images/icon-user.png' bindtap='toUser'/>
<cover-image class='map-icon map-icon_reset' src='/images/reset.png' bindtap='toReset'/>
</map>
</view>
<view class='main-btn' bindtap='toScan'>
<text class='main-text'>掃碼解鎖</text>
</view>
複製代碼
小程序爲咱們提供了不少好用的API,開發時能夠去查看 小程序API
只須要調用一下 wx.getLocation(OBJECT) 這個API就能夠很輕鬆地獲取到當前所在位置
wx.getLocation({
type: 'gcj02',
success: (res) => {
let longitude = res.longitude;
let latitude = res.latitude;
this.setData({
longitude,
latitude
})
})
複製代碼
地圖類小程序中,map組件上最主要地交互,莫過於重置定位這個按鈕
重置定位功能實現起來很簡單,只須要先建立一個map上下文,再調用moveToLocation()API就能夠實現
onReady() {
// 建立map上下文 保存map信息的對象
this.mapCtx = wx.createMapContext('myMap');
}
複製代碼
在使用摩拜單車小程序地時候,若是縮放過地圖視野,那麼每次重置定位後,都要再去手動縮放地圖尋找單車,由於單車扎堆在一塊兒了
在青桔單車中,體驗就好多了,重置定位後,也會重置地圖視野地縮放級別,就能很快速判斷附件單車位置,實現方法很簡單,只須要在重置定位後設置1s後調回縮放比toReset(){
//調回縮放比,提高體驗
setTimeout(()=>{
this.setData({
scale: 18
})
},1000)
this.mapCtx.moveToLocation();
}
複製代碼
這也是一個小小的細節,地圖類的小程序均可以用得上,實現的效果以下,這個體驗很酷
爲了實現一些更加高級的功能,我不得不作一些假數據,來模擬更加逼真的體驗。
我簡單的寫了一個方法用來在當前定位的座標點附件隨機生成一批單車。
tocreate(res) {
// 隨機單車數量設置 這裏設置爲1-20輛
let ran = Math.ceil(Math.random() * 20);
let markers = this.data.markers;
for(let i = 0; i < ran; i++) {
// 定義一個臨時單車對象
var t_bic = {
"id": 0,
"title":'去這裏',
"iconPath": "/images/map-bicycle.png",
"callout":{},
"latitude": 0,
"longitude": 0,
"width": 52.5,
"height": 30
}
// 隨機
var sign_a = Math.random();
var sign_b = Math.random();
// 單車分佈密集度設置
var a = (Math.ceil(Math.random() * 99)) * 0.00002;
var b = (Math.ceil(Math.random() * 99)) * 0.00002;
t_bic.id = i;
t_bic.longitude = (sign_a > 0.5 ? res.longitude + a : res.longitude - a);
t_bic.latitude = (sign_b > 0.5 ? res.latitude + b : res.latitude - b);
markers.push(t_bic);
}
//將模擬的單車數據暫時存儲到本地
wx.setStorage({
key: 'bicycle',
data: markers
})
this.setData({
markers
})
}
複製代碼
接在來只要在map組件的bindregionchange事件中調用僞造單車的函數就好了
bindregionchange事件能在map視野發送變化時觸發,可是我不但願地圖稍做移動就會刷新單車,因此還須要簡單模擬一下移動刷新單車的閾值regionchange(e){
// 拿到起點經緯度
if(e.type == 'begin') {
this.mapCtx.getCenterLocation({
type: 'gcj02',
success: (res) => {
this.setData({
lastLongitude: res.longitude,
lastLatitude: res.latitude
})
}
})
}
// 拿到當前經緯度
if (e.type == 'end') {
this.mapCtx.getCenterLocation({
type: 'gcj02',
success: (res) => {
let lon_distance = res.longitude - this.data.lastLongitude;
let lat_distance = res.latitude - this.data.lastLatitude;
// console.log(lon_distance,lat_distance)
// 判斷屏幕移動距離,若是超過設定的閾值,模擬刷新單車
if (Math.abs(lon_distance) >= 0.0035 || Math.abs(lat_distance) >= 0.0022){
console.log('刷新單車')
this.setData({
// 刷新單車以前先清空原來的單車
markers: []
})
this.tocreate(res)
}
}
})
}
}
複製代碼
這樣,就作出了以下的效果
大家應該早就發現,地圖上的單車中,距離最近的那輛單車頭上會有離我最近
一個小氣泡。 這個就是檢索出最近的單車的功能,摩拜單車就實現了這個功能,但是青桔單車官方並無加入這個小的體驗,之後應該也會有吧。這裏我嘗試去實現了一下
遍歷當前地圖上的每一輛單車和中心座標點的距離,存到一個數組中
遍歷數組,找出其中的最小值,並返回最小值的索引
在最小值的索引對應的單車中添加氣泡提示
nearestBic(res) {
// 找出最近的單車
let markers = this.data.markers;
let min_index = 0;
let distanceArr = []; //存放單車距離的數組
for (let i = 0; i < markers.length; i++) {
let lon = markers[i].longitude;
let lat = markers[i].latitude;
// 計算距離 sqrt((x1-x2)^2 + (y1-y2)^2 )
let t = Math.sqrt((lon - res.longitude) * (lon - res.longitude) + (lat - res.latitude) * (lat - res.latitude));
let distance = t;
// 將每一次計算的距離加入數組 distanceArr
distanceArr.push(distance)
}
//從距離數組中找出最小值
let min = distanceArr[0];
for (let i = 0; i < distanceArr.length; i++) {
if (parseFloat(distanceArr[i]) < parseFloat(min)) {
min = distanceArr[i];
min_index = i;
}
}
let callout = "markers[" + min_index + "].callout";
// 清除舊的氣泡,設置新氣泡
wx.getStorage({
key: 'bicycle',
success: (res) => {
this.setData({
markers: res.data,
[callout]: {
"content": '離我最近',
"color": "#ffffff",
"fontSize": "16",
"borderRadius": "50",
"padding": "10",
"bgColor": "#0082FCaa",
"display": 'ALWAYS'
}
})
}
})
}
複製代碼
將這個函數在每次刷新單車和map視野改變的時候調用,就能看到以下的效果了,詳細調用過程請移步:源碼
嗯。。。這個功能我以爲仍是有必要的,在一些場景中會遇到。
好比:我想騎車,眼前沒有車。
或者只有一輛車,打開微信掃碼,這時糟糕的結果出現了:該單車暫時沒法使用。
我仍是想騎車,不想走路,地圖的功能就發揮做用了,我會查看地圖附近別的單車,這時候看到了一些單車,可是得走一段路才能找到它,若是能夠點一下這輛單車,就自動規劃步行的路線就行了。
因而乎,我大膽地作了一個實現,以下圖
接下來說講,怎麼去實現它
想要實現自動路徑規劃的功能,本身去實現基本上不可能,咱們須要藉助第三方強大的力量來作到。
首先不知道你會不會這樣想:What?騰訊地圖裏面用高德SDK?
這沒有什麼不能夠的,在微信小程序中,不管是百度地圖、高德地圖、仍是騰訊地圖,都爲小程序專門提供了Javascript SDK
高德地圖微信小程序 SDK 能幫助咱們在小程序中獲取到豐富的地址描述、POI和實時天氣數據,以及實現地址解析和逆地址解析等功能,很是強大,不過這裏咱們只須要使用到它路徑規劃的功能
騰訊地圖和百度地圖都沒有爲微信小程序提供自動路徑規劃的功能,因此高德地圖仍是很貼心的。
想要使用它,必須前往高德地圖開放平臺進行註冊,獲取到本身的key,詳細的步驟在高德地圖微信小程序SDK入門指南中介紹得很清楚
下載好後把它解壓,在項目目錄新建一個libs文件夾把它放進去
接着在須要用到得js文件頂部引入
var amapFile = require('../../libs/amap-wx.js');
var myAmapFun = new amapFile.AMapWX({ key: '你的key' });
複製代碼
有了它,就能夠寫一個專門負責規則路徑得方法
route(bic){
// 獲取當前中心經緯度
this.mapCtx.getCenterLocation({
success: (res) => {
// 調用高德地圖步行路徑規劃API
myAmapFun.getWalkingRoute({
origin: `${res.longitude},${res.latitude}`,
destination: `${bic.longitude},${bic.latitude}`,
success: (data) => {
let points = [];
if (data.paths && data.paths[0] && data.paths[0].steps) {
let steps = data.paths[0].steps;
for (let i = 0; i < steps.length; i++) {
let poLen = steps[i].polyline.split(';');
for (let j = 0; j < poLen.length; j++) {
points.push({
longitude: parseFloat(poLen[j].split(',')[0]),
latitude: parseFloat(poLen[j].split(',')[1])
})
}
}
}
// 設置map組件polyline,繪製線路
this.setData({
polyline: [{
points: points,
color: "#ffffffaa",
arrowLine:true,
borderColor: "#3CBCA3",
borderWidth:2,
width: 5,
}]
});
}
})
}
})
}
複製代碼
微信小程序map組件提供了polyline屬性,它能在map組件上方跟據設置好的點來繪製路徑
路徑的顏色和樣式均可以設置,哇~簡直有點酷
在這裏,爲了致敬青桔單車,我儘可能的把路徑的風格作得青桔單車類似😀,而後咱們再來回顧一下效果
地圖主界面打理好了,接下來寫一下登陸界面吧。
登陸頁面看似簡單,可是想要作出一個體驗不錯的登陸界面,實際實現起來,裏面的邏輯仍是蠻多的
在青桔單車小程序中,發現了這樣一個小細節,輸入框中輸入的手機號會自動進行分割,感受這是一個不錯的用戶體驗,分割顯示的手機號,能使得輸入過程當中的錯誤一目瞭然,看起來更爽
爲了模仿出這個體驗,我按照本身的邏輯去實現了它。
個人實現邏輯思路
手機號碼都是11位的,分爲三段 XXX
空格 XXXX
空格 XXXX
,咱們在第3次輸入和第7次輸入的數字後追加空格,那不就能實現這個效果了麼
由於加入了兩個空格,因此設置輸入框最大長度爲13位
input的value
屬性綁定到邏輯層的data中的定義的phoneText,以後就能夠用js來改變它的顯示 重要!!
設置bindinput屬性,讓每次輸入都執行一下input 函數
<input class='input' placeholder='請輸入手機號' maxlength="13" value='{{phoneText}}' bindinput='input'/>
我寫了這個input方法來實現手機號的分割
input(e) {
let value = e.detail.value;
//正則過濾
value = value.replace(/[\u4E00-\u9FA5`~!@#$%^&*()_+<>?:"{},.\/;'[\]\-\sa-zA-Z]*/g, "");
let result = [];
for (let i = 0; i < value.length; i++) {
if (i == 3 || i == 7) {
result.push(" ", value.charAt(i));
}
else {
result.push(value.charAt(i));
}
}
this.setData({
phoneText: result.join("")
})
}
複製代碼
注:不過爲了作出這個效果,仍是作出一些了妥協,那就是不能調用微信內置的數字鍵盤輸入。不然將會看不到這個分割的效果
其實使用微信內置的鍵盤,能夠很方便的規避掉非法字符的輸入,也就是數字之外的字符,如:英文字母,標點符號等。
在輸入框未完成基本的填寫以前,按鈕應該是不可用的,驗證碼輸入框應該要作隱藏,待用戶填寫完以後,驗證碼輸入框出現,驗證碼輸入完畢後按鈕亮起,這樣的設定應該更加符合用戶的心理暗示
這個頁面,涉及到兩個按鈕 獲取驗證碼
以及下一步
和 一個 清楚輸入框圖標
實現效果以下,具體實現代碼請移步源碼
實現掃碼解鎖,只須要調用小程序的 wx.scanCode() 這個API,就能調用相機的掃碼功能,固然,掃碼以前先進行登陸檢查,若未登陸,切換到登陸界面,因爲只是前端功能的實現,因此掃碼後直接跳轉到解鎖界面
toScan(){
if (!app.globalData.loginStatus) {
wx.showModal({
title: '提示',
content: '請先登陸',
success: (res) => {
if (res.confirm) {
wx.navigateTo({
url: '/pages/login/login'
})
}
}
})
} else {
wx.scanCode({
success: (res) => {
onlyFromCamera: false,
console.log('掃碼成功');
wx.navigateTo({
url: '/pages/unlock/unlock',
})
}
})
}
}
複製代碼
解鎖後進入騎行狀態,效果以下:
騎行狀態下,只顯示當前騎行車輛,並在車輛上方添加氣泡,代表騎行中
共享單車計費都是跟據使用時長來判斷的,因爲沒有後端數據,這裏也只能寫一個計時器方法Time()來模擬計費
Time(){
let s = 0;
let m = 0
// 計時開始
this.timer = setInterval(() => {
this.setData({
second: s++
})
if (s == 60) {
s = 0;
m++;
setTimeout(() => {
this.setData({
minute: m
});
}, 1000)
};
}, 1000)
}
複製代碼
當騎行開始時,調用計時器,開始計時,點擊結束騎行,計時器中止,跟據時長計價,並跳轉到支付頁面
由於時間比較短,項目有一些功能還未加入,也有一些待改進的地方,後續會抽時間慢慢打磨,若是你有更好的想法,也能夠聯繫我一塊兒完善。
最後,再次附上個人項目地址:改良版青🍊單車
若是喜歡,別吝嗇你的Star哦!