微信公眾號(hào)項(xiàng)目介紹
當(dāng)前只對(duì)實(shí)現(xiàn)思路進(jìn)行說(shuō)明,不作基礎(chǔ)介紹办桨!詳細(xì)信息查看微信官方文檔及git地址!
實(shí)現(xiàn)功能
新關(guān)注自動(dòng)回復(fù)站辉;
被動(dòng)回復(fù)呢撞;
推送消息;
自定義菜單饰剥;
js-sdk使用殊霞;
網(wǎng)頁(yè)授權(quán)獲取用戶(hù)信息
koa2環(huán)境搭建
-
koa-generator的安裝
cnpm install -g koa-generator
-
koa2項(xiàng)目建立
koa2 -e wechat
使用飛鴿內(nèi)網(wǎng)穿透
使用比較簡(jiǎn)單就不作詳細(xì)介紹了!
接入微信公眾平臺(tái)開(kāi)發(fā)
編寫(xiě)reply中間件捐川;新建reply文件夾脓鹃,進(jìn)入文件夾新建index文件!
安裝所需依賴(lài):
cnpm i -S sha1
在app.js入口文件中路由配置前引入中間件古沥!
...
const reply = require("./reply")
...
//接收處理所有消息
app.use(reply());
// routes
app.use(index.routes(), index.allowedMethods())
...
驗(yàn)證消息的確來(lái)自微信服務(wù)器
微信服務(wù)器會(huì)發(fā)送兩種類(lèi)型的消息給開(kāi)發(fā)者服務(wù)器.
開(kāi)發(fā)者通過(guò)檢驗(yàn)signature對(duì)請(qǐng)求進(jìn)行校驗(yàn)(下面有校驗(yàn)方式)瘸右。若確認(rèn)此次GET請(qǐng)求來(lái)自微信服務(wù)器,請(qǐng)?jiān)瓨臃祷豦chostr參數(shù)內(nèi)容岩齿,則接入生效太颤,成為開(kāi)發(fā)者成功,否則接入失敗盹沈。
-
GET請(qǐng)求
-驗(yàn)證服務(wù)器的有效性
1)將token龄章、timestamp、nonce三個(gè)參數(shù)進(jìn)行字典序排序
2)將三個(gè)參數(shù)字符串拼接成一個(gè)字符串進(jìn)行sha1加密
3)開(kāi)發(fā)者獲得加密后的字符串可與signature對(duì)比乞封,標(biāo)識(shí)該請(qǐng)求來(lái)源于微信
-
POST請(qǐng)求
-微信服務(wù)器會(huì)將用戶(hù)發(fā)送的數(shù)據(jù)以post請(qǐng)求的方式轉(zhuǎn)發(fā)到開(kāi)發(fā)者服務(wù)器上
const config = require("../config/config")
const sha1 = require("sha1");
module.exports = () => {
return async(ctx, next) => {
const { signature, timestamp, nonce, echostr } = ctx.query;
const token = config.wechat.token;
let str = [token, timestamp, nonce].sort().join('');
const sha = sha1(str);
if (ctx.method === "GET" && sha === signature) {
//如果一樣說(shuō)明消息來(lái)自于微信服務(wù)器做裙,返回echostr給微信服務(wù)器
ctx.body = echostr;
} else if (ctx.method === "POST" && sha === signature) {
} else {
await next();
// ctx.body = "Failed"
}
}
}
在測(cè)試號(hào)中進(jìn)行配置,若無(wú)誤會(huì)提示配置成功肃晚!
繼續(xù)處理post請(qǐng)求锚贱,并實(shí)現(xiàn)回復(fù)消息
安裝依賴(lài)raw-body和xml2js:
cnpm i -S raw-body xml2js
處理POST請(qǐng)求,具體實(shí)現(xiàn)方法移步源碼查看关串!
...
const getRawBody = require("raw-body");
const { parseXML, formatMessage, tpl2xml } = require("../utils/tool")
const reply = require("./reply")
...
const data = await getRawBody(ctx.req, {
length: ctx.length,
limit: "1mb",
encoding: ctx.charset
})
const content = await parseXML(data);
// console.log(content);
const message = formatMessage(content.xml);
// console.log(message);
let replyBody = await reply(message);
// console.log(replyBody);
//生成xml數(shù)據(jù)
let xml = tpl2xml(replyBody, message);
// console.log(xml)
ctx.status = 200;
ctx.type = 'application/xml';
ctx.body = xml;
...
reply.js
/**
* 處理用戶(hù)發(fā)送的消息和內(nèi)容拧廊,返回不同的內(nèi)容給用戶(hù)
*/
module.exports = async(message) => {
let replyBody = "您在說(shuō)什么,我聽(tīng)不懂晋修!";
if (message.MsgType === "text") {
let content = message.Content;
if (content === "熱門(mén)") {
replyBody = "熱門(mén)";
} else if (content === "2") {
replyBody = "落地成盒";
} else if (content.match("愛(ài)")) {
replyBody = "我愛(ài)你~";
}
} else if (message.MsgType === "voice") {
options.msgType = "voice";
options.mediaId = message.MediaId;
console.log(message.Recognition);
} else if (message.MsgType === "event") {
if (message.Event === "subscribe") {
replyBody = "歡迎您的關(guān)注~\n" +
"回復(fù) 首頁(yè) 查看電影預(yù)告片頁(yè)面\n" +
"回復(fù) 熱門(mén) 查看最新熱門(mén)電影\n" +
"回復(fù) 文本 查看指定電影信息\n" +
"回復(fù) 語(yǔ)音 查看指定電影信息\n" +
"也可以點(diǎn)擊下面的菜單按鈕來(lái)了解其它信息~";
} else if (message.Event === "unsubscribe") {
console.log("用戶(hù)取消關(guān)注了吧碾!")
} else if (message.Event === "CLICK") {
replyBody = "您可以按照以下提示來(lái)進(jìn)行操作~\n" +
"回復(fù) 首頁(yè) 查看電影預(yù)告片頁(yè)面\n" +
"回復(fù) 熱門(mén) 查看最新熱門(mén)電影\n" +
"回復(fù) 文本 查看指定電影信息\n" +
"回復(fù) 語(yǔ)音 查看指定電影信息\n" +
"也可以點(diǎn)擊下面的菜單按鈕來(lái)了解其它信息~";
}
}
return replyBody;
}
自定義菜單及微信JS-SDK分享接口實(shí)例
根目錄新建wechat文件夾,進(jìn)入文件夾創(chuàng)建menu.js和wechat.js文件墓卦。
wechat.js封裝了access_token倦春、jsapi_ticket、創(chuàng)建和刪除菜單!
自定義菜單
/*routes/index.js*/
...
// 創(chuàng)建實(shí)例對(duì)象
const Wechat = require("../wechat/wechat")
const wechatApi = new Wechat();
//menu.js文件重新配置菜單
router.get('/updateMenu', async(ctx, next) => {
let result = await wechatApi.createMenu(menu);
ctx.body = result
})
...
JSSDK使用步驟
-
步驟一:綁定域名
先登錄微信公眾平臺(tái)進(jìn)入“公眾號(hào)設(shè)置”的“功能設(shè)置”里填寫(xiě)“JS接口安全域名”溅漾。
備注:登錄后可在“開(kāi)發(fā)者中心”查看對(duì)應(yīng)的接口權(quán)限山叮。
-
步驟二:引入JS文件
在需要調(diào)用JS接口的頁(yè)面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.6.0.js
如需進(jìn)一步提升服務(wù)穩(wěn)定性添履,當(dāng)上述資源不可訪問(wèn)時(shí),可改訪問(wèn):http://res2.wx.qq.com/open/js/jweixin-1.6.0.js (支持https)脑又。
備注:支持使用 AMD/CMD 標(biāo)準(zhǔn)模塊加載方法加載
-
步驟三:通過(guò)config接口注入權(quán)限驗(yàn)證配置
所有需要使用JS-SDK的頁(yè)面必須先注入配置信息暮胧,否則將無(wú)法調(diào)用(同一個(gè)url僅需調(diào)用一次,對(duì)于變化url的SPA的web app可在每次url變化時(shí)進(jìn)行調(diào)用,目前Android微信客戶(hù)端不支持pushState的H5新特性问麸,所以使用pushState來(lái)實(shí)現(xiàn)web app的頁(yè)面會(huì)導(dǎo)致簽名失敗往衷,此問(wèn)題會(huì)在Android6.2中修復(fù))。
wx.config({ debug: true, // 開(kāi)啟調(diào)試模式,調(diào)用的所有api的返回值會(huì)在客戶(hù)端alert出來(lái)严卖,若要查看傳入的參數(shù)席舍,可以在pc端打開(kāi),參數(shù)信息會(huì)通過(guò)log打出哮笆,僅在pc端時(shí)才會(huì)打印来颤。 appId: '', // 必填,公眾號(hào)的唯一標(biāo)識(shí) timestamp: , // 必填稠肘,生成簽名的時(shí)間戳 nonceStr: '', // 必填福铅,生成簽名的隨機(jī)串 signature: '',// 必填,簽名 jsApiList: [] // 必填项阴,需要使用的JS接口列表 });
-
步驟四:通過(guò)ready接口處理成功驗(yàn)證
wx.ready(function(){ // config信息驗(yàn)證后會(huì)執(zhí)行ready方法滑黔,所有接口調(diào)用都必須在config接口獲得結(jié)果之后,config是一個(gè)客戶(hù)端的異步操作环揽,所以如果需要在頁(yè)面加載時(shí)就調(diào)用相關(guān)接口略荡,則須把相關(guān)接口放在ready函數(shù)中調(diào)用來(lái)確保正確執(zhí)行。對(duì)于用戶(hù)觸發(fā)時(shí)才調(diào)用的接口歉胶,則可以直接調(diào)用汛兜,不需要放在ready函數(shù)中。 });
-
步驟五:通過(guò)error接口處理失敗驗(yàn)證
wx.error(function(res){ // config信息驗(yàn)證失敗會(huì)執(zhí)行error函數(shù)跨扮,如簽名過(guò)期導(dǎo)致驗(yàn)證失敗序无,具體錯(cuò)誤信息可以打開(kāi)config的debug模式查看,也可以在返回的res參數(shù)中查看衡创,對(duì)于SPA可以在這里更新簽名帝嗡。 });
創(chuàng)建jssdk路由權(quán)限簽名
/*routes/index.js*/
...
const { appID } = require("../config/config").wechat;
...
//用于JS-SDK使用權(quán)限簽名算法
router.get('/jssdk', async(ctx, next) => {
/* JS-SDK使用權(quán)限(簽名算法)
簽名生成規(guī)則如下:參與簽名的字段包括noncestr(隨機(jī)字符串),
有效的jsapi_ticket, timestamp(時(shí)間戳), url(當(dāng)前網(wǎng)頁(yè)的URL旨袒,不包含#及其后面部分) 洛勉。
*/
//獲取傳入的url
let url = ctx.query.url;
const { ticket } = await wechatApi.fetchTicket();
const nonceStr = Math.random().toString().split(".")[1];
const timestamp = Date.now();
const arr = [`jsapi_ticket=${ticket}`, `noncestr=${nonceStr}`, `timestamp=${timestamp}`, `url=${url}`];
const str = arr.sort().join("&");
const signature = sha1(str);
ctx.body = {
appId: appID,
signature,
nonceStr,
timestamp
}
})
...
前端使用
<!-- /*public/test.html*/ -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://libs.cdnjs.net/zepto/1.2.0/zepto.min.js"></script>
<script src="http://res2.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
</head>
<body>
<div>123</div>
<script>
$(function() {
let shareUrl = location.href.split('#')[0];
$.ajax({
url: "http://caorui.max.svipss.top/jssdk",
data: {
url: shareUrl
},
success: function(data) {
wx.config({
//debug: true, // 開(kāi)啟調(diào)試模式,調(diào)用的所有api的返回值會(huì)在客戶(hù)端alert出來(lái),若要查看傳入的參數(shù)仅乓,可以在pc端打開(kāi),參數(shù)信息會(huì)通過(guò)log打出巢寡,僅在pc端時(shí)才會(huì)打印喉脖。
appId: data.appId, // 必填,公眾號(hào)的唯一標(biāo)識(shí)
timestamp: data.timestamp, // 必填抑月,生成簽名的時(shí)間戳
nonceStr: data.nonceStr, // 必填树叽,生成簽名的隨機(jī)串
signature: data.signature, // 必填,簽名
jsApiList: [ // 必填谦絮,需要使用的JS接口列表
"updateAppMessageShareData",
"updateTimelineShareData"
]
});
wx.ready(function() {
wx.updateAppMessageShareData({
title: '分享測(cè)試12dsd', // 分享標(biāo)題
desc: '分享描述cgngn', // 分享描述
link: shareUrl, // 分享鏈接
imgUrl: '分享圖標(biāo)', // 分享圖標(biāo)
success: function() {
// 用戶(hù)確認(rèn)分享后執(zhí)行的回調(diào)函數(shù)
}
});
wx.updateTimelineShareData({
title: '分享測(cè)試12fsf', // 分享標(biāo)題
link: shareUrl, // 分享鏈接
imgUrl: '分享圖標(biāo)', // 分享圖標(biāo)
success: function() {
// 用戶(hù)確認(rèn)分享后執(zhí)行的回調(diào)函數(shù)
}
});
wx.error(function(res) {
alert(res.errMsg); // 正式環(huán)境記得關(guān)閉疤馑小!2阒濉P远А!
});
})
}
})
})
</script>
</body>
</html>
網(wǎng)頁(yè)授權(quán)獲取用戶(hù)信息
具體而言叫胖,網(wǎng)頁(yè)授權(quán)流程分為四步:
1草冈、引導(dǎo)用戶(hù)進(jìn)入授權(quán)頁(yè)面同意授權(quán),獲取code
2瓮增、通過(guò)code換取網(wǎng)頁(yè)授權(quán)access_token(與基礎(chǔ)支持中的access_token不同)
3怎棱、如果需要,開(kāi)發(fā)者可以刷新網(wǎng)頁(yè)授權(quán)access_token钉赁,避免過(guò)期
4蹄殃、通過(guò)網(wǎng)頁(yè)授權(quán)access_token和openid獲取用戶(hù)基本信息(支持UnionID機(jī)制)
在routes/index.js添加路由
//微信網(wǎng)頁(yè)授權(quán)獲取code
router.get("/oauth", async(ctx, next) => {
let redirect_uri = `http%3a%2f%2fcaorui.max.svipss.top/oauth.html`;
ctx.redirect(`https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appID}&redirect_uri=${redirect_uri}&response_type=code&scope=snsapi_userinfo&state=STATE&connect_redirect=1#wechat_redirect`)
})
//獲取授權(quán)后的用戶(hù)信息
router.get("/getUserInfo", async(ctx, next) => {
//獲取code值
let code = ctx.query.code;
if (!code) {
ctx.redirect('http://caorui.max.svipss.top/oauth')
}
let result = await wechatApi.getOauthAccessToken(code);
let data = await wechatApi.getOauthUserinfo(result.access_token, result.openid);
ctx.body = data;
})
通過(guò)code獲取AccessToken 和 獲取授權(quán)后的用戶(hù)資料 的方法查看wechat.js文件!
在public/oauth.html中使用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://libs.cdnjs.net/zepto/1.2.0/zepto.min.js"></script>
<script src="http://res2.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
</head>
<body>
<div>
<button id="oauth">獲取用戶(hù)信息</button>
<div id="userInfo"></div>
</div>
<script>
$(function() {
//獲取url參數(shù)
function getQueryVariable(variable) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
if (pair[0] == variable) {
return pair[1];
}
}
return (false);
}
var userinfo = JSON.parse(localStorage.getItem("userinfo"));
// console.log(userinfo)
if (userinfo) {
$("#oauth").hide();
var html = `
<image src="${userinfo.headimgurl}"/>
<h2>你已經(jīng)登錄</h2>
`;
$("#userInfo").html(html);
return;
}
$("#oauth").on("click", function() {
location.;
})
var code = getQueryVariable("code");
if (code) {
$.ajax({
url: "http://caorui.max.svipss.top/getUserInfo",
data: {
code
},
success: function(data) {
$("#oauth").hide();
// console.log(data)
localStorage.setItem("userinfo", JSON.stringify(data));
var html = `
<image src="${data.headimgurl}"/>
<p>nickname:${data.nickname}</p>
<p>country:${data.country}</p>
<p>province:${data.province}</p>
<p>city:${data.city}</p>
<p>openid:${data.openid}</p>
`
$("#userInfo").html(html)
}
})
}
})
</script>
</body>
</html>
結(jié)果如下: