自底向上的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

例子(類數組操作):

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應用程序異步讀取存儲在用戶計算機上的文件(或原始數據緩衝區)的內容,使用 FileBlob 對象指定要讀取的文件或數據。

https://developer.mozilla.org...

FileReader和前面的所提到的內容不同的地方在於,這個API有事件,你可以使用onXXXaddEventListener進行監聽.

基本工作流程:

  1. 獲取用戶提供的文件對象(通過input或者拖拽)

    1. 或者自己創建File或者(Blob)對象
  2. 新建一個FileReader()實例
  3. 監聽對應的方法來獲取讀取內容完成後的回調
  4. 利用不同的方法讀取文件內容

    1. 讀取爲fileReader.ArrayBuffer()
    2. 讀取爲DataURLfileReader.readAsDataURL()
    3. 讀取爲字符串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,可以被用在任何使用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類型的內容會彈出下載對話框的特性.

我們有如下對策:

  1. 創建一個File對象修改他的type爲application/octet-stream
  2. 使用這個File利用URL.createObjectURL()創建一個URL
  3. 重定向到這個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上幾篇不錯的指引

分別是:

參考

https://github.com/SheetJS/js...

https://github.com/eligrey/Fi...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章