伺服器使用 nodejs 請求獲取微信小程式圖片的教程,附詳細程式碼。
此文於2018.09.19完成,無法保證現在微信小程式的介面沒有改變。
調研
首先看微信小程式的 獲取二維碼 文件,可以看到微信支援三種介面,其中只有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 返回的圖片流,是先獲取到完整的二進位制資料,再返回到客戶端的。如果可以直接把傳回來的每一個資料塊直接發到客戶端,無疑可以縮短響應時間,貌似這裡可以進行優化。
- 涉及到了編碼和解碼的問題,這塊內容要多學習。