項目地址:前端
客戶端:https://github.com/jrainlau/taxi-together-clientvue
服務端:https://github.com/jrainlau/taxi-together-serverpython
小程序二維碼:git
通過爲期兩個晚上下班時間的努力,終於把我第一個小程序開發完成併發布上線了。整個過程還算順利,因爲使用了 mpvue
方案進行開發,故能夠享受和 vue
一致的流暢開發體驗;後臺系統使用了 python3
+ flask
框架進行,使用最少的代碼完成了小程序的後臺邏輯。github
除了開發以外,還實實在在地體驗了一把微信小程序的開發流程,包括開發者工具的使用、體驗版的發佈、上線的申請等等。這些開發體驗都很是值得被記錄下來,因而便趁熱打鐵,寫下這篇文章。vuex
因爲公司裏有至關多的同事都住在同一個小區,因此上下班的時候常常會在公司羣裏組織拼車。可是因爲徹底依賴聊天記錄,且上下班拼車的同事也不少,依賴羣聊很容易把消息刷走,並且容易形成信息錯亂。既然如此,那麼徹底能夠開發一個小工具把這些問題解決。npm
發起拼車的人把出發地點、目的地點、打車信息以卡片的形式分享出來,參與拼車的人點擊卡片就能選擇參加拼車,而且能看到同車拼友是誰,拼單的信息等等內容。flask
交互流程以下:小程序
能夠看到,邏輯是很是簡單的,咱們只須要保證生成拼單、分享拼單、進入拼單和退出拼單這四個功能就好。微信小程序
需求和功能已經肯定好,首先按照小程序官網的介紹,註冊好小程序並拿到 appId
,接下來能夠開始進行後臺邏輯的開發。
因爲時間倉促,功能又簡單,因此並無考慮任何高併發等複雜場景,僅僅考慮功能的實現。從需求的邏輯能夠知道,其實後臺只須要維護兩個列表,分別存儲當前全部拼車單以及當前全部參與了拼車的用戶便可,其數據結構以下:
當前全部拼單列表 billsList
當前全部參與了拼車的用戶列表 inBillUsers
當用戶肯定並分享了一個拼單以後,會直接新建一個拼單,同時把該用戶添加到當前全部參與了拼車的用戶列表列表裏面,而且添加到該拼單的成員列表當中:
只要維護好這兩個列表,接下來就是具體的業務邏輯了。
爲了快速開發,這裏我使用了 python3
+ flask
框架的方案。不懂 python
的讀者看到這裏也不用緊張,代碼很是簡單且直白,看看也無妨。
首先新建一個 BillController
類:
class BillController:
billsList = []
inBillUsers = []
接下來會在這個類的內部添加建立拼單、獲取拼單、參與拼單、退出拼單、判斷用戶是否在某一拼單中、圖片上傳的功能。
getBill()
該方法接收客戶端傳來的拼單ID,而後拿這個ID去檢索是否存在對應的拼單。若存在則返回對應的拼單,不然報錯給客戶端。
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)
createBill()
該方法會接收來自客戶端的用戶信息和拼單信息,分別添加到 billsList
和 inBillUsers
當中。
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': '用戶已經在拼單中!'
}, 1)
self.billsList.append(bill)
self.inBillUsers.append(user)
return response({
'billsList': self.billsList,
'inBillUsers': self.inBillUsers
})
建立完成後,會返回當前的 billsList
和 inBillUsers
到客戶端。
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': '用戶已經在拼單中!'
}, 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
})
leaveBill()
接收客戶端傳來的用戶ID和拼單ID,而後刪除掉兩個列表裏面的該用戶。
這個函數還有一個功能,若是判斷到這個拼單ID所對應的拼單成員爲空,會認爲該拼單已經做廢,會直接刪除掉這個拼單以及所對應的車輛信息圖片。
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
})
inBill()
接收客戶端傳來的用戶ID,接下來會根據這個用戶ID去 inBillUsers
裏面去檢索該用戶所對應的拼單,若是能檢索到,會返回其所在的拼單。
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
})
uploadImg()
接收客戶端傳來的拼單ID和圖片資源,先存儲圖片,而後把該圖片的路徑寫入對應拼單ID的拼單當中。
def uploadImg(self, ctx):
billId = ctx.form['billId']
file = ctx.files['file']
filename = file.filename
file.save(os.path.join('./imgs', filename))
# 把圖片信息掛載到對應的拼單
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
})
完成了業務邏輯的功能,接下來就是把它們分發給不一樣的路由了:
@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)
完整的代碼能夠直接到倉庫查看,這裏僅展現關鍵的內容。
前端藉助 vue-cli
直接使用了mpvue的mpvue-quickstart來初始化項目,具體過程再也不細述,直接進入業務開發部分。
首先,微信小程序的API都是callback風格,爲了使用方便,我把用到的小程序API都包裝成了 Promise
,統一放在 src/utils/wx.js
內部,相似下面這樣:
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)
}
})
})
因爲開發習慣,我喜歡把全部接口請求都放在store裏面的 actions
當中,因此這個小程序也是須要用到 Vuex
。但因爲小程序每個Page都是一個新的Vue實例,因此按照Vue的方式,用全局 Vue.use(Vuex)
是不會把 $store
註冊到實例當中的,這一步要手動來。
在 src/
目錄下新建一個 store.js
文件,而後在裏面進行使用註冊:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({})
接下來在 src/main.js
當中,手動在Vue的原型裏註冊一個 $store
:
import Vue from 'vue'
import App from './App'
import Store from './store'
Vue.prototype.$store = Store
這樣,之後在任何的Page裏均可以經過 this.$store
來操做這個全局Store了。
和後臺系統的邏輯對應,前端也要構造好各個請求的API接口,這樣的作法可以避免把API邏輯分散到頁面四處,具備清晰、易維護的優點。
/**
* @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
* 檢查用戶是否已經存在於某一拼單中
*/
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 出發時間
* @param { String } from 出發地點
* @param { String } to 目的地點
* @param { String } billId 拼單ID
* 建立拼單
*/
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
}
新建一個 src/pages/index
目錄,做爲小程序的首頁。
該首頁的業務邏輯以下:
進入首頁的時候先獲取用戶信息,獲得userId
而後用userId去請求判斷是否已經處於拼單
如果,則跳轉到對應拼單Id的詳情頁
若否,才容許新建拼單
在 onShow
的生命週期鉤子中實現上述邏輯:
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`)
}
},
當用戶填寫完拼單後,會點擊一個帶有 open-type="share"
屬性的button,而後會觸發 onShareAppMessage
生命週期鉤子的邏輯把拼單構形成卡片分享出去。當分享成功後會跳轉到對應拼單ID的參加拼單頁。
onShareAppMessage (result) {
let title = '一塊兒拼車'
let path = '/pages/index'
if (result.from === 'button') {
this.billId = 'billId-' + new Date().getTime()
title = '我發起了一個拼車'
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
})
// 分享成功後,會帶着billId跳轉到參加拼單頁
wx.redirectTo(`../join/main?billId=${this.billId}`)
},
fail (e) {
console.log(e)
}
}
},
新建一個 src/pages/join
目錄,做爲小程序的「參加拼單頁」。
該頁面的運行邏輯以下:
首先會獲取從url裏面帶來的billId
其次會請求一次userInfo,獲取userId
而後拿這個userId去檢查該用戶是否已經處於拼單
若是已經處於拼單,那麼就會獲取一個新的billId代替從url獲取的
拿當前的billId去查詢對應的拼單信息
若是billId都無效,則redirect到首頁
因爲要獲取url攜帶的內容,親測 onShow()
是不行的,只能在 onLoad()
裏面獲取:
async onLoad (options) {
// 1. 首先會獲取從url裏面帶來的billId
this.billId = options.billId
// 2. 其次會請求一次userInfo,獲取userId
this.userInfo = await this.$store.dispatch('getUserInfo')
// 3. 而後拿這個userId去檢查該用戶是否已經處於拼單
const inBill = await this.$store.dispatch('checkInBill', this.userInfo.userId)
// 4. 若是已經處於拼單,那麼就會有一個billId
if (inBill.inBill) {
this.billId = inBill.inBill.billId
}
// 5. 若是沒有處於拼單,那麼將請求當前billId的拼單
// 6. 若是billId都無效,則redirect到首頁,不然檢查當前用戶是否處於該拼單當中
await this.getBillInfo()
}
此外,當用戶點擊「參與拼車」後,須要從新請求拼單信息,以刷新視圖拼車人員列表;當用戶點擊「退出拼車」後,要重定向到首頁。
通過上面幾個步驟,客戶端的邏輯已經完成,能夠進行預發佈了。
若是要發佈預發佈版本,須要運行 npm run build
命令,打包出一個生產版本的包,而後經過小程序開發者工具的上傳按鈕上傳代碼,並填寫測試版本號:
接下來能夠在小程序管理後臺→開發管理→開發版本當中看到體驗版小程序的信息,而後選擇發佈體驗版便可:
當肯定預發佈測試無誤以後,就能夠點擊「提交審覈」,正式把小程序提交給微信團隊進行審覈。審覈的時間很是快,在3小時內基本都可以有答覆。
值得注意的是,小程序全部請求的API,都必須通過域名備案和使用https證書,同時要在設置→開發設置→服務器域名裏面把API添加到白名單才能夠正常使用。
這個小程序如今已經發布上線了,算是完總體驗了一把小程序的開發樂趣。小程序獲得了微信團隊的大力支持,之後的生態只會愈來愈繁榮。當初小程序上線的時候我也對它有一些抵觸,但後來想了想,這只不過是前端工程師所需面對的又一個「端「而已,沒有必要爲它戴上有色眼鏡,多掌握一些老是好的。
「一塊兒打車吧」微信小程序依然是一個玩具般的存在,僅供本身學習和探索,固然也歡迎各位讀者可以貢獻代碼,參與開發~
SegmentFault Hackathon 2018 盛大開場,Let's hack !
轉載自:https://mp.weixin.qq.com/s/67x-OfkFzLFjTu_qyHJwAA