摘要:一起打車吧微信小程序依然是一個玩具般的存在,僅供自己學(xué)習(xí)和探索,當(dāng)然也歡迎各位讀者能夠貢獻(xiàn)代碼,參與開發(fā)
小程序名稱:一起打車吧
項目地址:
客戶端:https://github.com/jrainlau/t...
服務(wù)端:https://github.com/jrainlau/t...
小程序二維碼:
經(jīng)過為期兩個晚上下班時間的努力,終于把我第一個小程序開發(fā)完成并發(fā)布上線了。整個過程還算順利,由于使用了mpvue方案進(jìn)行開發(fā),故可以享受和vue一致的流暢開發(fā)體驗;后臺系統(tǒng)使用了python3+flask框架進(jìn)行,使用最少的代碼完成了小程序的后臺邏輯。除了開發(fā)之外,還實實在在地體驗了一把微信小程序的開發(fā)流程,包括開發(fā)者工具的使用、體驗版的發(fā)布、上線的申請等等。這些開發(fā)體驗都非常值得被記錄下來,于是便趁熱打鐵,寫下這篇文章。
一、需求&功能由于公司里有相當(dāng)多的同事都住在同一個小區(qū),所以上下班的時候經(jīng)常會在公司群里組織拼車。但是由于完全依賴聊天記錄,且上下班拼車的同事也很多,依賴群聊很容易把消息刷走,而且容易造成信息錯亂。既然如此,那么完全可以開發(fā)一個小工具把這些問題解決。
發(fā)起拼車的人把出發(fā)地點(diǎn)、目的地點(diǎn)、打車信息以卡片的形式分享出來,參與拼車的人點(diǎn)擊卡片就能選擇參加拼車,并且能看到同車拼友是誰,拼單的信息等等內(nèi)容。
交互流程如下:
可以看到,邏輯是非常簡單的,我們只需要保證生成拼單、分享拼單、進(jìn)入拼單和退出拼單這四個功能就好。
需求和功能已經(jīng)確定好,首先按照小程序官網(wǎng)的介紹,注冊好小程序并拿到appId,接下來可以開始進(jìn)行后臺邏輯的開發(fā)。
二、后臺邏輯開發(fā)由于時間倉促,功能又簡單,所以并沒有考慮任何高并發(fā)等復(fù)雜場景,僅僅考慮功能的實現(xiàn)。從需求的邏輯可以知道,其實后臺只需要維護(hù)兩個列表,分別存儲當(dāng)前所有拼車單以及當(dāng)前所有參與了拼車的用戶即可,其數(shù)據(jù)結(jié)構(gòu)如下:
當(dāng)前所有拼單列表billsList
當(dāng)前所有參與了拼車的用戶列表inBillUsers
當(dāng)用戶確定并分享了一個拼單之后,會直接新建一個拼單,同時把該用戶添加到當(dāng)前所有參與了拼車的用戶列表列表里面,并且添加到該拼單的成員列表當(dāng)中:
只要維護(hù)好這兩個列表,接下來就是具體的業(yè)務(wù)邏輯了。
為了快速開發(fā),這里我使用了python3+flask框架的方案。不懂python的讀者看到這里也不用緊張,代碼非常簡單且直白,看看也無妨。
首先新建一個BillController類:
class BillController: billsList = [] inBillUsers = []
接下來會在這個類的內(nèi)部添加創(chuàng)建拼單、獲取拼單、參與拼單、退出拼單、判斷用戶是否在某一拼單中、圖片上傳的功能。
1、獲取拼單getBill()該方法接收客戶端傳來的拼單ID,然后拿這個ID去檢索是否存在對應(yīng)的拼單。若存在則返回對應(yīng)的拼單,否則報錯給客戶端。
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()
該方法會接收來自客戶端的用戶信息和拼單信息,分別添加到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)建完成后,會返回當(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,然后刪除掉兩個列表里面的該用戶。
這個函數(shù)還有一個功能,如果判斷到這個拼單ID所對應(yīng)的拼單成員為空,會認(rèn)為該拼單已經(jīng)作廢,會直接刪除掉這個拼單以及所對應(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,接下來會根據(jù)這個用戶ID去inBillUsers里面去檢索該用戶所對應(yīng)的拼單,如果能檢索到,會返回其所在的拼單。
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和圖片資源,先存儲圖片,然后把該圖片的路徑寫入對應(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)) # 把圖片信息掛載到對應(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/") def getImg(filename): return send_from_directory("./imgs", filename)
完整的代碼可以直接到倉庫查看,這里僅展示關(guān)鍵的內(nèi)容。
三、前端業(yè)務(wù)開發(fā)前端借助vue-cli直接使用了mpvue的mpvue-quickstart來初始化項目,具體過程不再細(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、注冊全局Store
由于開發(fā)習(xí)慣,我喜歡把所有接口請求都放在store里面的actions當(dāng)中,所以這個小程序也是需要用到Vuex。但由于小程序每一個Page都是一個新的Vue實例,所以按照Vue的方式,用全局Vue.use(Vuex)是不會把$store注冊到實例當(dāng)中的,這一步要手動來。
在src/目錄下新建一個store.js文件,然后在里面進(jìn)行使用注冊:
import Vue from "vue" import Vuex from "vuex" Vue.use(Vuex) export default new Vuex.Store({})
接下來在src/main.js當(dāng)中,手動在Vue的原型里注冊一個$store:
import Vue from "vue" import App from "./App" import Store from "./store" Vue.prototype.$store = Store
這樣,以后在任何的Page里都可以通過this.$store來操作這個全局Store了。
2、構(gòu)建好請求的API接口和后臺系統(tǒng)的邏輯對應(yīng),前端也要構(gòu)造好各個請求的API接口,這樣的做法能夠避免把API邏輯分散到頁面四處,具有清晰、易維護(hù)的優(yōu)勢。
/** * @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ā)時間 * @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、填寫拼單并實現(xiàn)分享功能實現(xiàn)
新建一個src/pages/index目錄,作為小程序的首頁。
該首頁的業(yè)務(wù)邏輯如下:
進(jìn)入首頁的時候先獲取用戶信息,得到userId
然后用userId去請求判斷是否已經(jīng)處于拼單
若是,則跳轉(zhuǎn)到對應(yīng)拼單Id的詳情頁
若否,才允許新建拼單
在onShow的生命周期鉤子中實現(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)用戶填寫完拼單后,會點(diǎn)擊一個帶有open-type="share"屬性的button,然后會觸發(fā)onShareAppMessage生命周期鉤子的邏輯把拼單構(gòu)造成卡片分享出去。當(dāng)分享成功后會跳轉(zhuǎn)到對應(yīng)拼單ID的參加拼單頁。
onShareAppMessage (result) { let title = "一起拼車" let path = "/pages/index" if (result.from === "button") { this.billId = "billId-" + new Date().getTime() title = "我發(fā)起了一個拼車" 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跳轉(zhuǎn)到參加拼單頁 wx.redirectTo(`../join/main?billId=${this.billId}`) }, fail (e) { console.log(e) } } },4、參與拼單&退出拼單功能實現(xiàn)
新建一個src/pages/join目錄,作為小程序的“參加拼單頁”。
該頁面的運(yùn)行邏輯如下:
首先會獲取從url里面帶來的billId
其次會請求一次userInfo,獲取userId
然后拿這個userId去檢查該用戶是否已經(jīng)處于拼單
如果已經(jīng)處于拼單,那么就會獲取一個新的billId代替從url獲取的
拿當(dāng)前的billId去查詢對應(yīng)的拼單信息
如果billId都無效,則redirect到首頁
由于要獲取url攜帶的內(nèi)容,親測onShow()是不行的,只能在onLoad()里面獲取:
async onLoad (options) { // 1. 首先會獲取從url里面帶來的billId this.billId = options.billId // 2. 其次會請求一次userInfo,獲取userId this.userInfo = await this.$store.dispatch("getUserInfo") // 3. 然后拿這個userId去檢查該用戶是否已經(jīng)處于拼單 const inBill = await this.$store.dispatch("checkInBill", this.userInfo.userId) // 4. 如果已經(jīng)處于拼單,那么就會有一個billId if (inBill.inBill) { this.billId = inBill.inBill.billId } // 5. 如果沒有處于拼單,那么將請求當(dāng)前billId的拼單 // 6. 如果billId都無效,則redirect到首頁,否則檢查當(dāng)前用戶是否處于該拼單當(dāng)中 await this.getBillInfo() }
此外,當(dāng)用戶點(diǎn)擊“參與拼車”后,需要重新請求拼單信息,以刷新視圖拼車人員列表;當(dāng)用戶點(diǎn)擊“退出拼車”后,要重定向到首頁。
經(jīng)過上面幾個步驟,客戶端的邏輯已經(jīng)完成,可以進(jìn)行預(yù)發(fā)布了。
四、預(yù)發(fā)布&申請上線如果要發(fā)布預(yù)發(fā)布版本,需要運(yùn)行npm run build命令,打包出一個生產(chǎn)版本的包,然后通過小程序開發(fā)者工具的上傳按鈕上傳代碼,并填寫測試版本號:
接下來可以在小程序管理后臺→開發(fā)管理→開發(fā)版本當(dāng)中看到體驗版小程序的信息,然后選擇發(fā)布體驗版即可:
當(dāng)確定預(yù)發(fā)布測試無誤之后,就可以點(diǎn)擊“提交審核”,正式把小程序提交給微信團(tuán)隊進(jìn)行審核。審核的時間非常快,在3小時內(nèi)基本都能夠有答復(fù)。
值得注意的是,小程序所有請求的API,都必須經(jīng)過域名備案和使用https證書,同時要在設(shè)置→開發(fā)設(shè)置→服務(wù)器域名里面把API添加到白名單才可以正常使用。
五、后記這個小程序現(xiàn)在已經(jīng)發(fā)布上線了,算是完整體驗了一把小程序的開發(fā)樂趣。小程序得到了微信團(tuán)隊的大力支持,以后的生態(tài)只會越來越繁榮。當(dāng)初小程序上線的時候我也對它有一些抵觸,但后來想了想,這只不過是前端工程師所需面對的又一個“端“而已,沒有必要為它戴上有色眼鏡,多掌握一些總是好的。
“一起打車吧”微信小程序依然是一個玩具般的存在,僅供自己學(xué)習(xí)和探索,當(dāng)然也歡迎各位讀者能夠貢獻(xiàn)代碼,參與開發(fā)~
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/94442.html
摘要:一起打車吧微信小程序依然是一個玩具般的存在,僅供自己學(xué)習(xí)和探索,當(dāng)然也歡迎各位讀者能夠貢獻(xiàn)代碼,參與開發(fā) 小程序名稱:一起打車吧 項目地址:客戶端:https://github.com/jrainlau/t... 服務(wù)端:https://github.com/jrainlau/t... 小程序二維碼:showImg(https://segmentfault.com/img/bV80...
摘要:用戶綁定的邏輯主要復(fù)雜在既需要考慮微信本身的接口在不同情況下提供的數(shù)據(jù)不同,另外一方面就是考慮本身用戶模塊的業(yè)務(wù)邏輯問題。針對每一節(jié)課以及每一節(jié)系列課程生成小程序太陽碼主要涉及到幾個細(xì)節(jié)問題。 感覺已經(jīng)好久沒寫程序了,最近這段時間,一方面是學(xué)習(xí)了python,然后折騰了scrapy框架,用python寫了下守護(hù)進(jìn)程程序監(jiān)聽任務(wù)以及用redis做隊列任務(wù)通信,并開進(jìn)程來處理爬蟲任務(wù)。以上...
摘要:整個小程序所有分包大小不超過單個分包主包大小不能超過微信小程序主流框架對比應(yīng)該算是最早發(fā)布的小程序開發(fā)框架,提供了類的語法風(fēng)格和特性,現(xiàn)階段應(yīng)該也是應(yīng)用最廣泛的框架吧。不過微信官方為了防止下載離線包的時間過程,也嚴(yán)格限制了小程序包的體積。 那些年我們踩過的坑css樣式不能引用本地圖片資源,只能引用線上資源(background-image),引用本地圖片資源只能用標(biāo)簽。{{}}不能執(zhí)行...
摘要:根據(jù)官方文檔,用搭建腳手架。全局安裝創(chuàng)建一個基于模板的新項目安裝依賴啟動構(gòu)建生成的目錄結(jié)構(gòu)如圖。示例圖小知識點(diǎn),標(biāo)簽?zāi)0寮瓤梢杂美镆部梢杂眯〕绦蚶锏模热绲龋谳啿D中運(yùn)用方便高效。 1、根據(jù)官方文檔,用mpvue搭建腳手架。 # 全局安裝 vue-cli $ npm install --global vue-cli # 創(chuàng)建一個基于 mpvue-quickstart 模板的新項目 $...
閱讀 2379·2023-04-25 20:07
閱讀 3314·2021-11-25 09:43
閱讀 3674·2021-11-16 11:44
閱讀 2539·2021-11-08 13:14
閱讀 3187·2021-10-19 11:46
閱讀 905·2021-09-28 09:36
閱讀 3000·2021-09-22 10:56
閱讀 2383·2021-09-10 10:51