Koa2微信公众号开发之消息管理
一、简介
上一节koa2微信公众号开发(一),我们搭建好了本地调试环境并且接入了微信公众测试号。这一节我们就来看看公众号的消息管理。并实现一个自动回复功能。
github源码:
阅读建议:
二、接收消息
当普通微信用户向公众账号发消息时,微信服务器将post消息的xml数据包到开发者填写的url上。
2.1 接收普通消息数据格式
xml的结构基本固定,不同的消息类型略有不同。
用户发送文本消息时,微信公众账号接收到的xml数据格式如下所示:
<xml> <tousername><![cdata[touser]]></tousername> <fromusername><![cdata[fromuser]]></fromusername> <createtime>createtime</createtime> <msgtype><![cdata[text]]></msgtype> <content><![cdata[this is a test]]></content> <msgid>1234567890123456</msgid> </xml>
用户发送图片消息时,微信公众账号接收到的xml数据格式如下所示:
<xml> <tousername><![cdata[touser]]></tousername> <fromusername><![cdata[fromuser]]></fromusername> <createtime>1348831860</createtime> <msgtype><![cdata[image]]></msgtype> <picurl><![cdata[this is a url]]></picurl> <mediaid><![cdata[media_id]]></mediaid> <msgid>1234567890123456</msgid> </xml>
其他消息消息类型的结构请查阅【微信公众平台开发文档】
对于post请求的处理,koa2没有封装获取参数的方法,需要通过自己解析上下文context中的原生node.js请求对象request。我们将用到这个模块来拿到数据。
2.2 先来优化之前的代码
这一节的代码紧接着上一届实现的代码,在上一届的基础上轻微改动了下。
'use strict' const koa = require('koa') const app = new koa() const crypto = require('crypto') // 将配置文件独立到config.js const config = require('./config') app.use(async ctx => { // get 验证服务器 if (ctx.method === 'get') { const { signature, timestamp, nonce, echostr } = ctx.query const token = config.wechat.token let hash = crypto.createhash('sha1') const arr = [token, timestamp, nonce].sort() hash.update(arr.join('')) const shasum = hash.digest('hex') if (shasum === signature) { return ctx.body = echostr } ctx.status = 401 ctx.body = 'invalid signature' } else if (ctx.method === 'post') { // post接收数据 // todo } }); app.listen(7001);
这儿我们在只在get中验证了签名值是否合法,实际上我们在post中也应该验证签名。
将签名验证写成一个函数
function getsignature (timestamp, nonce, token) { let hash = crypto.createhash('sha1') const arr = [token, timestamp, nonce].sort() hash.update(arr.join('')) return hash.digest('hex') }
优化代码,再post中也加入验证
... app.use(async ctx => { const { signature, timestamp, nonce, echostr } = ctx.query const token = config.wechat.token if (ctx.method === 'get') { if (signature === getsignature(timestamp, nonce, token)) { return ctx.body = echostr } ctx.status = 401 ctx.body = 'invalid signature' }else if (ctx.method === 'post') { if (signature !== getsignature(timestamp, nonce, token)) { ctx.status = 401 return ctx.body = 'invalid signature' } // todo } }); ...
到这儿我们都没有开始实现接受xml数据包的功能,而是在修改之前的代码。这是为了演示在实际开发中的过程,写任何代码都不是一步到位的,好的代码都是改出来的。
2.3 接收公众号普通消息的xml数据包
现在开始进入本节的重点,接受xml数据包并转为json
$ npm install raw-body --save
... const getrawbody = require('raw-body') ... // todo // 取原始数据 const xml = await getrawbody(ctx.req, { length: ctx.request.length, limit: '1mb', encoding: ctx.request.charset || 'utf-8' }); console.log(xml) return ctx.body = 'success' // 直接回复success,微信服务器不会对此作任何处理
给你的测试号发送文本消息,你可以在命令行看见打印出如下数据
<xml> <tousername><![cdata[gh_9d2d49e7e006]]></tousername> <fromusername><![cdata[obp2t0wk8lm4vikmmtjffpk6owlo]]></fromusername> <createtime>1516940059</createtime> <msgtype><![cdata[text]]></msgtype> <content><![cdata[javascript之禅]]></content> <msgid>6515207943908059832</msgid> </xml>
恭喜,到此你已经可以接收到xml数据了。???? 但是我们还需要将xml转为json方便我们的使用,我们将用到xml2js这个包
$ npm install xml2js --save
我们需要写一个解析xml的异步函数,返回一个promise对象
function parsexml(xml) { return new promise((resolve, reject) => { xml2js.parsestring(xml, { trim: true, explicitarray: false, ignoreattrs: true }, function (err, result) { if (err) { return reject(err) } resolve(result.xml) }) }) }
接着调用parsexml方法,并打印出结果
... const formatted = await parsexml(xml) console.log(formatted) return ctx.body = 'success'
一切正常的话*(实际开发中你可能会遇到各种问题)*,命令行将打印出如下json数据
{ tousername: 'gh_9d2d49e7e006', fromusername: 'obp2t0wk8lm4vikmmtjffpk6owlo', createtime: '1516941086', msgtype: 'text', content: 'javascript之禅', msgid: '6515212354839473910' }
到此,我们就能处理微信接收到的消息了,你可以自己测试关注、取消关注、发送各种类型的消息看看这个类型的消息所对应的xml数据格式都是怎么样的
三、回复消息
当用户发送消息给公众号时(或某些特定的用户操作引发的事件推送时),会产生一个post请求,开发者可以在响应包(get)中返回特定xml结构,来对该消息进行响应(现支持回复文本、图片、图文、语音、视频、音乐)。严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。
3.1 被动回复用户消息数据格式
前面说了交互的数据格式为xml,接收消息是xml的,我们回复回去也应该是xml。
微信公众账号回复用户文本消息时的xml数据格式如下所示:
<xml> <tousername><![cdata[touser]]></tousername> <fromusername><![cdata[fromuser]]></fromusername> <createtime>12345678</createtime> <msgtype><![cdata[text]]></msgtype> <content><![cdata[你好]]></content> </xml>
微信公众账号回复用户图片消息时的xml数据格式如下所示:
<xml> <tousername><![cdata[touser]]></tousername> <fromusername><![cdata[fromuser]]></fromusername> <createtime>12345678</createtime> <msgtype><![cdata[image]]></msgtype> <image><mediaid><![cdata[media_id]]></mediaid></image> </xml>
篇幅所限就不一一列举了,请查阅【微信公众平台开发文档】
前面的代码都是直接回复success,不做任何处理。先来撸一个自动回复吧。收到消息后就回复这儿是javascript之禅
// return ctx.body = 'success' // 直接success ctx.type = 'application/xml' return ctx.body = `<xml> <tousername><![cdata[${formatted.fromusername}]]></tousername> <fromusername><![cdata[${formatted.tousername}]]></fromusername> <createtime>${new date().gettime()}</createtime> <msgtype><![cdata[text]]></msgtype> <content><![cdata[这儿是javascript之禅]]></content> </xml>`
3.2 使用ejs模板引擎处理回复内容
从这一小段代码中可以看出,被动回复消息就是把你想要回复的内容按照约定的xml格式返回即可。但是一段一段的拼xml那多麻烦。我们来加个模板引擎方便我们处理xml。模板引擎有很多,是其中一种,它使用起来十分简单
首先下载并引入
$ npm install ejs --save
如果你之前没用过现在只需要记住下面这几个语法,以及ejs.compile()方法
- <% code %>:运行 javascript 代码,不输出
- <%= code %>:显示转义后的 html内容
- <%- code %>:显示原始 html 内容
可以先看看这个ejs的小demo:
const ejs = require('ejs') let tpl = ` <xml> <tousername><![cdata[<%-tousername%>]]></tousername> <fromusername><![cdata[<%-fromusername%>]]></fromusername> <createtime><%=createtime%></createtime> <msgtype><![cdata[<%=msgtype%>]]></msgtype> <content><![cdata[<%-content%>]]></content> </xml> ` const compiled = ejs.compile(tpl) let mess = compiled({ tousername: '1234', fromusername: '12345', createtime: new date().gettime(), msgtype: 'text', content: 'javascript之禅', }) console.log(mess) /* 将打印出如下信息 *================ <xml> <tousername><![cdata[1234]]></tousername> <fromusername><![cdata[12345]]></fromusername> <createtime>1517037564494</createtime> <msgtype><![cdata[text]]></msgtype> <content><![cdata[javascript之禅]]></content> </xml> */
现在来编写被动回复消息的模板,各种if else,这儿就直接贴代码了
<xml> <tousername><![cdata[<%-tousername%>]]></tousername> <fromusername><![cdata[<%-fromusername%>]]></fromusername> <createtime><%=createtime%></createtime> <msgtype><![cdata[<%=msgtype%>]]></msgtype> <% if (msgtype === 'news') { %> <articlecount><%=content.length%></articlecount> <articles> <% content.foreach(function(item){ %> <item> <title><![cdata[<%-item.title%>]]></title> <description><![cdata[<%-item.description%>]]></description> <picurl><![cdata[<%-item.picurl || item.picurl || item.pic || item.thumb_url %>]]></picurl> <url><![cdata[<%-item.url%>]]></url> </item> <% }); %> </articles> <% } else if (msgtype === 'music') { %> <music> <title><![cdata[<%-content.title%>]]></title> <description><![cdata[<%-content.description%>]]></description> <musicurl><![cdata[<%-content.musicurl || content.url %>]]></musicurl> <hqmusicurl><![cdata[<%-content.hqmusicurl || content.hqurl %>]]></hqmusicurl> </music> <% } else if (msgtype === 'voice') { %> <voice> <mediaid><![cdata[<%-content.mediaid%>]]></mediaid> </voice> <% } else if (msgtype === 'image') { %> <image> <mediaid><![cdata[<%-content.mediaid%>]]></mediaid> </image> <% } else if (msgtype === 'video') { %> <video> <mediaid><![cdata[<%-content.mediaid%>]]></mediaid> <title><![cdata[<%-content.title%>]]></title> <description><![cdata[<%-content.description%>]]></description> </video> <% } else { %> <content><![cdata[<%-content%>]]></content> <% } %> </xml>
现在就可以使用我们写好的模板回复xml消息了
... const formatted = await parsexml(xml) console.log(formatted) let info = {} let type = 'text' info.msgtype = type info.createtime = new date().gettime() info.tousername = formatted.fromusername info.fromusername = formatted.tousername info.content = 'javascript之禅' return ctx.body = compiled(info)
我们可以把这个回复消息的功能写成一个函数
function reply (content, fromusername, tousername) { var info = {} var type = 'text' info.content = content || '' // 判断消息类型 if (array.isarray(content)) { type = 'news' } else if (typeof content === 'object') { if (content.hasownproperty('type')) { type = content.type info.content = content.content } else { type = 'music' } } info.msgtype = type info.createtime = new date().gettime() info.tousername = tousername info.fromusername = fromusername return compiled(info) }
在回复消息的时候直接调用这个方法即可
... const formatted = await parsexml(xml) console.log(formatted) const content = 'javascript之禅' const replymessagexml = reply(content, formatted.tousername, formatted.fromusername) return ctx.body = replymessagexml
现在为了测试我们所写的这个功能,来实现一个【学我说话】的功能:
回复音乐将返回一个音乐类型的消息,回复文本图片,语音,公众号将返回同样的内容,当然了你可以在这个基础上进行各种发挥。
.... const formatted = await parsexml(xml) console.log(formatted) let content = '' if (formatted.content === '音乐') { content = { type: 'music', content: { title: 'lemon tree', description: 'lemon tree', musicurl: 'http://mp3.com/xx.mp3' }, } } else if (formatted.msgtype === 'text') { content = formatted.content } else if (formatted.msgtype === 'image') { content = { type: 'image', content: { mediaid: formatted.mediaid }, } } else if (formatted.msgtype === 'voice') { content = { type: 'voice', content: { mediaid: formatted.mediaid }, } } else { content = 'javascript之禅' } const replymessagexml = reply(content, formatted.tousername, formatted.fromusername) console.log(replymessagexml) ctx.type = 'application/xml' return ctx.body = replymessagexml
nice,到此时我们的测试号已经能够根据我们的消息做出相应的回应了
本篇再上一节的代码基础上做了一些优化,并重点讲解微信公众号的消息交互,最后实现了个【学我说话】的小功能。下一篇,我们将继续补充消息管理相关的知识。最后再说一句:看文档 ????
参考链接
微信公众平台开发文档:
raw-body:
xml2js: github.com/leonidas-fr…
ejs:
源码:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: 检查网络信息的bat[批处理]文件