Node.js之快速搭建微信公眾號伺服器 [全教程原始碼,拿去就能跑]

Jeery_譚金傑發表於2019-03-19

本篇著重介紹如何使用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框架,而且這種模式應該是微信公眾號的最基礎模式,
是全棧工程師應該掌握的基本技能,後期分享更多的資料端和後端技術,希望大家多多支援,點讚的夜夜
做新郎。' 如果有問題和反饋,可以下面留言



複製程式碼

相關文章