首頁  >  文章  >  web前端  >  客戶端儲存雜談

客戶端儲存雜談

伊谢尔伦
伊谢尔伦原創
2017-01-23 13:03:421404瀏覽

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

先看看他們的共同點:

共同特點

基於客戶端的儲存

實際上,「客戶端時間儲存」​​的意思是,資料傳給了瀏覽器的儲存API,它將資料存在本機裝置中的一塊區域,該區域同樣也是它儲存其他使用者特定資訊如個人偏好、快取的地方。除了儲存數據,這些 API 可以用來檢索數據,在某些情況下還能執行搜尋和批次操作。

置於沙盒中的

所有這四個儲存 API 都將資料綁在一個單獨的「來源」(origin)上。

空間限制(Quotas)

你能想像,如果任何網站都被允許往毫不知情的硬碟裡填充以千兆字節計的數據,該有多混亂。因此,瀏覽器對儲存容​​量施加了限制。如果你的應用程式試圖超出限制,瀏覽器通常會顯示一個對話框,讓使用者確認增加。您可能以為瀏覽器對單一來源(origin)可使用的所有儲存都加以相同單獨的限制,但多數儲存機制都是單獨加以限制的。若 Quota API 被採納,這種情況可能會改變。但就現在來說,把瀏覽器當作一個二維矩陣,其維度分別是「來源」(origin)和「儲存」(storage)。 abc.example可能會允許最多存 5MB 的 Web Storage, 25MB 的 Web SQL 資料庫,但因使用者拒絕存取被禁止使用 Indexed DataBase。 Quota API 將問題放到一起來看,讓您查詢還有多少可用空間,有多少空間正在使用。

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

資料庫處理(Transactions)

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

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

多數儲存格式都支援同步和非同步模式。同步模式是阻塞的,表示在下一行 js 程式碼執行之前,儲存操作會完整執行。非同步模式會使得後面的 js 程式碼在資料庫操作完成之前執行。儲存操作會在背景環境中執行,當操作完成的時候,應用程式會以回呼函數被呼叫這種形式接收通知,而這個函數必須在呼叫的時候被指定。

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

某些 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 的優點

數年以來,被所有現代瀏覽器支持, iOS 和Android 系統下也支持(IE 從IE8 開始支援)。

簡單的API簽章。

同步 API,呼叫簡單。

語意事件可保持其他標籤和視窗同步。

Web Storage 的弱點

使用同步 API(這是得到最廣泛支援的模式)儲存大量的或複雜的資料時效能差。

缺少索引導致檢索大量的或複雜的資料時效能差。 (搜尋操作需要手動遍歷所有項目。)

儲存或讀取大量的或複雜的資料結構時效能差,因為需要手動序序列化成字串或將字串反序列化。主要的瀏覽器實作只支援字串(儘管規範沒這麼說的)。

需要確保資料的持續性和完整性,因為資料是有效非結構化的(effectively unstructured)。

Web SQL Database

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

Web SQL Database 的優點

被主要的行動瀏覽器(Android Browser, Mobile Safari, Opera Mobile)以及一些 PC 瀏覽器(Chrome, Safari, Opera) 支援。

作為非同步 API, 整體而言表現很好。資料庫互動不會鎖定使用者介面。 (同步API也可用於 WebWorkers。)

良好的搜尋效能,因為資料可以根據搜尋鍵進行索引。

強大,因為它支援事務性資料庫模型(transactional database model)。

剛性的資料結構更容易保持資料的完整性。

Web SQL Database 的弱點

過時,不會被 IE 或 Firefox 支持,在某些階段可能會被從其他瀏覽器淘汰。

學習曲線陡峭,要求掌握關聯式資料庫和SQL的知識。

物件-關係阻抗失配(object-relational impedance mismatch).

降低敏捷性,因為資料庫模式必須預先定義,與表中的所有記錄必須匹配相同的結構。

Indexed Database (IndexedDB)

到目前為止,我們已經看到,Web Storage 和 Web SQL Database 都有各種的優點和缺點。 Indexed Database 產生於這兩個早期 API 的經驗,可視為一種結合兩者優點而不招致其劣勢得到嘗試。

Indexed Database 是一個 「物件儲存」 (object stores) 的集合,可以直接把物件放進去。這個儲存有點像 SQL 表,但在這種情況下,物件的結構沒有約束,所以不需要預先定義什麼。所以這和 Web Storage 有點像是,擁有多個資料庫、每個資料庫又有多個儲存(store)的特色。但不像 Web Storage那樣, 還擁有重要的效能優勢: 非同步接口,可以在儲存上建立索引,以提高搜尋速度。

IndexedDB 的優點

作為非同步API整體表現良好。資料庫互動不會鎖定使用者介面。 (同步 API 也可用於 WebWorkers。)

良好的搜尋效能,因為資料可以根據搜尋鍵進行索引。

支援版本控制。

強大,因為它支援事務性資料庫模型(transactional database model)。

因為資料模型簡單,學習曲線也相當簡單。

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

IndexedDB 的弱點

非常複雜的API,導致大量的嵌套回調。

FileSystem

上面的 API 都是適用於文字和結構化數據,但當涉及到大檔案和二進位內容時,我們需要一些其他的東西。幸運的是,我們現在有了檔案系統 API 標準(FileSystem API standard)。它給每個網域一個完整的層次化的檔案系統,至少在 Chrome 下面,這些都是使用者的硬碟上的真正的檔案。就單一檔案的讀寫而言, API 建立在現有的 File API之上。

FileSystem(文件系統) API 的有點

可以存儲大量的內容和二進製文件,很適合圖像,音頻,視頻,PDF,等。

作為非同步 API, 效能良好。

FileSystem API 的弱點

很早期的標準,只有 Chrome 和 Opera 支援。

沒有事務(transaction)支援。

沒有內建的搜尋/索引支援。

來看程式碼

本部分比較不同的 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&#39;d upgrade our database here. 
            // This will never run here, because we&#39;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:php.cn");
        }  
    );
}

保存一次簽到 (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.time,checkin.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",  &#39;readwrite&#39;); 
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);

当然,在 IndexedDB 解决方案使用索引,我们先前在 “mood” 表中创建的索引,称为“moodindex”。我们用一个指针遍历每次签到以匹配查询。注意这个指针模式也可以用于整个存储;因此,使用索引就像我们在商店里的一个窗口前,只能看到匹配的对象(类似于在传统数据库中的“视图”)。

var store = db.transaction("checkins", &#39;readonly&#39;).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"], &#39;readonly&#39;).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)这篇文章。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn