微信JSSDK使用指南


    這篇文章主要來說說微信JSSDK的一些配置和微信分享的使用,包括從前端sdk文件引入到server端和微信server的交互。

另外Tangide已經把一些工作做好了。我會簡要說一說怎么把它用起來。在開發的過程中主要參考了微信官方開發文檔,因為微信提供的功能略多,我會定期更新文檔,假設你已經熟悉了微信JSSDK的開發流程能夠直接跳過前面的步驟,直接跳轉到Tangide與微信JSSDK的使用結合 :)。


1. 首先得有個公眾號

微信把公眾號細分為服務號訂閱號

接口權限各不同樣,假設你記不住你的公眾號有沒有相應功能的權限,能夠到開發人員中心->接口權限表相應查看。

2. 配置JS接口安全域名

微信限制了JS接口使用的域名。詳細在:公眾號設置->功能設置頁面。好在能夠同一時候配置3個,並且能夠改動: js-api

這意味着配置能夠復用了。反復勞動降低了:)。

3. 引入JS文件

http://res.wx.qq.com/open/js/jweixin-1.0.0.js這個是微信官方提供的sdk。能夠在h5頁面放個<script>標簽把它引入進來。

4. 注入權限驗證配置

微信為了驗證調用SDK的合法性,須要我們的server與微信server交互獲取配置信息。

wx.config({
    debug: true, // 開啟調試模式,調用的全部api的返回值會在clientalert出來。若要查看傳入的參數。能夠在pc端打開,參數信息會通過log打出。僅在pc端時才會打印。
    appId: '', // 必填。公眾號的唯一標識
    timestamp: , // 必填。生成簽名的時間戳
    nonceStr: '', // 必填,生成簽名的隨機串
    signature: '',// 必填。簽名,見附錄1
    jsApiList: [] // 必填,須要使用的JS接口列表,全部JS接口列表見附錄2
});

一般來說僅僅有是否開啟調式模式由client決定,其它的都是通過http請求server獲得,以下我們就來看看怎樣從服務獲取配置信息。

5. 服務端獲取配置信息

這是整個過程相對關鍵的一步,你必須要緩存好兩個token: jsapi_ticket以及access_tokenaccess_token有效時間為2個小時。向微信server請求頻率限制為:每天2000次。微信還有其它一些參數的頻率限制,你能夠看這里。


以下來說說獲取配置信息的詳細步驟:

get config uml

來看看server端是怎么處理的:

nodejsserver端:

var fs = require('fs');
var path = require('path');
var urlParser = require('url');
var TokenManager = require('./token_manager');

var Handler = function() {
};

Handler.doit = function(req, res) {
    try {
        var query = urlParser.parse(req.url, true).query;
        if(!query || !query.url) {
            throw new Error('invalid params');
        }
        var realUrl= atob(decodeURI(query.url));
    }
    catch(e) {
        res.status(401).json({code: -1, message: e});
        res.end();
        return;
    }
    Handler.tokens.getJsApiTicket(realUrl, function(err, config) {
        if(!!err) {
           res.status(500).json({code: -1, message: err});
        }
        else {
           res.status(200).json({code: 0, message: 'success', data: config});
        }
        res.end();
    });
};

Handler.init = function(app, express, config) {
    Handler.opts = {
        appid: config.appid,
        secret:config.secret
    };
    Handler.tokens = TokenManager.create(opts);
    app.post('/weixin/get-config.php', Handler.doit);
};

module.exports = Handler;

//////////////////////////////////////////////////////////////////////////////
//token manager

var TokenManager = function(opts) {
    this.opts = opts;
    if(!opts.appid || !opts.secret) {
        thow new Error('appid and secret should not be empty!!!');
    }
    var tokenPath = path.join(__dirname, 'tokens.json');
    this.loadToken = opts.loadToken || function(type, cb) {
        fs.readFile(tokenPath, 'utf8', function(err, data) {
            if(!!err) {
                return cb(err);
            }
            try {
                data = data || JSON.stringify({});
                data = JSON.parse(data);
            }
            catch(e) {
               return cb(e);
            }
            cb(null, data[type] || {});
        });
    };
    this.saveToken = opts.saveToken || function(token, cb) {
        fs.readFile(tokenPath, 'utf8', function(err, data) {        
            if(!!err) {
                return cb(err);
            }
            data = data || {};
            var type = Object.keys(token)[0];
            data[type] = token[type];
            fs.writeFile(tokenPath, JSON.stringify(data), cb);
        });
    };
};

var isValid = function() {
    return !!this.token && (new Date().getTime()) < this.expireTime;
};

var load = function(stream, cb) {
    var chunks = [];
    stream.on('data', function(chunk) {
        chunks.push(chunk);
    });
    stream.on('error', function(err) {
        cb(err);
    });
    stream.on('end', function() {
        cb(null, Buffer.concat(chunks));
    });
};

var request = function(url, cb) {
    require('https').get(url, function(res) {
        load(res, cb);
    }).on('error', function(err) {
        cb(err);
    });
};

var wrapper = function(cb) {
    return function(err, rawData) {
         cb = cb || function() {};
         if(!!err) {
             return cb(err);
         }
         try {
             var data = rawData.toString('utf-8');
             data = JSON.parse(data);
         }
         catch(err) {
              return cb(err);
         }
         if(data && data.errcode) {
              err = new Error(data.errmsg);
              err.name = 'WeChatAPIError';
              err.code = data.errcode;
              return cb(err, data);
         }
         cb(null, data);
    };
};

TokenManager.prototype.getAccessToken = function(cb) {
    var self = this;
    this.loadToken('accessToken', function(err, token) {
         if(err) {
            cb(err);
         }
         else if(isValid.call(token)) {
            cb(null, token.token);
         }
         else {
              var url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential' +
                        this.opts.appid + this.opts.secret;
              request(url, wrapper(function(err, data) {
                  if(err) {
                      return cb(err);
                  }
                  console.assert(data && data.token);
                  var expireTime = (new Date().getTime()) + (data.expires_in - 10) * 1000;
                  var token = {
                      accessToken: {
                          'token': data.token,
                          'expireTime': expireTime
                      }
                  };
                  self.saveToken(token, function(err) {
                       if(err) {
                           return cb(err);
                       }
                       cb(null, data.token);
                  });
              });
         }
    });
};

TokenManager.prototype.getJsApiTicket = function(cb) {
    var self = this;
    this.loadToken('jsApiTicket', function(err, token) {
         if(err) {
            cb(err);
         }
         else if(isValid.call(token)) {
            cb(null, token.token);
         }
         else {
              self.getAccessToken(function(err, token) {
              if(err) {
                  return cb(err);
              }
              console.assert(token);
              var url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?

type=jsapi&access_token=' + token; request(url, wrapper(function(err, data) { if(err) { return cb(err); } console.assert(data && data.token); var expireTime = (new Date().getTime()) + (data.expires_in - 10) * 1000; var token = { jsApiTicket: { 'token': data.ticket, 'expireTime': expireTime } }; self.saveToken(token, function(err) { if(err) { return cb(err); } cb(null, data.token); }); }); } }); }; var createNonceStr = function(length) { return Math.random().toString(36).substr(2, length || 15); }; var createTimestamp = function() { return parseInt(new Date().getTime()/1000, 10) + ''; }; var crypto = require('crypto'); var genSignature = function() { var raw = function() { var keys = Object.keys(arguments); var newArgs = {}; keys.sort().forEach(function(key) { newArgs[key.toLowerCase()] = arguments[key]; }); var str = ''; for(var k in newArgs) { str += '&' + k + '=' + newArgs[k]; } return str.substr(1); }; var str = raw.apply(null, arguments); return crypto.createHash('sha1').update(str).digest('hex'); }; TokenManager.prototype.getJSSDKConfig = function(url, cb) { var self = this; this.getJsApiTicket(function(err, ticket) { if(err) { return cb(err); } console.assert(ticket); var nonceStr = createNonceStr(); var timestamp= createTimestamp(); var signature= genSignature(nonceStr, timestamp, ticket, url); var ret = { appId: self.opts.appid, nonceStr: nonceStr, timestamp: timestamp, signature: signature, url: url, jsApiList: [ 'checkJsApi', 'chooseImage', 'previewImage', 'uploadImage', 'onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareWeibo' ] }; cb(null, ret); }); }; module.exports.create = function(opts) { if(!this.inst) { this.inst = new TokenManager(opts); } return inst; };

    代碼比較長,將就看吧!假設看過微信開發文檔中的關於授權的大致流程,以上代碼就沒有什么問題了。須要注意的是,默認token保存在本地文件,假設有其它cluster需求能夠自行注入函數來處理。

phpserver端

<?php
class JSSDK {
  private $appId;
  private $appSecret;
  public function __construct($appId, $appSecret) {
    $this->appId = $appId;
    $this->appSecret = $appSecret;
  }
  public function getSignPackage($url) {
    $jsapiTicket = $this->getJsApiTicket();
    if(!$url) {
        $url = "http://$_SERVER[HTTP_HOST]" . '/weixin/demo/index.html';
    }
    $timestamp = time();
    $nonceStr = $this->createNonceStr();
    // 這里參數的順序要依照 key 值 ASCII 碼升序排序
    $string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr&timestamp=$timestamp&url=$url";
    $signature = sha1($string);
    $signPackage = array(
      "appId"     => $this->appId,
      "nonceStr"  => $nonceStr,
      "timestamp" => $timestamp,
      "url"       => $url,
      "signature" => $signature,
      "rawString" => $string
    );
    return $signPackage; 
  }
  private function createNonceStr($length = 16) {
    $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    $str = "";
    for ($i = 0; $i < $length; $i++) {
      $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
    }
    return $str;
  }

  private function getFileName($name) {
    //$fileName = sys_get_temp_dir() . "/weixin-" . $name;
    $fileName = dirname(__FILE__) . "/weixin-". $name;
    return $fileName;
  }

  private function saveData($name, $content) {
    $fileName = $this->getFileName($name);

    $fp = fopen($fileName, "wb+");
    if($fp) {
        fwrite($fp, $content);
        fclose($fp);
    }

    return;
  }

  private function loadData($name, $defaultContent) {
    $fileName = $this->getFileName($name);
    $content = file_get_contents($fileName);

    if($content) {
        return $content;
    }
    else {
        return $defaultContent;
    }

    return;
  }

  private function getJsApiTicket() {
    $data = json_decode($this->loadData("jsapi_ticket.json", '{"jsapi_ticket":"", "expire_time":0}'));
    if ($data->expire_time < time()) {
      $accessToken = $this->getAccessToken();
      $url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=$accessToken";
      $res = json_decode($this->httpGet($url));
      $ticket = $res->ticket;
      if ($ticket) {
        $data->expire_time = time() + 3600;
        $data->jsapi_ticket = $ticket;
        $this->saveData("jsapi_ticket.json",  json_encode($data));
      }
    } else {
      $ticket = $data->jsapi_ticket;
    }
    return $ticket;
  }

  private function getAccessToken() {
    $data = json_decode($this->loadData("access_token.json", '{"access_token":"", "expire_time": 0}'));
    if ($data->expire_time < time()) {
      $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$this->appId&secret=$this->appSecret";
      $res = json_decode($this->httpGet($url));
      if(!isset($res->access_token)) {
        print_r($res);
        exit;
      }

      $access_token = $res->access_token;
      if ($access_token) {
        $data->expire_time = time() + 3000;
        $data->access_token = $access_token;
        $this->saveData("access_token.json", json_encode($data));
      }
    } else {
      $access_token = $data->access_token;
    }
    return $access_token;
  }

  private function httpGet($url) {
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_TIMEOUT, 500);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($curl, CURLOPT_URL, $url);
    $res = curl_exec($curl);
    curl_close($curl);
    return $res;
  }
}

session_start();
if(!isset($_REQUEST['url'])) {
    echo "invalid arguments";
    exit;
}

$appID = "你的公眾號appid";
$appSecret = "你的公眾號secret";

$url = $_REQUEST['url'];
$url = base64_decode($url);

$jssdk = new JSSDK($appID, $appSecret);
$signPackage = $jssdk->getSignPackage($url); 
$signPackage['jsApiList'] = array('checkJsApi', 'chooseImage', 
            'onMenuShareTimeline', 'onMenuShareAppMessage', 
            'onMenuShareQQ', 'previewImage', 'uploadImage', 
            'downloadImage', 'getNetworkType');
echo json_encode($signPackage);
?

>

php版本號流程都是一樣。僅僅是語言差異罷了。

6. 寫個demo來驗證一下

因為微信團隊限制沒有提供主動分享的接口,我們僅僅能准備好要分享的數據。等到用戶點擊右上角的分享button的時候,才把我們之前准備好的數據分享出去。

demo包括一個能夠動態設置文本的input標簽,你能夠動態改動其內容,然后點擊微信右上角的分享給朋友查看效果。

<!DOCTYPE HTML>
<html> <head> <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> <meta http-equiv="pragma" content="no-cache"/> <meta http-equiv="Expires" content="0" /> <meta http-equiv="Content-Type" content="type=text/html; charset=utf-8"/> <title>wechat-jssdk demo</title> <style> body { text-align: center; } input { width: 200px; height: 80px; font-size:40px; } </style> <script type="text/javascript" src = "http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script> <script type="text/javascript"> function get(uri) { return http(uri, 'GET', null); } function http(uri, method, data, headers) { alert('promise: ' + typeof Promise); return new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open(method, uri, true); if(headers) { for(var p in headers) { xhr.setRequestHeader(p, headers[p]); } } xhr.addEventListener('readystatechange', function(e) { if(xhr.readyState === 4) { if(String(xhr.status).match(/^2\d\d$/)) { try { var data = JSON.parse(xhr.responseText); resolve(data); } catch(e) { reject(e); } } else { reject(xhr); } } }); xhr.send(data); }); } function wechatShare(cfg) { var inter = setInterval(function() { try { wx.config(cfg); clearInterval(inter); } catch(e) { console.log('wait ready'); } }, 200); var titleItem = document.getElementById('shareTitle'); titleItem.onchange = function() { var shareTitle = titleItem.value; alert('shareTitle: ' + shareTitle); wx.onMenuShareAppMessage({ title: shareTitle, // 分享標題 desc: 'TangIDE test onMenuShareAppMessage', // 分享描寫敘述 link: window.location.href, // 分享鏈接 imgUrl: 'http://img1.bdstatic.com/static/common/widget/search_box_home/logo/home_white_logo_0ddf152.png', type: 'link', // 分享類型,music、video或link,不填默覺得link success: function () { // 用戶確認分享后運行的回調函數 }, cancel: function () { // 用戶取消分享后運行的回調函數 } }); }; wx.ready(function() { /* wx.onMenuShareAppMessage({ title: '1234567', // 分享標題 desc: 'TangIDE test onMenuShareAppMessage', // 分享描寫敘述 link: window.location.href, // 分享鏈接 imgUrl: 'http://img1.bdstatic.com/static/common/widget/search_box_home/logo/home_white_logo_0ddf152.png', type: 'link', // 分享類型,music、video或link,不填默覺得link success: function () { // 用戶確認分享后運行的回調函數 }, cancel: function () { // 用戶取消分享后運行的回調函數 } }); */ }); wx.error(function(res) { }); } function regConfig() { var uri = 'http://www.i5r.com.cn/weixin/php/json_config.php' + '?url=' + window.btoa(location.href); get(uri).then(function(data) { data.debug = true; wechatShare(data); }).catch(function(err) { alert('request error:' + err); }); } window.onload = function() { setTimeout(regConfig, 600); }; </script> </head> <body> <p><input placeholder="分享標題" type="text" id="shareTitle" required="required"/></p> </body> </html>

    config信息驗證后會運行ready方法。全部接口調用都必須在config接口獲得結果之后,config是一個client的異步操作。所以假設須要在頁面載入時就調用相關接口。則須把相關接口放在ready函數中調用來確保正確運行。對於用戶觸發時才調用的接口。則能夠直接調用,不須要放在ready函數中。所以說假設須要分享的東西不變,就直接放在ready事件處理函數去做號了(如上面demo的ready事件處理函數),假設要分享的東西一直在變,就像demo演示的那樣動態的改動要分享的title

demo演示(微信環境)

7. TangIDE與微信SDK的結合

    上面的這些代碼看似有點多,只是幸好強大的tangIDE已經幫我們完畢非常多工作了:
- [1] 提供微信配置控件,自己主動完畢微信JSSDK環境搭建

        打開TangIDE在左側控件欄。SNS控件->微信設置直接拖入你須要調用jssdk接口的場景就可以。
- [2] 各種SNS組件支持
        打開TangIDE在左側控件欄。SNS控件選擇想要的組件直接拖入你須要調用的場景就可以。


end


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM