最近在开发中用到了浏览器端的二进制文件相关的功能。所以想写篇文章记录整理以及分享。
JS涉及二进制操作的API有很多,包括:Blob,File,FileReader,TypedArray,DataView,ArrayBuffer等。其中,ArrayBuffer是最重要的。无论想在浏览器上操作什么文件,怎样操作,都要围绕ArrayBuffer进行
ArrayBuffer介绍及使用ArrayBuffer获得文件
一个ArrayBuffer对象是一段纯二进制的数据。Buffer可以理解为浏览器分配给这个二进制文件的一段内存,无论哪个二进制文件,只要想在浏览器上操作就必然会有一个对应的ArrayBuffer对象存在。下面我们先自己创建一个:
const buffer = new ArrayBuffer(8);
console.log(buffer.byteLength); // 输出8
在这里我们直接创建了一个8个字节长度的ArrayBuffer对象。
ArrayBuffer还有一个最重要的特性就是没有任何方法直接操作。由于无法直接操作,所以从Array那里沿袭过来的方法只有一个,就是ArrayBuffer.prototype.slice。这个方法的实现和Array.prototype.slice几乎一模一样,唯一的区别在于其返回的是一个新的ArrayBuffer对象而不是一个Array对象。通过这个方法就可以轻松实现ArrayBuffer对象的复制:
{
const buffer = new ArrayBuffer(8);
const anotherBuffer = buffer.slice(0);
console.log(anotherBuffer.byteLength); // 输出8
console.log(buffer === anotherBuffer); // 输出false
}
对ArrayBuffer对象的复制就等于对文件本身的复制。
但是我们总归要操作二进制文件,既然ArrayBuffer不让我们直接操作,那我们该怎么办?这里就轮到TypedArray和DataView登场了。这里顺便给出生成文件及地址的方法
{
const buffer = new ArrayBuffer(12);
const u8 = new Uint8Array(buffer);
const str = "Hello World!";
for(let i = 0; i < str.length; i++){
u8[i] = str.codePointAt(i);
}
const file = new File([u8.buffer], "aFile.txt", { type: "text/plain" });
const domUrl = URL.createObjectURL(file);
window.location.href = domUrl;
}
以上代码可以视情况简化,比方说Uint8Array创建时不传入ArrayBuffer对象时会自动创建一个,而生成文本时File可以直接传入字符串等。但是即便如此,也必须牢记ArrayBuffer是核心这一事实。
接下来讲解一下各种场景下如何获得文件对应的ArrayBuffer。
通过上传控件获取文件时
这是获取文件最常见的途径,当用户上传控件并选择一个文件时,我们就能在该控件的DOM对象中找到一个files属性,该属性是一个FileList类型的类数组,里面就有用户刚刚选择的一个或多个文件,都是File类型的对象。这个File类型其实刚刚已经出现过了,那么怎么从File类型的对象里拿到对应的ArrayBuffer呢?非常简单:
{
// 对象获取过程略......
file.arrayBuffer().then(buffer => /* process the ArrayBuffer */);
}
File是Blob的子类,而Blob就有直接获取arrayBuffer的方法,File的对象也可以直接调用,返回一个Promise。此时就可以使用TypedArray或DataView对取得的文件进行处理了,比方说我觉得文件太大,想切割一下再上传:
{
// 对象获取过程略......
file.arrayBuffer().then(buffer => {
const u8 = new Uint8Array(buffer);
// 循环略......
const u8Chunk = u8.subarray(start, end);
const fileChunk = const file = new File([u8], `chunk${start}_${end}`);
const form = document.createElement("form");
const formData = new FormData(form);
formData.append("file", fileChunk);
// 略......
});
}
实际文件切割上传还涉及更多东西,这里不深入。
通过API接口获取文件
这种也比较常见。我们工作时经常会出现报表导出等功能。这种情况简单的做法就是直接window.location.href跳API地址,但是这种做法有一定的局限性。一个是只能发起get请求,假如请求需要的参数量较大就有点捉急;一个没有回调,无法追踪进度,如果文件较大,用户可能会疯狂点击,体验不好。但是如果使用ajax下载文件就可以完美解决上述痛点。
{
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.responseType = "arraybuffer";
xhr.onload = function() {
// 略......
const ab = this.response;
if(ab instanceof ArrayBuffer) {
// 文件类型可以从Content-type响应头获取
// 文件名可以从Content-disposition响应头获取
const file = new File([ab], filename, {type});
const domUrl = URL.createObjectURL(file);
const saveLink = document.createElement("a");
saveLink.href = domUrl;
saveLink.download = filename;
// 模拟一次点击事件
const event = document.createEvent('MouseEvents');
event.initMouseEvent("click", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
saveLink.dispatch(event);
}else {
// 降级处理
}
}
}
显然这样一来就可以使用post发送数据,并且可以在回调中修改一下按钮状态之类的,方便了很多。
有的时候即使设置了responseType为arraybuffer也无法获得ArrayBuffer类型的数据,可以降级到直接打开,在此之前,可以优先考虑使用fetch获取数据。
{
fetch(url, {
// ...请求参数
}).then(res => {
if(res.ok) {
// 略......
return res.arrayBuffer()
}
}).then(buffer => {
// 操作buffer
})
}
同样可以使用post方法发送数据以及使用回调。可以判断一下兼容性酌情使用。
使用canvas处理图片
在浏览器上可以利用canvas直接处理图片,不需要了解图片特定的格式。举个例子:
{
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
let imgData = null;
img.onload = () => {
const width = img.width;
const height = img.height;
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
imgData = ctx.getImageData(0, 0, width, height);
console.log(imgData) // 打印出一个ImageData对象
ctx.putImageData(imgData, 0, 0);
canvas.toDataURL();
}
// 示意,实际上可能会有跨域问题
img.src = 'https://iph.href.lu/879x200';
}
ImageData对象有个data属性,这是一个Uint8ClampedArray类型的对象,其中每四个数值代表一个像素,分别代表R,G,B,A的值,我们可以根据需要进行修改,如将图片透明度减半,就将每个像素的第四个值除以2再取个整就行了。但是这里必须注意,Uint8ClampedArray对象虽然可以直接通过imgData.data.buffer取得ArrayBuffer对象,却不可以用它去写图片文件,因为这个对象存储的只是图片的描述性信息,并不是真正的图片格式数据。这里要获取图片文件应该这么来:
{
// 略......
img.onload = () => {
// 略......
ctx.putImageData(imgData, 0, 0);
// 直接获得base64编码的图片
const dataUrl = canvas.toDataURL();
// 或者直接获得文件
canvas.toBlob(blob => {
blob.arrayBuffer().then(buffer => /* process the ArrayBuffer */);
})
}
// 示意,实际上可能会有跨域问题
img.src = 'https://iph.href.lu/879x200';
}
以上两种方法都可以指定图片格式,在支持有损压缩的图片格式时还可以指定压缩率,如:
{
const dataUrl = canvas.toDataURL('image/jpeg', 0.5);
// 或者
canvas.toBlob(blob => {
blob.arrayBuffer().then(buffer => /* process the ArrayBuffer */);
}, 'image/jpeg', 0.5)
}
总结
关于文件这块没有覆盖到的还有很多,但是以我的观点,把ArrayBuffer作为核心来进行思考总可以找到突破口。