koa2微信公眾號(hào)開(kāi)發(fā)及JS-SDK的使用

微信公眾號(hào)項(xiàng)目介紹

當(dāng)前只對(duì)實(shí)現(xiàn)思路進(jìn)行說(shuō)明,不作基礎(chǔ)介紹办桨!詳細(xì)信息查看微信官方文檔git地址

實(shí)現(xiàn)功能

  1. 新關(guān)注自動(dòng)回復(fù)站辉;

  2. 被動(dòng)回復(fù)呢撞;

  3. 推送消息;

  4. 自定義菜單饰剥;

  5. js-sdk使用殊霞;

  6. 網(wǎng)頁(yè)授權(quán)獲取用戶(hù)信息

koa2環(huán)境搭建

  1. koa-generator的安裝

    cnpm install -g koa-generator
    
  2. 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ā)者成功,否則接入失敗盹沈。

  1. 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)源于微信

  2. 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ì)提示配置成功肃晚!

360截圖167401157264113_看圖王.png

繼續(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)限山叮。

  • 步驟三:通過(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é)果如下:

360截圖17321128447023.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末你踩,一起剝皮案震驚了整個(gè)濱河市诅岩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌带膜,老刑警劉巖吩谦,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異膝藕,居然都是意外死亡式廷,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)芭挽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)滑废,“玉大人,你說(shuō)我怎么就攤上這事袜爪∪涑茫” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵辛馆,是天一觀的道長(zhǎng)俺陋。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么腊状? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任诱咏,我火速辦了婚禮,結(jié)果婚禮上缴挖,老公的妹妹穿的比我還像新娘袋狞。我一直安慰自己,他們只是感情好映屋,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布硕并。 她就那樣靜靜地躺著,像睡著了一般秧荆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上埃仪,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天乙濒,我揣著相機(jī)與錄音,去河邊找鬼卵蛉。 笑死颁股,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的傻丝。 我是一名探鬼主播甘有,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼葡缰!你這毒婦竟也來(lái)了亏掀?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤泛释,失蹤者是張志新(化名)和其女友劉穎滤愕,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體怜校,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡间影,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了茄茁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片魂贬。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖裙顽,靈堂內(nèi)的尸體忽然破棺而出付燥,到底是詐尸還是另有隱情,我是刑警寧澤锦庸,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布机蔗,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏萝嘁。R本人自食惡果不足惜梆掸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望牙言。 院中可真熱鬧酸钦,春花似錦、人聲如沸咱枉。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蚕断。三九已至欢伏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間亿乳,已是汗流浹背硝拧。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留葛假,地道東北人障陶。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像聊训,于是被迫代替她去往敵國(guó)和親抱究。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348