我認可,最近比較懶了,博客也好久沒更新了,太對不住本身了,作了一段時間小程序開發,總結了一些知識點,直接上菜。web
這裏放上一張流程圖:redis
說明: 小程序調用wx.login() 獲取 臨時登陸憑證code ,並回傳到開發者服務器。 開發者服務器以code換取 用戶惟一標識openid 和 會話密鑰session_key。 以後開發者服務器能夠根據用戶標識來生成自定義登陸態,用於後續業務邏輯中先後端交互時識別用戶身份。
這裏的流程用代碼來講明下:數據庫
一、小程序調用wx.login()獲取臨時登陸憑證code
二、小程序拿到code後,使用wx.request將code連同登陸返回的用戶信息一塊兒發送至後臺接口
三、後臺接口收到code和用戶信息後,調用微信登陸憑證校驗接口獲取session_key、openid(session_key、openid記得保存數據庫,後續請求要用到)json
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的對象。
四、簽名校驗小程序
爲何要簽名校驗?後端
其實,就是爲了保證數據的完整性,微信發送的數據會進行一次簽名,若是不進行簽名驗證的話,不能確保數據必定來自微信平臺,存在被劫持的風險。
如何進行簽名校驗?微信小程序
剛說到登陸和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; }
五、保存用戶信息,使用openid做爲惟一標識(同一個appid下用戶的openid是一致的,不一樣的appid下可能不一樣)api
六、返回登錄成功後的內容,包括token等服務器
小程序的支付通常都是涉及到訂單操做了,整個流程通常以下:微信
一、小程序發送訂單支付請求,入參爲訂單號或訂單id
二、接口收到訂單請求後,查詢訂單詳情,包括訂單總金額等
三、接口調用微信的統一下單接口,返回一些支付前的參數信息,好比支付的金額、交易編號等
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); } }); }); }
四、小程序拿到返回的returnParams後,調用wx.requestPayment的api發送支付請求便可
五、獲取支付通知
獲取支付通知的目的在於判斷用戶支付結果,如上面的notify_url,就是支付通知的接口,該接口中咱們能夠獲取到訂單的編號等信息,而後將數據庫中的訂單改成已支付等狀態並保存支付數據信息
獲取小程序用戶手機號即微信手機號其實比較簡單,流程以下:
一、小程序端使用以下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後發送至後臺接口 } })
二、接口收到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也比較簡單,我這裏用的是eggjs框架,引入的是socket.io,即便用socket.io實現ws協議,具體流程以下:
一、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連接時的受權等操做,具體後面介紹。
二、在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中去。
三、eggjs中配置路由ws路由
在router.js中配置以下:
// socket.io const { io } = app; io.of('/').route('exchange', io.controller.nsp.exchange);
其中exchange表示ws指定的事件使用io.controller.nsp.exchange處理。
四、小程序端引入weapp.socket.io.js(這是由於小程序不支持socket.io,因此須要這個包去作適配)
五、小程序端測試代碼:
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環境了,後續能夠根據這個進行隨意消息通知,或者一些實時圖表展現的場景也可使用這個實現。