申请测试公众号
首先登录微信公众平台,选择自己的公众号登录。登录成功后点击开发者工具,选择公众平台测试账号

点击进去后我们可以申请一个测试用的公众号,可以体验所有高级接口,这里我们要配置一个线上的接口地址,在验证 Tonken,和收发消息时微信都会请求我们配置的地址,这里推荐一个好用的内网穿透工具,可以把我们本地的项目地址映射到外网上,方便我们调试
小米球内网穿透工具
这里我生成的线上地址是 http://songzx.ngrok2.xiaomiqiu.cn/,下面我们会用这个地址作为我们的公众号的接口配置地址

实现Tonken验证
首先新建一个空白的 node 项目
接着安装一些常用的依赖
接在在项目根路径下新建 index.js,初始代码如下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | const express = require("express")const app = express()
 
 app.get("/",(req,res)=>{
 res.send('Hello World')
 })
 
 app.listen(8088,()=>{
 console.log("running 127.0.0.1:8088");
 })
 
 | 
然后启动项目并用浏览器访问 127.0.0.1:8088可以看到如下结果,表示服务启动成功

现在我们实现验证 tonken 的逻辑
首先安装如下依赖,用作加密处理
然后新建 util 和 router 两个文件夹,分别放置我们的统一的方法和普通请求方法
然后新建 util -> validateToken.js 文件,代码如下,这个方法专门用来验证微信传递过来的 Tonken
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 
 | var crypto = require("crypto");
 
 function sha1(str) {
 var md5sum = crypto.createHash("sha1");
 md5sum.update(str);
 str = md5sum.digest("hex");
 return str;
 }
 
 
 function validateToken(req) {
 return new Promise((resolve, reject) => {
 let query = req.query;
 let signature = query.signature;
 let echostr = query["echostr"];
 let timestamp = query["timestamp"];
 let nonce = query["nonce"];
 let oriArray = new Array();
 oriArray[0] = nonce;
 oriArray[1] = timestamp;
 oriArray[2] = "admin123";
 oriArray.sort();
 let original = oriArray.join("");
 let scyptoString = sha1(original);
 if (signature == scyptoString) {
 
 resolve(echostr);
 } else {
 reject(false);
 }
 });
 }
 
 module.exports = validateToken;
 
 | 
然后新建 router -> weChat.js 文件,这个文件专门用来处理微信发送过来的请求,在这个文件中编写如下代码
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | const express = require("express");const router = express.Router();
 const validateToken = require("../util/validateToken");
 
 
 router.get("/", (req, res) => {
 validateToken(req).then((t) => {
 res.send(t);
 });
 });
 
 module.exports = router;
 
 | 
最后修改一下index.js文件,引入我们新建的  router.js 文件
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | const express = require("express");const app = express();
 const path = require("path");
 const weChat = require(path.resolve(__dirname, "./router/weChat"));
 
 app.use(weChat);
 
 app.listen(8088, () => {
 console.log("running 127.0.0.1:8088");
 });
 
 | 
现在我们去微信公众号配置页面中测试一下

页面中弹出 配置成功 就表示我们验证 Tonken 的业务已经完成了
获取Tonken并定时刷新
微信中获取 Tonken 要发送一个 get 请求来获取,并且这个 Tonken 有过期时间,我们需要自己保存这个 Tonken 并定时刷新,以保证 Tonken 有效性
微信官方对于获取 Tonken 的描述
接口调用说明
- 请求方式: GET
- 请求地址:https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
既然要用到请求,我们安装一个 axios 用来发送请求
然后在根目录新建 public -> tonken.json ,用来存放我们获取到的 tonken,也是对 tonken 的一种持久化存储方式,json文件内容为空即可
接着新建 util -> tonkenConfig.js 文件,代码如下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 
 | const fs = require("fs");const path = require("path");
 const http = require("axios");
 const fileUrl = path.resolve(__dirname, "../public/tonken.json");
 const APPID = "wx2188729b190d357d";
 const APPSECRET = "d976b0e6262b829ba003e9a24032447c";
 let INTERTIME = (7200 - 60) * 1000;
 
 
 function setTonken() {
 return new Promise((resolve, reject) => {
 http
 .get(
 `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`
 )
 .then((res) => {
 
 INTERTIME = (res.data.expires_in - 60) * 1000;
 
 fs.writeFile(
 fileUrl,
 JSON.stringify({
 tonken: res.data.access_token,
 }),
 () => {
 
 resolve();
 }
 );
 });
 });
 }
 
 
 function timingSetTonken() {
 
 setInterval(() => {
 setTonken();
 }, INTERTIME);
 }
 
 
 function getTonken() {
 return new Promise((resolve, reject) => {
 
 fs.readFile(fileUrl, (err, data) => {
 
 resolve(JSON.parse(data).tonken);
 });
 });
 }
 
 
 module.exports = {
 setTonken,
 getTonken,
 timingSetTonken,
 };
 
 | 
然后在 router -> weChat.js 中引入 tonkenConfig.js 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | const express = require("express");const router = express.Router();
 const validateToken = require("../util/validateToken");
 const { setTonken, timingSetTonken } = require("../util/tonkenConfig");
 
 setTonken().then(() => {
 
 timingSetTonken();
 });
 
 
 router.get("/", (req, res) => {
 validateToken(req).then((t) => {
 res.send(t);
 });
 });
 
 module.exports = router;
 
 | 
此时我们在启动项目后会自动调用一下获取 tonken 的接口,然后从接口中获取到一个过期时间,微信返回的过期时间是以秒为单位,减去60秒是为了下一次tonken时与这次tonken之间的平滑过渡,之后每隔这个时间会重新获取一次tonken
我们将这个tonken写入到了一个json文件中,我们可以在任何文件中通过如下方法获取tonken
| 12
 3
 4
 5
 
 | const { getTonken } = require("./util/tonkenConfig");
 getTonken().then((tonken) => {
 console.log(tonken);
 });
 
 | 
接收微信消息并回复
官方对于接收消息的描述
简单说就是:我们在微信公众号中发送消息后,微信会发送一个 post 请求给我们上面配置的地址,参数时一段 xml 文本,我们需要解析这个 xml,并按照微信指定的格式回复一个 xml 格式的字符串,注意是回复 xml 格式的字符串
首先安装依赖,用来解析post请求中的xml参数
| 1
 | npm install express-xml-bodyparser
 | 
然后在 index.js 文件中引用并配置中间件
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | const express = require("express");const app = express();
 const path = require("path");
 const weChat = require(path.resolve(__dirname, "./router/weChat"));
 const xmlparser = require('express-xml-bodyparser');
 app.use(express.json());
 app.use(express.urlencoded());
 app.use(xmlparser());
 
 app.use(weChat);
 
 app.listen(8088, () => {
 console.log("running 127.0.0.1:8088");
 });
 
 | 
然后在 weChat.js 中添加一个 post 请求,打印一下看看微信给我们发过来的是什么东西
| 12
 3
 4
 5
 
 | router.post("/", (req, res) => {
 console.log(req.body);
 res.send("");
 });
 
 | 
重启项目,我们往微信公众号中随便发送一个消息

解析后的参数如下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | {xml: {
 tousername: [ 'gh_a0f004c20d2b' ],
 fromusername: [ 'olttN6WJOYe-lTysV8_tsnZ7-HMQ' ],
 createtime: [ '1621416487' ],
 msgtype: [ 'text' ],
 content: [ 'hello' ],
 msgid: [ '23213103466653274' ]
 }
 }
 
 | 
拿到参数后我们可以根据参数中的 msgtype 判断传递过来的消息类型,以及 content 是消息内容,获取到了参数,接下要做的就是根据消息回复内容了
官方被动回复用户消息文档
下面是一个回复消息的模板代码,可以很方便的帮助我们生成指定的 xml 格式的字符串
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 
 | exports.textMessage = function (message) {
 var createTime = new Date().getTime();
 return `<xml>
 <ToUserName><![CDATA[${message.FromUserName}]]></ToUserName>
 <FromUserName><![CDATA[${message.ToUserName}]]></FromUserName>
 <CreateTime>${createTime}</CreateTime>
 <MsgType><![CDATA[text]]></MsgType>
 <Content><![CDATA[${message.reply}]]></Content>
 </xml>`;
 };
 
 exports.imageMessage = function (message) {
 var createTime = new Date().getTime();
 return `<xml>
 <ToUserName><![CDATA[${message.FromUserName}]]></ToUserName>
 <FromUserName><![CDATA[${message.ToUserName}]]></FromUserName>
 <CreateTime>${createTime}</CreateTime>
 <MsgType><![CDATA[image]]></MsgType>
 <Image>
 <MediaId><![CDATA[${message.mediaId}]]></MediaId>
 </Image>
 </xml>`;
 };
 
 exports.voiceMessage = function (message) {
 var createTime = new Date().getTime();
 return `<xml>
 <ToUserName><![CDATA[${message.FromUserName}]]></ToUserName>
 <FromUserName><![CDATA[${message.ToUserName}]]></FromUserName>
 <CreateTime>${createTime}</CreateTime>
 <MsgType><![CDATA[voice]]></MsgType>
 <Voice>
 <MediaId><![CDATA[${message.mediaId}]]></MediaId>
 </Voice>
 </xml>`;
 };
 
 exports.videoMessage = function (message) {
 var createTime = new Date().getTime();
 return `<xml>
 <ToUserName><![CDATA[${message.FromUserName}]]></ToUserName>
 <FromUserName><![CDATA[${message.ToUserName}]]></FromUserName>
 <CreateTime>${createTime}</CreateTime>
 <MsgType><![CDATA[video]]></MsgType>
 <Video>
 <MediaId><![CDATA[${message.mediaId}]]></MediaId>
 <Title><![CDATA[${message.title}]]></Title>
 <Description><![CDATA[${message.description}]]></Description>
 </Video>
 </xml>`;
 };
 
 exports.articleMessage = function (message) {
 var createTime = new Date().getTime();
 return `<xml>
 <ToUserName><![CDATA[${message.FromUserName}]]></ToUserName>
 <FromUserName><![CDATA[${message.ToUserName}]]></FromUserName>
 <CreateTime>${createTime}</CreateTime>
 <MsgType><![CDATA[news]]></MsgType>
 <ArticleCount>${message.articles.length}</ArticleCount>
 <Articles>
 ${message.articles
 .map(
 (article) =>
 `<item><Title><![CDATA[${article.title}]]></Title>
 <Description><![CDATA[${article.description}]]></Description>
 <PicUrl><![CDATA[${article.img}]]></PicUrl>
 <Url><![CDATA[${article.url}]]></Url></item>`
 )
 .join("")}
 </Articles>
 </xml>`;
 };
 
 | 
在 weChat.js 中引入上面的模板,这里我把模板代码放到了 util -> template.js 中,然后修改刚刚新建的 post 方法
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | const template = require("../util/template");
 
 
 router.post("/", (req, res) => {
 let xml = req.body.xml;
 let msgtype = xml.msgtype[0];
 switch (msgtype) {
 case "text":
 
 let message = {
 FromUserName: xml.fromusername[0],
 ToUserName: xml.tousername[0],
 reply: "你好呀,我是通过代码回复你的",
 };
 res.send(template.textMessage(message));
 break;
 
 default:
 res.send("");
 break;
 }
 });
 
 | 
我们现在在发送消息试一试

我们看到公众号已经可以回答我们了。