服务器使用 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 返回的图片流,是先获取到完整的二进制数据,再返回到客户端的。如果可以直接把传回来的每一个数据块直接发到客户端,无疑可以缩短响应时间,貌似这里可以进行优化。
- 涉及到了编码和解码的问题,这块内容要多学习。