9实战篇6心情签到页面开发
本资源由 itjc8.com 收集整理
实战篇 6:心情签到页面开发
「新鲜天气」的心情签到页面结构比较简单,本小节主要介绍三部分内容:
- 使用日历插件
- 用户授权和登录流程
- 使用小程序云开发的数据库功能
使用日历插件
心情签到页面最重要的模块就是日历,日历使用了一个开源的日历插件,在小程序内使用插件需要经过下面三步。
1. 在小程序管理后台添加三方服务插件
登录小程序管理后台,依次进入「设置 -> 第三方服务」搜索日历插件的 AppID(wx92c68dae5a8bb046)就可以搜索到「极点日历」,这时候申请授权即可。
2. 在 app.json 中增加插件配置
第二步是在项目的 app.json 中增加 plugins
字段内容:
"plugins": {
"calendar": {
"version": "1.1.3",
"provider": "wx92c68dae5a8bb046"
}
}
3. 在 diary 页面增加组件配置
在 pages/diary/index.json 的页面配置中的 usingComponents
里增加 calendar
的插件地址:
{
"usingComponents": {
"calendar": "plugin://calendar/calendar",
"icon": "../../components/icon/index"
}
}
经过上面三步之后,我们就可以在页面中使用 <calendar />
标签了。具体日历的用法,可以参考它的 wiki 主页。
设置日历的心情颜色
在心情设置上,笔者设计了 5 种心情,由 5 种颜色来表示,具体数值如下:
// client/pages/diary/index.js Page data
emotions: ['serene', 'hehe', 'ecstatic', 'sad', 'terrified'],
colors: {
serene: '#64d9fe',
hehe: '#d3fc1e',
ecstatic: '#f7dc0e',
sad: '#ec238a',
terrified: '#ee1aea'
}
签到不同的心情,最终在日历上会展现出下面的效果:
要在某天设置该天的背景颜色,需要使用日历的 days-color
属性,这里笔者将 days-color
与 daysStyle
进行绑定:
<!--diary/index.wxml-->
<calendar days-color="{{daysStyle}}" />
daysStyle
的计算和赋值是在 setCalendarColor
方法内的:
// diary/index.js
setCalendarColor(year, month) {
year = year || new Date().getFullYear()
month = month || new Date().getMonth() + 1
// 从数据库读取数据
getEmotionByOpenidAndDate(this.data.openid, year, month)
.then((r) => {
const data = r.data || []
const styles = []
const now = new Date()
const today = dateFormat(now)
let todayEmotion = ''
let colors = this.data.colors
// 遍历日期,存在表情的日期则设置对应的颜色
data.forEach((v) => {
let ts = v.tsModified
let date = new Date(ts)
let day = date.getDate()
if (today === dateFormat(date)) {
todayEmotion = v.emotion || ''
}
styles.push({
month: 'current',
day,
color: 'black',
background: colors[v.emotion]
})
})
// 设置 daysStyle
this.setData({
lastMonth: `${year}-${('00' + month).slice(-2)}`,
showPublish: true,
todayEmotion,
daysStyle: styles
})
})
.catch((e) => {
wx.showToast({
title: '加载已签数据失败,请稍后再试',
icon: 'none',
duration: 3000
})
})
}
日历事件绑定
当日历切换月份的时候,我们应该获取当前切换到的月份,获取当前月份的心情数据,所以在 calendar
上绑定 dateChange
事件:
<!--diary/index.wxml-->
<calendar binddateChange="dateChange" />
// diary/index.js page
dateChange(e) {
// console.log(e)
let {currentYear, currentMonth} = e.detail
this.setData({
daysStyle: []
})
this.setCalendarColor(currentYear, currentMonth)
}
小程序用户登录和授权流程
在心情签到的功能开发中,需要得到用户信息,获取用户信息需要用户账号授权才可以。用户账号授权是小程序开发中经常碰见的技术点,本节重点介绍下小程序的登录授权机制。
小程序开发文档中有一张很完整的流程图(见下图),笔者会围绕这张图来介绍用户授权流程,然后结合云函数来实现一个获取用户授权信息的功能。
从这张图来看,整个数据通信过程包含了小程序、开发者服务器(云函数)和微信接口服务,这三方是都参与其中的,整个流程跟公众号和第三方登录授权流程都基本类似。
整个授权流程可分为下面五个步骤。
1. wx.login
获取临时登录凭证 code
「小程序」内调用 wx.login
方法,如果用户是第一次授权或者授权过期,则会弹出授权窗口,提示用户个人信息会被授权给第三方服务使用。这时候如果用户同意授权,则会拿到临时登录凭证 code,这个临时登录凭证有效期只有 5 分钟。我们拿到这个临时登录凭证需要调用「开发者服务器(云函数)」的接口,将临时凭证发送给服务器,然后「开发者服务器」调用「微信接口服务」的 jscode2session
接口获取 openid
和 session_key
。
wx.login({
success: () => {
if (res.code) {
// example: 081LXytJ1xxxxcdfxxx1FWxdfdsfXyth
// 将 code 发送给开发者服务器
}
}
})
2. 获取 openid
和 session_key
微信内,同一用户在任意小程序、公众号或者服务号中,都会有一个不同的唯一标识 openid
,所以可以认为,我们在应用中获取的用户 openid
是唯一的,并且该用户在另外一个应用中的 openid
跟其他应用的是不同的。
session_key
是微信服务派发给我们的一个用户登录有效性的凭证,通过它我们可以间接维护用户微信的登录态。
获取 openid
和 session_key
需要调用微信的接口:
https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
这个接口的参数为:
参数 | 必填 | 说明 |
---|---|---|
appid | 是 | 小程序唯一标识 |
secret | 是 | 小程序的 app secret |
js_code | 是 | 登录时获取的 code |
grant_type | 是 | 填写为 authorization_code |
其中 appid
和 secret
可以在小程序管理后台找到,具体路径为「设置 -> 开发设置 -> 开发者 ID」。appid
直接可见,而 secret
需要点击「生成」链接,并用开发者账号的微信扫码才能生成,生成之后需自行保存。
secret
是授权中保证安全性的一个重要 ID,不能外泄,因此必须放在开发者自己的服务器上使用,不能直接放到前端页面调用微信服务接口,因为如果这样的话 secret
就暴露了,这也是整个授权过程需要小程序、开发者服务器、微信服务三方都介入的原因。如果忘记或泄露了 secret
,需要在微信后台重置。
js_code
就是第一步中我们通过 wx.login
获取到的临时授权凭证 code。有了appid
、secret
和js_code
,我们可以写一个云函数来请求微信的 jscode2session
接口:
// 云函数名称:jscode2session
const API_URL = 'https://api.weixin.qq.com/sns/jscode2session'
const request = require('request')
const querystring = require('querystring')
/*<jdists import="../../inline/utils.js" />*/
/*<remove>*/
const $ = require('../../inline/utils')
/*</remove>*/
exports.main = async (event) => {
let {code} = event
// 这里微信的 id 和 secret 从配置文件中获取
let {id, sk} = $.getWechatAppConfig()
const data = {
appid: id,
secret: sk,
js_code: code,
grant_type: 'authorization_code'
}
let url = API_URL + '?' + querystring.stringify(data)
return new Promise((resolve, reject) => {
request.get(url, (error, response, body) => {
if (error || response.statusCode !== 200) {
reject(error)
} else {
try {
const r = JSON.parse(body)
resolve(r)
} catch (e) {
reject(e)
}
}
})
})
}
有了 jscode2session
这个云函数,我们就可以在小程序中调用云函数,将 wx.login
获取的 code 作为参数传递过去:
wx.login({
success: (res) => {
if(res.code){
wx.cloud.callFunctions({
name: 'jscode2session',
data: {
code: res.code
}
}).then(res => {
let {openid = '', session_key = ''} = res.result || {}
console.log(openid, session_key)
wx.setStorage({
key: 'openid',
data: openid
})
})
}
})
关于获取到的 session_key
,我们还需要注意以下两点。
session_key
和wx.login
获取的 code 是一一对应的,同一 code 只能换取一次session_key
。每次调用wx.login
,都会下发一个新的 code 和对应的session_key
,为了保证用户体验和登录态的有效性,开发者需要清楚用户需要重新登录时才去调用wx.login
。session_key
是有时效性的,即便是不调用wx.login
,session_key
也会过期,过期时间跟用户使用小程序的频率成正相关,但具体的时间长短开发者和用户都是获取不到的。
由于 session_key
具有实效性,因而我们可以将 session_key
存入本地缓存,每次进入小程序的时候判断下 session_key
是否过期即可:
wx.setStorage({
key: 'session_key',
data: session_key
})
3. 获取用户昵称等信息
获取用户信息需要用到 open-type="getUserInfo"
的 button
组件,具体做法是:
<button open-type="getUserInfo" bindgetuserinfo="getUserInfo">使用该功能需要授权登录</button>
上面的代码定义了一个 getUserInfo
类型的按钮,如果授权成功,则调用页面的 getUserInfo
方法(通过 bindgetuserinfo
绑定的)。getUserInfo
代码如下:
getUserInfo(){
wx.getUserInfo({
success: (res) => {
let rs = res.userInfo
this.setData({
nickname: rs.nickName,
avatarUrl: rs.avatarUrl
})
}
})
}
获取到的用户信息,包括以下几部分:
参数 | 类型 | 说明 |
---|---|---|
userInfo | OBJECT | 用户信息对象,不包含 openid 等敏感信息 |
rawData | String | 不包括敏感信息的原始数据字符串,用于计算签名 |
signature | String | 使用 sha1( rawData + sessionkey ) 得到字符串,用于校验用户信息 |
encryptedData | String | 包括敏感数据在内的完整用户信息的加密数据 |
iv | String | 加密算法的初始向量 |
除了实战中使用的包含用户昵称和头像的 userInfo
外,还有敏感信息的 encryptedData
字段,如果需要使用该字段,则需要按照加密数据解密算法的文档来解密。
4. 解密敏感数据
尽管心情签到功能并没有涉及敏感信息的解密,这里笔者还是简单介绍下如何解密敏感数据。
解密敏感信息需要用到小程序的 AppID 和 session_key
,在开发者文档中有提供 Node.js 版本的解密 demo,下面来简单实现个云函数:
const crypto = require('crypto');
/*<jdists import="../../inline/utils.js" />*/
/*<remove>*/
const $ = require('../../inline/utils')
/*</remove>*/
function WXBizDataCrypt(appId, sessionKey) {
this.appId = appId;
this.sessionKey = sessionKey;
}
WXBizDataCrypt.prototype.decryptData = function (encryptedData, iv) {
// base64 decode
const sessionKey = new Buffer(this.sessionKey, 'base64');
encryptedData = new Buffer(encryptedData, 'base64');
iv = new Buffer(iv, 'base64');
let decoded;
try {
// 解密
const decipher = crypto.createDecipheriv('aes-128-cbc', sessionKey, iv);
// 设置自动 padding 为 true,删除填充补位
decipher.setAutoPadding(true);
decoded = decipher.update(encryptedData, 'binary', 'utf8');
decoded += decipher.final('utf8');
decoded = JSON.parse(decoded);
} catch (err) {
throw new Error('Illegal Buffer');
}
if (decoded.watermark.appid !== this.appId) {
throw new Error('Illegal Buffer');
}
return decoded;
};
exports.main = async (event) => {
let {iv, data, session_key} = event
// 这里微信的 id 和 secret 从配置文件中获取
let appId = $.getWechatAppConfig().id
return new Promise((resolve, reject) => {
const pc = new WXBizDataCrypt(appId, session_key)
resolve(pc.decryptData(data, iv))
})
}
5. 检测 session_key
是否失效
前面提到 session_key
需要存入本地缓存,但是存在可能失效的情况,小程序提供的 wx.checkSession
方法可以检测当前的 session_key
是否失效,如果失效则重新调用 wx.login
登录授权流程。
wx.checkSession
方法并不需要传入任何有关 session_key
的信息参数,而是小程序自己去调自己的服务来查询用户最近一次生成的 session_key
是否过期。如果当前 session_key
过期,就让用户来重新登录,更新 session_key
,并将最新的 session_key
存入用户数据表中。
整个授权流程图和代码
用代码来表示如下:
// 或者在 app.js 内使用 onLaunch
onLoad(){
let loginFlag = wx.getStorageSync('session_key');
if (loginFlag) {
// 检查 session_key 是否过期
wx.checkSession({
// session_key 有效(未过期)
success: function() {
// 业务逻辑处理
},
// session_key 过期
fail: function() {
// session_key 过期,重新登录
this.doLogin();
}
});
) else {
// 无 session_key,作为首次登录
this.doLogin();
}
}
使用云开发数据库来存储心情数据
可以在小程序中通过 wx.cloud
相关的方法使用云开发的数据库,而且它支持权限设置,很方便存储 UGC(用户原创内容)的数据(在第 3 节介绍过)。在心情签到功能中,就使用了小程序云的数据库,用它来存储用户的签到心情。
在云开发控制台创建数据库
首先从小程序开发者工具中的「云开发」进入数据库管理 tab,然后点击「添加集合」,创建一个 diary
的集合(数据库)。
这个集合中的文档数据格式如下图所示。
其中,_id
和 _openid
是系统自动生成的,文档(表)中其他字段的意思解释如下:
- emotion:当天的心情,一共有 5 种心情
- tsModified:签到的时间戳
- openid:根据授权信息获取的 openid,跟
_openid
一致
有了数据库的信息,需要编写增加一条心情数据(addEmotion
)和获取某个月份所有心情数据(getEmotionByOpenidAndDate
)。
增加心情数据
// lib/api.js
// 初始化 cloud 环境
wx.cloud.init({
env: 'envID'
})
// 获取数据库实例
const db = wx.cloud.database()
// 用户心情签到
export const addEmotion = (openid, emotion) => {
return db.collection('diary').add({
data: {
openid,
emotion,
tsModified: Date.now()
}
})
}
获取心情数据
// lib/api.js
// 初始化 cloud 环境
wx.cloud.init({
env: 'envID'
})
// 获取数据库实例
const db = wx.cloud.database()
// 根据用户 openid 和日期获取心情数据
export const getEmotionByOpenidAndDate = (openid, year, month) => {
const _ = db.command
year = parseInt(year)
month = parseInt(month)
let start = new Date(year, month - 1, 1).getTime()
let end = new Date(year, month, 1).getTime()
// console.log(start, end, `${year}-${nextMonth}-01 00:00:00`,`${year}-${month}-01 00:00:00`)
return db
.collection('diary')
.where({
openid,
tsModified: _.gte(start).and(_.lt(end))
})
.get()
}
小程序云开发的数据库为了提升查询性能,不能够一次查询出来超过20条以上的数据。所以上面的代码最多能够查询出20条数据,当签到数据超过20天(一个月最多31天),这时候就需要做两次查询(根据tsModified
正序,反序各取一次),然后合并数据了,所以最后的代码如下:
export const getEmotionByOpenidAndDate = (openid, year, month) => {
const _ = db.command
year = parseInt(year)
month = parseInt(month)
let start = new Date(year, month - 1, 1).getTime()
let end = new Date(year, month, 1).getTime()
// 这里因为限制 limit 20,所以查询两次,一共31条(最多31天)记录
// 正序反序各取一次,使用 orderBy 排序
return new Promise((resolve, reject) => {
Promise.all([
db
.collection('diary')
.where({
openid,
tsModified: _.gte(start).and(_.lt(end))
})
.orderBy('tsModified', 'desc')
.limit(15)
.get(),
db
.collection('diary')
.where({
openid,
tsModified: _.gte(start).and(_.lt(end))
})
.orderBy('tsModified', 'asc')
.limit(16)
.get()
])
.then((data) => {
let [data1, data2] = data
let set = new Set()
data1 = data1.data || []
data2 = data2.data || []
data = data1.concat(data2).filter((v) => {
if (set.has(v._id)) {
return false
}
set.add(v._id)
return true
})
resolve({data})
})
.catch((e) => {
console.log(e)
reject(e)
})
})
}
心情数据是根据 openid
和月份获取的,日期范围为:月份 1 日的凌晨 0 点(start)到下一月份 1 日的凌晨 0 点(end),在云数据库中可以使用 _.gte(start).and(_.lt(end))
,即大于等于 start
小于 end
。
这里计算 start
和 end
的时候,遇见了 Date
兼容性的两个问题:
localDateString
问题- 时区问题
localDateString
问题
笔者一开始使用将日期转化成类似 2018-01-01 00:00:00
的格式,然后使用 new Date('2018-01-01 00:00:00')
,可以得到 Date 实例,这在开发者工具和 Android 手机上都没有问题,但是在 iOS 系统下却识别成了 Invalid Date
,变成了 1970-01-01
。这是因为 iOS 上 localDateString
(本地时间)的问题,使用 new Date().toLocaleDateString()
就可以知道,iOS 下识别的数据是 2018/01/01 00:00:00
这样的格式的。
时区问题
小程序云开发的云函数和数据库是面向全球开发者的,它们使用的时区并不是我们的东八区(北京时间),因此我们在获取 Date 的时候就要小心,简单拼接 2018-01-01 00:00:00
获取的时间并不是北京时间,数据库存入的数据如果使用北京时间(本地 JS),那么获取数据的时候就应该使用北京时间(云端执行 JS 时)。
为了解决 Date 的问题,笔者在计算时区的时候,都转换成了 UTC 标准时间,比如在云函数中,笔者使用了 new Date().getUTCHours()
这样的时间,详见 server/inline/utils.js。
而在获取特定某一天的 Date 实例的时候,则使用 new Date(year, month, day)
的方式,这样在数据库获取某个月份时间戳时,就不会出现不同系统环境不同数值的问题,详见 client/lib/api.js 的 getEmotionByOpenidAndDate
方法。
使用 navigator 增加跳转
心情签到页面做完之后,还需要在天气预报页面给它做跳转。在天气预报页面增加跳转的 WXML 代码如下:
<!--weather/index.wxml-->
<view class="navigator" bindtap="goDiary">
<icon type="edit"/>
</view>
页面绑定了事件 goDiary
代码:
// weather/index.js
Page({
goDiary() {
let url = `/pages/diary/index`
wx.navigateTo({
url
})
}
})
在心情签到页面,顶部导航需要增加返回操作:
<!--diary/index.wxml-->
<view class="navigator">
<icon type="back" bindtap="goBack"/>
</view>
// diary/index.js
Page({
goBack() {
wx.navigateBack()
}
})
心情签到页面整体流程图
小结
本节介绍了新鲜天气日历使用、用户授权流程和数据库操作。
日历使用需要在小程序管理后台搜索对应的插件 id,然后申请授权。日历的日期背景颜色是跟当时签到心情相对应的,当切换了日历的月份之后,应该重新获取当前月份的签到数据信息。
用户授权流程由小程序、开发者服务器和微信接口服务三方参与,整个流程包括调用 wx.login
授权获取临时登录凭证,使用临时登录凭证获取 openid
和 session_key
,以及获取用户信息三个步骤。session_key
可以用于解密敏感数据,但是 session_key
具有时效性,需要调用 wx.checkSession
方法来校验其是否失效。
云开发的数据库每条记录自带 _openid
字段,可以单独来设置数据库权限。笔者在心情签到功能中主动通过授权获得用户 openid
然后增加记录。在进行跟日期、时间戳相关的数据查询时应该注意云环境的时区,最佳实践是使用格林尼治时间,使用 Date
对象的时候也应该注意生产环节和本地环境 localeDateString
的差异。