Node.js之快速搭建微信公众号服务器 [全教程源码,拿去就能跑]

997 阅读6分钟

本篇着重介绍如何使用Node.js去搭建一个微信公众号平台的服务器,上一篇介绍的是如何使用express框架和mongodb数据库以及session、cookie去前后端交互,有兴趣的可以去看看。

项目的基本思路(ReadMe):

# 微信公众号开发
## 1、验证服务器有效性
* 填写服务器配置信息
  * url  开发者服务器地址
    * 通过ngrok工具将本地地址转化外网能访问的地址(内网穿透)
    * 指令: ngrok http 3000
  * token 参与微信加密签名的参数
* 验证服务器消息有效性
  * 将token、timestamp、nonce三个参数进行字典序排序
    * 因为要排序,最好组合成数组: [token、timestamp、nonce]
    * token来自于页面填写的  timestamp、nonce来自于微信发送过来的查询字符串
    * 字典序排序是按照0-9a-z的顺序进行排序,对应的是数组的sort方法
  * 将三个参数字符串拼接成一个字符串进行sha1加密
    * 数组的join方法就是用来拼串
  * 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
    * 成功微信服务器要求返回echostr 
    * 失败说明消息不是微信服务器,返回error

## 2、自动回复
* 接受用户发送的消息
  * 微信会发送两种类型消息: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

## 3、模块化项目
* 目的:
  * 模块功能单一化
  * 方便今后维护、扩展、更加健壮
* 将微信加密签名算法方法合并
* 提取了接受用户消息的三个方法,封装成工具函数
  * 封装用来获取用户发送的消息的工具函数
  * 封装将xml数据解析为js对象的工具函数
  * 封装格式化js对象的方法的工具函数
* 封装中间件函数模块,采用的
  * app.use(reply())
  * reply() 方法 返回值是一个中间件函数 --> 更有利于扩展函数的功能
  * 将相关模块依赖放进来并且修改好模块路径

## 4、封装回复6种消息模板文件
* 回复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框架,而且这种模式应该是微信公众号的最基础模式,
是全栈工程师应该掌握的基本技能,后期分享更多的数据端和后端技术,希望大家多多支持,点赞的夜夜
做新郎。' 如果有问题和反馈,可以下面留言