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 相关文章推荐
JavaScript获取页面上某个元素的代码
Mar 13 Javascript
js 3秒后跳转页面的实现代码
Mar 10 Javascript
checkbox勾选判断代码分析
Jun 11 Javascript
初识Node.js
Mar 20 Javascript
jquery处理页面弹出层查询数据等待操作实例
Mar 25 Javascript
在JavaScript中处理时间之getHours()方法的使用
Jun 10 Javascript
jQuery实现hover合成事件的方法
Aug 06 Javascript
jQuery使用siblings获取某元素所有同辈(兄弟姐妹)元素用法示例
Jan 30 Javascript
解决vue 项目引入字体图标报错、不显示等问题
Sep 01 Javascript
解决vue项目使用font-awesome,build后路径的问题
Sep 01 Javascript
swiper4实现移动端导航切换
Oct 16 Javascript
JavaScript正则表达式验证登录实例
Mar 18 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
PHP如何透过ODBC来存取数据库
2006/10/09 PHP
深入了解php4(1)--回到未来
2006/10/09 PHP
基于OpenCV的PHP图像人脸识别技术
2009/10/11 PHP
php HandlerSocket的使用
2011/05/02 PHP
PHP防止注入攻击实例分析
2014/11/03 PHP
smarty模板引擎中内建函数if、elseif和else的使用方法
2015/01/22 PHP
php实现常见图片格式的水印和缩略图制作(面向对象)
2016/06/15 PHP
PHP Laravel中的Trait使用方法
2019/01/20 PHP
Laravel框架中队列和工作(Queues、Jobs)操作实例详解
2020/04/06 PHP
javascript 打印页面代码
2009/03/24 Javascript
JavaScript中的prototype使用说明
2010/04/13 Javascript
Chosen 基于jquery的选择框插件使用方法
2012/05/30 Javascript
优化RequireJS项目的相关技巧总结
2015/07/01 Javascript
jQuery自适应轮播图插件Swiper用法示例
2016/08/24 Javascript
JavaScript初学者必看“new”
2017/06/12 Javascript
详解KOA2如何手写中间件(装饰器模式)
2018/10/11 Javascript
elementUI多选框反选的实现代码
2019/04/03 Javascript
JavaScript中的一些实用小技巧总结
2019/04/07 Javascript
纯javascript实现选择框的全选与反选功能
2019/04/08 Javascript
Python的Django框架中的select_related函数对QuerySet 查询的优化
2015/04/01 Python
python实现QQ邮箱/163邮箱的邮件发送
2019/01/22 Python
Python @property原理解析和用法实例
2020/02/11 Python
建筑专业自荐信
2013/10/18 职场文书
公司人力资源的自我评价
2014/01/02 职场文书
和平主题的演讲稿
2014/01/12 职场文书
企业总经理岗位职责
2014/02/13 职场文书
优秀管理者事迹材料
2014/05/22 职场文书
投标授权委托书范文
2014/08/02 职场文书
民主生活会对照检查材料(统计局)
2014/09/21 职场文书
药店采购员岗位职责
2014/09/30 职场文书
党的群众路线教育实践活动实施方案
2014/10/31 职场文书
三年级学生期末评语
2014/12/26 职场文书
2015年度个人工作总结报告
2015/10/24 职场文书
python 如何在list中找Topk的数值和索引
2021/05/20 Python
ROS系统将python包编译为可执行文件的简单步骤
2021/07/25 Python
vue3获取当前路由地址
2022/02/18 Vue.js