為了提升Web應用的使用者體驗,想必很多開發者都會項目中引入離線資料存儲機制。可是面對各種各樣的離線資料技術,哪一種才是最能滿足項目需求的呢?本文将幫助各位找到最合适的那一個。
引言
随着HTML5的到來,各種Web離線資料技術進入了開發人員的視野。諸如AppCache、localStorage、sessionStorage和IndexedDB等等,每一種技術都有它們各自适用的範疇。比如AppCache就比較适合用于離線起動應用,或者在離線狀态下使應用的一部分功能照常運作。接下來我将會為大家作詳細介紹,并且用一些代碼片段來展示如何使用這些技術。
AppCache
如果你的Web應用中有一部分功能(或者整個應用)需要在脫離伺服器的情況下使用,那麼就可以通過AppCache來讓你的使用者在離線狀态下也能使用。你所需要做的就是建立一個配置檔案,在其中指定哪些資源需要被緩存,哪些不需要。此外,還能在其中指定某些聯機資源在脫機條件下的替代資源。
AppCache的配置檔案通常是一個以.appcache結尾的文本檔案(推薦寫法)。檔案以CACHE MANIFEST開頭,包含下列三部分内容:
-
– 指定了哪些資源在使用者第一次通路站點的時候需要被下載下傳并緩存CACHE
-
– 指定了哪些資源需要在聯機條件下才能通路,這些資源從不被緩存NETWORK
-
– 指定了上述資源在脫機條件下的替代資源FALLBACK
示例
首先,你需要在頁面上指定AppCache的配置檔案:
<!DOCTYPE html>
<html manifest="manifest.appcache">
...
</html>
在這裡千萬記得在伺服器端釋出上述配置檔案的時候,需要将MIME類型設定為text/cache-manifest,否則浏覽器無法正常解析。
接下來是建立之前定義好的各種資源。我們假定在這個示例中,你開發的是一個互動類站點,使用者可以在上面聯系别人并且發表評論。使用者在離線的狀态下依然可以通路網站的靜态部分,而聯系以及發表評論的頁面則會被其它頁面替代,無法通路。
好的,我們這就着手定義那些靜态資源:
CACHE MANIFEST
CACHE:
/about.html
/portfolio.html
/portfolio_gallery/image_1.jpg
/portfolio_gallery/image_2.jpg
/info.html
/style.css
/main.js
/jquery.min.js
備注:配置檔案寫起來有一點很不友善。舉例來說,如果你想緩存整個目錄,你不能直接在CACHE部分使用通配符(*),而是隻能在NETWORK部分使用通配符把所有不應該被緩存的資源寫出來。
你不需要顯式地緩存包含配置檔案的頁面,因為這個頁面會自動被緩存。接下來我們為聯系和評論的頁面定義FALLBACK部分:
FALLBACK:
/contact.html /offline.html
/comments.html /offline.html
最後我們用一個通配符來阻止其餘的資源被緩存:
NETWORK:
*
最後的結果就是下面這樣:
還有一件很重要的事情要記得:你的資源隻會被緩存一次!也就是說,如果資源更新了,它們不會自動更新,除非你修改了配置檔案。是以有一個最佳實踐是,在配置檔案中增加一項版本号,每次更新資源的時候順帶更新版本号:
# version 1
LocalStorage 和 SessionStorage
如果你想在Javascript代碼裡面儲存些資料,那麼這兩個東西就派上用場了。前一個可以儲存資料,永遠不會過期(expire)。隻要是相同的域和端口,所有的頁面中都能通路到通過LocalStorage儲存的資料。舉個簡單的例子,你可以用它來儲存使用者設定,使用者可以把他的個人喜好儲存在目前使用的電腦上,以後打開應用的時候能夠直接加載。後者也能儲存資料,但是一旦關閉浏覽器視窗(譯者注:浏覽器視窗,window,如果是多tab浏覽器,則此處指代tab)就失效了。而且這些資料不能在不同的浏覽器視窗之間共享,即使是在不同的視窗中通路同一個Web應用的其它頁面。
備注:有一點需要提醒的是,LocalStorage和SessionStorage裡面隻能儲存基本類型的資料,也就是字元串和數字類型。其它所有的資料可以通過各自的toString()方法轉化後儲存。如果你想儲存一個對象,則需要使用JSON.stringfy方法。(如果這個對象是一個類,你可以複寫它預設的toString()方法,這個方法會自動被調用)。
我們不妨來看看之前的例子。在聯系人和評論的部分,我們可以随時儲存使用者輸入的東西。這樣一來,即使使用者不小心關閉了浏覽器,之前輸入的東西也不會丢失。對于jQuery來說,這個功能是小菜一碟。(注意:表單中每個輸入字段都有id,在這裡我們就用id來指代具體的字段)
$('#comments-input, .contact-field').on('keyup', function () {
// let's check if localStorage is supported
if (window.localStorage) {
localStorage.setItem($(this).attr('id'), $(this).val());
}
});
每次送出聯系人和評論的表單,我們需要清空緩存的值,我們可以這樣處理送出(submit)事件:
$('#comments-form, #contact-form').on('submit', function () {
// get all of the fields we saved
$('#comments-input, .contact-field').each(function () {
// get field's id and remove it from local storage
localStorage.removeItem($(this).attr('id'));
});
最後,每次加載頁面的時候,把緩存的值填充到表單上即可:
// get all of the fields we saved
$('#comments-input, .contact-field').each(function () {
// get field's id and get it's value from local storage
var val = localStorage.getItem($(this).attr('id'));
// if the value exists, set it
if (val) {
$(this).val(val);
IndexedDB
在我個人看來,這是最有意思的一種技術。它可以儲存大量經過索引(indexed)的資料在浏覽器端。這樣一來,就能在用戶端儲存複雜對象,大文檔等等資料。而且使用者可以在離線情況下通路它們。這一特性幾乎适用于所有類型的Web應用:如果你寫的是郵件用戶端,你可以緩存使用者的郵件,以供稍後再看;如果你寫的是相冊類應用,你可以離線儲存使用者的照片;如果你寫的是GPS導航,你可以緩存使用者的路線……不勝枚舉。
IndexedDB是一個面向對象的資料庫。這就意味着在IndexedDB中既不存在表的概念,也沒有SQL,資料是以鍵值對的形式儲存的。其中的鍵既可以是字元串和數字等基礎類型,也可以是日期和數組等複雜類型。這個資料庫本身建構于存儲(store,一個store類似于關系型資料中表的概念)的基礎上。資料庫中每個值都必須要有對應的鍵。每個鍵既可以自動生成,也可以在插入值的時候指定,也可以取自于值中的某個字段。如果你決定使用值中的字段,那麼隻能向其中添加Javascript對象,因為基礎資料類型不像Javascript對象那樣有自定義屬性。
在這個例子中,我們用一個音樂專輯應用作為示範。不過我并不打算在這裡從頭到尾展示整個應用,而是把涉及IndexedDB的部分挑出來解釋。如果大家對這個Web應用感興趣的話,文章的後面也提供了源代碼的下載下傳。首先,讓我們來打開資料庫并建立store:
// check if the indexedDB is supported
if (!window.indexedDB) {
throw 'IndexedDB is not supported!'; // of course replace that with some user-friendly notification
}
// variable which will hold the database connection
var db;
// open the database
// first argument is database's name, second is it's version (I will talk about versions in a while)
var request = indexedDB.open('album', 1);
request.onerror = function (e) {
console.log(e);
};
// this will fire when the version of the database changes
request.onupgradeneeded = function (e) {
// e.target.result holds the connection to database
db = e.target.result;
// create a store to hold the data
// first argument is the store's name, second is for options
// here we specify the field that will serve as the key and also enable the automatic generation of keys with autoIncrement
var objectStore = db.createObjectStore('cds', { keyPath: 'id', autoIncrement: true });
// create an index to search cds by title
// first argument is the index's name, second is the field in the value
// in the last argument we specify other options, here we only state that the index is unique, because there can be only one album with specific title
objectStore.createIndex('title', 'title', { unique: true });
// create an index to search cds by band
// this one is not unique, since one band can have several albums
objectStore.createIndex('band', 'band', { unique: false });
相信上面的代碼還是相當通俗易懂的。估計你也注意到上述代碼中打開資料庫時會傳入一個版本号,還用到了onupgradeneeded事件。當你以較新的版本打開資料庫時就會觸發這個事件。如果相應版本的資料庫尚不存在,則會觸發事件,随後我們就會建立所需的store。接下來我們還建立了兩個索引,一個用于标題搜尋,一個用于樂隊搜尋。現在讓我們再來看看如何增加和删除專輯:
// adding
$('#add-album').on('click', function () {
// create the transaction
// first argument is a list of stores that will be used, second specifies the flag
// since we want to add something we need write access, so we use readwrite flag
var transaction = db.transaction([ 'cds' ], 'readwrite');
transaction.onerror = function (e) {
console.log(e);
};
var value = { ... }; // read from DOM
// add the album to the store
var request = transaction.objectStore('cds').add(value);
request.onsuccess = function (e) {
// add the album to the UI, e.target.result is a key of the item that was added
// removing
$('.remove-album').on('click', function () {
var request = transaction.objectStore('cds').delete(/* some id got from DOM, converted to integer */);
request.onsuccess = function () {
// remove the album from UI
}
是不是看起來直接明了?這裡對資料庫所有的操作都基于事務的,隻有這樣才能保證資料的一緻性。現在最後要做的就是展示音樂專輯:
request.onsuccess = function (e) {
if (!db) db = e.target.result;
var transaction = db.transaction([ 'cds' ]); // no flag since we are only reading
var store = transaction.objectStore('cds');
// open a cursor, which will get all the items from database
store.openCursor().onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
var value = cursor.value;
$('#albums-list tbody').append('
'+ value.title +''+ value.band +''+ value.genre +''+ value.year +'
‘); // move to the next item in the cursor cursor.continue(); } }; }
這也不是十分複雜。可以看見,通過使用IndexedDB,可以很輕松的儲存複雜對象,也可以通過索引來檢索想要的内容:
function getAlbumByBand(band) {
var transaction = db.transaction([ 'cds' ]);
var index = store.index('band');
// open a cursor to get only albums with specified band
// notice the argument passed to openCursor()
index.openCursor(IDBKeyRange.only(band)).onsuccess = function (e) {
// render the album
// move to the next item in the cursor
cursor.continue();
}
});
使用索引的時候和使用store一樣,也能通過遊标(cursor)來周遊。由于同一個索引值名下可能有好幾條資料(如果索引不是unique的話),是以這裡我們需要用到IDBKeyRange。它能根據指定的函數對結果集進行過濾。這裡,我們隻想根據指定的樂隊進行檢索,是以我們用到了only()函數。也能使用其它類似于lowerBound(),upperBound()和bound()等函數,它們的功能也是不言自明的。
總結
可以看見,在Web應用中使用離線資料并不是十分複雜。希望通過閱讀這篇文章,各位能夠在Web應用中加入離線資料的功能,使得你們的應用更加友好易用。你可以在
這裡下載下傳所有的源碼,嘗試一下,或者修修改改,或者用在你們的應用中。