自底向上的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
的大小會產生錯誤
-
當建立的Buffer長度超過
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:
- 在讀寫時不用考慮平臺位元組序問題。
可以利用這個函式來進行判斷:
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
-
屬性(靜態)
-
屬性(例項)
-
方法(例項)
- 方法是在是太多了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
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物件用於描述檔案,這個物件雖然可以利用建構函式自行建立,但是大多數情況下都是利用瀏覽器上的<input>
元素或者拖拽API來獲取的.
File物件繼承Blob物件,所以繼承了Blob物件上的原型方法和屬性,和Blob純粹表示檔案不同,File更加接地氣一點,他還擁有了我們作業系統上常見的一些特徵:
例子:
// 建立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和前面的所提到的內容不同的地方在於,這個API有事件,你可以使用onXXX
和addEventListener
進行監聽.
基本工作流程:
-
獲取使用者提供的檔案物件(通過input或者拖拽)
- 或者自己建立File或者(Blob)物件
- 新建一個FileReader()例項
- 監聽對應的方法來獲取讀取內容完成後的回撥
-
利用不同的方法讀取檔案內容
fileReader.ArrayBuffer() fileReader.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
型別的內容會彈出下載對話方塊的特性.
我們有如下對策:
application/octet-stream URL.createObjectURL()
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上幾篇不錯的指引
分別是: