服務端使用 nodejs 獲取帶參微信小程式碼圖片

Fstar_發表於2019-04-20

伺服器使用 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();
複製程式碼

注意點:

  1. 這裡比較重要的是這個 res.setEncoding("binary");,因為伺服器預設返回的資料會編碼為 utf8 格式,但我們只需要二進位制,且二進位制轉 utf8 再轉回二進位制貌似會丟失資料(具體我還不知道為什麼)。

  2. 另外,這個返回的 req 物件,可以諸如 setHeader(name, value), getHeader(name), removeHeader(name) 的api,直到你使用 request.end() 才真正把請求傳送出去。如果你忘了呼叫 request.end 而執行了程式碼,過了一段時間會報一個超時錯誤。

  3. 考慮到返回的不一定是圖片,也有可能返回 JSON,所以做了一些判斷。

  4. 如果引數比較固定,你可以把圖片下載下來,將圖片路徑對映到 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;
複製程式碼

後面的話

  1. 原生 api 有點繁瑣,建議使用一些流行的請求庫,可讀性高且方便修改。
  2. 微信 api 返回的圖片流,是先獲取到完整的二進位制資料,再返回到客戶端的。如果可以直接把傳回來的每一個資料塊直接發到客戶端,無疑可以縮短響應時間,貌似這裡可以進行優化。
  3. 涉及到了編碼和解碼的問題,這塊內容要多學習。

參考

  1. www.cnblogs.com/chyingp/p/c…
  2. www.ruanyifeng.com/blog/2007/1…

相關文章