美文网首页
服务端使用 nodejs 获取带参微信小程序码图片

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

作者: 前端西瓜哥 | 来源:发表于2018-09-19 00:27 被阅读581次

    服务器使用 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();
    

    注意点:

    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. https://www.cnblogs.com/chyingp/p/charset-enc-dec.html
    2. http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

    相关文章

      网友评论

          本文标题:服务端使用 nodejs 获取带参微信小程序码图片

          本文链接:https://www.haomeiwen.com/subject/xmcbnftx.html