背景:目前,在开发基于微信的Web App应用,也就是借助微信所有资源,如公众号,账号系统和扫描JS-JDK等。后端是用node做中间件,依赖API服务(坑爹的是,API服务是用base64保存图片…)。现需实现一功能:用户选择图片,然后调用微信JS-JDK API,再上传至微信服务器(不能直接从微信里发送选择的图片),最后重微信服务器下载下来保存至我们自己服务器里。(下载下来后,需要转成data URIs)
关键代码:
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(response.body, 'binary').toString('base64');
this.body = prefix + base64;
yield next;
});
最后的响应结果也是有模有样的,看起来也很像data URIs,坑爹的这货是假的,假的。。。Google,百度,啥啥的都找了一遍,还是没结果(我寻找问题答案时,时常定位不准。。。),有以下答案的:
方案一:
// 别说了,网上的十有八九就是我上面那种方式!
方案二:
...
base64 = new Buffer(response.body).toString('base64');
方案三:
...
base64 = response.body.toString('base64');
方案四:
...
base64 = new Buffer(response.body, 'utf8').toString('base64');
一一试了个遍,结果千奇百怪,千万个草泥马奔腾啊!!!都不行,后来还是解决了,才有这文章。其实原理很简单,只是对编码的理解不够而已。(小白我前端一枚,目前在较为深入学习node,想走全栈,反正路还很长呢!!)
正确方案:
...
// 通过co-request向微信服务器发出请求
response = yield request.get({
url: url,
encoding: null // 指定编码
});
response = response.body.toString('base64');
重点在request.get的参数上,{ encoding: null },我会慢慢讲解下去(大神勿喷!!)
————————–分割线————————–
在这里会涉及几个关键知识
- Buffer对象
- 字符编码,uf8,binary和base64等
- co-request和request NPM包
Buffer对象
Buffer是一个像Array的对象,但它主要用于操作字节,也就是二进制数据,它的元素为16进制的两位数,即0到255的数值。(欲想较为深入了解,看《深入浅出nodejs》)
关键API,具体参考new Buffer 和 buf.toString
new Buffer(str[, encoding])
和
buf.toString([encoding][, start][, end])
字符编码,ascii, utf8,binary和base64等
字符编码,简单讲就是将我们显示器看到的字符编码成计算机识别的位(bit),比如:
- 小写字母 a 通过ascii编码成 0110 0001,十六进制表示成 0x61,占8位,1字节
- 中文 我 通过utf8 编码成 1110 0110 1000 1000 1001 0001,十六进制表示成 0xE68891,占24位,3字节
这里有几个关键点:
- 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等。
co-request和request NPM包
request是非常非常强大的模拟浏览器发送HTTP请求的模块,非常非常强大!!而co-request,是通过TJ大神写的co模块简单对request包装了下,实现 yield + promise 优雅实现异步控制流,摆脱倒金字塔的利器!!
————————–分割线————————–
好,基础知识就差不多了,回到我之前遇到的问题上,并对其讲解下:
response = yield request.get(url); // 通过co-request向微信服务器发出请求
// 处理响应,编码成base64
type = response.headers["content-type"];
prefix = "data:" + type + ";base64,";
base64 = new Buffer(response.body, 'binary').toString('base64');
为什么这段代码有问题??对于request方法它有一个关键的参数encoding
,默认值是utf8
,所以response.body
的值已经乱码的字符,再所以new Buffer(response.body, 'binary')
将其转换成二进制时,已经不是原来的二进制了,这解释了为什么“最后的响应结果也是有模有样的,看起来也很像data URIs,坑爹的这货是假的,假的。。。”。而通过将encoding
设为null
,也就不对原始数据编码,保持原始的二进制图片数据。
网上的大多数的方案一,其实是没有错的!!!错在我遇到的错误和解决方案不拉边,它适用于通过[fs.createReadStream(path[, options])](https://nodejs.org/api/all.ht… 读取本地图片,并转换成base64编码。
而方案四我觉得特别有趣
...
base64 = new Buffer(response.body, 'utf8').toString('base64');
我想,response.body
竟然是utf8
编码的,那我可以通过new Buffer(response.body, 'utf8')
,将其反编码成二进制数据(也就是binary),然后再转换成base64,结果试了不行!!!最后痛苦的想了一遍,才发现自己脑短路了,也许这问题对很对大神来说很明显错误,可我还是觉得有趣,不懂的可以仔细想想。
到了最后了,我想答案很明显了,原理很简单,无非原始的二进制数据才是转换base64的正确数据。。。其它的错误都是“白忙活”惹的祸!!
小白之手,大神勿喷,欢迎意见,及时添正!