【译】客户端存储(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)这篇文章。

相关文章
相关标签/搜索