Dive Into HTML5:本地存储(续)

浏览器限制

前面我们讨论了使用第三方插件实现本地存储的历史,我们指出了每种实现技术的不足之处,例如存储限制等。然而,新的 HTML5 Storage 标准依然有一些不足。现在,我们就要批判地看待这个问题,然后再详细地解释一下。HTML5 Storage 的不足有三个:“5 MB”,“QUOTA_EXCEEDED_ERR” 和 “不行!”。

“5 MB”是指默认情况下每一源头的存储空间大小。这个问题在不同浏览器的实现竟然惊人的一致,虽然对于 HTML5 Storage 标准仅仅将其作为一种建议。你需要记住一件事:你存储的是字符串,而不是数据的原始格式。如果你在存储很多的整数或者浮点数,这种差异就显现出来了。浮点数中的每一个数字都是作为一个字符存储的,而不是计算机内部浮点数的真实表示法。

“QUOTA_EXCEEDED_ERR” 是一个异常。如果你的存储超过了 5 MB,就会发出这个异常。“不行”则是下一个显然的问题的答案:“我可以要求更大的存储空间吗?”在本文写作的时间(2011年2月),还没有浏览器支持web开发人员请求更大的存储空间。有些浏览器(例如 Opera)允许用户控制每一个网站的存储空间大小,但是那仅仅是一个用户限制,不是作为一个 web 开发人员能够控制的。

HTML5 Storage 实战

下面我们开始尝试实际使用一下 HTML5 Storage。还记得我们曾经开发过一个跳棋游戏吗?我们的游戏还有一个小问题:如果在游戏过程中关闭浏览器,你就丢失掉全部进度了。但是,如果我们使用 HTML5 Storage,我们就可以将游戏进度保存到本地,就不会有这个问题。这里是我们已经做好的版本。试着走几步,然后关闭浏览器,再打开看看。如果你的浏览器支持 HTML5 Storage,我们的游戏就可以保存下你关闭浏览器时的棋子位置,甚至你当时选中的棋子。

这是如何实现的呢?我们每走一步都会调用这个函数:

function saveGameState() {
    if (!supportsLocalStorage()) { return false; }
    localStorage["halma.game.in.progress"] = gGameInProgress;
    for (var i = 0; i < kNumPieces; i++) {
	localStorage["halma.piece." + i + ".row"] = gPieces[i].row;
	localStorage["halma.piece." + i + ".column"] = gPieces[i].column;
    }
    localStorage["halma.selectedpiece"] = gSelectedPieceIndex;
    localStorage["halma.selectedpiecehasmoved"] = gSelectedPieceHasMoved;
    localStorage["halma.movecount"] = gMoveCount;
    return true;
}

正如你看到的那样,我们使用localStorage记录游戏是不是正在进行(gGameInProgressBoolean)。如果是,则遍历棋子数组(gPieces,JavaScript Array),保存每个棋子的行和列的值。然后保存下游戏其他状态,包括哪个棋子被选中(gSelectedPieceIndex,整型),是否有棋子在连跳过程中(gSelectedPieceHasMovedBoolean),以及总步数(gMoveCount,整型)。

页面加载时,我们不能调用newGame()函数,因为这个函数会将所有变量清零。我们需要调用的是resumeGame()函数。通过 HTML5 Storage,resumeGame()函数检查是否有游戏过程存储在本地的状态。如果有,则从localStorage对象将这些值恢复出来。

function resumeGame() {
    if (!supportsLocalStorage()) { return false; }
    gGameInProgress = (localStorage["halma.game.in.progress"] == "true");
    if (!gGameInProgress) { return false; }
    gPieces = new Array(kNumPieces);
    for (var i = 0; i < kNumPieces; i++) {
	var row = parseInt(localStorage["halma.piece." + i + ".row"]);
	var column = parseInt(localStorage["halma.piece." + i + ".column"]);
	gPieces[i] = new Cell(row, column);
    }
    gNumPieces = kNumPieces;
    gSelectedPieceIndex = parseInt(localStorage["halma.selectedpiece"]);
    gSelectedPieceHasMoved = localStorage["halma.selectedpiecehasmoved"] == "true";
    gMoveCount = parseInt(localStorage["halma.movecount"]);
    drawBoard();
    return true;
}

这个函数最重要的部分是我们前面反复警告过的一件事:所有数据都是以字符串的形式存储的。如果你存储的不是字符串,就需要自己进行类型转换。例如,游戏是否正在进行的标记(gGameInProgress)是一个Boolean类型。在saveGameState()函数中,我们仅仅将其存了下来,没有管它是什么类型:

localStorage["halma.game.in.progress"] = gGameInProgress;

但是在resumeGame()函数,我们需要从本地存储中以字符串的形式读取这些值,然后再手动做类型转换:

gGameInProgress = (localStorage["halma.game.in.progress"] == "true");

类似的,我们也需要将移动步数gMoveCount转换成整数。在saveGameState()函数中,我们这样存储这个值:

localStorage["halma.movecount"] = gMoveCount;

但是在resumeGame()函数中,我们需要将其值转换成整数。我们使用的是 JavaScript 内置的parseInt()函数:

gMoveCount = parseInt(localStorage["halma.movecount"]);

存储键值对类型以外的数据

相对于过去的种种技巧,HTML5 Storage 的前景确实相当乐观。一个新的 API 已经被标准化,并且在所有主流浏览器、平台和设备上已经实现。作为一个 web 开发人员,这并不是每天都能看到的事情。但是,我们必须承认,还有很多东西是“5MB键值对”所不能存储的。持久化存储应该支持更多格式。

其中一种观点是你已经熟悉了的:SQL。2007年,Google 发布了 Gears,作为一个跨浏览器的开源插件,它包含了一个基于 SQLite 的嵌入式数据库。这是 Web SQL Database 标准的早期原型。Web SQL Database(通常称为 WebDB)提供了一个 SQL 数据库的简单封装,允许你使用 JavaScript 做这样的事情:

openDatabase('documents', '1.0', 'Local document storage', 5*1024*1024, function (db) {
    db.changeVersion('', '1.0', function (t) {
        t.executeSql('CREATE TABLE docids (id, name)');
    }, error);
});

正如你看到的那样,这段代码中核心字符串在executeSql函数。这个字符串可以是任何支持的 SQL 语句,包括 SELECT、UPDATE、INSERT 和 DELETE。就像后台数据库编程一样,唯一区别是你用的是 JavaScript。

Web SQL Database 现在有四个浏览器或平台支持:

IE Firefox Safari Chrome Opera iPhone Android
4.0+ 4.0+ 10.5+ 3.0+ 2.0+

如果你曾经使用过数据库产品,就应该知道“SQL”有许多商业实现,而不仅仅是一个标准。(有人会说 HTML5 也是这个样子,但这有些区别。)当然,我们也有一些事实上的 SQL 标准,称为 SQL-92,但是现在没有数据库仅仅提供标准支持的操作。我们有 Oracle 的 SQL,Microsoft 的 SQL,MySQL 的 SQL,PostgreSQL 的 SQL 和 SQLite 的 SQL。事实上,每一个数据库产品都会增加自己独特的 SQL 特性,所以,当我们说 “SQLite 的 SQL” 的时候,你应该意识到这意味着什么。同时你需要指明,这是 “ SQLite X.Y.Z 版本所支持的 SQL”。

鉴于 SQL 这种不确定性,Web SQL Database 标准做了如下描述:

本标准实际已经陷入僵局:所有感兴趣的实现都使用了同一个 SQL 后端(SQLite),但是,我们需要多种独立实现,以便形成一种独立的标准。在另外一种实现出现之前,SQLite 的一个简单实现已经被选为 SQL 方言,不过,对于一个通用标准而言,这是不可接受的。

正是在这种背景下,我们引入了另外一种高级的、持久化的、本地存储机制:Indexed Database API,通常被称为 WebSimpleDB,现在则叫做 IndexedDB。

Indexed Database API 使用的是一种对象存储机制。对象存储与 SQL 数据库有许多共同点:都有“数据库”和“记录”的概念,每一个记录都有一组“属性”。每一个属性都要有一个指定的数据类型,这个数据类型在创建数据库的时候就被定义好了。你可以选择所有记录的一个子集,然后使用“游标”进行遍历。对对象的修改则通过所谓的“事务”进行。

如果你曾经做过 SQL 数据库编程,就会发现这些概念都十分雷同。二者主要区别在于,对象存储没有结构化查询语言的概念。你不需要类似”SELECT * from USERS where ACTIVE = 'Y'“这种语句。你需要做的是使用对象存储机制提供的函数,在名为 USERS 的数据库上打开一个游标,遍历所有记录,过滤掉不活动的用户,使用访问函数读取剩下记录的所有字段。An early walk-through of IndexedDB 是一篇关于 IndexedDB 如何工作的不错的教程,给出了关于 IndexedDB 和 Web SQL Database 的对比。

在本文写作的时候,IndexedDB 仅有 Firefox 4 实现了。(同时,Mozilla 宣布他们不准备实现 Web SQL Database。)Google 也宣布他们将考虑为 Chromium 和 Google Chrome 添加 IndexedDB 的支持。Microsoft 也认为 IndexedDB “是一个不错的解决方案”。

那么,作为一个 web 开发者,你能使用 IndexedDB 吗?目前,我们还没有任何技术上的演示程序。那么,一年以后呢?或许吧。

2 Comments

  1. ccc 2012年2月17日
    • DevBean 2012年2月26日

Leave a Reply