web浏览器中的base64编码解码

最近一个项目需要在页面中将远程图片转为base64字符串并传送,因此激发了我对浏览器中的base64编码解码方式的兴趣。

Base64介绍

Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2^6=64,所以每6个位元为一个单元,对应某个可打印字符。3个字节有24个位元,对应于4个Base64单元,及3个字节可由4个可打印字符来表示。在Base64中的可打印字符包括字母A-Z、a-z、数字0-9,这样共有62个字符,此外两个可打印字符号再不同的系统中不同。 ——wiki百科

Base64常用于在通常处理文本数据的场合,表示、传输、存储一些二进制数据,包括MIME的电子邮件及XML的一些复杂数据。

然而前端里接触Base64最多的想必就是Base64图片了,性能优化中很重要的一条就是减少请求,所以一些体积较小的图片可以转为Base64来减少请求量,很多前端框架的脚手架也默认考虑了这点。

编码流程

  • 1.把输入的数据转换为二进制形式;
  • 2.每3个8bit为一组拆分为4个6bit,8 3 = 6 4 = 24;
  • 3.拆分后的6bit的高位补两个0组成新的8bit;
  • 4.把新的8bit转为10进制,然后查表得到最终的字符串;
  • 5.如果最后不够3个8bit,分两种情况
    • 1.有2个8bit差1个8bit,由于8 * 2 = 6 + 6 + 4 = 16,可以拆出来2个6bit和一个不完整的4bit,base64编码规定给不足6bit的差几个bit补几个0,4bit低位补2个0,组成了6bit,由于一共需要4个6bit,前面一共产生了3个,还差一个,base64编码规定差几个6bit补几个’=’,这里差一个,所以在最后补一个=,这也是为什么有时候能看到base64最后以等号结束的原因
    • 2.有1个8bit差2个8bit,由于8 = 6 + 2,可以拆出来1个6bit和一个不完整的2bit,先给2bit低位补4个0组成6bit,然后还差2个6bit组成4个6bit,因此在最后添加两个=

编码表

索引 字符 索引 字符 索引 字符 索引 字符
0 A 16 Q 32 g 48 w
1 B 17 R 33 h 49 x
2 C 18 S 34 i 50 y
3 D 19 T 35 j 51 z
4 E 20 U 36 k 52 0
5 F 21 V 37 l 53 1
6 G 22 W 38 m 54 2
7 H 23 X 39 n 55 3
8 I 24 Y 40 o 56 4
9 J 25 Z 41 p 57 5
10 K 26 a 42 q 58 6
11 L 27 b 43 r 59 7
12 M 28 c 44 s 60 8
13 N 29 d 45 t 61 9
14 O 30 e 46 u 62 +
15 P 31 f 47 v 63 /

如字符串’abcd’转为base64:

流程/字符 abcd
ASCII 0x61 0x62 0x63 0x64
8bit 01100001 01100010 01100011 01100100
6bit 011000 010110 001001 100011 011001 00000 = =
十进制 24 22 9 35 25 0
对照编码表 YWJjZA==

网页中base64的转换

字符串

window.btoa()方法

window.btoa()函数是从String对象中创建一个base-64编码的ASCII字符串,其中字符串中的每个字符都被视为一个二进制数据字节。别看他名字有够随性的,但就是如名字般简单粗暴。

语法:

1
let encodedData = window.btoa(stringToEncode);

如:

1
console.log(window.btoa('123456abc'));  // 'MTIzNDU2YWJj'

window.atob()方法

从名字也可以看出就是window.btoa()返过来,对用base64编码过的字符串进行解码。

如果传入字符串的长度不是4的倍数,则抛出DOMException。

语法:

1
var decodedData = window.atob(encodedData);

如:

1
console.log(window.atob('MTIzNDU2YWJj'));  // '123456abc'

*越界异常

在各浏览器中,使用 window.btoa对Unicode字符串进行编码都会触发一个字符越界的异常.

先把Unicode字符串转换为UTF-8编码,可以解决这个问题

1
2
3
4
5
6
7
8
9
10
11
function utf8_to_b64( str ) {
return window.btoa(unescape(encodeURIComponent( str )));
}

function b64_to_utf8( str ) {
return decodeURIComponent(escape(window.atob( str )));
}

// Usage:
utf8_to_b64('? à la mode'); // "4pyTIMOgIGxhIG1vZGU="
b64_to_utf8('4pyTIMOgIGxhIG1vZGU='); // "? à la mode"

window.btoa()和window.atob()方法的兼容情况(2018-11-23)

btoa_atob兼容

可以看出兼容情况都很良好

如果要兼容IE——Base.js

编码

使用

1
var encodedData = Base64.encode(stringToEncode);


1
console.log(Base64.encode('123456abc'));  // 'MTIzNDU2YWJj'

解码

使用

1
var decodedData = Base64.decode(encodedData);

如:

1
console.log(Base64.decode('MTIzNDU2YWJj'));  // '123456abc'

文件

如果要实现文件的Base64编码,则需要FileReader。FileReader类型实现的是一种异步文件读取机制。可以把FileReader想象成XMLHttpRequest,区别只是它读取的是文件系统,而不是远程服务器。
具体使用这里不具体介绍,下面有个通过FileReader将test.txt文件编码为Base64的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getFileBase64 (file, cb) {
if (!file) throw new Error('Error! no param "file"(getFileBase64()).');

let reader = new FileReader();
reader.onload = function(e) {
let base64 = e.target.result; // 该文件的完整Base64

if (cb) cb(base64);
};
reader.onerror = function () {
alert('Read file fail.');
};
reader.readAsDataURL(file);
}

使用:

1
2
3
4
5
6
7
8
9
10
window.addEventListener("dragenter", function(event) { event.preventDefault(); }, false);
window.addEventListener("dragover", function(event) { event.preventDefault(); }, false);

window.addEventListener("drop", function(event) {
getFileBase64(event.dataTransfer.files[0], function (base64) {
console.log(base64)
});
event.preventDefault();

}, false);

结果(原test.txt内容为“123456abc”):

1
data:text/plain;base64,MTIzNDY1YWJj

目前(2018-11-23)filereader的兼容情况
filereader

图片地址

对于页面中的图片或图片url,可以通过canvas的toDataURL方法进行编码。注意url有域名的限制。

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
33
34
35
36
37
function getImgBase64(urlorImg, cb) {
if (!urlorImg) throw new Error('Error! no param "url"(getImgBase64).')

function getBase64 (imgObj) {
let _width = imgObj.width,
_height = imgObj.height;

canvas = document.createElement('canvas');
canvas.width = _width;
canvas.height = _height;

ctx = canvas.getContext("2d");
ctx.drawImage(imgObj, 0, 0, _width, _height);

return canvas.toDataURL('image/jpeg', 1);
}


if (typeof urlorImg === 'string') { // url
let _img = new Image();
_img.onload = function () {
let img64 = getBase64(_img)

if (cb) cb(img64);
};

_img.onerror = function () {
alert('Image load failed.');
};

_img.src = urlorImg;
} else { // Image Object
let img64 = getBase64(urlorImg)

if (cb) cb(img64);
}
}

使用

1
2
3
getImgBase64('https://fund.10jqka.com.cn/public/181024koi/dist/images/t-2.png', base64 => {
console.log(base64);
});

结果

1
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAiCAYAAAA6RwvCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpiOGI1YjBlZS01NDdmLTRiMDMtODI3Mi02NDRlMGQ3NTBhMjYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OTBCOUM5NjRDOEM4MTFFOTk2MDJCQTI0MEY1RDRBMEEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OTBCOUM5NjNDOEM4MTFFOTk2MDJCQTI0MEY1RDRBMEEiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpiOGI1YjBlZS01NDdmLTRiMDMtODI3Mi02NDRlMGQ3NTBhMjYiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6YjhiNWIwZWUtNTQ3Zi00YjAzLTgyNzItNjQ0ZTBkNzUwYTI2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+7UGEfgAAARRJREFUeNrs1zsKwkAQBuBNjDYGC7HzCopW9oq14gF8XE28h6LYq3gHEewEiSAY/4UJpHQzk2zQDPxkizw+JptN4oRhqPJQrspJFZCfgTSRVhoQz2DfGjJFKoiDnG115I7s6ZgJ0rbVEUUQXUPCKKnOJJmsGrOOdaZjoyNR7Wg7QMY0PtmAiGM8ZkfFMBILmsZs6Fwa07XRkai2tO0jIxofbUDYGEkIC5PGS++APOjcvW+vIQ3R76MFUkWuyAp5Zw3RiBlSJ8QSCbL+HokQjSQIKQgbIQHxJRBciE8Tk43gQEQRSRe0OOJCj2jAnWhuHhCmEL1IzdNAmN6aJ3JDXtIIXY7hT3gJKRNK2YQUv5z/B/kIMADNlEV/BbegDwAAAABJRU5ErkJggg==

跨域问题

由于在 <canvas>位图中的像素可能来自多种来源,包括从其他主机检索的图像或视频,因此不可避免的会出现安全问题。

在”被污染”的画布中调用以下方法将会抛出安全错误:

  • <canvas> 的上下文上调用getImageData()
  • <canvas> 上调用 toBlob()
  • <canvas> 上调用 toDataURL()
    这种机制可以避免未经许可拉取远程网站信息而导致的用户隐私泄露。

解决:
HTML 规范中图片有一个 crossorigin 属性,结合合适的 CORS 响应头,就可以实现在画布中使用跨域 <img> 元素的图像。

总结

  • 简单的字符串转换,可使用window.btoa()方法,要兼容IE则需引用Base64.js并使用对应方法;
  • 对于页面中图片、或同域名图片url的转换,可以使用基于canvas的getImgBase64()方法;
  • 对于文件可以使用基于FileReader的getFileBase64()方法;

参考资料: