Koa2微信公眾號(hào)開發(fā)之消息管理
一、簡(jiǎn)介
上一節(jié)Koa2微信公眾號(hào)開發(fā)(一),我們搭建好了本地調(diào)試環(huán)境并且接入了微信公眾測(cè)試號(hào)。這一節(jié)我們就來(lái)看看公眾號(hào)的消息管理。并實(shí)現(xiàn)一個(gè)自動(dòng)回復(fù)功能。
Github源碼: github.com/ogilhinn/ko…
閱讀建議:微信公眾平臺(tái)開發(fā)文檔mp.weixin.qq.com/wiki
二、接收消息
當(dāng)普通微信用戶向公眾賬號(hào)發(fā)消息時(shí),微信服務(wù)器將POST消息的XML數(shù)據(jù)包到開發(fā)者填寫的URL上。
2.1 接收普通消息數(shù)據(jù)格式
XML的結(jié)構(gòu)基本固定,不同的消息類型略有不同。
用戶發(fā)送文本消息時(shí),微信公眾賬號(hào)接收到的XML數(shù)據(jù)格式如下所示:
<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>
用戶發(fā)送圖片消息時(shí),微信公眾賬號(hào)接收到的XML數(shù)據(jù)格式如下所示:
<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>
其他消息消息類型的結(jié)構(gòu)請(qǐng)查閱【微信公眾平臺(tái)開發(fā)文檔】
對(duì)于POST請(qǐng)求的處理,koa2沒(méi)有封裝獲取參數(shù)的方法,需要通過(guò)自己解析上下文context中的原生node.js請(qǐng)求對(duì)象request。我們將用到row-body這個(gè)模塊來(lái)拿到數(shù)據(jù)。
2.2 先來(lái)優(yōu)化之前的代碼
這一節(jié)的代碼緊接著上一屆實(shí)現(xiàn)的代碼,在上一屆的基礎(chǔ)上輕微改動(dòng)了下。
'use strict'
const Koa = require('koa')
const app = new Koa()
const crypto = require('crypto')
// 將配置文件獨(dú)立到config.js
const config = require('./config')
app.use(async ctx => {
// GET 驗(yàn)證服務(wù)器
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接收數(shù)據(jù)
// TODO
}
});
app.listen(7001);
這兒我們?cè)谥辉贕ET中驗(yàn)證了簽名值是否合法,實(shí)際上我們?cè)赑OST中也應(yīng)該驗(yàn)證簽名。
將簽名驗(yàn)證寫成一個(gè)函數(shù)
function getSignature (timestamp, nonce, token) {
let hash = crypto.createHash('sha1')
const arr = [token, timestamp, nonce].sort()
hash.update(arr.join(''))
return hash.digest('hex')
}
優(yōu)化代碼,再POST中也加入驗(yàn)證
...
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
}
});
...
到這兒我們都沒(méi)有開始實(shí)現(xiàn)接受XML數(shù)據(jù)包的功能,而是在修改之前的代碼。這是為了演示在實(shí)際開發(fā)中的過(guò)程,寫任何代碼都不是一步到位的,好的代碼都是改出來(lái)的。
2.3 接收公眾號(hào)普通消息的XML數(shù)據(jù)包
現(xiàn)在開始進(jìn)入本節(jié)的重點(diǎn),接受XML數(shù)據(jù)包并轉(zhuǎn)為JSON
$ npm install raw-body --save
...
const getRawBody = require('raw-body')
...
// TODO
// 取原始數(shù)據(jù)
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' // 直接回復(fù)success,微信服務(wù)器不會(huì)對(duì)此作任何處理
給你的測(cè)試號(hào)發(fā)送文本消息,你可以在命令行看見打印出如下數(shù)據(jù)
<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>
恭喜,到此你已經(jīng)可以接收到XML數(shù)據(jù)了。😯 但是我們還需要將XML轉(zhuǎn)為JSON方便我們的使用,我們將用到xml2js這個(gè)包
$ npm install xml2js --save
我們需要寫一個(gè)解析XML的異步函數(shù),返回一個(gè)Promise對(duì)象
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)
})
})
}
接著調(diào)用parseXML方法,并打印出結(jié)果
... const formatted = await parseXML(xml) console.log(formatted) return ctx.body = 'success'
一切正常的話*(實(shí)際開發(fā)中你可能會(huì)遇到各種問(wèn)題)*,命令行將打印出如下JSON數(shù)據(jù)
{ ToUserName: 'gh_9d2d49e7e006',
FromUserName: 'oBp2T0wK8lM4vIkmMTJfFpk6Owlo',
CreateTime: '1516941086',
MsgType: 'text',
Content: 'JavaScript之禪',
MsgId: '6515212354839473910' }
到此,我們就能處理微信接收到的消息了,你可以自己測(cè)試關(guān)注、取消關(guān)注、發(fā)送各種類型的消息看看這個(gè)類型的消息所對(duì)應(yīng)的XML數(shù)據(jù)格式都是怎么樣的
三、回復(fù)消息
當(dāng)用戶發(fā)送消息給公眾號(hào)時(shí)(或某些特定的用戶操作引發(fā)的事件推送時(shí)),會(huì)產(chǎn)生一個(gè)POST請(qǐng)求,開發(fā)者可以在響應(yīng)包(Get)中返回特定XML結(jié)構(gòu),來(lái)對(duì)該消息進(jìn)行響應(yīng)(現(xiàn)支持回復(fù)文本、圖片、圖文、語(yǔ)音、視頻、音樂(lè))。嚴(yán)格來(lái)說(shuō),發(fā)送被動(dòng)響應(yīng)消息其實(shí)并不是一種接口,而是對(duì)微信服務(wù)器發(fā)過(guò)來(lái)消息的一次回復(fù)。
3.1 被動(dòng)回復(fù)用戶消息數(shù)據(jù)格式
前面說(shuō)了交互的數(shù)據(jù)格式為XML,接收消息是XML的,我們回復(fù)回去也應(yīng)該是XML。
微信公眾賬號(hào)回復(fù)用戶文本消息時(shí)的XML數(shù)據(jù)格式如下所示:
<xml> <ToUserName><![CDATA[toUser]]></ToUserName> <FromUserName><![CDATA[fromUser]]></FromUserName> <CreateTime>12345678</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[你好]]></Content> </xml>
微信公眾賬號(hào)回復(fù)用戶圖片消息時(shí)的XML數(shù)據(jù)格式如下所示:
<xml> <ToUserName><![CDATA[toUser]]></ToUserName> <FromUserName><![CDATA[fromUser]]></FromUserName> <CreateTime>12345678</CreateTime> <MsgType><![CDATA[image]]></MsgType> <Image><MediaId><![CDATA[media_id]]></MediaId></Image> </xml>
篇幅所限就不一一列舉了,請(qǐng)查閱【微信公眾平臺(tái)開發(fā)文檔】
前面的代碼都是直接回復(fù)success,不做任何處理。先來(lái)擼一個(gè)自動(dòng)回復(fù)吧。收到消息后就回復(fù)這兒是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模板引擎處理回復(fù)內(nèi)容
從這一小段代碼中可以看出,被動(dòng)回復(fù)消息就是把你想要回復(fù)的內(nèi)容按照約定的XML格式返回即可。但是一段一段的拼XML那多麻煩。我們來(lái)加個(gè)模板引擎方便我們處理XML。模板引擎有很多,ejs 是其中一種,它使用起來(lái)十分簡(jiǎn)單
首先下載并引入ejs
$ npm install ejs --save
如果你之前沒(méi)用過(guò)現(xiàn)在只需要記住下面這幾個(gè)語(yǔ)法,以及ejs.compile()方法
- <% code %>:運(yùn)行 JavaScript 代碼,不輸出
- <%= code %>:顯示轉(zhuǎn)義后的 HTML內(nèi)容
- <%- code %>:顯示原始 HTML 內(nèi)容
可以先看看這個(gè)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>
*/
現(xiàn)在來(lái)編寫被動(dòng)回復(fù)消息的模板,各種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>
現(xiàn)在就可以使用我們寫好的模板回復(fù)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)
我們可以把這個(gè)回復(fù)消息的功能寫成一個(gè)函數(shù)
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)
}
在回復(fù)消息的時(shí)候直接調(diào)用這個(gè)方法即可
... const formatted = await parseXML(xml) console.log(formatted) const content = 'JavaScript之禪' const replyMessageXml = reply(content, formatted.ToUserName, formatted.FromUserName) return ctx.body = replyMessageXml
現(xiàn)在為了測(cè)試我們所寫的這個(gè)功能,來(lái)實(shí)現(xiàn)一個(gè)【學(xué)我說(shuō)話】的功能:
回復(fù)音樂(lè)將返回一個(gè)音樂(lè)類型的消息,回復(fù)文本圖片,語(yǔ)音,公眾號(hào)將返回同樣的內(nèi)容,當(dāng)然了你可以在這個(gè)基礎(chǔ)上進(jìn)行各種發(fā)揮。
....
const formatted = await parseXML(xml)
console.log(formatted)
let content = ''
if (formatted.Content === '音樂(lè)') {
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,到此時(shí)我們的測(cè)試號(hào)已經(jīng)能夠根據(jù)我們的消息做出相應(yīng)的回應(yīng)了

本篇再上一節(jié)的代碼基礎(chǔ)上做了一些優(yōu)化,并重點(diǎn)講解微信公眾號(hào)的消息交互,最后實(shí)現(xiàn)了個(gè)【學(xué)我說(shuō)話】的小功能。下一篇,我們將繼續(xù)補(bǔ)充消息管理相關(guān)的知識(shí)。最后再說(shuō)一句:看文檔 😉
參考鏈接
微信公眾平臺(tái)開發(fā)文檔: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…
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Node.js中創(chuàng)建和管理外部進(jìn)程詳解
這篇文章主要介紹了Node.js中創(chuàng)建和管理外部進(jìn)程詳解,本文講解了執(zhí)行外部命令的方法、子進(jìn)程相關(guān)內(nèi)容等,需要的朋友可以參考下2014-08-08
nodeJs實(shí)現(xiàn)基于連接池連接mysql的方法示例
這篇文章主要介紹了nodeJs實(shí)現(xiàn)基于連接池連接mysql的方法,結(jié)合具體實(shí)例形式分析了nodejs連接池操作mysql數(shù)據(jù)庫(kù)連接的實(shí)現(xiàn)與使用技巧,需要的朋友可以參考下2018-02-02
Node.js的MongoDB驅(qū)動(dòng)Mongoose基本使用教程
這篇文章主要介紹了Node.js的MongoDB驅(qū)動(dòng)Mongoose的基本使用教程,前端js+后端Node.js+數(shù)據(jù)庫(kù)MongoDB是當(dāng)下流行的JavaScript全棧開發(fā)方案,需要的朋友可以參考下2016-03-03
Nodejs為什么選擇javascript為載體語(yǔ)言
準(zhǔn)備寫一個(gè)NodeJS方面的系列文章,由淺入深,循序漸進(jìn),秉承的理念是重思想,多實(shí)踐,勤能補(bǔ)拙,貴在堅(jiān)持。本文首先來(lái)點(diǎn)基礎(chǔ)知識(shí)的開篇吧。2015-01-01
koa2實(shí)現(xiàn)登錄注冊(cè)功能的示例代碼
這篇文章主要介紹了koa2實(shí)現(xiàn)登錄注冊(cè)功能的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-12-12
node.js 動(dòng)態(tài)執(zhí)行腳本
其中的Script對(duì)象,就與require('vm')返回的對(duì)象很相似,而實(shí)質(zhì)上,vm模塊就是對(duì)Script對(duì)象的封裝。2016-06-06
node.js中的fs.rename方法使用說(shuō)明
這篇文章主要介紹了node.js中的fs.rename方法使用說(shuō)明,本文介紹了fs.rename的方法說(shuō)明、語(yǔ)法、接收參數(shù)、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下的相關(guān)資料2014-12-12
詳解nodeJS之二進(jìn)制buffer對(duì)象
本篇文章主要介紹了nodeJS之二進(jìn)制buffer對(duì)象,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06

