07月29, 2019

小程序生成二维码无法Node.js保存

起因

有这样一个场景:

张三的程序P1是用HTML开发的,部分页面结合了微信的JS-SDK。

李四的程序P2是完全使用微信小程序开发的。

现在张三想和李四合作,将P1和P2整合成一个完整的程序P。有一种方案A,是把P2再开发个HTML版本后和P1整合。

但是问题来了。P2中的一些功能使用HTML浏览器无法完美实现,以iOS移动端为例,比如其中一个自定义相机的功能A在微信内浏览器不支持,而在Safari中支持。检测设备方向的功能,在微信内浏览器支持,而在Safari不支持。这就很搞了,有么有?

于是只能另辟蹊径,采用另外一种方案B,在P1的HTML程序中调用微信获取小程序码API实现不同用户生成不同的参数的小程序码图片,用户长按扫码后进入小程序使用响应的功能。

此方案理论上是可行的,不过做的过程中又遇到了问题,即调用API生成的小程序码使用Node.js保存出错,下面就来看下如何解决这个问题。

分析

这里先列下我的运行环境:

  • macOS 10.14.5
  • Node v10.15.3
  • ThinkJS 2.2.8

一开始我的代码是这么写的:

const Base = require('./base.js');
const rp = require('request-promise');
const fs = require('fs');

module.exports = class extends Base {
    async testAction() {
        let getUnlimitedUrl = 'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=';
        let access_token =
            '填入你自己的微信小程序的access_token';
        getUnlimitedUrl += access_token;

        const options = {
            method: 'POST',
            uri: getUnlimitedUrl,
            body: {
                // scene: '',
                scene: 'id=160',
                page: 'pages/index/index'
            },
            json: true // Automatically stringifies the body to JSON
        };
        let result = await rp(options);


        await fs.writeFileSync(think.ROOT_PATH + '/www/static/image/qrcode_test1.jpg', result);

        return this.success(result);

    }

};

你会发现,卧槽,这个代码写的太好了,完全没有错误。但是,当我点击保存下来的图片预览时,图片却一直在转圈圈。就是下面这个鬼样子:

alt

这是咋回事呢?难道苍天嫉妒我?我查看了下微信返回的数据,发下是如下这个鸟样:

alt

一大段乱码诶。而且数据的格式并不是你微信API宣称的那样严谨:

alt

算了,我也不跟你计较。但是这一堆乱码代表的啥,确实不懂。于是开始Google和百度。网上类似的问题有很多,但方法一个一个尝试后,都是无效的,例如网上提到的如下方法:

  • 方案一:在头尾像模像样的增加base64编码所缺的字符

    controller.action('image', function * (next) {
    ......
    
    token = yield base.getAccessToken(); // 获取access_token
    url = resource.genFetchImage(token, mediaId); // 组合请求图片的链接
    
    response = yield request.get(url); // 通过co-request向微信服务器发出请求
    
    // 处理响应,编码成base64
    type = response.headers["content-type"];
    prefix = "data:" + type + ";base64,";
    base64 = new Buffer.from(response.body, 'binary').toString('base64');
    
    this.body = prefix + base64;
    yield next;
    });
    
  • 方案二:将返回的数据先转成buffer再转成base64的字符串
    ...
    base64 = new Buffer.from(response.body).toString('base64');
    
  • 方案三:将返回的数据直接转成base64的字符串
    ...
    base64 = response.body.toString('base64');
    
  • 方案四:将返回的数据先转成'utf8'的buffer,再转成base64的字符串
    ...
    base64 = new Buffer(response.body, 'utf8').toString('base64');
    

这四个方案都是扯淡!!!!都是扯淡!!!!根本不是这个原因。

后来去研究图片保存到计算机上一般以什么形式保存?得到答案:字节(byte)。嗯,好熟悉的样子,好像想起了1byte = 8 bit。

alt

如上图一个5像素*5像素(共25像素)的RGB图片,一共有25× 8× 3个字节,大概占用0.6kb。当然这个图片太小了,有点不正常。

举个正常的:以一张尺寸为900 × 600的图片为例,图片共有像素数:

900 × 600 = 540,000像素(Pixel)。

如果图片是RGB 色彩模式,占用的空间是:

900 × 600 × 3 = 1,620,000 字节(bytes).

大部分程序系统使用兆来衡量图片大小,下面解释一下字节和兆的关系。

1兆(MB) = 1024 × 1024 = 1,048,576 字节, 也就是2的20次方。

那么刚才这张图片是多少M呢?

一张尺寸为900×600的RGB图片占的内存大小:

900 × 600 × 3 = 1,620,000 字节(bytes) = 1.582 M

那么,刚才的那四种方案,好像是在做这个事情:将数据在字符和字节之间相互转换,并且尝试使用不同的编码。

那么字符又有哪些类别呢?

字符编码有:ASCII,Unicode 、UTF-8 和 base64等。

base64编码是用来解决把不可打印的内容塞进可打印内容的需求的。比如把图片存到数据库,图片数据归根到底还是一堆二进制串(总不能把这些二进制串直接存到数据库吧),用base64编码后的显示成的字符串就大大缩小的长度,可以存到数据库。

寻寻觅觅,后来终于发现,这种乱码的符号“���������”就是因为在字节和字符之间相互转换的过程中出现的,调查发现,我使用的npm包“request-promise”依赖“request”包,而正是后者的官网上隐藏着一段很深的话(坑):

alt

(Note: if you expect binary data, you should set encoding: null.) 注意:如果你需要二进制数据(图片就属于这一类别),你应该将编码格式设成null。

然后,按照官网文档的建议,我设置了下,奇迹发生了。

alt

代码仅仅增加了一段:

        const options = {
            method: 'POST',
            uri: getUnlimitedUrl,
            encoding: null,
            body: {
                // scene: '',
                scene: 'id=160',
                page: 'pages/index/index'
            },
            json: true // Automatically stringifies the body to JSON
        };

返回的数据就变成如下:

alt

alt

  • utf8编码是常用的字符编码,它向下兼容ascii编码。并不是所有的 byte串 都能成功解码成人们能识别的 chat串,它是有解码算法(参考wiki),所以我们像���\u001d�)u�m\u001f�\u001a�͸��E这样常见的乱码是由于解码出错造成的。
  • 对于不能识别的byte串会解码成�,重点是�这货竟然有相应的utf8编码,编码为0xFFFD。这里有个关键点,很多byte串是无法正确解码的,但他们都会用�表示,而�字符又只有一种编码,所以对二进制数据如:图片,视频等,通过utf8编码并保存到变量后,是无法通过utf8原样解码成原来二进制的。
  • binary编码,也就是二进制编码,通常通过consle打印,为了“好看”会打印成16进制。
  • Base64是一种基于64个可打印字符来表示二进制数据的表示方法。对于我通常会用于将图片转换成data URLs,为了减少请求,或充分利用localStorage等。

黄金屋

自定义相机代码(感兴趣的用户可以测试下,这段代码在微信iOS端内的浏览器不支持):

<script>
    class SimpleCamera extends HTMLElement {

        constructor() {
            super();
        }

        connectedCallback() {
            const shadow = this.attachShadow({
                mode: 'open'
            })

            this.videoElement = document.createElement('video')
            this.canvasElement = document.createElement('canvas')
            this.videoElement.setAttribute('playsinline', true)
            this.canvasElement.style.display = 'none'

            shadow.appendChild(this.videoElement)
            shadow.appendChild(this.canvasElement)
        }

        open(constraints) {
            console.log(navigator);
            return navigator.mediaDevices.getUserMedia(constraints).then((mediaStream) => {
                this.videoElement.srcObject = mediaStream
                console.log(mediaStream)
                this.videoElement.onloadedmetadata = (e) => {
                    this.videoElement.play()
                }
            })
        }

        _drawImage() {
            const imageWidth = this.videoElement.videoWidth
            const imageHeight = this.videoElement.videoHeight

            const context = this.canvasElement.getContext('2d')
            this.canvasElement.width = imageWidth
            this.canvasElement.height = imageHeight

            context.drawImage(this.videoElement, 0, 0, imageWidth, imageHeight)

            return {
                imageHeight,
                imageWidth
            }
        }

        takeBlobPhoto() {
            const {
                imageHeight,
                imageWidth
            } = this._drawImage()
            this.canvasElement.style.display = "block"
            const card = document.createElement('div')
            card.classList.add('card')
            document.querySelector('.wrapper').appendChild(card)
            card.appendChild(this.canvasElement)
            return new Promise((resolve, reject) => {
                this.canvasElement.toBlob((blob) => {
                    resolve({
                        blob,
                        imageHeight,
                        imageWidth
                    })
                })
            })
        }

        takeBase64Photo({
            type,
            quality
        } = {
            type: 'png',
            quality: 1
        }) {
            const {
                imageHeight,
                imageWidth
            } = this._drawImage()

            const base64 = this.canvasElement.toDataURL('image/' + type, quality)

            this.canvasElement.style.display = "block"
            const card = document.createElement('div')
            card.classList.add('card')
            document.querySelector('.wrapper').appendChild(card)
            card.appendChild(this.canvasElement)
            return {
                base64,
                imageHeight,
                imageWidth
            }
        }

    }


    customElements.define('simple-camera', SimpleCamera)
</script>
<div class="wrapper">
    <div class="card">
        <simple-camera></simple-camera>

        <div class="active">
            <button id="btnBlobPhoto">Take Blob</button>
            <button id="btnBase64Photo">Take Base64</button>
        </div>
    </div>
</div>

<script>
    (async function() {
        const camera = document.querySelector('simple-camera')
        const btnBlobPhoto = document.querySelector('#btnBlobPhoto')
        const btnBase64Photo = document.querySelector('#btnBase64Photo')

        await camera.open({
            video: {
                facingMode: 'user'
            }
        })

        btnBlobPhoto.addEventListener('click', async event => {
            const photo = await camera.takeBlobPhoto()
        })

        btnBase64Photo.addEventListener('click', async event => {
            const photo = camera.takeBase64Photo({
                type: 'jpeg',
                quality: 0.8
            })
        })
    }())
</script>

参考:

https://segmentfault.com/a/1190000002787763

https://nodejs.org/api/buffer.html#buffer_class_method_buffer_from_array

https://www.jianshu.com/p/1af904e9a6e4

http://www.ruanyifeng.com/blog/2008/06/base64.html

http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

https://blog.csdn.net/charleslei/article/details/50993861

https://github.com/request/request#readme

https://zh.wikipedia.org/wiki/UTF-8#UTF-8%E7%9A%84%E7%B7%A8%E7%A2%BC%E6%96%B9%E5%BC%8F

https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html

本文链接:https://www.daguanren.cc/post/wxacode_saved_problem.html

-- EOF --

Comments