起因
有这样一个场景:
张三的程序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);
}
};
你会发现,卧槽,这个代码写的太好了,完全没有错误。但是,当我点击保存下来的图片预览时,图片却一直在转圈圈。就是下面这个鬼样子:
这是咋回事呢?难道苍天嫉妒我?我查看了下微信返回的数据,发下是如下这个鸟样:
一大段乱码诶。而且数据的格式并不是你微信API宣称的那样严谨:
算了,我也不跟你计较。但是这一堆乱码代表的啥,确实不懂。于是开始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。
如上图一个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”包,而正是后者的官网上隐藏着一段很深的话(坑):
(Note: if you expect binary data, you should set encoding: null.) 注意:如果你需要二进制数据(图片就属于这一类别),你应该将编码格式设成null。
然后,按照官网文档的建议,我设置了下,奇迹发生了。
代码仅仅增加了一段:
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
};
返回的数据就变成如下:
- 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
Comments