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 选择器部分整理
Oct 28 Javascript
从零开始学习jQuery (三) 管理jQuery包装集
Feb 23 Javascript
js优化针对IE6.0起作用(详细整理)
Dec 25 Javascript
JQuery遍历json数组的3种方法
Nov 08 Javascript
jQuery实现Twitter的自动文字补齐特效
Nov 28 Javascript
jquery利用命名空间移除绑定事件的方法
Mar 11 Javascript
JavaScript 基础函数_深入剖析变量和作用域
May 18 Javascript
详解react-router如何实现按需加载
Jun 15 Javascript
fetch 使用及如何接收JS传值
Nov 11 Javascript
JS中使用cavas截图网页并解决跨域及模糊问题
Nov 13 Javascript
vue el-table实现行内编辑功能
Dec 11 Javascript
vuex中store存储store.commit和store.dispatch的用法
Jul 24 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
第十三节--对象串行化
2006/11/16 PHP
字符串长度函数strlen和mb_strlen的区别示例介绍
2014/09/09 PHP
cnblogs TagCloud基于jquery的实现代码
2010/06/11 Javascript
JavaScript传递变量: 值传递?引用传递?
2011/02/22 Javascript
js 函数调用模式小结
2011/12/26 Javascript
9行javascript代码获取QQ群成员具体实现
2013/10/16 Javascript
table对象中的insertRow与deleteRow使用示例
2014/01/26 Javascript
javascript 回调函数详解
2014/11/11 Javascript
javascript强制点击广告的方法
2015/02/06 Javascript
jquery用ajax方式从后台获取json数据后如何将内容填充到下拉列表
2015/08/26 Javascript
JavaScript中iframe实现局部刷新的几种方法汇总
2016/01/06 Javascript
浅析JavaScript中作用域和作用域链
2016/12/06 Javascript
实例分析浏览器中“JavaScript解析器”的工作原理
2016/12/12 Javascript
vue2.0多条件搜索组件使用详解
2020/03/26 Javascript
原生js 封装get ,post, delete 请求的实例
2017/08/11 Javascript
vue自定v-model实现表单数据双向绑定问题
2018/09/03 Javascript
JavaScript异步操作的几种常见处理方法实例总结
2020/05/11 Javascript
vue+springboot+element+vue-resource实现文件上传教程
2020/10/21 Javascript
paramiko模块安装和使用(远程登录服务器)
2014/01/27 Python
Python爬虫中urllib库的进阶学习
2018/01/05 Python
python购物车程序简单代码
2018/04/18 Python
用python求一个数组的和与平均值的实现方法
2019/06/29 Python
Python-copy()与deepcopy()区别详解
2019/07/12 Python
Python CVXOPT模块安装及使用解析
2019/08/01 Python
德国鞋子网上商店:Omoda.de
2017/03/31 全球购物
自荐信的五个重要部分
2013/10/29 职场文书
英文自荐信
2013/12/15 职场文书
《灯光》教学反思
2014/02/08 职场文书
自立自强的名人事例
2014/02/10 职场文书
优秀高中生事迹材料
2014/02/11 职场文书
安全教育月活动总结
2014/05/05 职场文书
保护环境倡议书100字
2014/05/19 职场文书
护士求职信
2014/07/05 职场文书
保护校园环境倡议书
2015/04/28 职场文书
原来闭幕词是这样写的呀!
2019/07/01 职场文书
你离财务总监还有多远?速览CFO的岗位职责
2019/11/18 职场文书