JavaScript 提供了一些 API 来处理文件或原始文件数据,例如:File、Blob、FileReader、ArrayBuffer、base64 等。下面就来看看它们都是如何使用的,它们之间又有何区别和联系!
ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区,是内存中一段固定长度的连续数据存储区的引用,你无法直接操作或修改它,只能通过 DataView 对象或 TypedArrray 对象来访问。这些对象用于读取和写入缓冲区内容。
ArrayBuffer不是一个Array类型,如果想要判断其类型,可以使用
toString.call(new ArrayBuffer()) === '[object, ArrayBuffer]'
ArrayBuffer 本身就是一个黑盒,不能直接读写所存储的数据,需要借助以下视图对象来读写:
首先要弄清楚 TypedArray 的概念, 这是 ES2015(又称ES6) 中新出的一个接口, 不能直接被实例化, 也就是说如下代码会报错。
new TypedArray()
因为这个接口就是一个抽象接口, 就像java中的抽象接口一样, 是不能被实例化的, 只能实例化实现该接口的子类. Uint8Array 就是实现 TypedArray 接口的一个子类。
就 Nodejs 而言, 可以使用 Buffer 操作二进制数据, 那对前端 JS 而言, 在 TypeArray 出现之前, 是没有可以直接操作二进制数据的类的, 这也与前端很少需要操作二进制数据相关。
所以 TypeArray 接口的作用是操作二进制数据。
TypeArray 是一个类数组结构, 也就是说数组可以用的函数, 比如 arr[0], slice, copy 等方法, TypeArray 也可以使用。
所有的类型化数组都是基于 ArrayBuffer 进行操作的,你可以借此观察到每个元素的确切字节表示,因此二进制格式中的数字编码方式具有重要意义。
除了 Int8Array、Unit8Array 和 Uint8ClampedArray 以外的其他类型数组都将每个元素存储为多个字节。这些字节可以按照从最高有效位到最低有效位(大端序)或从最低有效位到最高有效位(小端序)的顺序进行排序。请参阅字节序以了解更多。类型化数组始终使用平台的本机字节顺序。如果要在缓冲区中写入和读取时指定字节顺序,应该使用 DataView。
DataView 视图是一个可以从二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序(endianness)问题。
DataView 访问器(accessor)提供了对如何访问数据的明确控制,而不管执行代码的计算机的字节序如何。
// dataview.setInt16(byteOffset, value [, littleEndian])
const littleEndian = (() => {
const buffer = new ArrayBuffer(2);
new DataView(buffer).setInt16(0, 256, true /* 小端对齐 */);
// Int16Array 使用平台的字节序。
return new Int16Array(buffer)[0] === 256;
})();
console.log(littleEndian); // true 或 false
TypedArray 视图的字节顺序与底层的计算机体系结构有关。在大多数计算机体系结构中,包括 x86 架构的处理器,字节顺序是 Little Endian。因此,当使用 TypedArray 视图时,它们默认采用 Little Endian 字节顺序。
然而,并非所有计算机体系结构都使用 Little Endian 字节顺序。例如,某些 ARM 架构的处理器使用的是 Big Endian 字节顺序。在这些体系结构上,TypedArray 视图会自动适应相应的字节顺序。
因此,需要注意的是,尽管 TypedArray 视图在大多数情况下默认采用与机器相关的字节顺序(通常是 Little Endian),但具体的字节顺序仍取决于底层的计算机体系结构。如果需要确保特定的字节顺序,可以使用 DataView 视图并显式指定字节顺序。
不深入讨论二进制数据的工作原理,让我们看一个简单的例子:
var buffer = new ArrayBuffer(2) // array buffer for two bytes
var bytes = new Uint8Array(buffer) // views the buffer as an array of 8 bit integers
bytes[0] = 65 // ASCII for 'A'
bytes[1] = 66 // ASCII for 'B'
现在我们可以将其转换为 Blob 对象,从中创建一个 Data URI,并将其作为一个新的文本文件打开:
var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri)
这将在一个新的浏览器窗口中显示文本 'AB'。
你可以看到在前面的例子中,我们先写入了表示 'A' 的字节,然后是表示 'B' 的字节,但我们也可以使用 Uint16Array,将这两个字节一次性写入一个 16 位的数字中:
var buffer = new ArrayBuffer(2) // array buffer for two bytes
var word = new Uint16Array(buffer) // views the buffer as an array with a single 16 bit integer
var value = (65 << 8) + 66 // we shift the 'A' into the upper 8 bit and add the 'B' as the lower 8 bit.
word[0] = value // write the 16 bit (2 bytes) into the typed array
// Let's create a text file from them:
var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri)
但等等?我们看到的是"BA"而不是之前的"AB"!发生了什么?
让我们仔细看一下我们写入数组的值:
65 decimal = 01 00 00 01 binary
66 decimal = 01 00 00 10 binary
// what we did when we wrote into the Uint8Array:
01 00 00 01 01 00 00 10
<bytes[0]-> <bytes[1]->
// what we did when we created the 16-bit number:
var value = (01 00 00 01 00 00 00 00) + 01 00 00 10
= 01 00 00 01 01 00 00 10
你可以看到我们写入 Uint8Array 和 Uint16Array 的 16 位数值是相同的,那为什么结果会不同呢?
答案是,一个超过一个字节的值的字节顺序取决于系统的字节序(大小端序)。让我们来验证一下:
var buffer = new ArrayBuffer(2)
// create two typed arrays that provide a view on the same ArrayBuffer
var word = new Uint16Array(buffer) // this one uses 16 bit numbers
var bytes = new Uint8Array(buffer) // this one uses 8 bit numbers
var value = (65 << 8) + 66
word[0] = (65 << 8) + 66
console.log(bytes) // will output [66, 65]
console.log(word[0] === value) // will output true
当我们查看各个字节时,我们发现 B 的值确实被写入了缓冲区的第一个字节,而不是 A 的值,但当我们读回这个 16 位数字时,它是正确的!
这是因为浏览器默认使用小端序(Little Endian)的数字。
这是什么意思?
让我们假设一个字节可以保存一个单个数字,因此数字123将占用三个字节:1、2和3。小端序(Little Endian)意味着多字节数值的较低位数字先存储,因此在内存中它将按照3、2、1的顺序存储。
还有一种大端序(Big Endian)格式,其中字节按照我们预期的顺序存储,从最高位数字开始,所以在内存中它将按照1、2、3的顺序存储。
只要计算机知道数据的存储方式,它就可以为我们进行转换,并从内存中得到正确的数字。
让我们看看另一种读写 ArrayBuffer 的方式:DataView,想象一下,您想要编写一个需要一些文件头的二进制文件,如下所示:
顺便说一下:这是 BMP 文件头的结构。
除了使用各种类型化数组进行操作,我们还可以使用DataView:
var buffer = new ArrayBuffer(14)
var view = new DataView(buffer)
view.setUint8(0, 66) // Write one byte: 'B'
view.setUint8(1, 67) // Write one byte: 'M'
view.setUint32(2, 1234) // Write four byte: 1234 (rest filled with zeroes)
view.setUint16(6, 0) // Write two bytes: reserved 1
view.setUint16(8, 0) // Write two bytes: reserved 2
view.setUint32(10, 0) // Write four bytes: offset
我们的ArrayBuffer现在包含以下数据:
Byte | 0 | 1 | 2 | 3 | 4 | 5 | ... |
Type | I8 | I8 | I32 | ... |
Data | B | M |00000000|00000000|00000100|11010010| ... |
在上面的示例中,我们使用DataView将两个Uint8写入前两个字节,然后是占用接下来四个字节的Uint32,依此类推。
很酷。现在让我们回到我们的简单文本例子。
我们也可以使用DataView而不是之前使用的Uint16Array来写入一个Uint16,以保存我们的两个字符字符串'AB':
var buffer = new ArrayBuffer(2) // array buffer for two bytes
var view = new DataView(buffer)
var value = (65 << 8) + 66 // we shift the 'A' into the upper 8 bit and add the 'B' as the lower 8 bit.
view.setUint16(0, value)
// Let's create a text file from them:
var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri)
等一下,什么?我们得到期望的字符串'AB',而不是上次写入Uint16时得到的'BA'!也许setUint16默认为大端序(Big Endian)?
根据DataView的规范,setUint16方法的定义如下:
DataView.prototype.setUint16 ( byteOffset, value [ , littleEndian ] )
根据规范,如果没有明确指定littleEndian参数,则默认为false,即使用大端序(Big Endian)。因此,使用DataView的setUint16方法写入值时,默认情况下采用的是大端序。这就解释了为什么使用DataView写入Uint16时,我们得到了正确的字符串'AB',而不是'BA'。
Blob 全称为 binary large object ,即二进制大对象,它是 JavaScript 中的一个对象,表示原始的类似文件的数据。下面是 MDN 中对 Blob 的解释:
Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。
实际上,Blob 对象是包含有只读原始数据的类文件对象。简单来说,Blob 对象就是一个不可修改的二进制文件。
可以使用 Blob() 构造函数来创建一个 Blob:
new Blob(array, options);
其有两个参数:
options:可选的 BlobPropertyBag 字典,它可能会指定如下两个属性
这里整理 2 种图片本地预览的方式:
<body>
<h1>1.DataURL方式:</h1>
<input type="file" accept="image/*" onchange="selectFileForDataURL(event)">
<img id="output1">
<h1>2.Blob方式:</h1>
<input type="file" accept="image/*" onchange="selectFileForBlob(event)">
<img id="output2">
<script>
// 1.DataURL方式:
async function selectFileForDataURL() {
const reader = new FileReader();
reader.onload = function () {
const output = document.querySelector("#output1")
output.src = reader.result;
}
reader.readAsDataURL(event.target.files[0]);
}
//2.Blob方式:
async function selectFileForBlob(){
const reader = new FileReader();
const output = document.querySelector("#output2");
const imgUrl = window.URL.createObjectURL(event.target.files[0]);
output.src = imgUrl;
reader.onload = function(event){
window.URL.revokeObjectURL(imgUrl);
}
}
</script>
</body>
File对象继承了Blob对象的所有属性和方法,可以使用File对象的slice()方法进行文件切片操作,将大文件切割成较小的分片,并逐个上传这些分片。
// 分片上传对象
var ChunkUploader = function(file) {
this.file = file;
this.chunkSize = 1024 * 1024; // 每个分片的大小,这里设置为1MB
this.totalChunks = Math.ceil(file.size / this.chunkSize);
this.currentChunk = 0;
this.uploadedChunks = [];
this.isPaused = false;
};
// 上传下一个分片
ChunkUploader.prototype.uploadNextChunk = function() {
if (this.currentChunk >= this.totalChunks) {
console.log("文件上传完成");
return;
}
if (this.isPaused) {
console.log("上传已暂停");
return;
}
var start = this.currentChunk * this.chunkSize;
var end = Math.min(start + this.chunkSize, this.file.size);
var chunk = this.file.slice(start, end);
var formData = new FormData();
formData.append("file", chunk);
formData.append("chunkIndex", this.currentChunk);
// 发起上传请求,这里使用XMLHttpRequest示例
var xhr = new XMLHttpRequest();
xhr.open("POST", "/upload", true);
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
// 上传成功,记录已上传分片信息
this.uploadedChunks.push(this.currentChunk);
// 继续上传下一个分片
this.currentChunk++;
this.uploadNextChunk();
} else {
// 上传失败,处理错误
console.error("上传失败:", xhr.status, xhr.statusText);
}
};
xhr.onerror = () => {
console.error("上传出错");
};
xhr.send(formData);
};
// 暂停上传
ChunkUploader.prototype.pauseUpload = function() {
this.isPaused = true;
};
// 继续上传
ChunkUploader.prototype.resumeUpload = function() {
this.isPaused = false;
this.uploadNextChunk();
};
// 创建文件上传实例
var fileInput = document.getElementById("fileInput");
var chunkUploader = new ChunkUploader(fileInput.files[0]);
// 启动上传
chunkUploader.uploadNextChunk();
// 暂停上传
chunkUploader.pauseUpload();
// 继续上传
chunkUploader.resumeUpload();
要通过HTTP请求获取文件并下载,您可以使用XMLHttpRequest或Fetch API来执行请求,并使用Blob对象进行文件下载。以下是一个示例代码,演示如何请求并下载文件:
function downloadFile(url, fileName) {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "blob";
xhr.onload = function () {
if (xhr.status === 200) {
var blob = xhr.response;
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = fileName;
var event = document.createEvent("MouseEvents");
event.initEvent("click", true, false);
a.dispatchEvent(event);
}
};
xhr.send();
}
function downloadFile(url, fileName) {
fetch(url)
.then(function (response) {
if (response.ok) {
return response.blob();
} else {
throw new Error("File download failed");
}
})
.then(function (blob) {
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = fileName;
var event = document.createEvent("MouseEvents");
event.initEvent("click", true, false);
a.dispatchEvent(event);
})
.catch(function (error) {
console.error("File download error:", error);
});
}
当我们希望本地图片在上传之前,先进行一定压缩,再提交,从而减少传输的数据量。
在前端我们可以使用 Canvas 提供的 toDataURL() 方法来实现,该方法接收 type 和 encoderOptions 两个可选参数:
<body>
<input type="file" accept="image/*" onchange="loadFile(event)" />
<script>
// 将base64转化为File对象
const base64ToFile = (base64String, fileName, fileType) => {
const byteCharacters = atob(base64String.split(",")[1]);
const byteArrays = [];
for (let i = 0; i < byteCharacters.length; i++) {
byteArrays.push(byteCharacters.charCodeAt(i));
}
const byteArray = new Uint8Array(byteArrays);
return new File([byteArray], fileName, { type: fileType });
};
const compress = (file, maxWidth, maxHeight, quality) => {
return new Promise((resolve, reject) => {
const image = new Image();
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
image.onload = () => {
let width = image.width;
let height = image.height;
// 计算压缩后的尺寸
if (maxWidth && width > maxWidth) {
height *= maxWidth / width;
width = maxWidth;
}
if (maxHeight && height > maxHeight) {
width *= maxHeight / height;
height = maxHeight;
}
// 设置 Canvas 的尺寸
canvas.width = width;
canvas.height = height;
// 在 Canvas 上绘制压缩后的图片
ctx.drawImage(image, 0, 0, width, height);
// 转换为压缩后的图片数据
const compressedDataUrl = canvas.toDataURL("image/jpeg", quality);
// 将base64转化为File对象
const compressedFile = base64ToFile(
compressedDataUrl,
encodeURIComponent(file.name),
file.type
);
resolve(compressedFile);
};
// 加载图片
image.src = URL.createObjectURL(file);
});
};
// 通过 AJAX 提交到服务器
const uploadFile = (url, file) => {
let formData = new FormData();
let request = new XMLHttpRequest();
formData.append("image", file);
request.open("POST", url, true);
request.send(formData);
}
const loadFile = (event) => {
const file = event.target.files[0];
const compressedFile = compress(file);
uploadFile("https://httpbin.org/post", compressedFile);
};
</script>
</body>
其实 Canvas 对象除了提供 toDataURL() 方法之外,它还提供了一个 toBlob() 方法,该方法的语法如下:
canvas.toBlob(callback, mimeType, qualityArgument)
和 toDataURL() 方法相比,toBlob() 方法是异步的,因此多了个 callback 参数,这个 callback 回调方法默认的第一个参数就是转换好的 blob文件信息。
看定义的话,先翻翻 MDN:
ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。
Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。
从定义可以知晓,两者都是对二进制数据进行操作。MDN描述的还比较模糊,《现代 JavaScript 教程》中写的比较清楚:“基本的二进制对象是 ArrayBuffer —— 对固定长度的连续内存空间的引用”。Blob支持的类型更加复合,既然也是操作二进制数据所以核心也是基于ArrayBuffer,但更主要是对文件进行操作。所以两者大部分情况下能够替换使用也就很容易理解了。但还不够解释以上其他问题。
继续查找资料,翻到Chrome设计文档Chrome's Blob Storage System Design中有对Blob详细描述。
之前的疑问在这里就有答案了:
If the in-memory space for blobs is getting full, or a new blob is too large to be in-memory, then the blob system uses the disk. This can either be paging old blobs to disk, or saving the new too-large blob straight to disk.
大意是说,当blob的内存空间占满时,或者新创建的blob太大,剩余的内存空间放不下了,blob会转存到磁盘中。可以是转存旧的blob数据,也可以是将新的blob直接存储到磁盘。
同时还提到了,在使用blob时应该避免快速创建非常多的blob,特别是数据量非常大的,这会导致浏览器要将blob写入到磁盘后才能渲染器才能继续处理后续数据。这样也就解释了为什么之前blob有出现卡顿的情况。
总结一下差异:
综合看来,如果Axios处理文件数据,还是配置blob比较适合。
另一个问题,Axios中为什么说blob仅浏览器可用?这个比较容易找到答案,贺师俊在知乎有个回答:
注意,Blob并不像ArrayBuffer是JS语言内置的,而是Web API,Node.js的API里就没有Blob。这也是为什么MDN说「Blobs can represent data that isn't necessarily in a JavaScript-native format」(中文版的翻译「Blob表示的不一定是JavaScript原生格式的数据」反而比英文原文难理解)。
不看这说明是真不理解MDN的那段描述,在《现代 JavaScript 教程》中其实也有提到,但只在Blob章节开头提了ArrayBuffer是ECMA 标准的一部分,没提说Blob是不是,看着也是会觉得有些奇怪。
不过这个回答是2020年的,当时Node还不支持Blob,到Node18版本发布已经正式支持Blob类型了,详细的可以看Node官方文档class-blob中History表,所以现在Node中也是支持Blob了。
最后是Stream,先说说Stream模式与Arraybuffer(Node中对应的是Buffer)模式应用的差异。
在大文件读取的场景下,使用Arraybuffer会将所有数据全部写入内存后再处理,文件很大时很可能导致内存爆了。如果使用Stream数据依然是存入内存,但存入的数据会立即就开始处理,不必等到所有数据加载完再开始,这样只需要消耗极小的内存就能完成对文件的处理。
Node中是有Stream模式相关的API,那浏览器呢?也是有的,Chrome从59版本开始其实是有Stream API的,网络请求需要配合fetch使用。
翻阅代码,可以发现Axios浏览器请求还是基于XMLHttpRequest的,axios/lib/adapters/xhr.js源码中responseType数据没有处理直接传入XMLHttpRequest对象的。
那么XMLHttpRequest的responseType是否支持设置为stream?来看看WHATWG对XMLHttpRequest支持的类型描述:
enum XMLHttpRequestResponseType {
"",
"arraybuffer",
"blob",
"document",
"json",
"text"
};
可知,XMLHttpRequest是不支持的。咦?很奇怪,axiox文档怎么写的是支持?
翻了一圈issue,发现有提到axios准备增加一个新的adapter(使用的是fetch)来支持stream。回头又找了一圈代码,没发现有新增的模块。继续翻翻issue和discussions,之前的相关信息都已经关闭了,但在Axios next的关联中有一个相关issue还是打开的,相关PR也还未合并,查看代码版本目前处于beta5,也有半年没更新了。
也难怪主版本中没看到过相关代码,目前看来相关改动还没有确定下来。实际测试中stream也没支持成功,流数据返回的话会解析成字符串。如果非常想在axios中接收stream数据,可以尝试使用还在测试中的模块,将adapter配置更换一下。
总之目前为止,如果想使用stream传输数据还是转向用fetch吧。
Base64(radix-64)是一种基于64个可打印字符来表示二进制数据的表示方法。由于,所以每6个比特为一个单元,对应某个可打印字符。3个字节相当于24个比特,对应于4个 Base64 单元,即3个字节可由4个可打印字符来表示。这 64 个字符包括大小写字母(A-Z, a-z)、数字(0-9)以及两个特殊字符(+ 和 /)。
这意味着 Base64 格式的字符串或文件的尺寸约是原始尺寸的 133%(增加了大约 33%)。如果编码的数据很少,增加的比例可能会更高。例如:长度为 1 的字符串 "a" 进行 Base64 编码后是 "YQ==",长度为 4,尺寸增加了 3 倍。
Base64 常用于在通常处理文本数据的场合,表示、传输、存储一些二进制数据,包括 MIME 的电子邮件及 XML 的一些复杂数据。
Base64 编码在网络上的一个常见应用是对二进制数据进行编码,以便将其纳入 data: URL 中。
在 Base64 编码中,每三个字节的二进制数据被编码为四个字符,如果最后剩下的字节不足三个,则会进行填充。
具体的填充规则如下:
以下是一个示例,将一个字符串 "Hello, World!" 进行 Base64 编码:
将字符串转换为对应的二进制数据。例如,使用 UTF-8 编码将 "Hello, World!" 转换为字节数组:[72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]。
将每三个字节的数据分组,并将其转换为对应的 Base64 字符。对于每个分组,将其转换为四个字符。
第一个分组:72, 101, 108 → 01001000, 01100101, 01101100 → 010010, 000110, 010101, 101100 → S, G, V, s
第二个分组:108, 111, 44 → 01101100, 01101111, 00101100 → 011011, 000110, 111100, 101100 → b, G, 9, s
第三个分组:32, 87, 111 → 00100000, 01010111, 01101111 → 001000, 000010, 101111, 101100 → I, F, v, s
第四个分组:114, 108, 100 → 01110010, 01101100, 01100100 → 011100, 100110, 110100 → c, m, Q
第五个分组:33 → 00100001 → 001000 010000 → I, Q 最后一个分组只有一个字节,编码结果为两个字符,然后在末尾添加两个 "="。
将每个分组得到的字符连接起来,得到最终的 Base64 编码字符串:SGVsbG8sIFdvcmxkIQ==
在 JavaScript 中,有两个函数被分别用来处理解码和编码 Base64 字符串:
由于 JavaScript 字符串是 16 位编码的字符串,在大多数浏览器中,在 Unicode 字符串上调用 window.btoa,如果一个字符超过了 8 位 ASCII 编码字符的范围,就会引起 Character Out Of Range 异常。
有两种可能的方法来解决这个问题:
encodeURIComponent 会将字符转换为 UTF-8 编码的字节序列
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"
该方案由 Johan Sundström 提出。
另一个可能的解决方案是不利用现在已经废弃的 'unescape' 和 'escape' 函数。不过这个方案并没有对输入的字符串进行 base64 编码。注意,utf8_to_b64 和 b64EncodeUnicode 的输出结果的不同。采用这种方式可能会导致与其他应用程序的互操作性问题。
function b64EncodeUnicode(str) {
return btoa(encodeURIComponent(str));
}
function UnicodeDecodeB64(str) {
return decodeURIComponent(atob(str));
}
b64EncodeUnicode("✓ à la mode"); // "JUUyJTlDJTkzJTIwJUMzJUEwJTIwbGElMjBtb2Rl"
UnicodeDecodeB64("JUUyJTlDJTkzJTIwJUMzJUEwJTIwbGElMjBtb2Rl"); // "✓ à la mode"
"use strict";
// Array of bytes to Base64 string decoding
function b64ToUint6(nChr) {
return nChr > 64 && nChr < 91
? nChr - 65
: nChr > 96 && nChr < 123
? nChr - 71
: nChr > 47 && nChr < 58
? nChr + 4
: nChr === 43
? 62
: nChr === 47
? 63
: 0;
}
function base64DecToArr(sBase64, nBlocksSize) {
const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, ""); // Remove any non-base64 characters, such as trailing "=", whitespace, and more.
const nInLen = sB64Enc.length;
const nOutLen = nBlocksSize
? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
: (nInLen * 3 + 1) >> 2;
const taBytes = new Uint8Array(nOutLen);
let nMod3;
let nMod4;
let nUint24 = 0;
let nOutIdx = 0;
for (let nInIdx = 0; nInIdx < nInLen; nInIdx++) {
nMod4 = nInIdx & 3;
nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (6 * (3 - nMod4));
if (nMod4 === 3 || nInLen - nInIdx === 1) {
nMod3 = 0;
while (nMod3 < 3 && nOutIdx < nOutLen) {
taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
nMod3++;
nOutIdx++;
}
nUint24 = 0;
}
}
return taBytes;
}
/* Base64 string to array encoding */
function uint6ToB64(nUint6) {
return nUint6 < 26
? nUint6 + 65
: nUint6 < 52
? nUint6 + 71
: nUint6 < 62
? nUint6 - 4
: nUint6 === 62
? 43
: nUint6 === 63
? 47
: 65;
}
function base64EncArr(aBytes) {
let nMod3 = 2;
let sB64Enc = "";
const nLen = aBytes.length;
let nUint24 = 0;
for (let nIdx = 0; nIdx < nLen; nIdx++) {
nMod3 = nIdx % 3;
// To break your base64 into several 80-character lines, add:
// if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) {
// sB64Enc += "\r\n";
// }
nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24);
if (nMod3 === 2 || aBytes.length - nIdx === 1) {
sB64Enc += String.fromCodePoint(
uint6ToB64((nUint24 >>> 18) & 63),
uint6ToB64((nUint24 >>> 12) & 63),
uint6ToB64((nUint24 >>> 6) & 63),
uint6ToB64(nUint24 & 63),
);
nUint24 = 0;
}
}
return (
sB64Enc.substring(0, sB64Enc.length - 2 + nMod3) +
(nMod3 === 2 ? "" : nMod3 === 1 ? "=" : "==")
);
}
/* UTF-8 array to JS string and vice versa */
function UTF8ArrToStr(aBytes) {
let sView = "";
let nPart;
const nLen = aBytes.length;
for (let nIdx = 0; nIdx < nLen; nIdx++) {
nPart = aBytes[nIdx];
sView += String.fromCodePoint(
nPart > 251 && nPart < 254 && nIdx + 5 < nLen /* six bytes */
? /* (nPart - 252 << 30) may be not so safe in ECMAScript! So…: */
(nPart - 252) * 1073741824 +
((aBytes[++nIdx] - 128) << 24) +
((aBytes[++nIdx] - 128) << 18) +
((aBytes[++nIdx] - 128) << 12) +
((aBytes[++nIdx] - 128) << 6) +
aBytes[++nIdx] -
128
: nPart > 247 && nPart < 252 && nIdx + 4 < nLen /* five bytes */
? ((nPart - 248) << 24) +
((aBytes[++nIdx] - 128) << 18) +
((aBytes[++nIdx] - 128) << 12) +
((aBytes[++nIdx] - 128) << 6) +
aBytes[++nIdx] -
128
: nPart > 239 && nPart < 248 && nIdx + 3 < nLen /* four bytes */
? ((nPart - 240) << 18) +
((aBytes[++nIdx] - 128) << 12) +
((aBytes[++nIdx] - 128) << 6) +
aBytes[++nIdx] -
128
: nPart > 223 && nPart < 240 && nIdx + 2 < nLen /* three bytes */
? ((nPart - 224) << 12) +
((aBytes[++nIdx] - 128) << 6) +
aBytes[++nIdx] -
128
: nPart > 191 && nPart < 224 && nIdx + 1 < nLen /* two bytes */
? ((nPart - 192) << 6) + aBytes[++nIdx] - 128
: /* nPart < 127 ? */ /* one byte */
nPart,
);
}
return sView;
}
function strToUTF8Arr(sDOMStr) {
let aBytes;
let nChr;
const nStrLen = sDOMStr.length;
let nArrLen = 0;
/* mapping… */
for (let nMapIdx = 0; nMapIdx < nStrLen; nMapIdx++) {
nChr = sDOMStr.codePointAt(nMapIdx);
if (nChr >= 0x10000) {
nMapIdx++;
}
nArrLen +=
nChr < 0x80
? 1
: nChr < 0x800
? 2
: nChr < 0x10000
? 3
: nChr < 0x200000
? 4
: nChr < 0x4000000
? 5
: 6;
}
aBytes = new Uint8Array(nArrLen);
/* transcription… */
let nIdx = 0;
let nChrIdx = 0;
while (nIdx < nArrLen) {
nChr = sDOMStr.codePointAt(nChrIdx);
if (nChr < 128) {
/* one byte */
aBytes[nIdx++] = nChr;
} else if (nChr < 0x800) {
/* two bytes */
aBytes[nIdx++] = 192 + (nChr >>> 6);
aBytes[nIdx++] = 128 + (nChr & 63);
} else if (nChr < 0x10000) {
/* three bytes */
aBytes[nIdx++] = 224 + (nChr >>> 12);
aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
aBytes[nIdx++] = 128 + (nChr & 63);
} else if (nChr < 0x200000) {
/* four bytes */
aBytes[nIdx++] = 240 + (nChr >>> 18);
aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
aBytes[nIdx++] = 128 + (nChr & 63);
nChrIdx++;
} else if (nChr < 0x4000000) {
/* five bytes */
aBytes[nIdx++] = 248 + (nChr >>> 24);
aBytes[nIdx++] = 128 + ((nChr >>> 18) & 63);
aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
aBytes[nIdx++] = 128 + (nChr & 63);
nChrIdx++;
} /* if (nChr <= 0x7fffffff) */ else {
/* six bytes */
aBytes[nIdx++] = 252 + (nChr >>> 30);
aBytes[nIdx++] = 128 + ((nChr >>> 24) & 63);
aBytes[nIdx++] = 128 + ((nChr >>> 18) & 63);
aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
aBytes[nIdx++] = 128 + (nChr & 63);
nChrIdx++;
}
nChrIdx++;
}
return aBytes;
}
测试
/* Tests */
const sMyInput = "Base 64 \u2014 Mozilla Developer Network";
const aMyUTF8Input = strToUTF8Arr(sMyInput);
const sMyBase64 = base64EncArr(aMyUTF8Input);
alert(sMyBase64);
const aMyUTF8Output = base64DecToArr(sMyBase64);
const sMyOutput = UTF8ArrToStr(aMyUTF8Output);
alert(sMyOutput);
Blob URL 和 Data URL 是两种不同的 URL 方案,用于在浏览器中表示和使用数据。
Blob URL(或称为 Object URL)是一种特殊的 URL 格式,用于表示 Blob 对象的地址。它通过使用 URL.createObjectURL()
方法生成,该方法接受一个 Blob 或 File 对象作为参数,并返回一个唯一的 URL,该 URL 可以用于引用该 Blob 对象。Blob URL 的格式通常是以 "blob:" 开头,后面跟随一个唯一的标识符。
Data URL 是一种用于嵌入数据的 URL 格式,可以直接将数据嵌入到 URL 中。它的格式如下:
data:[<mediatype>][;base64],<data>
其中 <mediatype>
是数据的 MIME 类型,例如 text/plain
、image/jpeg
等;;base64
是可选的,表示数据是否使用 Base64 编码;<data>
是实际的数据内容。
Blob URL 和 Data URL 的区别主要在于数据的来源和用途:
在选择使用 Blob URL 还是 Data URL 时,需要根据具体的使用场景和数据大小来进行权衡。如果涉及到大型或二进制数据,Blob URL 通常更合适;而对于小型或文本数据,Data URL 可能更方便。
encodeURIComponent() 函数通过将特定字符的每个实例替换成代表字符的 UTF-8 编码的一个、两个、三个或四个转义序列来编码 URI(只有由两个“代理”字符组成的字符会被编码为四个转义序列)。与 encodeURI() 相比,此函数会编码更多的字符,包括 URI 语法的一部分。
以下是 encodeURIComponent 的编码过程:
对于每个字符,判断是否属于以下字符集之一:
如果字符属于上述字符集之一,则保持不变。
对于不属于上述字符集的字符:
以下是一个示例,将一个字符串 "шеллы" 进行编码:
console.log(`?x=${encodeURIComponent('шеллы')}`);
// Expected output: "?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"
以"ш"字符为例,看其是如何被编码的:
// 获取字符"ш"的码点
"ш".charCodeAt() // 1096
// 转成十六进制
Number(1096).toString(16) // '448'
// 对照以下 Unicode 十六进制转化 UTF-8 编码方式表,448 介于 U+0080 到 U+07FF 之间
Unicode符号范围 UTF-8编码方式
(十六进制) (二进制)
0000 0000-0000 007F(U+0000 到 U+007F) 0xxxxxxx
0000 0080-0000 07FF(U+0080 到 U+07FF) 110xxxxx 10xxxxxx
0000 0800-0000 FFFF(U+0800 到 U+FFFF) 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF(U+10000 到 U+10FFFF) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
// 将其转化成二进制,然后从 "ш" 的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0
Number(1096).toString(2) // '10001001000'
// 填充后得到 UTF-8 编码方式
11010001 10001000
// 然后,转成十六进制,每个十六进制数对应四位二进制数
1101(D) 0001(1) 1000(8) 1000(8) -> %D1%88
Base64-MDN
Base64-维基百科
JavaScript中"ArrayBuffer"对象与"Blob"对象到底有什么区别?
谈谈JS二进制:File、Blob、FileReader、ArrayBuffer、Base64
axios中responseType配置blob、arraybuffer、stream值有什么差异