Koa2微信公众号开发之消息管理


Posted in Javascript onMay 16, 2018

一、简介

上一节Koa2微信公众号开发(一),我们搭建好了本地调试环境并且接入了微信公众测试号。这一节我们就来看看公众号的消息管理。并实现一个自动回复功能。

Github源码: github.com/ogilhinn/ko…

阅读建议:微信公众平台开发文档mp.weixin.qq.com/wiki

二、接收消息

当普通微信用户向公众账号发消息时,微信服务器将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。我们将用到row-body这个模块来拿到数据。

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。模板引擎有很多,ejs 是其中一种,它使用起来十分简单

首先下载并引入ejs

$ npm install ejs --save

如果你之前没用过现在只需要记住下面这几个语法,以及ejs.compile()方法

  1. <% code %>:运行 JavaScript 代码,不输出
  2. <%= code %>:显示转义后的 HTML内容
  3. <%- 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,到此时我们的测试号已经能够根据我们的消息做出相应的回应了

Koa2微信公众号开发之消息管理

本篇再上一节的代码基础上做了一些优化,并重点讲解微信公众号的消息交互,最后实现了个【学我说话】的小功能。下一篇,我们将继续补充消息管理相关的知识。最后再说一句:看文档 ?

参考链接

微信公众平台开发文档:mp.weixin.qq.com/wiki
raw-body:https://github.com/stream-utils/raw-body
xml2js: github.com/Leonidas-fr…
ejs:github.com/mde/ejs
源码: github.com/ogilhinn/ko…

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jquery中的$(document).ready()与window.onload的区别
Nov 18 Javascript
JavaScript 学习笔记 Black.Caffeine 09.11.28
Nov 30 Javascript
JQuery 应用 JQuery.groupTable.js
Dec 15 Javascript
页面调用单个swf文件,嵌套出多个方法。
Nov 21 Javascript
13 个JavaScript 性能提升技巧分享
Jul 26 Javascript
javascript制作幻灯片(360度全景图片)
Jul 28 Javascript
Vue2.0生命周期的理解
Aug 20 Javascript
angularjs通过过滤器返回超链接的方法
Oct 26 Javascript
js使用swiper实现层叠轮播效果实例代码
Dec 12 Javascript
详解vue的数据劫持以及操作数组的坑
Apr 18 Javascript
详解使用uni-app开发微信小程序之登录模块
May 09 Javascript
微信小程序获取用户信息及手机号(后端TP5.0)
Sep 12 Javascript
js实现鼠标单击Tab表单切换效果
May 16 #Javascript
Koa2微信公众号开发之本地开发调试环境搭建
May 16 #Javascript
解决Mac下安装nmp的淘宝镜像失败问题
May 16 #Javascript
为什么使用koa2搭建微信第三方公众平台的原因
May 16 #Javascript
详解基于Koa2开发微信二维码扫码支付相关流程
May 16 #Javascript
AngularJS标签页tab选项卡切换功能经典实例详解
May 16 #Javascript
解决Mac node版本升级失败的问题
May 16 #Javascript
You might like
星际争霸 Starcraft 编年史
2020/03/14 星际争霸
php使用glob函数快速查询指定目录文件的方法
2014/11/15 PHP
PHP正则验证Email的方法
2015/06/15 PHP
深入解析PHP的Yii框架中的缓存功能
2016/03/29 PHP
PHP反射原理与用法深入分析
2019/09/28 PHP
Laravel等框架模型关联的可用性浅析
2019/12/15 PHP
理解Javascript_10_对象模型
2010/10/16 Javascript
ajax中get和post的说明及使用与区别
2012/12/23 Javascript
简单模拟node.js中require的加载机制
2016/10/27 Javascript
jQuery操作json常用方法示例
2017/01/04 Javascript
angularjs实现的前端分页控件示例
2017/02/10 Javascript
js实现颜色阶梯渐变效果(Gradient算法)
2017/03/21 Javascript
jQuery获取table下某一行某一列的值实现代码
2017/04/07 jQuery
运用jQuery写的验证表单(实例讲解)
2017/07/06 jQuery
详谈js中标准for循环与foreach(for in)的区别
2017/11/02 Javascript
JavaScript满天星导航栏实现方法
2018/03/08 Javascript
微信小程序网络封装(简单高效)
2018/08/06 Javascript
原生JS实现的简单小钟表功能示例
2018/08/30 Javascript
在Layui 的表格模板中,实现layer父页面和子页面传值交互的方法
2019/09/10 Javascript
Vue快速实现通用表单验证功能
2019/12/05 Javascript
Vue数字输入框组件使用方法详解
2020/02/10 Javascript
[01:04:48]VGJ.S vs TNC Supermajor 败者组 BO3 第一场 6.6
2018/06/07 DOTA
Python实现测试磁盘性能的方法
2015/03/12 Python
遍历python字典几种方法总结(推荐)
2016/09/11 Python
对django的User模型和四种扩展/重写方法小结
2019/08/17 Python
win10环境下配置vscode python开发环境的教程详解
2019/10/16 Python
解析python 类方法、对象方法、静态方法
2020/08/15 Python
意大利灯具购物网站:Lampade.it
2018/10/18 全球购物
解释i节点在文件系统中的作用
2013/11/26 面试题
迅雷Cued工作心得体会
2014/01/27 职场文书
学校运动会霸气口号
2014/06/07 职场文书
党员个人对照检查材料思想汇报
2014/09/16 职场文书
乡镇党员干部四风对照检查材料思想汇报
2014/09/27 职场文书
客户经理岗位职责
2015/01/31 职场文书
怎么用Python识别手势数字
2021/06/07 Python
使用python绘制横竖条形图
2022/04/21 Python