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