服务端使用 nodejs 获取带参微信小程序码图片

服务器使用 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…
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值