- 小程序名稱:一起打車吧
- 項(xiàng)目地址:客戶端:https://github.com/jrainlau/t...
服務(wù)端:https://github.com/jrainlau/t...
- 小程序二維碼:
經(jīng)過為期兩個(gè)晚上下班時(shí)間的努力,終于把我第一個(gè)小程序開發(fā)完成并發(fā)布上線了。整個(gè)過程還算順利考杉,由于使用了mpvue
方案進(jìn)行開發(fā),故可以享受和vue
一致的流暢開發(fā)體驗(yàn)借跪;后臺(tái)系統(tǒng)使用了python3
+flask
框架進(jìn)行胚宦,使用最少的代碼完成了小程序的后臺(tái)邏輯益兄。除了開發(fā)之外脊凰,還實(shí)實(shí)在在地體驗(yàn)了一把微信小程序的開發(fā)流程抖棘,包括開發(fā)者工具的使用、體驗(yàn)版的發(fā)布、上線的申請(qǐng)等等切省。這些開發(fā)體驗(yàn)都非常值得被記錄下來最岗,于是便趁熱打鐵,寫下這篇文章朝捆。
一般渡、需求&功能
由于公司里有相當(dāng)多的同事都住在同一個(gè)小區(qū),所以上下班的時(shí)候經(jīng)常會(huì)在公司群里組織拼車芙盘。但是由于完全依賴聊天記錄驯用,且上下班拼車的同事也很多,依賴群聊很容易把消息刷走何陆,而且容易造成信息錯(cuò)亂晨汹。既然如此豹储,那么完全可以開發(fā)一個(gè)小工具把這些問題解決贷盲。
發(fā)起拼車的人把出發(fā)地點(diǎn)、目的地點(diǎn)剥扣、打車信息以卡片的形式分享出來巩剖,參與拼車的人點(diǎn)擊卡片就能選擇參加拼車,并且能看到同車拼友是誰钠怯,拼單的信息等等內(nèi)容佳魔。
交互流程如下:
可以看到,邏輯是非常簡(jiǎn)單的晦炊,我們只需要保證生成拼單鞠鲜、分享拼單、進(jìn)入拼單和退出拼單這四個(gè)功能就好断国。
需求和功能已經(jīng)確定好贤姆,首先按照小程序官網(wǎng)的介紹,注冊(cè)好小程序并拿到appId
稳衬,接下來可以開始進(jìn)行后臺(tái)邏輯的開發(fā)霞捡。
二、后臺(tái)邏輯開發(fā)
由于時(shí)間倉(cāng)促薄疚,功能又簡(jiǎn)單碧信,所以并沒有考慮任何高并發(fā)等復(fù)雜場(chǎng)景,僅僅考慮功能的實(shí)現(xiàn)街夭。從需求的邏輯可以知道砰碴,其實(shí)后臺(tái)只需要維護(hù)兩個(gè)列表,分別存儲(chǔ)當(dāng)前所有拼車單以及當(dāng)前所有參與了拼車的用戶即可板丽,其數(shù)據(jù)結(jié)構(gòu)如下:
- 當(dāng)前所有拼單列表
billsList
- 當(dāng)前所有參與了拼車的用戶列表
inBillUsers
當(dāng)用戶確定并分享了一個(gè)拼單之后呈枉,會(huì)直接新建一個(gè)拼單,同時(shí)把該用戶添加到當(dāng)前所有參與了拼車的用戶列表列表里面,并且添加到該拼單的成員列表當(dāng)中:
只要維護(hù)好這兩個(gè)列表碴卧,接下來就是具體的業(yè)務(wù)邏輯了弱卡。
為了快速開發(fā),這里我使用了python3
+flask
框架的方案住册。不懂python
的讀者看到這里也不用緊張婶博,代碼非常簡(jiǎn)單且直白,看看也無妨荧飞。
首先新建一個(gè)BillController
類:
class BillController:
billsList = []
inBillUsers = []
接下來會(huì)在這個(gè)類的內(nèi)部添加創(chuàng)建拼單凡人、獲取拼單、參與拼單叹阔、退出拼單挠轴、判斷用戶是否在某一拼單中、圖片上傳的功能耳幢。
1岸晦、獲取拼單getBill()
該方法接收客戶端傳來的拼單ID,然后拿這個(gè)ID去檢索是否存在對(duì)應(yīng)的拼單睛藻。若存在則返回對(duì)應(yīng)的拼單启上,否則報(bào)錯(cuò)給客戶端。
def getBill(self, ctx):
ctxBody = ctx.form
billId = ctxBody['billId']
try:
return response([item for item in self.billsList if item['billId'] == billId][0])
except IndexError:
return response({
'errMsg': '拼單不存在店印!',
'billsList': self.billsList,
}, 1)
2冈在、創(chuàng)建拼單createBill()
該方法會(huì)接收來自客戶端的用戶信息和拼單信息,分別添加到billsList
和inBillUsers
當(dāng)中按摘。
def createBill(self, ctx):
ctxBody = ctx.form
user = {
'userId': ctxBody['userId'],
'billId': ctxBody['billId'],
'name': ctxBody['name'],
'avatar': ctxBody['avatar']
}
bill = {
'billId': ctxBody['billId'],
'from': ctxBody['from'],
'to': ctxBody['to'],
'time': ctxBody['time'],
'members': [user]
}
if ctxBody['userId'] in [item['userId'] for item in self.inBillUsers]:
return response({
'errMsg': '用戶已經(jīng)在拼單中包券!'
}, 1)
self.billsList.append(bill)
self.inBillUsers.append(user)
return response({
'billsList': self.billsList,
'inBillUsers': self.inBillUsers
})
創(chuàng)建完成后,會(huì)返回當(dāng)前的billsList
和inBillUsers
到客戶端炫贤。
3溅固、參與拼單joinBill()
接收客戶端傳來的用戶信息和拼單ID,把用戶添加到拼單和inBillUsers
列表中照激。
def joinBill(self, ctx):
ctxBody = ctx.form
billId = ctxBody['billId']
user = {
'userId': ctxBody['userId'],
'name': ctxBody['name'],
'avatar': ctxBody['avatar'],
'billId': ctxBody['billId']
}
if ctxBody['userId'] in [item['userId'] for item in self.inBillUsers]:
return response({
'errMsg': '用戶已經(jīng)在拼單中发魄!'
}, 1)
theBill = [item for item in self.billsList if item['billId'] == billId]
if not theBill:
return response({
'errMsg': '拼單不存在'
}, 1)
theBill[0]['members'].append(user)
self.inBillUsers.append(user)
return response({
'billsList': self.billsList,
'inBillUsers': self.inBillUsers
})
4、退出拼單leaveBill()
接收客戶端傳來的用戶ID和拼單ID俩垃,然后刪除掉兩個(gè)列表里面的該用戶励幼。
這個(gè)函數(shù)還有一個(gè)功能,如果判斷到這個(gè)拼單ID所對(duì)應(yīng)的拼單成員為空口柳,會(huì)認(rèn)為該拼單已經(jīng)作廢苹粟,會(huì)直接刪除掉這個(gè)拼單以及所對(duì)應(yīng)的車輛信息圖片。
def leaveBill(self, ctx):
ctxBody = ctx.form
billId = ctxBody['billId']
userId = ctxBody['userId']
indexOfUser = [i for i, member in enumerate(self.inBillUsers) if member['userId'] == userId][0]
indexOfTheBill = [i for i, bill in enumerate(self.billsList) if bill['billId'] == billId][0]
indexOfUserInBill = [i for i, member in enumerate(self.billsList[indexOfTheBill]['members']) if member['userId'] == userId][0]
# 刪除拼單里面的該用戶
self.billsList[indexOfTheBill]['members'].pop(indexOfUserInBill)
# 刪除用戶列表里面的該用戶
self.inBillUsers.pop(indexOfUser)
# 如果拼單里面用戶為空跃闹,則直接刪除這筆拼單
if len(self.billsList[indexOfTheBill]['members']) == 0:
imgPath = './imgs/' + self.billsList[indexOfTheBill]['img'].split('/getImg')[1]
if os.path.exists(imgPath):
os.remove(imgPath)
self.billsList.pop(indexOfTheBill)
return response({
'billsList': self.billsList,
'inBillUsers': self.inBillUsers
})
5嵌削、判斷用戶是否在某一拼單中inBill()
接收客戶端傳來的用戶ID毛好,接下來會(huì)根據(jù)這個(gè)用戶ID去inBillUsers
里面去檢索該用戶所對(duì)應(yīng)的拼單,如果能檢索到苛秕,會(huì)返回其所在的拼單肌访。
def inBill(self, ctx):
ctxBody = ctx.form
userId = ctxBody['userId']
if ctxBody['userId'] in [item['userId'] for item in self.inBillUsers]:
return response({
'inBill': [item for item in self.inBillUsers if ctxBody['userId'] == item['userId']][0],
'billsList': self.billsList,
'inBillUsers': self.inBillUsers
})
return response({
'inBill': False,
'billsList': self.billsList,
'inBillUsers': self.inBillUsers
})
6、圖片上傳uploadImg()
接收客戶端傳來的拼單ID和圖片資源艇劫,先存儲(chǔ)圖片吼驶,然后把該圖片的路徑寫入對(duì)應(yīng)拼單ID的拼單當(dāng)中。
def uploadImg(self, ctx):
billId = ctx.form['billId']
file = ctx.files['file']
filename = file.filename
file.save(os.path.join('./imgs', filename))
# 把圖片信息掛載到對(duì)應(yīng)的拼單
indexOfTheBill = [i for i, bill in enumerate(self.billsList) if bill['billId'] == billId][0]
self.billsList[indexOfTheBill]['img'] = url_for('getImg', filename=filename)
return response({
'billsList': self.billsList
})
完成了業(yè)務(wù)邏輯的功能店煞,接下來就是把它們分發(fā)給不同的路由了:
@app.route('/create', methods = ['POST'])
def create():
return controller.createBill(request)
@app.route('/join', methods = ['POST'])
def join():
return controller.joinBill(request)
@app.route('/leave', methods = ['POST'])
def leave():
return controller.leaveBill(request)
@app.route('/getBill', methods = ['POST'])
def getBill():
return controller.getBill(request)
@app.route('/inBill', methods = ['POST'])
def inBill():
return controller.inBill(request)
@app.route('/uploadImg', methods = ['POST'])
def uploadImg():
return controller.uploadImg(request)
@app.route('/getImg/<filename>')
def getImg(filename):
return send_from_directory('./imgs', filename)
完整的代碼可以直接到倉(cāng)庫(kù)查看蟹演,這里僅展示關(guān)鍵的內(nèi)容。
三顷蟀、前端業(yè)務(wù)開發(fā)
前端借助vue-cli
直接使用了mpvue的mpvue-quickstart來初始化項(xiàng)目酒请,具體過程不再細(xì)述,直接進(jìn)入業(yè)務(wù)開發(fā)部分鸣个。
首先羞反,微信小程序的API都是callback風(fēng)格,為了使用方便毛萌,我把用到的小程序API都包裝成了Promise
苟弛,統(tǒng)一放在src/utils/wx.js
內(nèi)部喝滞,類似下面這樣:
export const request = obj => new Promise((resolve, reject) => {
wx.request({
url: obj.url,
data: obj.data,
header: { 'content-type': 'application/x-www-form-urlencoded', ...obj.header },
method: obj.method,
success (res) {
resolve(res.data.data)
},
fail (e) {
console.log(e)
reject(e)
}
})
})
1阁将、注冊(cè)全局Store
由于開發(fā)習(xí)慣,我喜歡把所有接口請(qǐng)求都放在store里面的actions
當(dāng)中右遭,所以這個(gè)小程序也是需要用到Vuex
做盅。但由于小程序每一個(gè)Page都是一個(gè)新的Vue實(shí)例,所以按照Vue的方式窘哈,用全局Vue.use(Vuex)
是不會(huì)把$store
注冊(cè)到實(shí)例當(dāng)中的吹榴,這一步要手動(dòng)來。
在src/
目錄下新建一個(gè)store.js
文件滚婉,然后在里面進(jìn)行使用注冊(cè):
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({})
接下來在src/main.js
當(dāng)中图筹,手動(dòng)在Vue的原型里注冊(cè)一個(gè)$store
:
import Vue from 'vue'
import App from './App'
import Store from './store'
Vue.prototype.$store = Store
這樣,以后在任何的Page里都可以通過this.$store
來操作這個(gè)全局Store了让腹。
2远剩、構(gòu)建好請(qǐng)求的API接口
和后臺(tái)系統(tǒng)的邏輯對(duì)應(yīng),前端也要構(gòu)造好各個(gè)請(qǐng)求的API接口骇窍,這樣的做法能夠避免把API邏輯分散到頁(yè)面四處瓜晤,具有清晰、易維護(hù)的優(yōu)勢(shì)腹纳。
/**
* @param {} {commit}
* 獲取用戶公開信息
*/
async getUserInfo ({ commit }) {
const { userInfo } = await getUserInfo({
withCredenitals: false
})
userInfo.avatar = userInfo.avatarUrl
userInfo.name = userInfo.nickName
userInfo.userId = encodeURIComponent(userInfo.nickName + userInfo.city + userInfo.gender + userInfo.country)
commit('GET_USER_INFO', userInfo)
return userInfo
},
/**
* @param {} {commit}
* @param { String } userId 用戶ID
* 檢查用戶是否已經(jīng)存在于某一拼單中
*/
async checkInBill ({ commit }, userId) {
const res = await request({
method: 'post',
url: `${apiDomain}/inBill`,
data: {
userId
}
})
return res
},
/**
* @param {} {commit}
* @param { String } userId 用戶ID
* @param { String } name 用戶昵稱
* @param { String } avatar 用戶頭像
* @param { String } time 出發(fā)時(shí)間
* @param { String } from 出發(fā)地點(diǎn)
* @param { String } to 目的地點(diǎn)
* @param { String } billId 拼單ID
* 創(chuàng)建拼單
*/
async createBill ({ commit }, { userId, name, avatar, time, from, to, billId }) {
const res = await request({
method: 'post',
url: `${apiDomain}/create`,
data: {
userId,
name,
avatar,
time,
from,
to,
billId
}
})
commit('GET_BILL_INFO', res)
return res
},
/**
* @param {} {commit}
* @param { String } billId 拼單ID
* 獲取拼單信息
*/
async getBillInfo ({ commit }, billId) {
const res = await request({
method: 'post',
url: `${apiDomain}/getBill`,
data: {
billId
}
})
return res
},
/**
* @param {} {commit}
* @param { String } userId 用戶ID
* @param { String } name 用戶昵稱
* @param { String } avatar 用戶頭像
* @param { String } billId 拼單ID
* 參加拼單
*/
async joinBill ({ commit }, { userId, name, avatar, billId }) {
const res = await request({
method: 'post',
url: `${apiDomain}/join`,
data: {
userId,
name,
avatar,
billId
}
})
return res
},
/**
* @param {} {commit}
* @param { String } userId 用戶ID
* @param { String } billId 拼單ID
* 退出拼單
*/
async leaveBill ({ commit }, { userId, billId }) {
const res = await request({
method: 'post',
url: `${apiDomain}/leave`,
data: {
userId,
billId
}
})
return res
},
/**
* @param {} {commit}
* @param { String } filePath 圖片路徑
* @param { String } billId 拼單ID
* 參加拼單
*/
async uploadImg ({ commit }, { filePath, billId }) {
const res = await uploadFile({
url: `${apiDomain}/uploadImg`,
header: {
'content-type': 'multipart/form-data'
},
filePath,
name: 'file',
formData: {
'billId': billId
}
})
return res
}
3痢掠、填寫拼單并實(shí)現(xiàn)分享功能實(shí)現(xiàn)
新建一個(gè)src/pages/index
目錄驱犹,作為小程序的首頁(yè)。
該首頁(yè)的業(yè)務(wù)邏輯如下:
- 進(jìn)入首頁(yè)的時(shí)候先獲取用戶信息足画,得到userId
- 然后用userId去請(qǐng)求判斷是否已經(jīng)處于拼單
- 若是雄驹,則跳轉(zhuǎn)到對(duì)應(yīng)拼單Id的詳情頁(yè)
- 若否,才允許新建拼單
在onShow
的生命周期鉤子中實(shí)現(xiàn)上述邏輯:
async onShow () {
this.userInfo = await this.$store.dispatch('getUserInfo')
const inBill = await this.$store.dispatch('checkInBill', this.userInfo.userId)
if (inBill.inBill) {
wx.redirectTo(`../join/main?billId=${inBill.inBill.billId}&fromIndex=true`)
}
},
當(dāng)用戶填寫完拼單后淹辞,會(huì)點(diǎn)擊一個(gè)帶有open-type="share"
屬性的button荠医,然后會(huì)觸發(fā)onShareAppMessage
生命周期鉤子的邏輯把拼單構(gòu)造成卡片分享出去。當(dāng)分享成功后會(huì)跳轉(zhuǎn)到對(duì)應(yīng)拼單ID的參加拼單頁(yè)桑涎。
onShareAppMessage (result) {
let title = '一起拼車'
let path = '/pages/index'
if (result.from === 'button') {
this.billId = 'billId-' + new Date().getTime()
title = '我發(fā)起了一個(gè)拼車'
path = `pages/join/main?billId=${this.billId}`
}
return {
title,
path,
success: async (res) => {
await this.$store.dispatch('createBill', { ...this.userInfo, ...this.billInfo })
// 上傳圖片
await this.$store.dispatch('uploadImg', {
filePath: this.imgSrc,
billId: this.billId
})
// 分享成功后彬向,會(huì)帶著billId跳轉(zhuǎn)到參加拼單頁(yè)
wx.redirectTo(`../join/main?billId=${this.billId}`)
},
fail (e) {
console.log(e)
}
}
},
4、參與拼單&退出拼單功能實(shí)現(xiàn)
新建一個(gè)src/pages/join
目錄攻冷,作為小程序的“參加拼單頁(yè)”娃胆。
該頁(yè)面的運(yùn)行邏輯如下:
- 首先會(huì)獲取從url里面帶來的billId
- 其次會(huì)請(qǐng)求一次userInfo,獲取userId
- 然后拿這個(gè)userId去檢查該用戶是否已經(jīng)處于拼單
- 如果已經(jīng)處于拼單等曼,那么就會(huì)獲取一個(gè)新的billId代替從url獲取的
- 拿當(dāng)前的billId去查詢對(duì)應(yīng)的拼單信息
- 如果billId都無效里烦,則redirect到首頁(yè)
由于要獲取url攜帶的內(nèi)容,親測(cè)onShow()
是不行的禁谦,只能在onLoad()
里面獲刃埠凇:
async onLoad (options) {
// 1\. 首先會(huì)獲取從url里面帶來的billId
this.billId = options.billId
// 2\. 其次會(huì)請(qǐng)求一次userInfo,獲取userId
this.userInfo = await this.$store.dispatch('getUserInfo')
// 3\. 然后拿這個(gè)userId去檢查該用戶是否已經(jīng)處于拼單
const inBill = await this.$store.dispatch('checkInBill', this.userInfo.userId)
// 4\. 如果已經(jīng)處于拼單州泊,那么就會(huì)有一個(gè)billId
if (inBill.inBill) {
this.billId = inBill.inBill.billId
}
// 5\. 如果沒有處于拼單丧蘸,那么將請(qǐng)求當(dāng)前billId的拼單
// 6\. 如果billId都無效,則redirect到首頁(yè)遥皂,否則檢查當(dāng)前用戶是否處于該拼單當(dāng)中
await this.getBillInfo()
}
此外力喷,當(dāng)用戶點(diǎn)擊“參與拼車”后,需要重新請(qǐng)求拼單信息演训,以刷新視圖拼車人員列表弟孟;當(dāng)用戶點(diǎn)擊“退出拼車”后,要重定向到首頁(yè)样悟。
經(jīng)過上面幾個(gè)步驟拂募,客戶端的邏輯已經(jīng)完成,可以進(jìn)行預(yù)發(fā)布了窟她。
四陈症、預(yù)發(fā)布&申請(qǐng)上線
如果要發(fā)布預(yù)發(fā)布版本,需要運(yùn)行npm run build
命令礁苗,打包出一個(gè)生產(chǎn)版本的包爬凑,然后通過小程序開發(fā)者工具的上傳按鈕上傳代碼,并填寫測(cè)試版本號(hào):
接下來可以在小程序管理后臺(tái)→開發(fā)管理→開發(fā)版本當(dāng)中看到體驗(yàn)版小程序的信息试伙,然后選擇發(fā)布體驗(yàn)版即可:
當(dāng)確定預(yù)發(fā)布測(cè)試無誤之后嘁信,就可以點(diǎn)擊“提交審核”于样,正式把小程序提交給微信團(tuán)隊(duì)進(jìn)行審核。審核的時(shí)間非撑司福快穿剖,在3小時(shí)內(nèi)基本都能夠有答復(fù)。
值得注意的是卦溢,小程序所有請(qǐng)求的API糊余,都必須經(jīng)過域名備案和使用https證書,同時(shí)要在設(shè)置→開發(fā)設(shè)置→服務(wù)器域名里面把API添加到白名單才可以正常使用单寂。
五贬芥、后記
這個(gè)小程序現(xiàn)在已經(jīng)發(fā)布上線了,算是完整體驗(yàn)了一把小程序的開發(fā)樂趣宣决。小程序得到了微信團(tuán)隊(duì)的大力支持蘸劈,以后的生態(tài)只會(huì)越來越繁榮。當(dāng)初小程序上線的時(shí)候我也對(duì)它有一些抵觸尊沸,但后來想了想威沫,這只不過是前端工程師所需面對(duì)的又一個(gè)“端“而已,沒有必要為它戴上有色眼鏡洼专,多掌握一些總是好的棒掠。
“一起打車吧”微信小程序依然是一個(gè)玩具般的存在,僅供自己學(xué)習(xí)和探索屁商,當(dāng)然也歡迎各位讀者能夠貢獻(xiàn)代碼烟很,參與開發(fā)~
作者: jrainlau
鏈接:人類身份驗(yàn)證 - SegmentFault
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)棒假,非商業(yè)轉(zhuǎn)載請(qǐng)注明出處溯职。