服務端使用 nodejs 獲取帶參微信小程式碼圖片
首先看微信小程式的獲取二維碼 文件,可以看到微信支援三種介面,其中只有B介面沒有生成個數限制,長遠來看,我選擇使用 B 介面。
根據文件,要使用 B 介面生成小程式碼,就需要一個 access_token,這個 token 可以通過另一個介面傳入appId和金鑰來獲得。詳情看該介面文件。
實現
獲取 access_token
nodejs 的版本為 8.x。
考慮到服務端傳送的請求並不多,不打算引入 request、axios 之類的三方庫,用原生 https 模組實現(其實我只是想學習 nodejs 的原生 api 哈)。
首先,要獲取 access_token,要用到 appid 和 appsecret。
const https = require('https'); https.get(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${appsecret}`, res => { let resData = ''; res.on('data', data => { resData += data; }); res.on('end', () => { resData = JSON.parse(resData); }) }) 複製程式碼
通過 end 事件,我們獲得了返回的完整的 JSON 物件 resData。
如果引數正確的話,會返回{"access_token":"ACCESS_TOKEN","expires_in":7200}
這樣的 JSON 物件。expires_in 指的是 token 的有效期時間,單位是秒,獲取了這個物件後,我添加了一個 timestamp 屬性,儲存當前時間,來確定這個 access_token 什麼時候過期。這個物件,你可以存在 global 下,但最好存到 redis,這樣即使你重啟伺服器就不需要重新獲取 access_token 了。
獲取小程式碼圖片
有了 access_token,我們就可以通過 post 請求來獲取圖片二進位制流了。
傳送 post 請求,要用到 https.request 方法,比 https.get 要複雜一點。
首先我們用自帶的 url 模組,將 url 字串轉換為 URL 物件。因為我們要用到 post 方法,並指定一些headers,所以還要給這個物件追加一些屬性。 url 字串轉為物件有兩種方法,一種是new URL(<urlString>)
,還有一個是url.parse(<urlString>)
。請不要使用第一種方式,因為給轉換後的物件新增屬性,然後轉為 JSON 物件時,不會存在(具體原因不明,有空我研究下。)第二種方式生成的物件則沒有這些問題。
具體程式碼如下:
const url = require('url'); let options = url.parse(`https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${access_token}`); // 新增必要的屬性 options = Object.assign(options, { method: 'POST', headers: { // 'Content-Length': Buffer.byteLength(post_data), 'Content-Type': 'application/json', 'Content-Length': post_data.length, } }); 複製程式碼
這裡的post_data
其實就是請求主題裡的資料。
注意獲取二維碼的 api 文件裡的 Bug & Tip 明確說明了,POST 引數需要轉成 json 字串,不支援 form 表單提交。
const post_data = JSON.stringify({ scene: '你要傳的引數',// 最多32個字元。 width: 200,// 生成的小程式碼寬度。 }); 複製程式碼
然後我們就可以用 https.request 方法去請求圖片了
let req = https.request(options, (res) => { let resData = ''; res.setEncoding("binary"); res.on('data', data => { resData += data; }); res.on('end', () => { // 微信api可能返回json,也可能返回圖片二進位制流。這裡要做個判斷。 // errcode:42001 是指 token 過期了,需要重新獲取。40001 是指token格式不對或很久之前的token。 const contentType = res.headers['content-type']; if ( !contentType.includes('image') ) { console.log('獲取小程式碼圖片失敗,微信api返回的json為:') console.log( JSON.parse(resData) ) return resolve(null); } const imgBuffer = Buffer.from(resData, 'binary'); resolve( {imgBuffer, contentType} ); }); }); req.on('error', (e) => { console.log('獲取微信小程式圖片失敗') console.error(e); }); req.write(post_data);// 寫入post請求的請求主體。 req.end(); 複製程式碼
注意點:
-
這裡比較重要的是這個
res.setEncoding("binary");
,因為伺服器預設返回的資料會編碼為 utf8 格式,但我們只需要二進位制,且二進位制轉 utf8 再轉回二進位制貌似會丟失資料(具體我還不知道為什麼)。 -
另外,這個返回的 req 物件,可以諸如
setHeader(name, value), getHeader(name), removeHeader(name)
的api,直到你使用request.end()
才真正把請求傳送出去。如果你忘了呼叫request.end
而執行了程式碼,過了一段時間會報一個超時錯誤。 -
考慮到返回的不一定是圖片,也有可能返回 JSON,所以做了一些判斷。
-
如果引數比較固定,你可以把圖片下載下來,將圖片路徑對映到 redis 上,做個快取。使用者第二次訪問的時候,直接傳對應的圖片就行了。
完整程式碼(僅供參考)
下面是完整程式碼和一些簡單的註釋,另外因為使用了 Koa 框架,需要用到 async/await 的同步方式,我把請求包裝成了 Promise。
const https = require('https'); const url = require('url'); const uid = '你要傳的引數'; const S_TO_MS = 1000;// 秒到毫秒的轉換。 if (!global.access_token || global.access_token.timestamp + global.access_token.expires_in * S_TO_MS <= new Date() - 300) { // 過期,獲取新的 token const appid = '小程式的appId'; const appsecret = '金鑰'; const accessTokenObj = await new Promise( (resolve, reject) =>{ https.get(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${appsecret}`, res => { let resData = ''; res.on('data', data => { resData += data; }); res.on('end', () => { resolve( JSON.parse(resData) ); }) }) }).catch(e => { console.log(e); }); // 這裡應該加一個判斷的,因為可能請求失敗,返回另一個 JSON 物件。 global.access_token = Object.assign(accessTokenObj, {timestamp: +new Date()}); } const access_token = global.access_token.access_token; const post_data = JSON.stringify({ scene: uid,// 最多32個字元。 width: 200,// 生成的小程式碼寬度。 }); let options = url.parse(`https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${access_token}`); options = Object.assign(options, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': post_data.length, } }); // 獲取圖片二進位制流 const {imgBuffer, contentType} = await new Promise((resolve, reject) => { let req = https.request(options, (res) => { let resData = ''; res.setEncoding("binary"); res.on('data', data => { resData += data; }); res.on('end', () => { // 微信api可能返回json,也可能返回圖片二進位制流。這裡要做個判斷。 const contentType = res.headers['content-type']; if ( !contentType.includes('image') ) { console.log('獲取小程式碼圖片失敗,微信api返回的json為:') console.log( JSON.parse(resData) ) return resolve(null); } const imgBuffer = Buffer.from(resData, 'binary'); resolve( {imgBuffer, contentType} ); }); }); req.on('error', (e) => { console.log('獲取微信小程式圖片失敗') console.error(e); }); req.write(post_data);// 寫入 post 請求的請求主體。 req.end(); }).catch(() => { return null; }); if (imgBuffer == null) { ctx.body = {code: 223, msg: '獲取小程式碼失敗'}; return; } ctx.res.setHeader('Content-type', contentType); ctx.body = imgBuffer; 複製程式碼
後面的話
- 原生 api 有點繁瑣,建議使用一些流行的請求庫,可讀性高且方便修改。
- 微信 api 返回的圖片流,是先獲取到完整的二進位制資料,再返回到客戶端的。如果可以直接把傳回來的每一個數據塊直接發到客戶端,無疑可以縮短響應時間,貌似這裡可以進行優化。
- 涉及到了編碼和解碼的問題,這塊內容要多學習。