【譯】客戶端存儲(Client-Side Storage)

本文轉載自:衆成翻譯
譯者:文藺
連接:http://www.zcfy.cc/article/660
原文:http://www.html5rocks.com/en/tutorials/offline/storage/javascript

介紹

本文是關於客戶端存儲(client-side storage)的。這是一個通用術語,包含幾個獨立但相關的 API: Web Storage、Web SQL Database、Indexed Database 和 File Access。每種技術都提供了在用戶硬盤上 —— 而非一般存儲數據的服務器 —— 存儲數據的獨特方式。這麼作主要基於如下兩點理由:(a)使 web app 離線可用; (b)改善性能。對於客戶端存儲使用狀況的詳細闡述,請看 HTML5Rocks 上的文章 《"離線": 這是什麼意思?我爲什麼要關心?》。html

這些 API 有着相似的做用範圍和規則。所以,在去看細節以前,咱們先了解他們的共同之處吧。html5

共同特色

基於客戶端的存儲

實際上,「客戶端時間存儲」的意思是,數據傳給了瀏覽器的存儲 API,它將數據存在本地設備中的一塊區域,該區域一樣也是它存儲其餘用戶特定信息如我的偏好、緩存的地方。除了存儲數據,這些 API 能夠用來檢索數據,且在某些狀況下還能執行搜索和批處理操做。java

置於沙盒中的

全部這四個存儲 API 都將數據綁到一個單獨的「源」(origin)上。例如,若 http://abc.example.com 保存了一些數據,那之後瀏覽器就只會容許 http://abc.example.com 獲取這些數據。當咱們談論「源」(origin)的時候,這意味着域(domain)必須徹底相同,因此 http://example.comhttp://def.example.com 都不行。端口(port)也必須匹配,所以 http://abc.example.com:123 也是不能訪問到 http://abc.example.com (端口默認爲80)存儲的數據。一樣,協議也必須同樣(像http vs https 等等)。git

空間限制(Quotas)

你能想象,若是任何網站都被容許往絕不知情的硬盤裏填充以千兆字節計的數據,該有多混亂。所以,瀏覽器對存儲容量施加了限制。若你的應用試圖超出限制,瀏覽器一般會顯示一個對話框,讓用戶確認增長。您可能覺得瀏覽器對單個源(origin)可以使用的全部存儲都加以同一單獨的限制,但多數存儲機制都是單獨加以限制的。若 Quota API 被採納,這種狀況可能會改變。但就如今來講,把瀏覽器看成一個二維矩陣,其維度分別是「源」(origin)和「存儲」(storage)。例如, "http://abc.example.com" 可能會容許最多存 5MB 的 Web Storage, 25MB 的 Web SQL 數據庫,但因用戶拒絕訪問被禁止使用 Indexed DataBase。 Quota API 將問題放到一塊兒來看,讓您查詢還有多少可用空間,有多少空間正在使用。web

有些狀況下,用戶也能先看到有多少存儲將被使用,例如,當用戶在 Chrome 應用商店中安裝一個應用時,他們將被提示預先接受其權限,其中包括存儲限制。(而該應用的)manifest 中的可能有個值是 「unlimited_storage」 (無限制存儲)。算法

數據庫處理(Transactions)

兩個 「數據庫」 的存儲格式支持數據處理。目的和一般的關係型數據庫使用數據處理是同樣的:保證數據庫完整。數據庫處理(Transactions)防止 「競爭條件」(race conditions) —— 這種狀況是:當兩個操做序列在同一時間被應用到數據庫中, 致使操做結果都沒法被預測,而數據庫也處於可疑的準確性(dubious accuracy)狀態。數據庫

同步和異步模式(Synchronous and Asynchronous Modes)

多數存儲格式都支持同步和異步模式。同步模式是阻塞的,意味着下一行 js 代碼執行以前,存儲操做會被完整執行。異步模式會使得後面的 js 代碼在數據庫操做完成以前執行。存儲操做會背景環境中執行,當操做完成的時候,應用會以回調函數被調用這種形式接收通知,這個函數須在調用的時候被指定。api

應當儘可能避免使用同步模式,它雖然看起來比較簡單,但操做完成時它會阻塞頁面渲染,在某些狀況下甚至會凍結整個瀏覽器。你可能注意到網站乃至是應用出現這種狀況,點擊一個按鈕,結果全部東西都用不了,當你還在想是否是崩潰了?結果一切又忽然恢復正常了。數組

某些 API 沒有異步模式,如 「localStorage」, 使用這些API時,應當仔細作好性能監測,並隨時準備切換到一個異步API,若是它形成了問題。

API 概述及比較

Web Storage

Web Storage 是一個叫作 localStorage 的持久對象。可使用 localStorage.foo = "bar" 保存值,以後可使用 localStorage.foo 獲取到 —— 甚至是瀏覽器關閉以後從新打開。還可使用一個叫作 sessionStorage 的對象,工做方式同樣,只是當窗口關閉以後會被清除掉。

Web Storage 是 NoSQL 鍵值對儲存(NoSQL key-value store)的一種.

Web Storage 的優勢
  1. 數年以來,被全部現代瀏覽器支持, iOS 和 Android 系統下也支持(IE 從 IE8 開始支持 )。

  2. 簡單的API簽名。

  3. 同步 API,調用簡單。

  4. 語義事件可保持其餘標籤和窗口同步。

Web Storage 的弱點
  1. 使用同步 API(這是獲得最普遍支持的模式)存儲大量的或者複雜的數據時性能差。

  2. 缺乏索引致使檢索大量的或複雜的數據時性能差。(搜索操做須要手動遍歷全部項。)

  3. 存儲或讀取大量的或複雜的數據結構時性能差,由於須要手動序序列化成字符串或將字符串反序列化。主要的瀏覽器實現只支持字符串(儘管規範沒這麼說的)。

  4. 須要保證數據的持續性和完整性,由於數據是有效非結構化(effectively unstructured)的。

Web SQL Database

Web SQL Database 是一個結構化的數據庫,具有典型 SQL驅動的關係數據庫(SQL-powered relational database)的全部功能和複雜度。Indexed Database 在二者之間。Web SQL Database 有自由形式的密鑰值對,有點像 Web Storage,但也有能力從這些值來索引字段,因此搜索速度要快得多。

Web SQL Database 的優勢
  1. 被主要的移動瀏覽器(Android Browser, Mobile Safari, Opera Mobile)以及一些 PC 瀏覽器(Chrome, Safari, Opera) 支持。

  2. 做爲異步 API, 整體而言性能很好。數據庫交互不會鎖定用戶界面。(同步API也可用於 WebWorkers。)

  3. 良好的搜索性能,由於數據能夠根據搜索鍵進行索引。

  4. 強大,由於它支持事務性數據庫模型(transactional database model)

  5. 剛性的數據結構更容易保持數據的完整性。

Web SQL Database 的弱點
  1. 過期,不會被 IE 或 Firefox 支持,在某些階段可能會被從其餘瀏覽器淘汰。

  2. 學習曲線陡峭,要求掌握關係數據庫和SQL的知識。

  3. 對象-關係阻抗失配(object-relational impedance mismatch).

  4. 下降敏捷性,由於數據庫模式必須預先定義,與表中的全部記錄必須匹配相同的結構。

Indexed Database (IndexedDB)

到目前爲止,咱們已經看到,Web Storage 和 Web SQL Database 都有各類的優點和弱點。 Indexed Database 產生於這兩個早期 API 的經驗,能夠看做是一種結合二者優勢而不招致其劣勢獲得嘗試。

Indexed Database 是一個 「對象存儲」 (object stores) 的集合,能夠直接把對象放進去。這個存儲有點像 SQL 表,但在這種狀況下,對象的結構沒有約束,因此不須要預先定義什麼。因此這和 Web Storage 有點像,擁有多個數據庫、每一個數據庫又有多個存儲(store)的特色。但不像 Web Storage那樣, 還擁有重要的性能優點: 異步接口,能夠在存儲上建立索引,以提升搜索速度。

IndexedDB 的優勢
  1. 做爲異步API整體表現良好。數據庫交互不會鎖定用戶界面。(同步 API 也可用於 WebWorkers。)

  2. 良好的搜索性能,由於數據能夠根據搜索鍵進行索引。

  3. 支持版本控制。

  4. 強大,由於它支持事務性數據庫模型(transactional database model)

  5. 由於數據模型簡單,學習曲線也至關簡單。

  6. 良好的瀏覽器支持: Chrome, Firefox, mobile FF, IE10.

IndexedDB 的弱點
  1. 很是複雜的API,致使大量的嵌套回調。

FileSystem

上面的 API 都是適用於文本和結構化數據,但涉及到大文件和二進制內容時,咱們須要一些其餘的東西。幸運的是,咱們如今有了文件系統 API 標準(FileSystem API standard)。它給每一個域一個完整的層次化的文件系統,至少在 Chrome 下面,這些都是用戶的硬盤上的真正的文件。就單個文件的讀寫而言, API 創建在現有的 File API之上。

FileSystem(文件系統) API 的有點
  1. 能夠存儲大量的內容和二進制文件,很適合圖像,音頻,視頻,PDF,等。

  2. 做爲異步 API, 性能良好。

FileSystem API 的弱點
  1. 很早的標準,只有 Chrome 和 Opera 支持。

  2. 沒有事務(transaction)支持。

  3. 沒有內建的搜索/索引支持。

來看代碼

本部分比較不一樣的 API 如何解決同一個問題。這個例子是一個 「地理情緒」(geo-mood) 簽到系統,在那裏你能夠記錄你在時間和地點的情緒。接口可以讓你在數據庫類型之間切換。固然,在現實狀況中,這可能顯得有點做(contrived),數據庫類型確定比其餘的更有意義,文件系統 API 根本不適用於這種應用!但爲了演示的目的,若是咱們能看到使用不一樣方式達到一樣的結果,這仍是有幫助的。還得注意,爲了保值可讀性,一些代碼片斷是通過重構的。

如今能夠來試試咱們的「地理情緒」(geo-mood)應用。

爲了讓 Demo 更有意思,咱們將數據存儲單獨拿出來,使用標準的面向對象的設計技術(standard object-oriented design techniques)。 UI 邏輯只知道有一個 store;它無需知道 store 是如何實現的,由於每一個 store 的方法是同樣的。所以 UI 層代碼能夠稱爲 store.setup()store.count() 等等。實際上,咱們的 store 有四種實現,每種對應一種存儲類型。應用啓動的時候,檢查 URL 並實例化對應的 store。

爲了保持 API 的一致性,全部的方法都是異步的,即它們將結果返回給調用方。Web Storage 的實現甚至也是這樣的,其底層實現是本地的。

在下面的演示中,咱們將跳過 UI 和定位邏輯,聚焦於存儲技術。

創建 Store

localStorage,咱們作個簡單的檢驗看存儲是否存在。若是不存在,則新建一個數組,並將其存儲在 localStorage 的 checkins(簽到) 鍵下面。首先,咱們使用 JSON 對象將結構序列化爲字符串,由於大多數瀏覽器只支持字符串存儲。

if  (!localStorage.checkins) localStorage.checkins = JSON.stringify([]);

Web SQL Database,數據庫結構若是不存在的話,咱們須要先建立。幸運的是,若是數據庫不存在,openDatabase 方法會自動建立數據庫;一樣,使用 SQL 句 「if not exists」 能夠確保新的 checkins 表 若是已經存在的話不會被重寫。咱們須要預先定義好數據結構,也就是, checkins 表每列的名稱和類型。每一行數據表明一次簽到。

this.db = openDatabase('geomood', '1.0', 'Geo-Mood Checkins', 8192);
this.db.transaction(function(tx) {
    tx.executeSql(
        "create table if not exists "
            + "checkins(id integer primary key asc, time integer, latitude float,"
            + "longitude float, mood string)",
         [], function() {
            console.log("siucc"); 
        }
    );
});

Indexed Database 啓動須要一些工做,由於它須要啓用一個數據庫版本系統。當咱們鏈接數據庫的時候要明確咱們須要那個版本,若是當前數據庫使用的是以前的版本或者還還沒有被建立,會觸發 onupgradeneeded 事件,當升級完成後 onsuccess 事件會被觸發。若是無需升級,onsuccess 事件立刻就會觸發。

另一件事就是建立 「mood」 索引,以便以後能很快地查詢到匹配的情緒。

var db;
var version = 1;
window.indexedStore = {};
window.indexedStore.setup = function(handler) { // attempt to open the database
    var request = indexedDB.open("geomood", version);  // upgrade/create the database if needed
    request.onupgradeneeded =  function(event)  {
        var db = request.result;
        if  (event.oldVersion <  1)  { // Version 1 is the first version of the database.
            var checkinsStore = db.createObjectStore("checkins",  { keyPath:  "time"  });
            checkinsStore.createIndex("moodIndex",  "mood",  { unique:  false  });
        }
        if  (event.oldVersion <  2)  {
            // In future versions we'd upgrade our database here. 
            // This will never run here, because we're version 1.
        }
        db = request.result;
    };
    request.onsuccess =  function(ev)  {  // assign the database for access outside
        db = request.result; handler();
        db.onerror =  function(ev)  {
            console.log("db error", arguments);
        };
    };
};

最後,啓動 FileSystem。咱們會把每種簽到 JSON 編碼後放在單獨的文件中,它們都在 「checkins/」 目錄下面。一樣這並不是 FileSystem API 最合適的用途,但對演示來講還挺好。

啓動在整個文件系統中拿到一個控制手柄(handle),用來檢查 「checkins/」 目錄。若是目錄不存在,使用 getDirectory 建立。

setup:  function(handler)  {
    requestFileSystem(
        window.PERSISTENT,
        1024*1024,
        function(fs)  {
            fs.root.getDirectory(
                "checkins",
                {},  // no "create" option, so this is a read op
                function(dir)  {
                    checkinsDir = dir;
                    handler();
                }, 
                function()  {
                    fs.root.getDirectory( "checkins",  {create:  true},  function(dir)  { checkinsDir = dir;
                        handler();
                    }, onError );
                }
            );
        },
        function(e)  {
            console.log("error "+e.code+"initialising - see http://goo.gl/YW0TI");
        }  
    );
}

保存一次簽到 (Check-in)

使用 localStorage,咱們只須要拿出 check-in 數組,在尾部添加一個,而後從新保存就行。咱們還須要使用 JSON 對象的方法將其以字符串的方式存起來。

var checkins = JSON.parse(localStorage["checkins"]);
checkins.push(checkin);
localStorage["checkins"] = JSON.stringify(checkins);

使用 Web SQL Database,全部的事情都在 transaction 中進行。咱們要在 checkins 表 建立新的一行,這是一個簡單的 SQL 調用,咱們使用 「?」 語法,而不是把全部的簽到數據都放到 「insert」 命令中,這樣更整潔,也更安全。真正的數據——咱們要保存的四個值——被放到第二行。「?」 元素會被這些值(checkin.timecheckin.latitude等等)替換掉。接下來的兩個參數是操做完成以後被調用的函數,分別在成功和失敗後調用。在這個應用中,咱們對全部操做使用相同的通用錯誤處理程序。這樣,成功回調函數就是咱們傳給搜索函數的句柄——確保句柄在成功的時候被調用,以便操做完成以後 UI 能接到通知(好比,更新目前爲止的簽到數量)。

store.db.transaction(function(tx) {
    tx.executeSql(
        "insert into checkins " + "(time, latitude, longitude, mood) values (?,?,?,?);", 
        [checkin.time, checkin.latitude, checkin.longitude, checkin.mood],
        handler, 
        store.onError
    ); 
});

一旦存儲創建起來,將其存儲到 IndexedDB 中就像 Web Storage 差很少簡單,還有異步工做的優勢。

var transaction = db.transaction("checkins",  'readwrite'); 
transaction.objectStore("checkins").put(checkin); 
transaction.oncomplete = handler;

使用 FileSystem API,新建文件並拿到相應的句柄,能夠用 FileWriter API 進行填充。

fs.root.getFile(
    "checkins/" + checkin.time,
    { create: true, exclusive: true }, 
    function(file) {
        file.createWriter(function(writer) {
            writer.onerror = fileStore.onError;
            var bb = new WebKitBlobBuilder;
            bb.append(JSON.stringify(checkin));
            writer.write(bb.getBlob("text/plain"));
            handler(); }, fileStore.onError);
    },
    fileStore.onError
);

搜索匹配項

接下來的函數找到全部匹配特定情緒的簽到,例如,用戶能看到他們在最近什麼時候何地過得很開心。使用 localStorage, 咱們必須手動遍歷每次簽到並將其與搜索的情緒對比,創建一個匹配列表。比較好的實踐是返回存儲數據的克隆,而不是實際的對象,由於搜索應該是一個只讀的操做;因此咱們將每一個匹配的簽到對象傳遞給通用的 clone() 方法進行操做。

var allCheckins = JSON.parse(localStorage["checkins"]);
var matchingCheckins = [];
allCheckins.forEach(function(checkin) {
    if (checkin.mood == moodQuery) {
        matchingCheckins.push(clone(checkin));
    } 
});
handler(matchingCheckins);

使用 Web SQL Database,咱們執行一次查詢,只返回咱們須要的行。但咱們仍須要手動遍從來累計簽到數據,由於數據庫 API 返回的是數據庫行,而不是一個數組。(對大的結果集來講這是好事,但就如今而言這增長了咱們須要的工做!)

var matchingCheckins = [];
store.db.transaction(function(tx) {
    tx.executeSql(
        "select * from checkins where mood=?",
        [moodQuery],
        function(tx, results) {
            for (var i = 0; i < results.rows.length; i++) {
                matchingCheckins.push(clone(results.rows.item(i)));
            }
            handler(matchingCheckins); 
        },
        store.onError
    );
});

固然,在 IndexedDB 解決方案使用索引,咱們先前在 「mood」 表中建立的索引,稱爲「moodindex」。咱們用一個指針遍歷每次簽到以匹配查詢。注意這個指針模式也能夠用於整個存儲;所以,使用索引就像咱們在商店裏的一個窗口前,只能看到匹配的對象(相似於在傳統數據庫中的「視圖」)。

var store = db.transaction("checkins", 'readonly').objectStore("checkins");
var request = moodQuery ? store.index("moodIndex").openCursor(new IDBKeyRange.only(moodQuery)) : store.openCursor();
request.onsuccess = function(ev) {
    var cursor = request.result;
    if (cursor) {
        handler(cursor.value);
        cursor["continue"]();
    } 
};

與許多傳統的文件系統同樣,FileSystem API 沒有索引,因此搜索算法(如 Unix中的 「grep」 命令)必須遍歷每一個文件。咱們從 「checkins/」 目錄中拿到 Reader API ,經過 readentries() 。對於每一個文件,再使用一個 reader,使用 readastext() 方法檢查其內容。這些操做都是異步的,咱們須要使用 readnext() 將調用連在一塊兒。

checkinsDir.createReader().readEntries(function(files) {
    var reader, fileCount = 0,
        checkins = [];
    var readNextFile = function() {
        reader = new FileReader();
        if (fileCount == files.length) return;
        reader.onload = function(e) {
            var checkin = JSON.parse(this.result);
            if (moodQuery == checkin.mood || !moodQuery) handler(checkin);
            readNextFile();
        };

        files[fileCount++].file(function(file) {
            reader.readAsText(file);
        });
    };
    readNextFile();
});

匹配計數

最後,咱們須要給全部簽到計數。

對localStorage,咱們簡單的反序列化簽到數組,讀取其長度。

handler(JSON.parse(localStorage["checkins"]).length);

對 Web SQL Database,能夠檢索數據庫中的每一行(select * from checkins),看結果集的長度。但若是咱們知道咱們在 SQL 中,有更容易和更快的方式 —— 咱們能夠執行一個特殊的 select 語句來檢索計數。它將返回一行,其中一列包含計數。

store.db.transaction(function(tx) {
    tx.executeSql("select count(*) from checkins;", [], function(tx, results) {
        handler(results.rows.item(0)["count(*)"]);
    }, store.onError);
});

不幸的是, IndexedDB 不提供任何計算方法,因此咱們只能本身遍歷。

var count = 0;
var request = db.transaction(["checkins"], 'readonly').objectStore("checkins").openCursor();
request.onsuccess = function(ev) {
    var cursor = request.result;
    cursor ? ++count && cursor["continue"]() : handler(count);
};

對於文件系統, directory reader 的 readentries() 方法提供一個文件列表,因此咱們返回該列表的長度就好。

checkinsDir.createReader().readEntries(function(files)  {
    handler(files.length);
});

總結

本文從較高層次的角度,講述了現代客戶端存儲技術。你也能夠看看 《離線應用概述》(overview on offline apps)這篇文章。

相關文章
相關標籤/搜索