自底向上的web数据操作指南
簡(jiǎn)介
本篇文章主要探討JavaScript中的數(shù)據(jù)操作.
JavaScript一直以來(lái)給人一種比較低能的感覺,例如無(wú)法讀取系統(tǒng)上的文件,不能做一些底層的操作.
所以在頁(yè)面上操作數(shù)據(jù)會(huì)交由服務(wù)器處理也就成了主流的做法.
但是很多人沒有發(fā)現(xiàn),實(shí)際上JavaScript以及在逐步增強(qiáng)這些功能,現(xiàn)在我們就已經(jīng)可以放心的在web端進(jìn)行文件操作了.
起因
N個(gè)月前我去新浪面試實(shí)習(xí),我提到了原來(lái)我做過(guò)一個(gè)頁(yè)面配合上傳Excel可以完成一些功能.
我的這番話勾起了面試官在實(shí)際編碼中遇到了一些問題,就是如何不通過(guò)服務(wù)器來(lái)操作數(shù)據(jù),我和她討論了一番,最后不了了之了(當(dāng)然也沒過(guò)).
N個(gè)月后實(shí)習(xí)被坑,成了無(wú)業(yè)游民閑來(lái)無(wú)事正好也好奇這個(gè)問題然后就研究了一下.
涉及的內(nèi)容
沒有非必要的內(nèi)容,對(duì)于文件操作來(lái)說(shuō)以下API都是必須了解的,本文也會(huì)漸進(jìn)式的討論這些內(nèi)容.
- Blob
- ArrayBuffer
- TypedArray
- DataView
- FileReader
- File
- URL
兼容性
我沒有詳細(xì)考證API的兼容性,不過(guò)從MDN提供的數(shù)據(jù)來(lái)看IE10以上的瀏覽器大部分都是兼容的.
總覽
一般來(lái)說(shuō)操作一個(gè)文件都要經(jīng)歷如下的步驟:
- 知道文件的地址(存放的位置)
- 讀取
- 保存到Buffer中,重復(fù)上步驟直至結(jié)束
- 進(jìn)行數(shù)據(jù)編輯
- 知道要寫入的地址
- 獲取要寫入的數(shù)據(jù),從Buffer中獲取還是所有數(shù)據(jù)
- 寫入
- 寫入完成
API名稱以及對(duì)應(yīng)的職責(zé):
| URL | 制造文件地址 |
| FileReader | 讀取文件的接口 |
| Blob | 用于在JavaScript表示文件 |
| File | 用于表示文件對(duì)象 |
| ArrayBuffer | 表示Buffer(僅僅提供一片內(nèi)存空間) |
| TypedArray | 基于數(shù)組操作Buffer上的數(shù)據(jù)(操作的最小單位是數(shù)組元素) |
| DataView | 基于字節(jié)操作Buffer上的數(shù)據(jù) |
上面描述的內(nèi)容之間的關(guān)系很復(fù)雜,這里我們逐步來(lái)進(jìn)行分析.
ArrayBuffer
https://developer.mozilla.org...ArrayBuffer對(duì)象用于表示一段緩沖區(qū)域(可以理解為一段可控的內(nèi)存區(qū)域),它僅僅表示這片被開辟的區(qū)域但是不提供操作方式.
const arraybuffer = new ArrayBuffer(8) // 創(chuàng)建一個(gè)長(zhǎng)度為8字節(jié)大小的Buffer默認(rèn)ArrayBuffer中每一個(gè)字節(jié)都被填充了0.
利用這個(gè)對(duì)象我們可以完成如下的操作:
-
獲取
- 該Buffer的大小(字節(jié))
- 該Buffer的副本(范圍)
-
修改
- 該Buffer的大小
-
判斷
- 給定的數(shù)據(jù)是否是操作視圖(實(shí)例方法)
-
異常
- 當(dāng)創(chuàng)建的Buffer長(zhǎng)度超過(guò)Number.MAX_SAFE_INTEGER的大小會(huì)產(chǎn)生錯(cuò)誤
DataView
https://developer.mozilla.org...DataView用于操作ArrayBuffer中的數(shù)據(jù),這也是它構(gòu)造函數(shù)中接受一個(gè)ArrayBuffer的原因:
const arraybuffer = new ArrayBuffer(8); const dataview = new DataView(arraybuffer); // 默認(rèn)的視圖大小就是buffer的大小 const offset = new DataView(arraybuffer, 0, arraybuffer.byteLength); // 默認(rèn)的偏移量以及長(zhǎng)度利用這個(gè)對(duì)象我們可以完成如下的操作:
-
獲取
- 被該視圖引入的Buffer(只讀)
- 該視圖從Buffer中讀取的自己長(zhǎng)度(只讀)
- 該視圖從Buffer中讀取的偏移量(只讀)
-
異常
- 如果由偏移(byteOffset)和字節(jié)長(zhǎng)度(byteLength)計(jì)算得到的結(jié)束位置超出了 buffer 的長(zhǎng)度.
-
寫入
- 使用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 |
簡(jiǎn)單實(shí)例:
const arraybuffer = new ArrayBuffer(1); // 一個(gè)字節(jié)const dataview = new DataView(arraybuffer); // 默認(rèn)的視圖大小就是buffer的大小dataview.setInt8(0,127) // 從0開始寫入一個(gè)int8(8位無(wú)符號(hào)整形,一個(gè)字節(jié))dataview.getInt8(0) // 從偏移0開始讀取一個(gè)int8(8位無(wú)符號(hào)整形,一個(gè)字節(jié))console.log(dataview.getInt8(0));dataview.setInt16(0,65535); // 錯(cuò)誤超出了ArrayBuffer的空間 int16占兩個(gè)字節(jié)字節(jié)序
簡(jiǎn)單來(lái)講-使用DataView:
- 在讀寫時(shí)不用考慮平臺(tái)字節(jié)序問題。
https://zh.wikipedia.org/wiki...
可以利用這個(gè)函數(shù)來(lái)進(jìn)行判斷:
var littleEndian = (function() {var buffer = new ArrayBuffer(2);new DataView(buffer).setInt16(0, 256, true /* 設(shè)置值時(shí)使用小端字節(jié)序 */);// Int16Array 使用系統(tǒng)字節(jié)序,由此可以判斷系統(tǒng)是否是小端字節(jié)序return new Int16Array(buffer)[0] === 256; })(); console.log(littleEndian); // true or falseTypedArray
https://developer.mozilla.org...在上面一節(jié)中我們使用get和set的方式基于數(shù)據(jù)類型來(lái)讀寫內(nèi)存(ArrayBuffer)中的數(shù)據(jù).
而所謂的TypedArray就是使用類似于操作數(shù)組的方式來(lái)操作我們的Buffer可以理解為數(shù)組中的每一個(gè)元素都是不同類型的數(shù)據(jù),這樣一來(lái)我們可以使用數(shù)組上的很多方法,相較于干巴巴的使用get和set更加靈活一些,少掉點(diǎn)頭發(fā).
名字叫做TypedArray的這個(gè)對(duì)象或者全局構(gòu)造函數(shù)并不存在于JavaScript中.因?yàn)轭愋蛿?shù)組并不只有一個(gè),但是TypedArray代指的這些內(nèi)容擁有統(tǒng)一的構(gòu)造函數(shù),統(tǒng)一的屬性統(tǒng)一的方法,不同的只是他們的名字以及所對(duì)應(yīng)的數(shù)據(jù)類型.
TypedArray()指的是以下的其中之一: Int8Array(); Uint8Array(); Uint8ClampedArray(); Int16Array(); Uint16Array(); Int32Array(); Uint32Array(); Float32Array(); Float64Array();看到這里我們立馬聯(lián)想到了之前DataView上不同的Get和Set,概念是一樣的,不同于ArrayBuffer的是,這里的最小數(shù)據(jù)單位是數(shù)組中的元素,不同類型元素所占用的空間是不同的,但是我們不需要考慮在字節(jié)層面上進(jìn)行控制.
接下來(lái)我們利用Int8Array來(lái)進(jìn)行討論:
-
構(gòu)造函數(shù)
- 傳入一個(gè)數(shù)值來(lái)表示類型數(shù)組中元素的數(shù)量
- 傳入任意一個(gè)類型數(shù)組在保留其原有的長(zhǎng)度上進(jìn)行數(shù)據(jù)類型轉(zhuǎn)換
-
方法(靜態(tài))
- Int8Array.from()通過(guò)可迭代對(duì)象創(chuàng)建一個(gè)類型數(shù)組
- Int8Array.of()通過(guò)可變參數(shù)創(chuàng)建一個(gè)類型數(shù)組
例子:
// 32無(wú)符號(hào)能表示的最大的數(shù)值 占4個(gè)字節(jié) const int32 = new Int32Array(1); // 使用length int32[0] = 4294967295;// 8位無(wú)符號(hào)能表示最大的內(nèi)容是127 占1個(gè)字節(jié) const int8 = new Int8Array(int32); // 使用另外一個(gè)類型數(shù)組 console.log(int8[0]) // -1 32位轉(zhuǎn)8位要確保,32位的值在8位的范圍內(nèi)否則無(wú)法保證精度const from = Int8Array.from([0,127]); console.log(from.length === 2) // trueconst of = Int8Array.of(0,127); console.log(of.length === 2)// true-
屬性(靜態(tài))
- TypedArray.BYTES_PER_ELEMENT
- TypedArray.length
- TypedArray.name
- get TypedArray[@@species\]
- TypedArray.prototype
-
屬性(實(shí)例)
- TypedArray.prototype.buffer
- TypedArray.prototype.byteLength
- TypedArray.prototype.byteOffset
- TypedArray.prototype.length
-
方法(實(shí)例)
- 方法是在是太多了Array上的方法TypedArray基本都有,例舉太多都是照搬MDN,給個(gè)貼上大家自行查閱吧.
- 方法列表
例子(類數(shù)組操作):
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 類數(shù)組而不是真的數(shù)組Blob
https://developer.mozilla.org...Blob` 對(duì)象表示一個(gè)不可變、原始數(shù)據(jù)的類文件對(duì)象。Blob 表示的不一定是JavaScript原生格式的數(shù)據(jù)
這說(shuō)明了什么意思,類似于ArrayBuffer一樣,ArrayBuffer本身沒有為了達(dá)到某種目的而提供具體的操作方法,他的存在就類似于一個(gè)占位符一樣,Blob對(duì)象也是類似的概念,在JavaScript中我們使用Blob對(duì)象來(lái)表示一個(gè)文件,當(dāng)這個(gè)文件需要進(jìn)行操作的時(shí)候我們?cè)诶闷渌緩綄?duì)這個(gè)Blob對(duì)象進(jìn)行操作.(個(gè)人理解)
Blob的API和ArrayBuffer非常相似,因?yàn)樗麄冇兄浅C芮械穆?lián)系,創(chuàng)建Blob對(duì)象有兩種方式,對(duì)應(yīng)著兩種具體的需求:
- 直接調(diào)用構(gòu)造函數(shù)傳入JavaScript中的數(shù)據(jù)結(jié)構(gòu)
- 使用File對(duì)象創(chuàng)建,用于表示文件
這里我們不討論由File對(duì)象創(chuàng)建的情況,這部分留到下節(jié)中討論.
-
構(gòu)造函數(shù)
- 你可以利用現(xiàn)有的JavaScript數(shù)據(jù)結(jié)構(gòu)來(lái)創(chuàng)建一個(gè)Blob對(duì)象
- 你可以選擇這個(gè)Blob對(duì)象的MIME類型
- 你可以控制這個(gè)Blob對(duì)象中的換行符在系統(tǒng)中表現(xiàn)的行為
- 具體參考
-
屬性(實(shí)例)
- size - Blob對(duì)象所包含的數(shù)據(jù)大小
- type - Blob對(duì)象所描述的MIME類型
-
方法(實(shí)例)
- slice()類似于ArrayBuffer.slice()從原有的Blob中分離出一部分組成新的Blob對(duì)象
例子:
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對(duì)象接受的第一個(gè)參數(shù)是一個(gè)數(shù)組.
Blob對(duì)象還可以根據(jù)其他數(shù)據(jù)結(jié)構(gòu)進(jìn)行創(chuàng)建:
- ArrayBuffer
- ArrayBufferView(TypedArray)
- Blob
乍一看Blob對(duì)象看似很雞肋,不過(guò)在JavaScript中能裝載數(shù)據(jù)還可以指定MIME類型,這種情況多半都是用于和外部進(jìn)行交互.
回顧前面的內(nèi)容,我們知道了如何創(chuàng)建一片內(nèi)存中的區(qū)域,還知道了如何利用不同的工具來(lái)對(duì)這篇內(nèi)存進(jìn)行操作,最重要的一個(gè)用于描述文件Blob對(duì)象接受ArrayBuffer和TypedArray,那么還能玩出什么花樣呢?
File
文件(File)接口提供有關(guān)文件的信息,并允許網(wǎng)頁(yè)中的 JavaScript 訪問其內(nèi)容。https://developer.mozilla.org...
File對(duì)象用于描述文件,這個(gè)對(duì)象雖然可以利用構(gòu)造函數(shù)自行創(chuàng)建,但是大多數(shù)情況下都是利用瀏覽器上的<input>元素或者拖拽API來(lái)獲取的.
File對(duì)象繼承Blob對(duì)象,所以繼承了Blob對(duì)象上的原型方法和屬性,和Blob純粹表示文件不同,File更加接地氣一點(diǎn),他還擁有了我們操作系統(tǒng)上常見的一些特征:
-
屬性(實(shí)例)
- lastModified 最后修改時(shí)間
- name 文件名稱
- size 文件大小
- type MIME類型
- 詳細(xì)介紹
-
構(gòu)造函數(shù)
- 詳細(xì)介紹
例子:
// 創(chuàng)建bufferconst buffer = new Int8Array(2);console.log(buffer.byteLength); // 2buffer[0] = 0;buffer[1] = 127console.log(buffer[0]); // 127// 利用buffer創(chuàng)建一個(gè)file對(duì)象const file = new File([buffer],'text.txt',{type:'text/plain',lastModified:Date.now()});// file繼承blob所以可以使用slice方法,返回一個(gè)blob對(duì)象const blob = file.slice(1,2,'text/plain');console.log(blob.size); //1File對(duì)象目前看來(lái)依然扮演者'載體'的角色,不過(guò)在將他交由其他的API的時(shí)候才是他真正發(fā)揮威力的地方.
FileReader
FileReader一看名字我就有一種想喊JavaScript(瀏覽器端)永不為奴的沖動(dòng).前面鋪墊了那么多終于可以看到真正可以實(shí)際利用的內(nèi)容了.
FileReader 對(duì)象允許Web應(yīng)用程序異步讀取存儲(chǔ)在用戶計(jì)算機(jī)上的文件(或原始數(shù)據(jù)緩沖區(qū))的內(nèi)容,使用 File 或 Blob 對(duì)象指定要讀取的文件或數(shù)據(jù)。https://developer.mozilla.org...
FileReader和前面的所提到的內(nèi)容不同的地方在于,這個(gè)API有事件,你可以使用onXXX和addEventListener進(jìn)行監(jiān)聽.
基本工作流程:
獲取用戶提供的文件對(duì)象(通過(guò)input或者拖拽)
利用不同的方法讀取文件內(nèi)容
示例1讀取計(jì)算機(jī)上的文件:
<!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><!-- 建議選中一個(gè)文本 --><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('沒有選擇任何內(nèi)容');}const file = files[0];console.log(file instanceof File); // trueconsole.log(file instanceof Blob); // trueconst reader = new FileReader();reader.addEventListener('abort',()=>console.log('讀取中斷時(shí)候觸發(fā)'));reader.addEventListener('error',()=>console.log('讀取錯(cuò)誤時(shí)候觸發(fā)'));reader.addEventListener('loadstart',()=>console.log('開始讀取的時(shí)候觸發(fā)'));reader.addEventListener('loadend',()=>console.log('讀取結(jié)束觸發(fā)'));reader.addEventListener('progress',()=>console.log('讀取過(guò)程中觸發(fā)'));// 當(dāng)內(nèi)容讀取完成后再load事件觸發(fā)reader.addEventListener('load',(event)=>{// 輸出文本文件的內(nèi)容console.log(event.target.result)});// 讀取一個(gè)文本文件reader.readAsText(file);});</script> </body></html>如果一切順利,你就可以從計(jì)算機(jī)上讀取一個(gè)文件,并且以文本的形式展現(xiàn)在了控制臺(tái)中.
而且不僅如此,利用:
reader.readAsArrayBuffer(file)我們可以讀取任何類型的數(shù)據(jù),然后再內(nèi)存中進(jìn)行修改,剩下的就差保存了.
FileReaderSync
這個(gè)API是FileReader的同步版本,這意味著代碼執(zhí)行到讀取的時(shí)候會(huì)等待文件的讀取,所以這個(gè)API只能在workers里面使用,如果在主線程中調(diào)用它會(huì)阻塞用戶界面的執(zhí)行.
由于是同步讀取,所以沒有回調(diào)掉必要存在,也就不需要監(jiān)聽事件了.
https://developer.mozilla.org...URL
前面我們討論完成了數(shù)據(jù)的讀取,在FileReader中我們已經(jīng)可以獲取ArrayBuffer然后使用DateView和TypedArray就可以修改ArrayBuffer完成文件的修改,接下來(lái)我們旅行中的最后一程.
https://developer.mozilla.org...在JavaScript(瀏覽器端)中我們可以使用URL來(lái)創(chuàng)建一個(gè)URL對(duì)象:
new URL('https://www.xxx.com?q=10')他返回的對(duì)象包含如下的內(nèi)容:
// 控制臺(tái) 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: ""可見該對(duì)象是一個(gè)工具對(duì)象用于幫助我們更加容易的處理URL.
例子(來(lái)自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'實(shí)際上這和Node中的URL對(duì)象十分相似:
// 終端 > 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: '' }它和我們討論的文件下載有什么關(guān)系呢,在我們?cè)跒g覽器中一切可以利用的資源都有唯一的標(biāo)識(shí)符那就是URL.
而我們自定義或者讀取的文件需要通過(guò)URL對(duì)象創(chuàng)建一個(gè)指向我們定義資源的鏈接.
那么URL對(duì)象上提供了兩個(gè)靜態(tài)方法:
- URL.createObjectURL() 創(chuàng)建根據(jù)URL或者Blob創(chuàng)建一個(gè)URL
- URL.revokeObjectURL() 銷毀之前已經(jīng)創(chuàng)建的URL實(shí)例
那么生成的這個(gè)URL,可以被用在任何使用URL的地方,在這個(gè)例子中我們讀取一個(gè)圖片,然后將它賦值給img標(biāo)簽的src屬性,這會(huì)在你的瀏覽器中打開一張圖片.
<!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('沒有選擇任何內(nèi)容');}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導(dǎo)出文件實(shí)踐
主要是利用瀏覽器在解析到MIME為application/octet-stream類型的內(nèi)容會(huì)彈出下載對(duì)話框的特性.
我們有如下對(duì)策:
上面這種方式簡(jiǎn)單粗,不過(guò)導(dǎo)出的文件你得修改文件名稱.
我們只需要稍稍利用利用a標(biāo)簽就可以優(yōu)雅的完成這項(xiàng)任務(wù):
constbuffer = 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上操作數(shù)據(jù)啦!
MDN上幾篇不錯(cuò)的指引
分別是:
- 在web應(yīng)用程序中操作文件指南
- JavaScript 類數(shù)組對(duì)象
- Base64的編碼與解碼
參考
https://github.com/SheetJS/js...https://github.com/eligrey/Fi...
總結(jié)
以上是生活随笔為你收集整理的自底向上的web数据操作指南的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 讲讲我和Spring创始级程序员共同re
- 下一篇: Nginx之windows下搭建