先進(jìn)章:開(kāi)始開(kāi)發(fā)(準(zhǔn)備階段)

1. 接入指南

 

接入微信公眾平臺(tái)開(kāi)發(fā),開(kāi)發(fā)者需要按照如下步驟完成:

  • 填寫(xiě)服務(wù)器配置

 

替代文字

說(shuō)明:URL是開(kāi)發(fā)者用來(lái)接收微信消息和事件的接口URL,該接口盡量寫(xiě)成兩個(gè)請(qǐng)求方式,1:get請(qǐng)求,用于驗(yàn)證如下地址有效性,2:post請(qǐng)求, 用于接收消息和事件,Token 可以隨便定義用于驗(yàn)證接口簽名有效性, EncodingAESKey是加密的密鑰,下面加密方式選兼容模式或者安全模式的時(shí)候開(kāi)發(fā)者可根據(jù)該密鑰對(duì)數(shù)據(jù)進(jìn)行加解密

 

  • 驗(yàn)證如上URL服務(wù)器地址的有效性

    根據(jù)官方文檔的說(shuō)明,微信驗(yàn)證接口會(huì)帶下面幾個(gè)參數(shù)

參數(shù) 描述
signature 微信加密簽名,signature結(jié)合了開(kāi)發(fā)者填寫(xiě)的token參數(shù)和請(qǐng)求中的timestamp參數(shù)、nonce參數(shù)
timestamp 時(shí)間戳
nonce 隨機(jī)數(shù)
echostr 隨機(jī)字符串

服務(wù)端接口接收到這些參數(shù)后進(jìn)行簽名驗(yàn)證,如下代碼:

exports.check = function (req, res, next) {
         // 在這里驗(yàn)證簽名
         var signature = req.query['signature'],
               timestamp = req.query['timestamp'],
               nonce = req.query['nonce'],
               echostr = req.query['echostr'];
          var sha1 = crypto.createHash('sha1'),
                sha1Str = sha1.update([config.weixin.token, timestamp, nonce].sort().join('')).digest('hex');
                res.writeHead(200, {'Content-Type': 'text/plain'});
               res.end((sha1Str === signature) ? echostr : '');
               return res;
};
  • 依據(jù)接口文檔實(shí)現(xiàn)業(yè)務(wù)邏輯

    這里就是根據(jù)業(yè)務(wù)需求,進(jìn)行接口調(diào)用的編程了,下面我會(huì)一一介紹

2. 獲取access_token

 

access_token是公眾號(hào)的全局唯一接口調(diào)用憑據(jù),公眾號(hào)調(diào)用各接口時(shí)都需使用access_token。該接口一天只能請(qǐng)求2000次,開(kāi)發(fā)者需要進(jìn)行妥善保存。access_token的存儲(chǔ)至少要保留512個(gè)字符空間。access_token的有效期目前為2個(gè)小時(shí),需定時(shí)刷新,重復(fù)獲取將導(dǎo)致上次獲取的access_token失效。

如下代碼事例:

exports.get_token = function (fn) {
        redis.get(weixin_token, function (err, token_str) {
            if (token_str) {
                return fn(err, JSON.parse(token_str));
            } else {
                request.get("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + app_id + "&secret=" + app_secret, function (err, response, body) {
                    if (JSON.parse(body).errcode == 45009) {
                        return fn(err)
                    } else {
                        redis.set(weixin_token, body, function (err) {
                            redis.expire(weixin_token, 7000, function () {
                                return fn(err, JSON.parse(body));
                            });
                        });
                    }
                });
            }
        });
    };

上面的事例代碼中,首先我會(huì)從redis中獲取到access_token,因?yàn)槲易畛醌@取access_token的時(shí)候?qū)懭氲絩edis中了,官方給的有效時(shí)間是7200秒,我放在redis中的有效時(shí)間是7000秒,所以我這里的token不會(huì)過(guò)期,過(guò)期后會(huì)重新調(diào)用接口獲取并寫(xiě)入redis

第二章:自定義菜單

 

實(shí)例代碼中只寫(xiě)入創(chuàng)建接口的調(diào)用,查詢(xún)和刪除就不舉例了。

  • 自定義菜單創(chuàng)建接口
    如下代碼事例以及說(shuō)明:
   get_token(function(err, obj){
        var access_token = obj.access_token;
        request.post({
            url: "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=" + access_token,
            json:  {
                "button":[
                    {
                        "type":"view",
                        "name":"工作臺(tái)",
                        "url":"http://worktile.com"
                    },
                    {
                        "name":"解決方案",
                        "sub_button": [
                          {
                            "type":"view",
                            "name":"研發(fā)",
                            "url":"https://pro.worktile.com/solution/dev"
                          }
                          {
                            "type":"view",
                            "name":"較佳實(shí)踐",
                            "url":"https://worktile.com/can"
                          }
                        ]
                    },
                    {
                        "name":"更多",
                        "sub_button":[
                            {
                                "type":"view",
                                "name":"下載應(yīng)用",
                                "url":"http://a.app.qq.com/o/simple.jsp?pkgname=com.worktile"
                            },
                            {
                                "type":"click",
                                "name":"合作",
                                "key":"work_together"
                            }]
                    }]
            }
        }, function(err, res, body){
            console.log(body)
        })
    });

這里get_token方法就是上面先進(jìn)章中#獲取access_token ,而且我這里是作為腳本執(zhí)行的,這樣方便以后隨便修改自定義菜單內(nèi)容


第三章:消息管理

 

  • 接收消息
    接收的消息分為普通消息和事件消息,統(tǒng)一有先進(jìn)章中 接入指南 填寫(xiě)的RUL接口來(lái)接收處理
exports.receive = function (req, res, next) {
        // 在這接收消息
        var xml = '';
        req.setEncoding('utf8');
        req.on('data', function (chunk) {
            xml += chunk;
        });
        req.on('end', function () {
            toJSON(xml, res);
        });
    };

說(shuō)明:receive方法就是接收用戶(hù)發(fā)給公眾號(hào)的消息,內(nèi)容格式是xml,toJSON就是我解析xml為json的方法,如下

 //解析器
    var toJSON = function (xml, res) {
        var msg = {};
        xml2js.parseString(xml, function (err, result) {
            var data = result.xml;
            msg.ToUserName = data.ToUserName[0];
            msg.FromUserName = data.FromUserName[0];
            msg.CreateTime = data.CreateTime[0];
            msg.MsgType = data.MsgType[0];
            switch (msg.MsgType) {
                case 'text' :
                    msg.Content = data.Content[0];
                    msg.MsgId = data.MsgId[0];
                    res.setHeader("Content-Type", "text/plain");
                    res.send("");
                    return handle_text(msg, res);
                    break;
                case 'image' :
                    msg.PicUrl = data.PicUrl[0];
                    msg.MsgId = data.MsgId[0];
                    msg.MediaId = data.MediaId[0];
                    res.setHeader("Content-Type", "text/plain");
                    res.send("");
                    break;
                case 'voice' :
                    msg.MediaId = data.MediaId[0];
                    msg.Format = data.Format[0];
                    msg.MsgId = data.MsgId[0];
                    res.setHeader("Content-Type", "text/plain");
                    res.send("");
                    break;
                case 'video' :
                    msg.MediaId = data.MediaId[0];
                    msg.ThumbMediaId = data.ThumbMediaId[0];
                    msg.MsgId = data.MsgId[0];
                    res.setHeader("Content-Type", "text/plain");
                    res.send("");
                    break;
                case 'location' :
                    msg.Location_X = data.Location_X[0];
                    msg.Location_Y = data.Location_Y[0];
                    msg.Scale = data.Scale[0];
                    msg.Label = data.Label[0];
                    msg.MsgId = data.MsgId[0];
                    res.setHeader("Content-Type", "text/plain");
                    res.send("");
                    break;
                case 'link' :
                    msg.Title = data.Title[0];
                    msg.Description = data.Description[0];
                    msg.Url = data.Url[0];
                    msg.MsgId = data.MsgId[0];
                    res.setHeader("Content-Type", "text/plain");
                    res.send("");
                    break;
                case 'event' :
                    msg.Event = data.Event[0];
                    if (data.EventKey && _.isArray(data.EventKey) && data.EventKey.length > 0) {
                        msg.EventKey = data.EventKey[0];
                        return handle_event(msg, res);
                    }
                    res.setHeader("Content-Type", "text/plain");
                    res.send("");
                    break;
            }
        });
    };

說(shuō)明:這里我用戶(hù)發(fā)過(guò)來(lái)的消息xml解釋稱(chēng)json數(shù)據(jù)msg, 根據(jù)不同的類(lèi)型做不同的處理,如上文本,圖片,音頻,視頻,鏈接,事件等消息

var handle_text = function (msg, res) {
        var text = msg.Content;
        if(text.trim() == "研發(fā)"){
            var data = {
                "touser":msg.FromUserName,
                "msgtype":"news",
                "news":{
                    "articles": [
                        {
                            "title":"重磅!Worktile 推出研發(fā)管理解決方案",
                            "description":"項(xiàng)目進(jìn)度清晰掌握,快速跟進(jìn)產(chǎn)品Bug,多維度統(tǒng)計(jì)報(bào)表,文件文檔有序管理",
                            "url":"https://pro.worktile.com/solution/dev",
                            "picurl":"https://wt-prj.oss.aliyuncs.com/b327e3a5666048279583e8e026ac6b87/4bb6e53c-8516-4466-b278-4f3b596e46db.png"
                        }
                    ]
                }
            };
            sendMessageToUser(data);
        }else if(text.trim() == "電商"){
            var data = {
                "touser":msg.FromUserName,
                "msgtype":"news",
                "news":{
                    "articles": [
                        {
                            "title":"Worktile 『電商解決方案』上線(xiàn)!",
                            "description":"降低運(yùn)營(yíng)成本,提高團(tuán)隊(duì)效率。日常運(yùn)營(yíng)、大促籌備、售后跟蹤、研發(fā)管理……盡在掌握。",
                            "url":"https://pro.worktile.com/solution/ecommerce",
                            "picurl":"https://cdn.worktile.com/solution/ecommerce.png"
                        }
                    ]
                }
            };
            sendMessageToUser(data);
        }
    };

說(shuō)明:這是對(duì)文本消息的處理,如上,如果接收到 研發(fā) 字樣的消息,公眾號(hào)會(huì)給該用戶(hù)發(fā)送一條新聞消息

    var handle_event = function (msg, res) {
        console.log("weixin receive message ===", msg)
        if (msg.Event == 'CLICK' && msg.EventKey == 'work_together') {
            var text = "Hello,謝謝對(duì) Worktile 的關(guān)注啦,請(qǐng)?jiān)L問(wèn)worktile官方網(wǎng)站了解。。。。。";
            var data = {
                touser : msg.FromUserName,
                msgtype: "text",
                text   : {
                    content: text
                }
            };
            sendMessageToUser(data);
        }
    };

說(shuō)明:這是對(duì)事件消息的處理,如上,如果接收到 msg.Event == 'CLICK' && msg.EventKey == 'work_together' 事件的消息,公眾號(hào)會(huì)給該用戶(hù)發(fā)送一條文本消息,當(dāng)然事件消息有很多,如:subscribe關(guān)注公眾號(hào)消息,unsubscribe取消關(guān)注,掃描帶參數(shù)二維碼事件,還有如上說(shuō)的自定義菜單事件, 上報(bào)地理位置事件等

    var sendMessageToUser = function ( data) {
        get_token(function (err, obj) {
            request.post({
                url    : "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=" + obj.access_token,
                headers: {"Content-Type": "application/json"},
                json   : data
            }, function (err, res, body) {
                console.log("sendmessage....", body)
            })
        });
    };

這個(gè)發(fā)放就是調(diào)用微信接口給用戶(hù)發(fā)送消息,那接下來(lái)咱們就看下發(fā)送消息

  • 發(fā)送消息
    發(fā)送消息分為,發(fā)送被動(dòng)消息,發(fā)送客服消息,發(fā)送模版消息

    1. 被動(dòng)消息如上接收消息后根據(jù)消息判斷發(fā)送給用戶(hù)的消息即是被動(dòng)消息
    2. 客服消息,是公眾號(hào)收到用戶(hù)來(lái)的消息客服根據(jù)內(nèi)容回復(fù)給用戶(hù)的消息
      如果用戶(hù)并沒(méi)有給公眾號(hào)發(fā)消息,此時(shí)客服是無(wú)法給用戶(hù)發(fā)送消息的,這是微信做的一個(gè)限制
      如下代碼:
      wtutil.get_token(function (err, obj) {
         //var text = "你好,這是一條消息,多謝支持...";
         //var data = {
         //    touser : "oy4hbwbd0MOMmn8aUtQWMcNxs8PI",
         //    msgtype: "text",
         //    text   : {
         //        content: text
         //    }
         //};
         var data = {
             touser : "oy4hbwbd0MOMmn8aUtQWMcNxs8PI",
             msgtype: "image",
             "image":
             {
                 "media_id":"ZqQGrsR6ivb273zLApNfkEdAP3UI8nHJTJ9ekelfJ8OhKUF6UG-o6YbOBv4uWf4R"
             }
         };
         request.post({
             url    : "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=" + obj.access_token,
             headers: {"Content-Type": "application/json"},
             json   : data
         }, function (err, res, body) {
             console.log(body);
         })
      });
      
      msgtype: 是消息類(lèi)型,上面注釋掉的是文本消息,下面是個(gè)圖片消息,touser是用戶(hù)的openid,這里我只是取過(guò)來(lái)直接使用的,這里跟上面接收消息后處理給用戶(hù)發(fā)消息有寫(xiě)重復(fù),就不多介紹了

 

模版消息相對(duì)來(lái)說(shuō)復(fù)雜一下,首先要從公眾號(hào)添加或者申請(qǐng)消息模版,如下圖

替代文字

那么有了消息模版之后就可以拿到模版ID,然后給用戶(hù)發(fā)送模版消息了

 

    get_token(function (err, obj) {
        var data = {
            "touser"     : "oy4hbwbd0MOMmn8aUtQWMcNxs8PI",
            "template_id": "z6yV_lOIAM-LQbsrG-B3hTQvwt8_4Y3wVU2PH9UW16c",
            "url"        : "https://worktile.com",
            "topcolor"   : "#FF00FF",
            "data"       : {
                "first"   : {
                    "value": "測(cè)試哈哈哈,顏色可以自定義",
                    "color": "#33FF00"
                },
                "one": {
                    "value": "one",
                    "color": "#173177"
                },
                "two": {
                    "value": "two",
                    "color": "#FF0033"
                },
                "three": {
                    "value": "three",
                    "color": "#173177"
                },
                "remark"  : {
                    "value": "remark,了解更多詳情,關(guān)注我。。。。",
                    "color": "#33FF00"
                }
            }
        };
        request.post({
            url    : "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + obj.access_token,
            headers: {"Content-Type": "application/json"},
            json   : data
        }, function (err, res, body) {
            console.log(body, "----")
        })
    });

需要說(shuō)明的是參數(shù)中data里面的key(first, one, two,three, remark)是模版中定義的,這里需要根據(jù)模版內(nèi)容來(lái)寫(xiě),還有可以設(shè)置每個(gè)字段的顏色值等屬性

到此消息管理這部分已經(jīng)說(shuō)的差不多了,咱們繼續(xù)介紹。。。

第四章:微信網(wǎng)頁(yè)開(kāi)發(fā)

 

這個(gè)章節(jié)跟前面幾章不同,前面幾章介紹的是公眾號(hào)開(kāi)發(fā)的一些東西,這個(gè)章節(jié)介紹的是網(wǎng)頁(yè)開(kāi)發(fā),主要針對(duì)h5應(yīng)用或者是頁(yè)面的開(kāi)發(fā),Worktile微信版就是微信網(wǎng)頁(yè)開(kāi)發(fā)完成的,下面咱們一步步的介紹。

  • 微信網(wǎng)頁(yè)授權(quán)
    微信網(wǎng)頁(yè)授權(quán)采用的是Oauth2.0的授權(quán)方式:
    先進(jìn)步:訪(fǎng)問(wèn)如下鏈接獲取code

    https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
    

    redirect_uri是你h5地址,授權(quán)成功后會(huì)把code加入到地址上,類(lèi)似于:https://weixin.worktile.com?code=xxx這樣
    第二步:通過(guò)code換取網(wǎng)頁(yè)授權(quán)access_token
    請(qǐng)求接口(get請(qǐng)求)

    https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
    

    返回的結(jié)果如下

    { "access_token":"ACCESS_TOKEN",    
    "expires_in":7200,    
    "refresh_token":"REFRESH_TOKEN",    
    "openid":"OPENID",    
    "scope":"SCOPE" }
    

    access_token是用戶(hù)授權(quán)的token,openid是用戶(hù)對(duì)于該公眾號(hào)的唯一標(biāo)示,refresh_token:可以調(diào)用刷新token的接口獲取較新的token
    第三步:獲取用戶(hù)信息(需scope為 snsapi_userinfo)
    請(qǐng)求接口(get請(qǐng)求)

    https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
    

    通過(guò)以上3個(gè)步驟就可以獲取用戶(hù)的信息,進(jìn)行用戶(hù)的管理操作了
     

  •  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: '',// 必填,簽名,見(jiàn)附錄1
     jsApiList: [] // 必填,需要使用的JS接口列表
    });
    
        `如上代碼中的timestamp, nonceStr, signature需要服務(wù)端做好簽名返回給頁(yè)面,這里可以使用異步調(diào)用的方式,如下為服務(wù)端簽名代碼`
    
    var sign = function (jsapi_ticket, url) {
    var ret = {
     jsapi_ticket: jsapi_ticket,
     nonceStr: createNonceStr(),
     timestamp: createTimestamp(),
     url: url
    };
    var string = raw(ret);
       jsSHA = require('jssha');
       shaObj = new jsSHA(string, 'TEXT');
    ret.signature = shaObj.getHash('SHA-1', 'HEX');
    return ret;
    };
    

    ret就是我們需要的簽名結(jié)果,其中jsapi_ticket是調(diào)用ticket接口獲取的,官方文檔中也有說(shuō)明https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token= access_token&type=jsapi, rul是獲取簽名的當(dāng)前網(wǎng)頁(yè)地址, nonceStr:隨機(jī)字符串,timestamp:當(dāng)前10位時(shí)間戳

    var raw = function (args) {
          var keys = Object.keys(args);
          keys = keys.sort()
          var newArgs = {};
         keys.forEach(function (key) {
              newArgs[key.toLowerCase()] = args[key];
         });
        var string = '';
        for (var k in newArgs) {
             string += '&' + k + '=' + newArgs[k];
       }
       string = string.substr(1);
       return string;
    };
    

    這個(gè)方法是對(duì)簽名對(duì)象的一個(gè)字符串格式化算法

  • 通過(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ù)中。
    });
    
  • 調(diào)用接口
    根據(jù)config里面 jsApiList的配置可以調(diào)用js-sdk的接口,如分享朋友圈,分享微信好友等。。。

    wx.onMenuShareTimeline({
     title: '', // 分享標(biāo)題
     link: '', // 分享鏈接
     imgUrl: '', // 分享圖標(biāo)
     success: function () { 
         // 用戶(hù)確認(rèn)分享后執(zhí)行的回調(diào)函數(shù)
     },
     cancel: function () { 
         // 用戶(hù)取消分享后執(zhí)行的回調(diào)函數(shù)
     }
    });

以上就是微信公眾號(hào)開(kāi)發(fā)的一些內(nèi)容,算是入門(mén)篇。其實(shí)關(guān)于微信的開(kāi)發(fā)還有很多可以做的事情,而且隨著需求越來(lái)越多,技術(shù)越來(lái)越完善,相信之后關(guān)于微信的開(kāi)發(fā)一定還會(huì)有更多驚喜!