小程序开发知识点总结
我承认,最近比较懒了,博客也很久没更新了,太对不住自己了,做了一段时间小程序开发,总结了一些知识点,直接上菜。
关于小程序登录
这里放上一张流程图:
说明:
小程序调用wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
开发者服务器以code换取 用户唯一标识openid 和 会话密钥session_key。
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
这里的流程用代码来说明下:
1、小程序调用wx.login()获取临时登录凭证code
2、小程序拿到code后,使用wx.request将code连同登录返回的用户信息一起发送至后台接口
3、后台接口收到code和用户信息后,调用微信登录凭证校验接口获取session_key、openid(session_key、openid记得保存数据库,后续请求要用到)
const WX_URL = 'https://api.weixin.qq.com/sns/jscode2session';
let sessionData = await ctx.curl(WX_URL, {
dataType: 'json',
data: {
grant_type: 'authorization_code',
js_code: code,
secret: this.config.weixin.secret,
appid: this.config.weixin.appid,
},
});
sessionData = sessionData.res.data;
其中:
sessionData即为包含session_key和openid的对象。
4、签名校验
为什么要签名校验?
其实,就是为了保证数据的完整性,微信发送的数据会进行一次签名,如果不进行签名验证的话,不能确保数据一定来自微信平台,存在被劫持的风险。
如何进行签名校验?
刚说到登录和code一起提交上来的用户信息,其实这里面还包含一个rawData和一个signature,rawData就是原始的用户信息json串,拿到这个串之后要拼接上sessionData里面的session_key,然后使用sha1加密,再和signature比对,ru'guo'y如果一致则校验通过,用代码标识如下:
// 验证用户信息完整性
const signature2 = crypto.createHash('sha1').update(fullUserInfo.rawData + sessionData.session_key).digest('hex');
console.log(fullUserInfo.signature, signature2);
if (fullUserInfo.signature !== signature2) {
ctx.body = { error: '登录失败' };
return;
}
5、保存用户信息,使用openid作为唯一标识(同一个appid下用户的openid是一致的,不同的appid下可能不同)
6、返回登陆成功后的内容,包括token等
关于小程序支付
小程序的支付一般都是涉及到订单操作了,整个流程一般如下:
1、小程序发送订单支付请求,入参为订单号或订单id
2、接口收到订单请求后,查询订单详情,包括订单总金额等
3、接口调用微信的统一下单接口,返回一些支付前的参数信息,比如支付的金额、交易编号等
const returnParams = await WeixinSerivce.createWxUnifiedOrder({
openid: userInfo.weixin_openid,
body: '可以随意指定哪些内容',
out_trade_no: orderInfo.order_sn,
total_fee: (parseFloat(orderInfo.actual_price) * 100),
spbill_create_ip: '',
});
await ctx.service.order.updateOrderPrepayId(orderId, (returnParams.package + '').replace('prepay_id=', ''));
其中,createWxUnifiedOrder的service如下:
// 创建统一下单
createWxUnifiedOrder(payInfo) {
wxPayment.init({
appid: this.config.weixin.appid, // 微信小程序appid
mch_id: this.config.weixin.mch_id, // 商户帐号ID
apiKey: this.config.weixin.partner_key, // 秘钥
// pfx: fs.readFileSync('./apiclient_cert.p12'), //微信商户平台证书 (optional,部分API需要使用)
});
return new Promise((resolve, reject) => {
wxPayment.createUnifiedOrder({
body: payInfo.body, // 商品或支付单简要描述
out_trade_no: payInfo.out_trade_no, // 商户系统内部的订单号,32个字符内、可包含字母
total_fee: payInfo.total_fee,
spbill_create_ip: payInfo.spbill_create_ip,
notify_url: this.config.weixin.notify_url,
trade_type: 'JSAPI',
product_id: payInfo.out_trade_no,
openid: payInfo.openid, // 用户openid
}, (err, result) => {
console.log('统一下单参数返回:', result);
if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') {
const returnParams = {
appid: result.appid,
timeStamp: parseInt(Date.now() / 1000) + '',
nonceStr: result.nonce_str,
package: 'prepay_id=' + result.prepay_id,
signType: 'MD5',
};
const paramStr = `appId=${returnParams.appid}&nonceStr=${returnParams.nonceStr}&package=${returnParams.package}&signType=${returnParams.signType}&timeStamp=${returnParams.timeStamp}&key=` + this.config.weixin.partner_key;
returnParams.paySign = md5(paramStr).toUpperCase();
resolve(returnParams);
} else {
reject(result);
}
});
});
}
4、小程序拿到返回的returnParams后,调用wx.requestPayment的api发送支付请求即可
5、获取支付通知
获取支付通知的目的在于判断用户支付结果,如上面的notify_url,就是支付通知的接口,该接口中我们可以获取到订单的编号等信息,然后将数据库中的订单改为已支付等状态并保存支付数据信息
关于获取用户手机号
获取小程序用户手机号即微信手机号其实比较简单,流程如下:
1、小程序端使用如下button获取encryptedData、iv数据后发送至后台接口
<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"> </button>
...
Page({
getPhoneNumber: function(e) {
console.log(e.detail.errMsg)
console.log(e.detail.iv)
console.log(e.detail.encryptedData)
// 这里拿到encryptedData、iv后发送至后台接口
}
})
2、接口收到encryptedData、iv后,再取出之前保存的当前用户的session_key,进行数据解密即可
const weixinUserInfo = await WeixinSerivce.decryptUserInfoData(currentUser.session_key, encryptedData, iv);
其中decryptUserInfoData如下:
// 解析微信登录用户数据
async decryptUserInfoData(sessionKey, encryptedData, iv) {
// base64 decode
const _sessionKey = Buffer.from(sessionKey, 'base64');
encryptedData = Buffer.from(encryptedData, 'base64');
iv = Buffer.from(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) {
return '';
}
if (decoded.watermark.appid !== this.config.weixin.appid) {
return '';
}
return decoded;
}
解密之后的weixinUserInfo里面即包含用户电话信息
关于生成小程序二维码
小程序的二维码可以调用微信接口获取,并且支持二维码中携带指定参数,这样其他用户扫描二维码后程序可以获取到之前放进二维码的参数。
广泛的应用场景是商品详情页的分享、用户的分销推广等,只要在分享的二维码中加入商品id或者当前分享人的id等数据,再次扫描之后便可以看到商品详情等数据或者知道新用户是谁推广过来的,当然应用场景还有很多,就不一一列举了。
获取小程序二维码接口如下:
async genReferQRCode(params) {
console.log(params);
const ctx = this.ctx;
let GET_WX_ACODE_URL = '';
const access_token = await ctx.service.weixin.getAccessToken();
GET_WX_ACODE_URL = `https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${access_token}`;
const res = await ctx.curl(GET_WX_ACODE_URL, {
// dataType: 'json',
method: 'POST',
contentType: 'json',
data: {
scene: params.scene,
// page: params.page || 'pages/index/index',
},
writeStream: require('fs').createWriteStream(params.filePath),
});
console.log(res.data);
}
调用的时候其实更简单:
const refer_code = 'qrcode_' + ctx.query.scene + '_' + ctx.service.utils.genRandomReferCode();
const filePath = ctx.app.config.upload.path + '/qrcode/' + refer_code + '.png';
await ctx.service.weixin.genReferQRCode({
scene: refer_code,
filePath,
page: ctx.query.page,
});
if (fs.existsSync(filePath)) {
ctx.body = {
img: ctx.app.config.upload.picDomain + '/qrcode/' + refer_code + '.png',
};
} else {
ctx.body = {
error: '生成二维码失败',
};
ctx.status = 500;
}
小程序端如何使用websocket实现实时消息推送等服务
其实,小程序是支持websocket的,比如拼多多等小程序,时时会看到页面顶部弹出"谁谁购买了什么商品"之类的消息。
实现websocket也比较简单,我这里用的是eggjs框架,引入的是socket.io,即使用socket.io实现ws协议,具体流程如下:
1、eggjs配置socket.io及redis(redis可以不用,cluster 模式下,使用 redis 可以较为简单的实现 clients/rooms 等信息共享,此处 redis 仅用于存储连接实例信息)
// socket.io配置
config.io = {
// init: { wsEngine: 'ws' }, // passed to engine.io
namespace: {
'/': {
connectionMiddleware: [ 'auth' ],
packetMiddleware: [],
},
},
redis: {
host: 'redis服务地址',
port: 6379,
password: 'redis密码',
},
};
// redis连接配置
config.redis = {
client: {
port: 6379,
host: 'redis服务地址',
password: 'redis密码',
db: 0,
},
};
这里我们指定了一个auth中间件,用来做ws链接时的授权等操作,具体后面介绍。
2、在app目录下新建io目录,io目录下新建controller、middleware目录,然后在controller下新建nsp.js用来接收ws消息,middleware下新建auth.js用来做ws授权等其他操作
1)先看下auth.js怎么写的?
'use strict';
// const _ = require('lodash');
const PREFIX = 'room';
module.exports = () => {
return async (ctx, next) => {
const { socket, app } = ctx;
const query = socket.handshake.query;
// 用户信息
const { room, userId } = query;
// 用户加入
socket.join(room);
// 备注:此处 app.redis 与插件无关,可用其他存储代替
await app.redis.sadd(PREFIX + ':' + room, userId);
console.log('当前加入用户:', userId);
await next();
// 用户离开
socket.leave(room);
console.log('当前离开用户:', userId);
await app.redis.srem(`${PREFIX}:${room}`, userId);
};
};
auth这里,我是直接拿到用户id、room信息之后将用户加入指定room,redis保存的是该room下的用户id集合。用户离开时将用户id移除该集合即可。(这里用到了redis的sorted set这一数据类型)
2)看下nsp.js如何处理用户消息的?
'use strict';
const Controller = require('egg').Controller;
const moment = require('moment');
class NspController extends Controller {
async exchange() {
console.log('socket收到消息了', moment.unix(moment().unix()).format('YYYY-MM-DD HH:mm:ss'));
const { ctx, app } = this;
const nsp = app.io.of('/');
const message = ctx.args[0] || {};
const socket = ctx.socket;
const client = socket.id;
try {
const { target, payload } = message;
console.log('收到的消息', message);
if (!target) return;
const msg = ctx.helper.parseMsg('exchange', payload, { client, target });
console.log('msg', msg);
nsp.emit(target, msg);
} catch (error) {
app.logger.error(error);
}
}
}
module.exports = NspController;
ctx.helper.parseMsg其实只是一个辅助方法,用来将入参整理,具体如下:
'use strict';
module.exports = {
parseMsg(action, payload = {}, metadata = {}) {
const meta = Object.assign({}, {
timestamp: Date.now(),
}, metadata);
return {
meta,
data: {
action,
payload,
},
};
},
};
所以这里的结果是将用户发过来的消息再emit到target中去。
3、eggjs中配置路由ws路由
在router.js中配置如下:
// socket.io
const { io } = app;
io.of('/').route('exchange', io.controller.nsp.exchange);
其中exchange表示ws指定的事件使用io.controller.nsp.exchange处理。
4、小程序端引入weapp.socket.io.js(这是因为小程序不支持socket.io,所以需要这个包去做适配)
5、小程序端测试代码:
onLaunch: function () {
var that = this;
that.connectWsSocket('notice', 1);
},
...
connectWsSocket(room, userId) {
const socket = io(`http://192.168.3.55:7002?room=${room}&userId=${userId}`) // 默认进notice的房间,后台直接向该房间推送消息即可
socket.on('connect', function () {
console.log('connected')
});
socket.on('res', d => {
console.log('received res: ', d)
})
setInterval(function(){
socket.emit('exchange', {
payload: {
title: 'this is a news'
},
target: 'res'
})
}, 10000);
},
这里我们监听res的事件,每隔10s发送一条消息至后台,并且指定消息接收的target事件为res,可以看到如下效果:
这是服务端效果:
这样就搭建了一个小程序端的ws环境了,后续可以根据这个进行随意消息通知,或者一些实时图表展示的场景也可以使用这个实现。