【译】客户端存储(Client-Side Storage)

本文转载自:众成翻译
译者:文蔺
链接:http://www.zcfy.cc/article/660
原文:http://www.html5rocks.com/en/tutorials/offline/storage/

引见

本文是关于客户端存储(client-side storage)的。这是一个通用术语,包含几个自力但相干的 API: Web Storage、Web SQL Database、Indexed Database 和 File Access。每种手艺都供应了在用户硬盘上 —— 而非一般存储数据的服务器 —— 存储数据的奇特体式格局。这么做主要基于以下两点来由:(a)使 web app 离线可用; (b)改良机能。关于客户端存储运用状况的细致论述,请看 HTML5Rocks 上的文章 《“离线”: 这是什么意义?我为什么要体贴?》。

这些 API 有着相似的作用局限和划定规矩。因而,在去看细节之前,我们先相识他们的配合之处吧。

配合特性

基于客户端的存储

现实上,“客户端时刻存储”的意义是,数据传给了浏览器的存储 API,它将数据存在当地装备中的一块地区,该地区一样也是它存储其他用户特定信息如个人偏好、缓存的处所。除了存储数据,这些 API 能够用来检索数据,且在某些状况下还能实行搜刮和批处置惩罚操纵。

置于沙盒中的

统统这四个存储 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 等等)。

空间限定(Quotas)

你能设想,假如任何网站都被许可往绝不知情的硬盘里添补以千兆字节计的数据,该有多杂沓。因而,浏览器对存储容量施加了限定。若你的运用试图超越限定,浏览器一般会显现一个对话框,让用户确认增添。您能够认为浏览器对单个源(origin)可运用的统统存储都加以统一零丁的限定,但多半存储机制都是零丁加以限定的。若 Quota API 被采用,这类状况能够会转变。但就如今来讲,把浏览器看成一个二维矩阵,其维度分别是“源”(origin)和“存储”(storage)。比方, “http://abc.example.com” 能够会许可最多存 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 的长处
  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)这篇文章。

    原文作者:文蔺
    原文地址: https://segmentfault.com/a/1190000005798773
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞