乐趣区

自底向上的web数据操作指南

简介
本篇文章主要探讨 JavaScript 中的数据操作.
JavaScript 一直以来给人一种比较低能的感觉, 例如无法读取系统上的文件, 不能做一些底层的操作.
所以在页面上操作数据会交由服务器处理也就成了主流的做法.
但是很多人没有发现, 实际上 JavaScript 以及在逐步增强这些功能, 现在我们就已经可以放心的在 web 端进行文件操作了.
起因
N 个月前我去新浪面试实习, 我提到了原来我做过一个页面配合上传 Excel 可以完成一些功能.
我的这番话勾起了面试官在实际编码中遇到了一些问题, 就是如何不通过服务器来操作数据, 我和她讨论了一番, 最后不了了之了(当然也没过).
N 个月后实习被坑, 成了无业游民闲来无事正好也好奇这个问题然后就研究了一下.
涉及的内容
没有非必要的内容, 对于文件操作来说以下 API 都是必须了解的, 本文也会渐进式的讨论这些内容.

Blob
ArrayBuffer
TypedArray
DataView
FileReader
File
URL

兼容性
我没有详细考证 API 的兼容性, 不过从 MDN 提供的数据来看 IE10 以上的浏览器大部分都是兼容的.
总览
一般来说操作一个文件都要经历如下的步骤:

知道文件的地址(存放的位置)
读取
保存到 Buffer 中, 重复上步骤直至结束
进行数据编辑
知道要写入的地址
获取要写入的数据, 从 Buffer 中获取还是所有数据
写入
写入完成

API 名称以及对应的职责:

名称
职责

URL
制造文件地址

FileReader
读取文件的接口

Blob
用于在 JavaScript 表示文件

File
用于表示文件对象

ArrayBuffer
表示 Buffer(仅仅提供一片内存空间)

TypedArray
基于数组操作 Buffer 上的数据(操作的最小单位是数组元素)

DataView
基于字节操作 Buffer 上的数据

上面描述的内容之间的关系很复杂, 这里我们逐步来进行分析.
ArrayBuffer
https://developer.mozilla.org…
ArrayBuffer 对象用于表示一段缓冲区域(可以理解为一段可控的内存区域), 它仅仅表示这片被开辟的区域但是不提供操作方式.
const arraybuffer = new ArrayBuffer(8) // 创建一个长度为 8 字节大小的 Buffer
默认 ArrayBuffer 中每一个字节都被填充了 0.
利用这个对象我们可以完成如下的操作:

获取

该 Buffer 的大小(字节)
该 Buffer 的副本(范围)

修改
该 Buffer 的大小

判断
给定的数据是否是操作视图(实例方法)

异常
当创建的 Buffer 长度超过 Number.MAX_SAFE_INTEGER 的大小会产生错误

const arraybuffer = new ArrayBuffer(8);

console.log(arraybuffer.byteLength); // 获取长度
console.log(arraybuffer.slice(4,8)); // 获取副本
// 截止到 2019 年 2 月 12 日 20:11:05 没有浏览器实现该功能
console.log(arraybuffer.transfer(arraybuffer,16));// 修改原有 Buffer
console.log(ArrayBuffer.isView({})) // false 是否是视图

DataView
https://developer.mozilla.org…
DataView 用于操作 ArrayBuffer 中的数据, 这也是它构造函数中接受一个 ArrayBuffer 的原因:
const arraybuffer = new ArrayBuffer(8);
const dataview = new DataView(arraybuffer); // 默认的视图大小就是 buffer 的大小
const offset = new DataView(arraybuffer, 0, arraybuffer.byteLength); // 默认的偏移量以及长度
利用这个对象我们可以完成如下的操作:

获取

被该视图引入的 Buffer(只读)
该视图从 Buffer 中读取的自己长度(只读)
该视图从 Buffer 中读取的偏移量(只读)

异常
如果由偏移(byteOffset)和字节长度(byteLength)计算得到的结束位置超出了 buffer 的长度.

写入
使用 xxx 类型写入(见下方)

读取
使用 xxx 类型读取

可以使用的类型:

类型名称
对应的方法

Int8
getInt8,setInt8

Uint8
getUint8,setUint8

Int16
getInt16,setInt16

Uint16
getUint16,setUint16

Int32
getInt32,setInt32

Uint32
getUint32,setUint32

Float32
getFloat32,setFloat32

Float64
getFloat64,setFloat64

简单实例:
const arraybuffer = new ArrayBuffer(1); // 一个字节
const dataview = new DataView(arraybuffer); // 默认的视图大小就是 buffer 的大小

dataview.setInt8(0,127) // 从 0 开始写入一个 int8(8 位无符号整形, 一个字节)
dataview.getInt8(0) // 从偏移 0 开始读取一个 int8(8 位无符号整形, 一个字节)
console.log(dataview.getInt8(0));
dataview.setInt16(0,65535); // 错误超出了 ArrayBuffer 的空间 int16 占两个字节
字节序
简单来讲 - 使用 DataView:
在读写时不用考虑平台字节序问题。

https://developer.mozilla.org…https://zh.wikipedia.org/wiki…

可以利用这个函数来进行判断:
var littleEndian = (function() {
var buffer = new ArrayBuffer(2);
new DataView(buffer).setInt16(0, 256, true /* 设置值时使用小端字节序 */);
// Int16Array 使用系统字节序,由此可以判断系统是否是小端字节序
return new Int16Array(buffer)[0] === 256;
})();
console.log(littleEndian); // true or false
TypedArray
https://developer.mozilla.org…
在上面一节中我们使用 get 和 set 的方式基于数据类型来读写内存 (ArrayBuffer) 中的数据.
而所谓的 TypedArray 就是使用类似于操作数组的方式来操作我们的 Buffer 可以理解为数组中的每一个元素都是不同类型的数据, 这样一来我们可以使用数组上的很多方法, 相较于干巴巴的使用 get 和 set 更加灵活一些, 少掉点头发.
名字叫做 TypedArray 的这个对象或者全局构造函数并不存在于 JavaScript 中. 因为类型数组并不只有一个, 但是 TypedArray 代指的这些内容拥有统一的构造函数, 统一的属性统一的方法, 不同的只是他们的名字以及所对应的数据类型.
TypedArray()指的是以下的其中之一:

Int8Array();
Uint8Array();
Uint8ClampedArray();
Int16Array();
Uint16Array();
Int32Array();
Uint32Array();
Float32Array();
Float64Array();
看到这里我们立马联想到了之前 DataView 上不同的 Get 和 Set, 概念是一样的, 不同于 ArrayBuffer 的是, 这里的最小数据单位是数组中的元素, 不同类型元素所占用的空间是不同的, 但是我们不需要考虑在字节层面上进行控制.
接下来我们利用 Int8Array 来进行讨论:

构造函数

传入一个数值来表示类型数组中元素的数量
传入任意一个类型数组在保留其原有的长度上进行数据类型转换

方法(静态)

Int8Array.from()通过可迭代对象创建一个类型数组
Int8Array.of()通过可变参数创建一个类型数组

例子:
// 32 无符号能表示的最大的数值 占 4 个字节
const int32 = new Int32Array(1); // 使用 length
int32[0] = 4294967295;

// 8 位无符号能表示最大的内容是 127 占 1 个字节
const int8 = new Int8Array(int32); // 使用另外一个类型数组
console.log(int8[0]) // -1 32 位转 8 位要确保,32 位的值在 8 位的范围内否则无法保证精度

const from = Int8Array.from([0,127]);
console.log(from.length === 2) // true

const of = Int8Array.of(0,127);
console.log(of.length === 2)// true

属性(静态)

TypedArray.BYTES_PER_ELEMENT

TypedArray.length
TypedArray.name
get TypedArray[@@species\]
TypedArray.prototype

属性(实例)

TypedArray.prototype.buffer
TypedArray.prototype.byteLength
TypedArray.prototype.byteOffset
TypedArray.prototype.length

方法(实例)

方法是在是太多了 Array 上的方法 TypedArray 基本都有, 例举太多都是照搬 MDN, 给个贴上大家自行查阅吧.
方法列表

例子(类数组操作):
const int8 = new Int8Array(2);
int8[0] = 0;
int8[1] = 127;

int8.forEach((value)=>console.log(value));

for (const elem of int8) {
console.log(elem);
}

Array.isArray(int8) // false 类数组而不是真的数组
Blob

https://developer.mozilla.org…Blob` 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是 JavaScript 原生格式的数据

这说明了什么意思, 类似于 ArrayBuffer 一样,ArrayBuffer 本身没有为了达到某种目的而提供具体的操作方法, 他的存在就类似于一个占位符一样,Blob 对象也是类似的概念, 在 JavaScript 中我们使用 Blob 对象来表示一个文件, 当这个文件需要进行操作的时候我们在利用其他途径对这个 Blob 对象进行操作.(个人理解)
Blob 的 API 和 ArrayBuffer 非常相似, 因为他们有着非常密切的联系, 创建 Blob 对象有两种方式, 对应着两种具体的需求:

直接调用构造函数传入 JavaScript 中的数据结构
使用 File 对象创建, 用于表示文件

这里我们不讨论由 File 对象创建的情况, 这部分留到下节中讨论.

构造函数

你可以利用现有的 JavaScript 数据结构来创建一个 Blob 对象
你可以选择这个 Blob 对象的 MIME 类型
你可以控制这个 Blob 对象中的换行符在系统中表现的行为
具体参考

属性(实例)

size – Blob 对象所包含的数据大小
type – Blob 对象所描述的 MIME 类型

方法(实例)
slice()类似于 ArrayBuffer.slice()从原有的 Blob 中分离出一部分组成新的 Blob 对象

例子:
const blob1 = new Blob([JSON.stringify({
content: ‘success’
})], {
type: ‘application/json’
});

const blob2 = new Blob([‘<a id=”a”><b id=”b”>hey!</b></a>’],{
type:’text/html’
});
注意:Blob 对象接受的第一个参数是一个数组.
Blob 对象还可以根据其他数据结构进行创建:

ArrayBuffer
ArrayBufferView(TypedArray)
Blob

https://developer.mozilla.org…
乍一看 Blob 对象看似很鸡肋, 不过在 JavaScript 中能装载数据还可以指定 MIME 类型, 这种情况多半都是用于和外部进行交互.
回顾前面的内容, 我们知道了如何创建一片内存中的区域, 还知道了如何利用不同的工具来对这篇内存进行操作, 最重要的一个用于描述文件 Blob 对象接受 ArrayBuffer 和 TypedArray, 那么还能玩出什么花样呢?
File
文件(File)接口提供有关文件的信息,并允许网页中的 JavaScript 访问其内容。https://developer.mozilla.org…

File 对象用于描述文件, 这个对象虽然可以利用构造函数自行创建, 但是大多数情况下都是利用浏览器上的 <input> 元素或者拖拽 API 来获取的.
File 对象继承 Blob 对象, 所以继承了 Blob 对象上的原型方法和属性, 和 Blob 纯粹表示文件不同,File 更加接地气一点, 他还拥有了我们操作系统上常见的一些特征:

属性(实例)

lastModified 最后修改时间
name 文件名称
size 文件大小
type MIME 类型
详细介绍

构造函数
详细介绍

例子:
// 创建 buffer
const buffer = new Int8Array(2);
console.log(buffer.byteLength); // 2
buffer[0] = 0;
buffer[1] = 127
console.log(buffer[0]); // 127
// 利用 buffer 创建一个 file 对象
const file = new File([buffer],’text.txt’,{
type:’text/plain’,
lastModified:Date.now()
});

// file 继承 blob 所以可以使用 slice 方法, 返回一个 blob 对象
const blob = file.slice(1,2,’text/plain’);
console.log(blob.size); //1
File 对象目前看来依然扮演者 ’ 载体 ’ 的角色, 不过在将他交由其他的 API 的时候才是他真正发挥威力的地方.
FileReader
FileReader 一看名字我就有一种想喊 JavaScript(浏览器端)永不为奴的冲动. 前面铺垫了那么多终于可以看到真正可以实际利用的内容了.

FileReader 对象允许 Web 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。https://developer.mozilla.org…

FileReader 和前面的所提到的内容不同的地方在于, 这个 API 有事件, 你可以使用 onXXX 和 addEventListener 进行监听.
基本工作流程:

获取用户提供的文件对象(通过 input 或者拖拽)
或者自己创建 File 或者 (Blob) 对象

新建一个 FileReader()实例
监听对应的方法来获取读取内容完成后的回调

利用不同的方法读取文件内容

读取为 fileReader.ArrayBuffer()

读取为 DataURLfileReader.readAsDataURL()

读取为字符串 fileReader.readAsText()

示例 1 读取计算机上的文件:
<!DOCTYPE html>
<html>

<head>
<meta charset=”utf-8″>
<meta http-equiv=”X-UA-Compatible” content=”IE=edge”>
<title>blob</title>
<meta name=”viewport” content=”width=device-width, initial-scale=1″>
</head>

<body>
<!– 建议选中一个文本 –>
<label for=”file”> 读取文件 <input id=”file” type=”file” ></label>
<script type=”text/javascript”>
document.getElementById(‘file’).addEventListener(‘change’,(event)=>{

const files = event.srcElement.files;

if(files.length === 0){
return console.log(‘ 没有选择任何内容 ’);
}

const file = files[0];

console.log(file instanceof File); // true
console.log(file instanceof Blob); // true

const reader = new FileReader();

reader.addEventListener(‘abort’,()=>console.log(‘ 读取中断时候触发 ’));
reader.addEventListener(‘error’,()=>console.log(‘ 读取错误时候触发 ’));
reader.addEventListener(‘loadstart’,()=>console.log(‘ 开始读取的时候触发 ’));
reader.addEventListener(‘loadend’,()=>console.log(‘ 读取结束触发 ’));
reader.addEventListener(‘progress’,()=>console.log(‘ 读取过程中触发 ’));

// 当内容读取完成后再 load 事件触发
reader.addEventListener(‘load’,(event)=>{

// 输出文本文件的内容
console.log(event.target.result)

});
// 读取一个文本文件
reader.readAsText(file);

});
</script>
</body>

</html>
如果一切顺利, 你就可以从计算机上读取一个文件, 并且以文本的形式展现在了控制台中.
而且不仅如此, 利用:
reader.readAsArrayBuffer(file)
我们可以读取任何类型的数据, 然后再内存中进行修改, 剩下的就差保存了.
FileReaderSync
这个 API 是 FileReader 的同步版本, 这意味着代码执行到读取的时候会等待文件的读取, 所以这个 API 只能在 workers 里面使用, 如果在主线程中调用它会阻塞用户界面的执行.
由于是同步读取, 所以没有回调掉必要存在, 也就不需要监听事件了.
https://developer.mozilla.org…
URL
前面我们讨论完成了数据的读取, 在 FileReader 中我们已经可以获取 ArrayBuffer 然后使用 DateView 和 TypedArray 就可以修改 ArrayBuffer 完成文件的修改, 接下来我们旅行中的最后一程.
https://developer.mozilla.org…
在 JavaScript(浏览器端)中我们可以使用 URL 来创建一个 URL 对象:
new URL(‘https://www.xxx.com?q=10’)
他返回的对象包含如下的内容:
// 控制台
new URL(‘https://www.xxx.com?q=10’)

URL
hash: “”
host: “www.xxx.com”
hostname: “www.xxx.com”
href: “https://www.xxx.com/?q=10”
origin: “https://www.xxx.com”
password: “”
pathname: “/”
port: “”
protocol: “https:”
search: “?q=10”
searchParams: URLSearchParams {}
username: “”
可见该对象是一个工具对象用于帮助我们更加容易的处理 URL.
例子(来自 MDN):
var a = new URL(“/”, “https://developer.mozilla.org”); // Creates a URL pointing to ‘https://developer.mozilla.org/’
var b = new URL(“https://developer.mozilla.org”); // Creates a URL pointing to ‘https://developer.mozilla.org’
var c = new URL(‘en-US/docs’, b); // Creates a URL pointing to ‘https://developer.mozilla.org/en-US/docs’
var d = new URL(‘/en-US/docs’, b); // Creates a URL pointing to ‘https://developer.mozilla.org/en-US/docs’
var f = new URL(‘/en-US/docs’, d); // Creates a URL pointing to ‘https://developer.mozilla.org/en-US/docs’
var g = new URL(‘/en-US/docs’, “https://developer.mozilla.org/fr-FR/toto”);
// Creates a URL pointing to ‘https://developer.mozilla.org/en-US/docs’
var h = new URL(‘/en-US/docs’, a); // Creates a URL pointing to ‘https://developer.mozilla.org/en-US/docs’
var i = new URL(‘/en-US/docs’, ”); // Raises a SYNTAX ERROR exception as ‘/en-US/docs’ is not valid
var j = new URL(‘/en-US/docs’); // Raises a SYNTAX ERROR exception as ‘about:blank/en-US/docs’ is not valid
var k = new URL(‘http://www.example.com’, ‘https://developers.mozilla.com’);
// Creates a URL pointing to ‘https://www.example.com’
var l = new URL(‘http://www.example.com’, b); // Creates a URL pointing to ‘https://www.example.com’
实际上这和 Node 中的 URL 对象十分相似:
// 终端
> Node
> new URL(‘https://www.xxx.com/?q=10’)
URL {
href: ‘https://www.xxx.com/?q=10’,
origin: ‘https://www.xxx.com’,
protocol: ‘https:’,
username: ”,
password: ”,
host: ‘www.xxx.com’,
hostname: ‘www.xxx.com’,
port: ”,
pathname: ‘/’,
search: ‘?q=10’,
searchParams: URLSearchParams {‘q’ => ’10’},
hash: ” }
它和我们讨论的文件下载有什么关系呢, 在我们在浏览器中一切可以利用的资源都有唯一的标识符那就是 URL.
而我们自定义或者读取的文件需要通过 URL 对象创建一个指向我们定义资源的链接.
那么 URL 对象上提供了两个静态方法:

URL.createObjectURL() 创建根据 URL 或者 Blob 创建一个 URL

URL.revokeObjectURL() 销毁之前已经创建的 URL 实例

那么生成的这个 URL, 可以被用在任何使用 URL 的地方, 在这个例子中我们读取一个图片, 然后将它赋值给 img 标签的 src 属性, 这会在你的浏览器中打开一张图片.
<!DOCTYPE html>
<html>

<head>
<meta charset=”utf-8″>
<meta http-equiv=”X-UA-Compatible” content=”IE=edge”>
<title>blob</title>
<meta name=”viewport” content=”width=device-width, initial-scale=1″>
</head>

<body>
<label for=”file”> 读取文件 <input id=”file” accept=”image/*” type=”file” ></label>
<img id=”img” src=”” alt=””>
<script type=”text/javascript”>
document.getElementById(‘file’).addEventListener(‘change’,(event)=>{

const files = event.srcElement.files;

if(files.length === 0){
return console.log(‘ 没有选择任何内容 ’);
}

const file = files[0];

document.getElementById(‘img’).src = URL.createObjectURL(file);

});
</script>
</body>

</html>
我们的图片被如下格式的 URL 所描述:
blob:http://127.0.0.1:5500/b285f19f-a4e2-48e7-b8c8-5eae11751593
导出文件实践
主要是利用浏览器在解析到 MIME 为 application/octet-stream 类型的内容会弹出下载对话框的特性.
我们有如下对策:

创建一个 File 对象修改他的 type 为 application/octet-stream

使用这个 File 利用 URL.createObjectURL()创建一个 URL
重定向到这个 URL, 让浏览器自动弹出下载框

const
buffer = new ArrayBuffer(1024),
array = new Int8Array(buffer);

array.fill(1);

const
blob = new Blob(array),
file = new File([blob],’test.txt’,{
lastModified:Date.now(),
type:’application/octet-stream’
});

saveAs(file,’test.txt’)

const url = window.URL.createObjectURL(file);

window.location.href = url;
上面这种方式简单粗, 不过导出的文件你得修改文件名称.
我们只需要稍稍利用利用 a 标签就可以优雅的完成这项任务:
const
buffer = new ArrayBuffer(1024),
array = new Int8Array(buffer);

array.fill(1);

const
blob = new Blob(array),
file = new File([blob],’test.txt’,{
lastModified:Date.now(),
type:’text/plain;charset=utf-8′
});

const
url = window.URL.createObjectURL(file),
a = document.createElement(‘a’);

a.href = url;
a.download = file.name; // see https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/a#%E5%B1%9E%E6%80%A7
a.click();
大功告成, 利用 HTML5 的 API 我们终于可以愉快的在 WEB 上操作数据啦!
MDN 上几篇不错的指引
分别是:

在 web 应用程序中操作文件指南
JavaScript 类数组对象
Base64 的编码与解码

参考

https://github.com/SheetJS/js…https://github.com/eligrey/Fi…

退出移动版