移动端File和Base64的坑

最近在做移动端的图片操作项目,会涉及到上传图片、Canvas、转换base64等操作,被坑得不能自理,不过好在都解决了~ 记录如下:

一、TypeError: FileConstructor is not a constructor

背景

这个错误是我在微信浏览器中点击上传图片按钮时发生的,具体逻辑为依次调用微信JSSDK中的wx.chooseImagewx.getLocalImgData,第二个接口会返回base64数据。

此时我们需要将base64转换为File对象,再通过FormData传给服务端来处理其他业务。所以我预先写了一个dataURL2file函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 将base64转换为file对象
* @param {string} dataURL base64数据
* @param {string} filename 文件名
*/
function dataURL2file(dataURL, filename = 'custom_file') {
var arr = dataURL.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
filename += '.' + mime.split('/')[1]
return new File([u8arr], filename, { type: mime });
}

上述代码在部分手机中直接报错:

1
2
3
// Reference to: new File()

TypeError: FileConstructor is not a constructor (evaluating 'new File')

原因是部分手机系统版本还不支持File,ios低于9.3(含)、安卓低于4.4(含)都不支持,见:caniuse

解决方法

Base64转换为Blob对象,blob的兼容性相对File会更好一些,参考caniuse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* dataURL 转成 blob
* @param dataURL
* @return blob
*/
function dataURL2blob(dataURL) {
let binaryString = atob(dataURL.split(',')[1]);
let arrayBuffer = new ArrayBuffer(binaryString.length);
let intArray = new Uint8Array(arrayBuffer);
let mime = dataURL.split(',')[0].match(/:(.*?);/)[1]
for (let i = 0, j = binaryString.length; i < j; i++) {
intArray[i] = binaryString.charCodeAt(i);
}
let data = [intArray];
let result;
try {
result = new Blob(data, { type: mime });
} catch (error) {
window.BlobBuilder = window.BlobBuilder ||
window.WebKitBlobBuilder ||
window.MozBlobBuilder ||
window.MSBlobBuilder;
if (error.name === 'TypeError' && window.BlobBuilder) {
var builder = new BlobBuilder();
builder.append(arrayBuffer);
result = builder.getBlob(type);
} else {
throw new Error('没救了');
}
}
return result;
}

当然,放不下dataURL2file这个函数,我们也可以做兼容处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function dataURL2file(dataURL, filename = 'custom_file') {
try {
var arr = dataURL.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
filename += '.' + mime.split('/')[1]
return new File([u8arr], filename, {
type: mime
});
} catch (error) {
console.warn('Browser does not support the File constructor,Will use blob instead of file')
return dataURL2blob(dataURL)
}
}

二、使用img.onload事件加载base64图片时,触发了onerror

背景

有时我们需要将本地的base64绘制到canvas中,如果直接调用canvasdrawImage方法,可能图片还没加载就触发了绘制,从而导致绘制失败。所以,我们需要保证图片已经加载完成再触发绘制:

1
2
3
4
5
6
7
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
var img = new Image()
img.onload = function() {
ctx.drawImage(img, 0, 0);
}
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANS...'

以上代码只在ios v10.3以下的版本会触发onerror错误,安卓尚未发现问题。

翻越各种文档都没有相关资料,只找到一个类似的问题:ie10-base64-encoded-image-load-error

猜测是base64的长度限制,导致加载失败。

解决方法

既然是长度问题,有两种方案:

方案一:将base64转成file对象,让服务端返回一个http的图片链接

  • 优点:无兼容性问题;
  • 缺点:多一次请求,且图片加载依赖网速

方案二:将base64转成objectUrl,见:createObjectURL - MDN

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 创建新的URL 对象表示指定的 File 对象或 Blob 对象。
* @param {string} dataURL base64
*/
function dataURL2ObjUrl(dataURL) {
window.URL = window.URL || window.webkitURL
if (window.URL && URL.createObjectURL) {
// dataURL2blob 此方法需额外定义
var blob = dataURL2blob(dataURL)
return URL.createObjectURL(blob)
}
return dataURL
}
  • 优点:无网络请求;
  • 缺点:兼容性,PC端慎用,移动端比较完美,见:caniuse

综上述,使用objectUrl是个不错的选择~

由于上述错误只发生在图片加载时,所以我重新封装了一下工具库的loadImage函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 异步加载图片
* @param {string} url 图片地址
* @param {boolean} is_cors 是否对此元素的CORS请求设置凭据标志
* @return {imageObject} img对象
*/
function loadImage(url, is_cors = true) {
return new Promise((resolve, reject) => {
var img = new Image()
if (is_cors) img.crossOrigin = 'Anonymous';
var objectURL = null
if (url.match(/^data:(.*);base64,/) && window.URL && URL.createObjectURL) {
objectURL = URL.createObjectURL(this.dataURL2blob(url))
url = objectURL
}
img.onload = () => {
objectURL && URL.revokeObjectURL(objectURL)
resolve(img)
}
img.onerror = () => {
reject(new Error('That image was not found.:' + url.length))
}
img.src = url
})
}

参考

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2019 FE blog All Rights Reserved.

访客数 : | 访问量 :