记一次基于 mpvue 的小程序开发及上线实战
小程序名称:一起打车吧
项目地址:
客户端:https://github.com/jrainlau/taxi-together-client
服务端:https://github.com/jrainlau/taxi-together-server
小程序二维码:
经过为期两个晚上下班时间的努力,终于把我第一个小程序开发完成并发布上线了。整个过程还算顺利,由于使用了 mpvue方案进行开发,故可以享受和 vue一致的流畅开发体验;后台系统使用了 python3+ flask框架进行,使用最少的代码完成了小程序的后台逻辑。
除了开发之外,还实实在在地体验了一把微信小程序的开发流程,包括开发者工具的使用、体验版的发布、上线的申请等等。这些开发体验都非常值得被记录下来,于是便趁热打铁,写下这篇文章。
一、需求&功能
由于公司里有相当多的同事都住在同一个小区,所以上下班的时候经常会在公司群里组织拼车。但是由于完全依赖聊天记录,且上下班拼车的同事也很多,依赖群聊很容易把消息刷走,而且容易造成信息错乱。既然如此,那么完全可以开发一个小工具把这些问题解决。
发起拼车的人把出发地点、目的地点、打车信息以卡片的形式分享出来,参与拼车的人点击卡片就能选择参加拼车,并且能看到同车拼友是谁,拼单的信息等等内容。
交互流程如下:
可以看到,逻辑是非常简单的,我们只需要保证生成拼单、分享拼单、进入拼单和退出拼单这四个功能就好。
需求和功能已经确定好,首先按照小程序官网的介绍,注册好小程序并拿到 appId,接下来可以开始进行后台逻辑的开发。
二、后台逻辑开发
由于时间仓促,功能又简单,所以并没有考虑任何高并发等复杂场景,仅仅考虑功能的实现。从需求的逻辑可以知道,其实后台只需要维护两个列表,分别存储当前所有拼车单以及当前所有参与了拼车的用户即可,其数据结构如下:
当前所有拼单列表 billsList
当前所有参与了拼车的用户列表 inBillUsers
当用户确定并分享了一个拼单之后,会直接新建一个拼单,同时把该用户添加到当前所有参与了拼车的用户列表列表里面,并且添加到该拼单的成员列表当中:
只要维护好这两个列表,接下来就是具体的业务逻辑了。
为了快速开发,这里我使用了 python3+ flask框架的方案。不懂 python的读者看到这里也不用紧张,代码非常简单且直白,看看也无妨。
首先新建一个 BillController类:
class BillController:billsList = []inBillUsers = []
接下来会在这个类的内部添加创建拼单、获取拼单、参与拼单、退出拼单、判断用户是否在某一拼单中、图片上传的功能。
1、获取拼单 getBill()
该方法接收客户端传来的拼单ID,然后拿这个ID去检索是否存在对应的拼单。若存在则返回对应的拼单,否则报错给客户端。
def getBill(self, ctx):ctxBody = ctx.formbillId = 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、创建拼单 createBill()
该方法会接收来自客户端的用户信息和拼单信息,分别添加到 billsList和 inBillUsers当中。
def createBill(self, ctx):ctxBody = ctx.formuser = {'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到客户端。
3、参与拼单 joinBill()
接收客户端传来的用户信息和拼单ID,把用户添加到拼单和 inBillUsers列表中。
def joinBill(self, ctx):ctxBody = ctx.formbillId = 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})
4、退出拼单 leaveBill()
接收客户端传来的用户ID和拼单ID,然后删除掉两个列表里面的该用户。
这个函数还有一个功能,如果判断到这个拼单ID所对应的拼单成员为空,会认为该拼单已经作废,会直接删除掉这个拼单以及所对应的车辆信息图片。
def leaveBill(self, ctx):ctxBody = ctx.formbillId = 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,接下来会根据这个用户ID去 inBillUsers里面去检索该用户所对应的拼单,如果能检索到,会返回其所在的拼单。
def inBill(self, ctx):ctxBody = ctx.formuserId = 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和图片资源,先存储图片,然后把该图片的路径写入对应拼单ID的拼单当中。
def uploadImg(self, ctx):billId = ctx.form['billId']file = ctx.files['file']filename = file.filenamefile.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)}})})
1、注册全局Store
由于开发习惯,我喜欢把所有接口请求都放在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了。
2、构建好请求的API接口
和后台系统的逻辑对应,前端也要构造好各个请求的API接口,这样的做法能够避免把API逻辑分散到页面四处,具有清晰、易维护的优势。
/*** @param {} {commit}* 获取用户公开信息*/async getUserInfo ({ commit }) {const { userInfo } = await getUserInfo({withCredenitals: false})userInfo.avatar = userInfo.avatarUrluserInfo.name = userInfo.nickNameuserInfo.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}
3、填写拼单并实现分享功能实现
新建一个 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)}}},
4、参与拼单&退出拼单功能实现
新建一个 src/pages/join目录,作为小程序的“参加拼单页”。
该页面的运行逻辑如下:
首先会获取从url里面带来的billId
其次会请求一次userInfo,获取userId
然后拿这个userId去检查该用户是否已经处于拼单
如果已经处于拼单,那么就会获取一个新的billId代替从url获取的
拿当前的billId去查询对应的拼单信息
如果billId都无效,则redirect到首页
由于要获取url携带的内容,亲测 onShow()是不行的,只能在 onLoad()里面获取:
async onLoad (options) {// 1. 首先会获取从url里面带来的billIdthis.billId = options.billId// 2. 其次会请求一次userInfo,获取userIdthis.userInfo = await this.$store.dispatch('getUserInfo')// 3. 然后拿这个userId去检查该用户是否已经处于拼单const inBill = await this.$store.dispatch('checkInBill', this.userInfo.userId)// 4. 如果已经处于拼单,那么就会有一个billIdif (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
记一次基于 mpvue 的小程序开发及上线实战的更多相关文章
- 基于mpvue搭建小程序项目框架
简介: mpvue框架对于从没有接触过小程序又要尝试小程序开发的人员来说,无疑是目前最好的选择.mpvue从底层支持 Vue.js 语法和构建工具体系,同时再结合相关UI组件库,便可以高效的实现小程序 ...
- mpvue微信小程序开发随笔
mpvue上手很快,学习成本低,目前是开源的,适合技术实力不是很强的公司采用. spring boot 做后台,开发效率杠杠的.建议会java的开发尽量使用spring boot 开发,省事. 最近用 ...
- 基于mpvue的小程序项目搭建的步骤
mpvue 是美团开源的一套语法与vue.js一致的.快速开发小程序的前端框架,按官网说可以达到小程序与H5界面使用一套代码.使用此框架,开发者将得到完整的 Vue.js 开发体验,同时为 H5 和小 ...
- 基于mpvue的小程序项目搭建的步骤一
未标题-1.png mpvue 是美团开源的一套语法与vue.js一致的.快速开发小程序的前端框架,按官网说可以达到小程序与H5界面使用一套代码.使用此框架,开发者将得到完整的 Vue.js 开发体验 ...
- 为什么选择MpVue进行小程序的开发
前言 mpvue是一款使用Vue.js开发微信小程序的前端框架.使用此框架,开发者将得到完整的 Vue.js 开发体验,同时为H5和小程序提供了代码复用的能力.如果想将 H5 项目改造为小程序,或开发 ...
- 微信小程序开发——进阶篇
由于项目的原因,最近的工作一直围绕着微信小程序.现在也算告一段落,是时候整理一下这段时间的收获了.我维护的小程序有两个,分别是官方小程序和一个游戏为主的小程序.两个都是用了wepy进行开发,就是这个: ...
- mpvue体验微信小程序开发
微信小程序 https://developers.weixin.qq.com/miniprogram/introduction/index.html?t=18082114 微信小程序是一种全新的连接用 ...
- mpvue构建小程序(步骤+地址)
mpvue 是一个使用 Vue.js 开发小程序的前端框架(美团的开源项目).框架基于 Vue.js 核心,mpvue 修改了 Vue.js 的 runtime 和 compiler 实现,使其可以运 ...
- mpvue 转小程序实践总结
介绍 Mpvue 是一个使用 Vue.js 开发小程序的前端框架. 基础介绍 框架基于 Vue.js 核心,修改了 Vue.js 的 runtime 和 compiler 实现,使其可以运行在小程序 ...
随机推荐
- 【VS开发】QueryPerformanceFrequency与QueryPerformanceCounter的使用
LARGE_INTEGER tima,timb; QueryPerformanceCounter(&tima); 在 Windows Server 2003 和 WindowsXP 中使用 Q ...
- 最新 映客java校招面经 (含整理过的面试题大全)
从6月到10月,经过4个月努力和坚持,自己有幸拿到了网易雷火.京东.去哪儿.映客等10家互联网公司的校招Offer,因为某些自身原因最终选择了映客.6.7月主要是做系统复习.项目复盘.LeetCode ...
- [转帖]linux命令dd
linux命令dd dd 是diskdump 的含义 之前学习过 总是记不住 用的还是少. http://embeddedlinux.org.cn/emb-linux/entry-level/20 ...
- 开发者福利!请及时领取您的SpreadJS临时部署授权码
SpreadJS 于2015年发布,至今已有4年历史,作为一款基于 HTML5 的纯前端电子表格控件,在短短四年间,即在财税.金融.计算机软件与服务.工业制造.大数据应用.电力能源.交通.物流运输.医 ...
- ORACLE查询进程,并杀死
用于存放常用SQL --查询主键在哪一列 --设置页大小 --设置行大小 col COLUMN_NAME for a20 --设置字段显示长度 col TABLE_NAME for a20 col O ...
- Java数组定义及方法
数组的描述 在数组中每个元素都具有相同的数据类型,是有序数据的集合.通俗的说数组就相当于一个容器.数组分为一维数组.二维数组和多维数组. 数组的特点: 数组类型是从抽象基类 Array 派生的引用 ...
- DecodingGenome(CodeForces-222E)【矩阵快速幂】
题目链接:https://vjudge.net/contest/333591#problem/L 题意:用m个字符构成长度为n的串,其中存在形如“ab”(表示a后不能放置b)的条件约束,问共有多少种构 ...
- Thinking In Java 4th Chap5 初始化和清理
类的构造器名必须与类名一致,且无返回类型,通过参数类型的不同(即使顺序不同也行)可以重载构造器,也可以以此技巧重载方法 this关键字:表示对“调用方法的那个对象的引用”,也可将当前对象传递给其他方法 ...
- Redis学习存档(2)——通过Java使用Redis:Jedis
一.创建项目,引入jedis jar包 可在百度搜索maven repository 进入后搜索jedis,复制依赖包到pom.xml文件中 <project xmlns="http: ...
- Date及DateFormat用法
Date 与DateFormat之间的转化String <————>Date Date与Calendar 之间的转化Long<————>Date 日历小程序 Scanner i ...