初衷
聽說2018將是小程式爆發的一年,也許是隨波追流吧,作為一名前端學習者,我也開始玩起了小程式,從原先在掘金看別人寫的小程式專案,到如今不知不覺就自己倒騰了一個多月,也想做點東西練練手,於是有了這個小專案。
專案介紹
今年,青桔單車登入了我所在的城市,外形簡約時尚,反正是特別喜歡。剛好我又在研究小程式,於是就想仿寫一個青桔單車小程式的前端實現,大廠的專案還是很牛逼的,有不少提升體驗的細節,值得學習,我也在其中注入了一些自己的想法,總體來說,我認為共享單車類小程式,其實體驗還可以做得更好。
在這個專案過程中,踩了挺多坑的,很值得記錄下來。於是行文,將我實現過程的種種,作為分享,希望能幫助到一些同學。
模擬單車重新整理
重置定位
判斷距離最近單車
點選單車自動路徑規劃
輸入框手機號自動分割
模擬登入
模擬掃碼騎行
結束騎行並支付
2018.6.13更新 修復了手機號顯示為null的Bug
Github原始碼地址:改良版青?單車有需要的歡迎fork ,如果喜歡,請給個Star
具體內容
接下來是具體內容介紹
目錄結構
●┣━ 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元件,下方為一個掃碼解鎖按鈕,map元件中有三個小控制元件,一條橫幅。看起來挺簡單吧,但是想要做出這樣的介面,得先稍思考一下。
map元件堪稱小程式最複雜的一個元件,它是由客戶端建立的原生元件,並且它的層級是最高的,不能通過 z-index 控制層級。這句話意味著,普通元件,無法覆蓋在它的上方。不過cover-view 和 cover-image 元件例外,接下來就要用到。
更多細節可以檢視map元件的官方文件map元件
使用彈性佈局,安排上方地圖,底部掃碼解鎖按鈕
實測發現,青桔單車的底部按鈕並非button元件,而是使用view元件 不過是新增了一些樣式。底部按鈕的高度是固定的,使用相對單位rpx。在不同的裝置上,map元件高度要能跟據裝置的螢幕高度自動拉伸或者收窄,而不影響到顯示效果。使用彈性佈局是最佳的解決方案,只要設定下方按鈕flex:1,上方自適應即可。
使用cover-image打造體驗和諧的控制元件
我們可以使用map元件得controls 做製作控制元件。也可以使用cover-image來製作。
體驗了一下其他的地圖類小程式,發現大部分的地圖控制元件使用了map元件的controls來製作,controls控制元件自帶按壓的互動效果,但是隻能使用圖片,無法設定樣式,且它們的寬高大小單位預設是px,在不同裝置上,實際體驗很詭異。
青桔單車使用cover-image是來製作覆蓋在地圖表面的控制元件,在樣式中通過rpx相對單位來設定控制元件大小,這能帶來令人舒心的效果,不僅如此,這幾天突然發現官方文件更新了,cover-image以後將完全代替controls
在map元件上做出陰影效果
在開發地圖首頁的過程中,最令我印象深刻的莫過於此了,畢竟這深深折磨過我好長一段時間。隨意舉兩個例子,先來看看這些小程式中的效果。
有沒有陰影效果,對於實際使用來說,完全沒有任何影響,但也許這就是前端吧,即使在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視野改變的時候呼叫,就能看到如下的效果了,詳細呼叫過程請移步:原始碼
實現手動選中單車自動規劃步行至路徑
嗯。。。這個功能我覺得還是有必要的,在一些場景中會遇到。
比如:我想騎車,眼前沒有車。
或者只有一輛車,開啟微信掃碼,這時糟糕的結果出現了:該單車暫時無法使用。
我還是想騎車,不想走路,地圖的功能就發揮作用了,我會檢視地圖附近別的單車,這時候看到了一些單車,但是得走一段路才能找到它,如果可以點一下這輛單車,就自動規劃步行的路線就好了。
於是乎,我大膽地做了一個實現,如下圖
接下來講講,怎麼去實現它
想要實現自動路徑規劃的功能,自己去實現基本上不可能,我們需要藉助第三方強大的力量來做到。
引入高德地圖SDK
首先不知道你會不會這樣想: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("")
})
}複製程式碼
注:不過為了做出這個效果,還是做出一些了妥協,那就是不能呼叫微信內建的數字鍵盤輸入。否則將會看不到這個分割的效果
其實使用微信內建的鍵盤,可以很方便的規避掉非法字元的輸入,也就是數字以外的字元,如:英文字母,標點符號等。
按鈕 可用&
不可用 邏輯
在輸入框未完成基本的填寫之前,按鈕應該是不可用的,驗證碼輸入框應該要做隱藏,待使用者填寫完之後,驗證碼輸入框出現,驗證碼輸入完畢後按鈕亮起,這樣的設定應該更加符合使用者的心理暗示
這個頁面,涉及到兩個按鈕 獲取驗證碼
以及下一步
和 一個 清楚輸入框圖示
- 手機號輸入框應是不能輸入非數字以外的字元的,雖然這裡肯定不會存在xss,但是為了嚴謹,還是用正則來過濾一下
- 當輸入框中存在內容的時候,清除內容按鈕出現,清空內容後,清除內容按鈕消失,所有按鈕不可用
- 在手機號碼填寫完後,獲取驗證碼按鈕變為可用狀態,驗證碼輸入框出現
- 手機號碼和驗證碼同時滿足填寫條件後 ,下一步按鈕變為可用狀態
- 點選下一步,或者獲取驗證碼時,要校驗手機號碼是否正確
實現效果如下,具體實現程式碼請移步原始碼
掃碼解鎖功能
實現掃碼解鎖,只需要呼叫小程式的 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哦!