Web应用中的离线数据存储

为了提高Web应用的用户体验,想必不少开发者都会项目中引入离线数据存储机制。但是面对各类各样的离线数据技术,哪种才是最能知足项目需求的呢?本文将帮助各位找到最合适的那一个。css

引言

随着HTML5的到来,各类Web离线数据技术进入了开发人员的视野。诸如AppCache、localStorage、sessionStorage和IndexedDB等等,每一种技术都有它们各自适用的范畴。好比AppCache就比较适合用于离线起动应用,或者在离线状态下使应用的一部分功能照常运行。接下来我将会为你们做详细介绍,而且用一些代码片断来展现如何使用这些技术。html

AppCache

若是你的Web应用中有一部分功能(或者整个应用)须要在脱离服务器的状况下使用,那么就能够经过AppCache来让你的用户在离线状态下也能使用。你所须要作的就是建立一个配置文件,在其中指定哪些资源须要被缓存,哪些不须要。此外,还能在其中指定某些联机资源在脱机条件下的替代资源。jquery

AppCache的配置文件一般是一个以.appcache结尾的文本文件(推荐写法)。文件以CACHE MANIFEST开头,包含下列三部份内容:git

  • CACHE – 指定了哪些资源在用户第一次访问站点的时候须要被下载并缓存
  • NETWORK – 指定了哪些资源须要在联机条件下才能访问,这些资源从不被缓存
  • FALLBACK – 指定了上述资源在脱机条件下的替代资源

示例

首先,你须要在页面上指定AppCache的配置文件:github

<!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:
*

最后的结果就是下面这样:

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

FALLBACK:
/contact.html /offline.html
/comments.html /offline.html

NETWORK:
*

还有一件很重要的事情要记得:你的资源只会被缓存一次!也就是说,若是资源更新了,它们不会自动更新,除非你修改了配置文件。因此有一个最佳实践是,在配置文件中增长一项版本号,每次更新资源的时候顺带更新版本号:

CACHE MANIFEST

# version 1

CACHE:
...

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 transaction = db.transaction([ 'cds' ], 'readwrite');
    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 store = transaction.objectStore('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) {
        var cursor = e.target.result;
        if (cursor) {
            // render the album
            // move to the next item in the cursor
            cursor.continue();
        }
    });
}

使用索引的时候和使用store同样,也能经过游标(cursor)来遍历。因为同一个索引值名下可能有好几条数据(若是索引不是unique的话),因此这里咱们须要用到IDBKeyRange。它能根据指定的函数对结果集进行过滤。这里,咱们只想根据指定的乐队进行检索,因此咱们用到了only()函数。也能使用其它相似于lowerBound(),upperBound()和bound()等函数,它们的功能也是不言自明的。

总结

能够看见,在Web应用中使用离线数据并非十分复杂。但愿经过阅读这篇文章,各位可以在Web应用中加入离线数据的功能,使得大家的应用更加友好易用。你能够在这里下载全部的源码,尝试一下,或者修修改改,或者用在大家的应用中。


原文:Real-World Off-Line Data Storage
转载自:伯乐在线 - njuyz

相关文章
相关标签/搜索