前端的数据库:IndexedDB入门

423 查看

应用程序需要数据。对大多数Web应用程序来说,数据在服务器端组织和管理,客户端通过网络请求获取。随着浏览器变得越来越有能力,因此可选择在浏览器存储和操纵应用程序数据。

本文向你介绍名为IndexedDB的浏览器端文档数据库。使用lndexedDB,你可以通过惯于在服务器端数据库几乎相同的方式创建、读取、更新和删除大量的记录。请使用本文中可工作的代码版本去体验,完整的源代码可以通过GitHub库找到。

读到本教程的结尾时,你将熟悉IndexedDB的基本概念以及如何实现一个使用IndexedDB执行完整的CRUD操作的模块化JavaScript应用程序。让我们稍微亲近IndexedDB并开始吧。

什么是IndexedDB

一般来说,有两种不同类型的数据库:关系型和文档型(也称为NoSQL或对象)。关系数据库如SQL Server,MySQL,Oracle的数据存储在表中。文档数据库如MongoDB,CouchDB,Redis将数据集作为个体对象存储。IndexedDB是一个文档数据库,它在完全内置于浏览器中的一个沙盒环境中(强制依照(浏览器)同源策略)。图1显示了IndexedDB的数据,展示了数据库的结构

图1:开发者工具查看一个object store

全部的IndexedDB API请参考完整文档

设计典范

IndexedDB的架构很像在一些流行的服务器端NOSQL数据库实现中的设计典范类型。面向对象数据通过object stores(对象仓库)进行持久化,所有操作基于请求同时在事务范围内执行。事件生命周期使你能够控制数据库的配置,错误通过错误冒泡来使用API管理。

对象仓库

object store是IndexedDB数据库的基础。如果你使用过关系数据库,通常可以将object store等价于一个数据库表。Object stores包括一个或多个索引,在store中按照一对键/值操作,这提供一种快速定位数据的方法。

当你配置一个object store,你必须为store选择一个键。键在store中可以以“in-line”或“out-of-line”的方式存在。in-line键通过在数据对象上引用path来保障它在object store的唯一性。为了说明这一点,想想一个包括电子邮件地址属性Person对象。您可以配置你的store使用in-line键emailAddress,它能保证store(持久化对象中的数据)的唯一性。另外,out-of-line键通过独立于数据的值识别唯一性。在这种情况下,你可以把out-of-line键比作一个整数值,它(整数值)在关系数据库中充当记录的主键。

图1显示了任务数据保存在任务的object store,它使用in-line键。在这个案例中,键对应于对象的ID值。

基于事务

不同于一些传统的关系数据库的实现,每一个对数据库操作是在一个事务的上下文中执行的。事务范围一次影响一个或多个object stores,你通过传入一个object store名字的数组到创建事务范围的函数来定义。

创建事务的第二个参数是事务模式。当请求一个事务时,必须决定是按照只读还是读写模式请求访问。事务是资源密集型的,所以如果你不需要更改data store中的数据,你只需要以只读模式对object stores集合进行请求访问。

清单2演示了如何使用适当的模式创建一个事务,并在这片文章的 Implementing Database-Specific Code 部分进行了详细讨论。

基于请求

直到这里,有一个反复出现的主题,您可能已经注意到。对数据库的每次操作,描述为通过一个请求打开数据库,访问一个object store,再继续。IndexedDB API天生是基于请求的,这也是API异步本性指示。对于你在数据库执行的每次操作,你必须首先为这个操作创建一个请求。当请求完成,你可以响应由请求结果产生的事件和错误。

本文实现的代码,演示了如何使用请求打开数据库,创建一个事务,读取object store的内容,写入object store,清空object store。

打开数据库的请求生命周期

IndexedDB使用事件生命周期管理数据库的打开和配置操作。图2演示了一个打开的请求在一定的环境下产生upgrade need事件。

图2:IndexedDB打开请求的生命周期

所有与数据库的交互开始于一个打开的请求。试图打开数据库时,您必须传递一个被请求数据库的版本号的整数值。在打开请求时,浏览器对比你传入的用于打开请求的版本号与实际数据库的版本号。如果所请求的版本号高于浏览器中当前的版本号(或者现在没有存在的数据库),upgrade needed事件触发。在uprade need事件期间,你有机会通过添加或移除stores,键和索引来操纵object stores。

如果所请求的数据库版本号和浏览器的当前版本号一致,或者升级过程完成,一个打开的数据库将返回给调用者。

错误冒泡

当然,有时候,请求可能不会按预期完成。IndexedDB API通过错误冒泡功能来帮助跟踪和管理错误。如果一个特定的请求遇到错误,你可以尝试在请求对象上处理错误,或者你可以允许错误通过调用栈冒泡向上传递。这个冒泡天性,使得你不需要为每个请求实现特定错误处理操作,而是可以选择只在一个更高级别上添加错误处理,它给你一个机会,保持你的错误处理代码简洁。本文中实现的例子,是在一个高级别处理错误,以便更细粒度操作产生的任何错误冒泡到通用的错误处理逻辑。

浏览器支持

也许在开发Web应用程序最重要的问题是:“浏览器是否支持我想要做的?“尽管浏览器对IndexedDB的支持在继续增长,采用率并不是我们所希望的那样普遍。图3显示了caniuse.com网站的报告,支持IndexedDB的为66%多一点点。最新版本的火狐,Chrome,Opera,Safar,iOS Safari,和Android完全支持IndexedDB,Internet Explorer和黑莓部分支持。虽然这个列表的支持者是令人鼓舞的,但它没有告诉整个故事。

图3:浏览器对IndexedDB的支持,来自caniuse.com

只有非常新版本的Safari和iOS Safari 支持IndexedDB。据caniuse.com显示,这只占大约0.01%的全球浏览器使用。IndexedDB不是一个你认为能够理所当然得到支持的现代Web API,但是你将很快会这样认为。

另一种选择

浏览器支持本地数据库并不是从IndexedDB才开始实现,它是在WebSQL实现之后的一种新方法。类似IndexedDB,WebSQL是一个客户端数据库,但它作为一个关系数据库的实现,使用结构化查询语言(SQL)与数据库通信。WebSQL的历史充满了曲折,但底线是没有主流的浏览器厂商对WebSQL继续支持。

如果WebSQL实际上是一个废弃的技术,为什么还要提它呢?有趣的是,WebSQL在浏览器里得到稳固的支持。Chrome, Safari, iOS Safari, and Android 浏览器都支持。另外,并不是这些浏览器的最新版本才提供支持,许多这些最新最好的浏览器之前的版本也可以支持。有趣的是,如果你为WebSQL添加支持来支持IndexedDB,你突然发现,许多浏览器厂商和版本成为支持浏览器内置数据库的某种化身。

因此,如果您的应用程序真正需要一个客户端数据库,你想要达到的最高级别的采用可能,当IndexedDB不可用时,也许您的应用程序可能看起来需要选择使用WebSQL来支持客户端数据架构。虽然文档数据库和关系数据库管理数据有鲜明的差别,但只要你有正确的抽象,就可以使用本地数据库构建一个应用程序。

IndexedDB是否适合我的应用程序?

现在最关键的问题:“IndexedDB是否适合我的应用程序?“像往常一样,答案是肯定的:“视情况而定。“首先当你试图在客户端保存数据时,你会考虑HTML5本地存储。本地存储得到广泛浏览器的支持,有非常易于使用的API。简单有其优势,但其劣势是无法支持复杂的搜索策略,存储大量的数据,并提供事务支持。

IndexedDB是一个数据库。所以,当你想为客户端做出决定,考虑你如何在服务端选择一个持久化介质的数据库。你可能会问自己一些问题来帮助决定客户端数据库是否适合您的应用程序,包括:

  • 你的用户通过浏览器访问您的应用程序,(浏览器)支持IndexedDB API吗 ?
  • 你需要存储大量的数据在客户端?
  • 你需要在一个大型的数据集合中快速定位单个数据点?
  • 你的架构在客户端需要事务支持吗?

如果你对其中的任何问题回答了“是的”,很有可能,IndexedDB是你的应用程序的一个很好的候选。

使用IndexedDB

现在,你已经有机会熟悉了一些的整体概念,下一步是开始实现基于IndexedDB的应用程序。第一个步骤需要统一IndexedDB在不同浏览器的实现。您可以很容易地添加各种厂商特性的选项的检查,同时在window对象上把它们设置为官方对象相同的名称。下面的清单展示了window.indexedDB,window.IDBTransaction,window.IDBKeyRange的最终结果是如何都被更新,它们被设置为相应的浏览器的特定实现。

现在,每个数据库相关的全局对象持有正确的版本,应用程序可以准备使用IndexedDB开始工作。

应用概述

在本教程中,您将学习如何创建一个使用IndexedDB存储数据的模块化JavaScript应用程序。为了了解应用程序是如何工作的,参考图4,它描述了任务应用程序处于空白状态。从这里您可以为列表添加新任务。图5显示了录入了几个任务到系统的画面。图6显示如何删除一个任务,图7显示了正在编辑任务时的应用程序。

图4:空白的任务应用程序

图5:任务列表
图6:删除任务
图7:编辑任务
现在你熟悉的应用程序的功能,下一步是开始为网站铺设基础。

铺设基础

这个例子从实现这样一个模块开始,它负责从数据库读取数据,插入新的对象,更新现有对象,删除单个对象和提供在一个object store删除所有对象的选项。这个例子实现的代码是通用的数据访问代码,您可以在任何object store上使用。

这个模块是通过一个立即执行函数表达式(IIFE)实现,它使用对象字面量来提供结构。下面的代码是模块的摘要,说明了它的基本结构。

用这样的结构,可以使这个应用程序的所有逻辑封装在一个名为app的单对象上。此外,数据库相关的代码在一个叫做db的app子对象上。

这个模块的代码使用IIFE,通过传递window对象来确保模块的适当范围。使用use strict确保这个函数的代码函数是按照(javascript严格模式)严格编译规则。db对象作为与数据库交互的所有函数的主要容器。最后,window对象检查app的实例是否存在,如果存在,模块使用当前实例,如果不存在,则创建一个新对象。一旦app对象成功返回或创建,db对象附加到app对象。

本文的其余部分将代码添加到db对象内(在implementation here会评论),为应用程序提供特定于数据库的逻辑。因此,如你所见本文后面的部分中定义的函数,想想父db对象移动,但所有其他功能都是db对象的成员。完整的数据库模块列表见清单2。

Implementing Database-Specific Code

对数据库的每个操作关联着一个先决条件,即有一个打开的数据库。当数据库正在被打开时,通过检查数据库版本来判断数据库是否需要任何更改。下面的代码显示了模块如何跟踪当前版本,object store名、某成员(保存了一旦数据库打开请求完成后的数据库当前实例)。

在这里,数据库打开请求发生时,模块请求版本1数据库。如果数据库不存在,或者版本小于1,upgrade needed事件在打开请求完成前触发。这个模块被设置为只使用一个object store,所以名字直接定义在这里。最后,实例成员被创建,它用于保存一旦打开请求完成后的数据库当前实例。

接下来的操作是实现upgrade needed事件的事件处理程序。在这里,检查当前object store的名字来判断请求的object store名是否存在,如果不存在,创建object store。

在这个事件处理程序里,通过事件参数e.target.result来访问数据库。当前的object store名称的列表在_db.objectStoreName的字符串数组上。现在,如果object store不存在,它是通过传递object store名称和store的键的定义(自增,关联到数据的ID成员)来创建。

模块的下一个功能是用来捕获错误,错误在模块不同的请求创建时冒泡。