客户端:https://github.com/jrainlau/t...javascript
通过为期两个晚上下班时间的努力,终于把我第一个小程序开发完成并发布上线了。整个过程还算顺利,因为使用了mpvue
方案进行开发,故能够享受和vue
一致的流畅开发体验;后台系统使用了python3
+flask
框架进行,使用最少的代码完成了小程序的后台逻辑。除了开发以外,还实实在在地体验了一把微信小程序的开发流程,包括开发者工具的使用、体验版的发布、上线的申请等等。这些开发体验都很是值得被记录下来,因而便趁热打铁,写下这篇文章。vue
因为公司里有至关多的同事都住在同一个小区,因此上下班的时候常常会在公司群里组织拼车。可是因为彻底依赖聊天记录,且上下班拼车的同事也不少,依赖群聊很容易把消息刷走,并且容易形成信息错乱。既然如此,那么彻底能够开发一个小工具把这些问题解决。java
发起拼车的人把出发地点、目的地点、打车信息以卡片的形式分享出来,参与拼车的人点击卡片就能选择参加拼车,而且能看到同车拼友是谁,拼单的信息等等内容。python
交互流程以下:git
能够看到,逻辑是很是简单的,咱们只须要保证生成拼单、分享拼单、进入拼单和退出拼单这四个功能就好。github
需求和功能已经肯定好,首先按照小程序官网的介绍,注册好小程序并拿到appId
,接下来能够开始进行后台逻辑的开发。vuex
因为时间仓促,功能又简单,因此并无考虑任何高并发等复杂场景,仅仅考虑功能的实现。从需求的逻辑能够知道,其实后台只须要维护两个列表,分别存储当前全部拼车单以及当前全部参与了拼车的用户便可,其数据结构以下:vue-cli
billsList
inBillUsers
当用户肯定并分享了一个拼单以后,会直接新建一个拼单,同时把该用户添加到当前全部参与了拼车的用户列表列表里面,而且添加到该拼单的成员列表当中:npm
只要维护好这两个列表,接下来就是具体的业务逻辑了。
为了快速开发,这里我使用了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
目录,做为小程序的首页。
该首页的业务逻辑以下:
在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携带的内容,亲测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添加到白名单才能够正常使用。
这个小程序如今已经发布上线了,算是完总体验了一把小程序的开发乐趣。小程序获得了微信团队的大力支持,之后的生态只会愈来愈繁荣。当初小程序上线的时候我也对它有一些抵触,但后来想了想,这只不过是前端工程师所需面对的又一个“端“而已,没有必要为它戴上有色眼镜,多掌握一些老是好的。
“一块儿打车吧”微信小程序依然是一个玩具般的存在,仅供本身学习和探索,固然也欢迎各位读者可以贡献代码,参与开发~