记一次基于mpvue的小程序开发及上线实战

栏目: JavaScript · 发布时间: 6年前

内容简介:写于 2018.04.20经过为期两个晚上下班时间的努力,终于把我第一个小程序开发完成并发布上线了。整个过程还算顺利,由于使用了由于公司里有相当多的同事都住在同一个小区,所以上下班的时候经常会在公司群里组织拼车。但是由于完全依赖聊天记录,且上下班拼车的同事也很多,依赖群聊很容易把消息刷走,而且容易造成信息错乱。既然如此,那么完全可以开发一个小工具把这些问题解决。

写于 2018.04.20

记一次基于mpvue的小程序开发及上线实战

经过为期两个晚上下班时间的努力,终于把我第一个小程序开发完成并发布上线了。整个过程还算顺利,由于使用了 mpvue 方案进行开发,故可以享受和 vue 一致的流畅开发体验;后台系统使用了 python3 + flask 框架进行,使用最少的代码完成了小程序的后台逻辑。除了开发之外,还实实在在地体验了一把微信小程序的开发流程,包括开发者 工具 的使用、体验版的发布、上线的申请等等。这些开发体验都非常值得被记录下来,于是便趁热打铁,写下这篇文章。

一、需求&功能

由于公司里有相当多的同事都住在同一个小区,所以上下班的时候经常会在公司群里组织拼车。但是由于完全依赖聊天记录,且上下班拼车的同事也很多,依赖群聊很容易把消息刷走,而且容易造成信息错乱。既然如此,那么完全可以开发一个小工具把这些问题解决。

发起拼车的人把出发地点、目的地点、打车信息以卡片的形式分享出来,参与拼车的人点击卡片就能选择参加拼车,并且能看到同车拼友是谁,拼单的信息等等内容。

交互流程如下:

记一次基于mpvue的小程序开发及上线实战

可以看到,逻辑是非常简单的,我们只需要保证生成拼单、分享拼单、进入拼单和退出拼单这四个功能就好。

需求和功能已经确定好,首先按照小程序官网的介绍,注册好小程序并拿到 appId ,接下来可以开始进行后台逻辑的开发。

二、后台逻辑开发

由于时间仓促,功能又简单,所以并没有考虑任何高并发等复杂场景,仅仅考虑功能的实现。从需求的逻辑可以知道,其实后台只需要维护两个列表,分别存储 当前所有拼车单 以及 当前所有参与了拼车的用户 即可,其数据结构如下:

  • 当前所有拼单列表 billsList
记一次基于mpvue的小程序开发及上线实战
  • 当前所有参与了拼车的用户列表 inBillUsers
记一次基于mpvue的小程序开发及上线实战

当用户确定并分享了一个拼单之后,会直接新建一个拼单,同时把该用户添加到 当前所有参与了拼车的用户列表 列表里面,并且添加到该拼单的成员列表当中:

记一次基于mpvue的小程序开发及上线实战

只要维护好这两个列表,接下来就是具体的业务逻辑了。

为了快速开发,这里我使用了 python3 + flask 框架的方案。不懂 python 的读者看到这里也不用紧张,代码非常简单且直白,看看也无妨。

首先新建一个 BillController 类:

class BillController:
	billsList = []
	inBillUsers = []
复制代码

接下来会在这个类的内部添加 创建拼单获取拼单参与拼单退出拼单判断用户是否在某一拼单中图片上传 的功能。

1、获取拼单 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)
复制代码

2、创建拼单 createBill()

该方法会接收来自客户端的 用户信息拼单信息 ,分别添加到 billsListinBillUsers 当中。

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
		})
复制代码

创建完成后,会返回当前的 billsListinBillUsers 到客户端。

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': '用户已经在拼单中!'
			}, 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.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 ,接下来会根据这个用户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
		})
复制代码

6、图片上传 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)
    }
  })
})

复制代码

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.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
    }
复制代码

3、填写拼单并实现分享功能实现

新建一个 src/pages/index 目录,作为小程序的首页。

该首页的业务逻辑如下:

  1. 进入首页的时候先获取用户信息,得到userId
  2. 然后用userId去请求判断是否已经处于拼单
  3. 若是,则跳转到对应拼单Id的详情页
  4. 若否,才允许新建拼单

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 目录,作为小程序的“参加拼单页”。

该页面的运行逻辑如下:

  1. 首先会获取从url里面带来的billId
  2. 其次会请求一次userInfo,获取userId
  3. 然后拿这个userId去检查该用户是否已经处于拼单
  4. 如果已经处于拼单,那么就会获取一个新的billId代替从url获取的
  5. 拿当前的billId去查询对应的拼单信息
  6. 如果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 命令,打包出一个生产版本的包,然后通过小程序开发者工具的 上传 按钮上传代码,并填写测试版本号:

记一次基于mpvue的小程序开发及上线实战

接下来可以在小程序管理后台→开发管理→开发版本当中看到体验版小程序的信息,然后选择发布体验版即可:

记一次基于mpvue的小程序开发及上线实战

当确定预发布测试无误之后,就可以点击“提交审核”,正式把小程序提交给微信团队进行审核。审核的时间非常快,在3小时内基本都能够有答复。

值得注意的是,小程序所有请求的API,都必须经过 域名备案使用https 证书,同时要在设置→开发设置→服务器域名里面把API添加到白名单才可以正常使用。

五、后记

这个小程序现在已经发布上线了,算是完整体验了一把小程序的开发乐趣。小程序得到了微信团队的大力支持,以后的生态只会越来越繁荣。当初小程序上线的时候我也对它有一些抵触,但后来想了想,这只不过是前端工程师所需面对的又一个“端“而已,没有必要为它戴上有色眼镜,多掌握一些总是好的。

“一起打车吧”微信小程序依然是一个玩具般的存在,仅供自己学习和探索,当然也欢迎各位读者能够贡献代码,参与开发~


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Data Mining

Web Data Mining

Bing Liu / Springer / 2011-6-26 / CAD 61.50

Web mining aims to discover useful information and knowledge from Web hyperlinks, page contents, and usage data. Although Web mining uses many conventional data mining techniques, it is not purely an ......一起来看看 《Web Data Mining》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

MD5 加密
MD5 加密

MD5 加密工具