本篇着重介绍如何使用Node.js去搭建一个微信公众号平台的服务器,上一篇介绍的是如何使用express框架和mongodb数据库以及session、cookie去前后端交互,有兴趣的可以去看看。
项目的基本思路(ReadMe):
* 填写服务器配置信息
* url 开发者服务器地址
* 通过ngrok工具将本地地址转化外网能访问的地址(内网穿透)
* 指令: ngrok http 3000
* token 参与微信加密签名的参数
* 验证服务器消息有效性
* 将token、timestamp、nonce三个参数进行字典序排序
* 因为要排序,最好组合成数组: [token、timestamp、nonce]
* token来自于页面填写的 timestamp、nonce来自于微信发送过来的查询字符串
* 字典序排序是按照0-9a-z的顺序进行排序,对应的是数组的sort方法
* 将三个参数字符串拼接成一个字符串进行sha1加密
* 数组的join方法就是用来拼串
* 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
* 成功微信服务器要求返回echostr
* 失败说明消息不是微信服务器,返回error
* 接受用户发送的消息
* 微信会发送两种类型消息:GET请求和POST请求
* GET请求用来验证服务器有效性
* POST请求用来接受用户发送的消息
* POST请求会携带两种参数:querystring参数 和 body参数
* 其中body参数需要用特殊方式接受
* 判断消息是否来自于微信服务器
* 接受用户发送的xml数据:req.on('data', data => {})
* 将xml数据解析为js对象:xml2js
* 将js对象格式化成为一个更好操作的对象
* 去掉xml
* 去掉值的[]
* 最后根据用户消息内容,返回特定的响应
* 响应数据必须是xml格式,具体参照官方文档
* https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140543
* 目的:
* 模块功能单一化
* 方便今后维护、扩展、更加健壮
* 将微信加密签名算法方法合并
* 提取了接受用户消息的三个方法,封装成工具函数
* 封装用来获取用户发送的消息的工具函数
* 封装将xml数据解析为js对象的工具函数
* 封装格式化js对象的方法的工具函数
* 封装中间件函数模块,采用的
* app.use(reply())
* reply() 方法 返回值是一个中间件函数 --> 更有利于扩展函数的功能
* 将相关模块依赖放进来并且修改好模块路径
* 回复6种类型,根据type来判断
* 里面尽可能少写重复代码,用字符串拼串的方式实现。
* 重复的字符串提取出来,不同的单独拼接
废话不多说,我们首先看编写好的入口文件。
'index.js'
const express = require('express');
const app = express();
const middle = require('./js/middle');
app.use(middle());
app.listen(3000, err => {
if (!err) {
console.log('服务器连接成功')
} else {
console.log('服务器连接失败')
}
});
'这里就是我们的入口文件,使用了端口号3000监听,
然后引入了express框架,还有自定义的中间件模块。'
接下来是我们的中间件模块
'这里我们引入了自定义的工具类模块,还有加密模块,sha1加密,
因为腾讯的微信公众号要求的sha1加密方式,所以我们必须配合'
const { makexml, xmltojs, userinfo } = require('./tools');
const sha1 = require('sha1');
const response = require('./response');
function middle() {
return async (req, res) => {
const { signature, echostr, timestamp, nonce } = req.query;
'//解构赋值,ES6写法,获取对应的值'
const token = 'nijingyu520';
const str = sha1([token, timestamp, nonce].sort().join(''));
if (req.method == 'GET') {
if (str === signature) {
res.end(echostr)
} else {
res.end('error')
return;
}
};
if (req.method == 'POST') {
if (str !== signature) {
res.send('error');
return;
}
'//上面三个if条件,判断接受的请求是否来自腾讯服务器,最终的str和signature
去对比是否相同,可以看成是一个通讯暗号,token在这里会定义一个,在公众号设
置那里也要设置一个相同的token,尽量复杂一些。'
const xmldata = await makexml(req);
const xmljs = xmltojs(xmldata);
const userinfos = userinfo(xmljs);
const ressendinfo = response(userinfos,res);
'//调用三个工具类函数,一个处理响应数据的函数,然后返回数据,
公众号要求返回的数据格式也是xml,而且格式要跟请求头格式的一样。'
res.send(ressendinfo);
}
}
}
module.exports = middle ;
'//这里我们暴露一个中间件函数,入口文件中的app.use()里面其实需要的是一个函数,
所以我们之间返回函数即可。'
接下来是工具类函数的模块
'tools.js'
const { parseString } = require('xml2js') '//一个npm包,将xml文件变成JS对象的'
module.exports = {
async makexml(req) {
return await new Promise((resolve, reject) => {
let xmldata = '';
req.on('data', data => {
xmldata += data.toString();
}).on('end', () => {
resolve(xmldata)
})
})
},
'//上面把xmldata的值作为这个makexml函数的返回值,es7的async特性,
由于POST请求的请求体这里无法通过req.body直接拿到,所以只能给req
绑定data时间,然后通过字符串拼接的情况获取xml数据,data事件可能
触发多次,所以在end事件中数据的完整性才能得到保障。'
xmltojs(makexml) {
let xmljs = '';
parseString(makexml, { trim: true }, (err, data) => {
if (!err) {
xmljs = data;
} else {
xmljs = 'error'
}
})
return xmljs;
},
'//调用xml2js的方法,把xml对象变成JS对象。 '
userinfo(xmljs) {
const { xml } = xmljs;
let userinfo = {};
for (let key in xml) {
const value = xml[key];
userinfo[key] = value[0];
}
return userinfo;
}
'//调用自定义的函数,遍历xml的对象,然后将其转换成好操作的js对象。'
}
处理响应数据的模块
'response.js'
const model = require('./model');
function response(userinfos) {
let options = {
ToUserName: userinfos.FromUserName,
FromUserName: userinfos.ToUserName,
CreateTime: Date.now(),
MsgType: 'text',
content: 'ä½ æ˜¯ç‹—'
};
'上面的options是将xml数据返回给微信服务器时必须有的
几个属性,下面的不同的用户发送的数据去设置不同的响应
,这里仁者见仁,只要最后的model模块跟我一样就可以了,
返回什么样的数据看各位自己的业务逻辑。'
if (userinfos.MsgType == 'text') {
} else if (userinfos.MsgType == 'image') {
options.MediaId = userinfos.MediaId;
options.MsgType = 'image';
} else if (userinfos.MsgType == 'voice') {
options.MsgType = 'voice';
options.MediaId = userinfos.MediaId;
} else if (userinfos.MsgType == 'video') {
options.MsgType = 'video';
options.MediaId = userinfos.MediaId;
}
else if (userinfos.MsgType == 'music') {
options.MsgType = 'music';
options.MusicUrl = userinfos.MusicUrl;
}
else if (userinfos.MsgType == 'news') {
options.MsgType = 'news';
options.PicUrl = userinfos.PicUrl;
options.Url = userinfos.Url;
}
return model(options)
}
module.exports = response;
最后是model模板模块,这个模块一般不会变的
'这个是返回的数据模板格式,微信规定的,其他的数据类型返回
用户端是无法解析的。'
function model(options) {
let ressendinfo =
`<xml>
<ToUserName><![CDATA[${options.ToUserName}]]></ToUserName>
<FromUserName><![CDATA[${options.FromUserName}]]></FromUserName>
<CreateTime>${options.CreateTime}</CreateTime>
<MsgType><![CDATA[${options.MsgType}]]></MsgType>
`;
if (options.MsgType == 'text') {
ressendinfo += `<Content><![CDATA[${options.content}]]></Content> </xml>`;
} else if (options.MsgType == 'image') {
ressendinfo += ` <Image>
<MediaId><![CDATA[${options.MediaId}]]></MediaId>
</Image> </xml>`
} else if (options.MsgType = 'voice') {
ressendinfo += `<Voice>
<MediaId><![CDATA[${options.MediaId}]]></MediaId>
</Voice>
</xml>`
} else if (options.MsgType = 'video') {
ressendinfo += `<Video>
<MediaId><![CDATA[${options.MediaId}]]></MediaId>
<Title><![CDATA[${options.title}]]></Title>
<Description><![CDATA[${options.description}]></Description >
</Video >
</xml > `
} else if (userinfos.MsgType == 'music') {
ressendinfo += `<Music>
<Title><![CDATA[${options.TITLE}]]></Title>
<Description><![CDATA[${options.DESCRIPTION}]]></Description>
<MusicUrl><![CDATA[${options.MUSIC_Url}]]></MusicUrl>
<HQMusicUrl><![CDATA[${options.HQ_MUSIC_Url}]]></HQMusicUrl>
<ThumbMediaId><![CDATA[${options.media_id}]]></ThumbMediaId>
</Music>
</xml>`
} else if (userinfos.MsgType == 'news') {
replyMessage += options.content.reduce((prev, curr) => {
}, '')
}
return ressendinfo;
}
module.exports = model;
'这个项目用到的第三方中间件很少,就一个express框架,而且这种模式应该是微信公众号的最基础模式,
是全栈工程师应该掌握的基本技能,后期分享更多的数据端和后端技术,希望大家多多支持,点赞的夜夜
做新郎。' 如果有问题和反馈,可以下面留言