构建 Angular 离线应用(二)

原文:Building Offline Applications with Angular

协议:CC BY-NC-SA 4.0

七、IndexedDB 简介

到目前为止,您已经缓存了应用框架和 HTTP GET 服务调用。RESTful 服务为数据检索提供 GET 调用。但是,HTTP 也支持 POST 来创建实体,PUT 和 PATCH 来更新,DELETE 来删除实体。除了 GET 调用之外,示例应用 Web Arcade 还不支持对服务调用的离线访问。

本章介绍了 IndexedDB,用于更高级的脱机操作。在本章中,你将对 IndexedDB 有一个基本的了解,它运行在浏览器的客户端。您将学习如何在 Angular 应用中使用 IndexedDB。JavaScript 提供 API 来与 IndexedDB 集成。您可以在 IndexedDB 中创建、检索、更新和删除数据,大多数现代浏览器都支持 indexed db。这一章着重于构建数据库,包括创建对象存储、索引等。在下一章中,您将通过创建和删除记录来处理数据。

传统上,web 应用使用各种客户端存储功能,包括 cookies、会话存储和本地存储。即使在今天,它们对于存储相当少量的数据也非常有用。另一方面,IndexedDB 为更复杂的客户端存储和检索提供了一个 API。大多数现代浏览器都支持 JavaScript API。IndexedDB 为相对大量的数据(包括 JSON 对象)提供持久存储。然而,没有一个数据库支持存储无限量的数据。相对于磁盘和设备的大小,浏览器对 IndexedDB 中存储的数据量设置了上限。

IndexedDB 对于持久化结构化数据很有用。它以键值对的形式保存数据。它像 NoSQL 数据库一样工作,支持使用包含数据记录的对象存储。对象存储类似于关系数据库中的表。传统的关系数据库在很大程度上使用根据列和约束(主键、外键等)具有预定义结构的表。).但是,IndexedDB 使用对象存储来保存数据记录。

IndexedDB 支持高性能搜索。借助于索引(在对象存储上定义)来组织数据,这有助于更快地检索数据。

术语

考虑以下使用 IndexedDB 的术语:

  • 对象存储:一个 IndexedDB 可能有一个或多个对象存储。每个对象存储充当数据的键值对的容器。如前所述,对象存储类似于关系数据库中的表。

    • 对象存储为 IndexedDB 提供结构。创建一个或多个对象存储作为应用数据的逻辑容器。例如,您可以创建一个名为users的对象存储来存储用户详细信息,并创建另一个名为games的对象存储来保存游戏相关对象的列表。
  • 事务:indexed db 上的数据操作是在事务的上下文中执行的。这有助于保持数据的一致性。记住,IndexedDB 在浏览器中的客户端存储和检索数据。用户可能会打开多个应用实例。它可以创建场景,其中创建/更新/删除操作由浏览器的每个实例部分执行。当更新操作正在进行时,其中一个浏览器可能会检索过时的数据。

    • 事务有助于避免前面提到的问题。事务锁定数据记录,直到操作完成。数据访问和修改操作是原子性的。也就是说,创建/更新/删除操作要么完全完成,要么完全回滚。检索操作仅在数据修改操作完成或回滚后执行。因此,retrieve 从不返回不一致和陈旧的数据对象。

    • IndexedDB 支持三种交易模式,即readonlyreadwriteversionchange、??。可以想象,readonly帮助检索操作,readwrite帮助创建/更新/删除操作。然而,versionchange模式有助于在 IndexedDB 上创建和删除对象存储。

  • 索引:索引有助于更快地检索数据。对象存储按照键的升序对数据进行排序。键隐式不允许重复值。您可以创建额外的索引,这些索引也可以作为唯一性约束。例如,在社会保险号或身份证号上添加索引可以确保 IndexedDB 中没有重复项。

  • 游标:游标帮助遍历对象存储中的记录。在查询和检索过程中迭代数据记录时,这很有用。

IndexedDB 入门

主流浏览器都支持 IndexedDB。API 使应用能够在浏览器中创建、存储、检索、更新和删除本地数据库中的记录。本章详细介绍了如何在本机浏览器 API 中使用 IndexedDB。

以下是使用 IndexedDB 时的典型步骤:

  1. 创建和/或打开一个数据库:第一次创建一个新的 IndexedDB 数据库。当用户返回 web 应用时,打开数据库以对数据库执行操作。

  2. 创建和/或使用对象存储库:用户第一次访问该功能时,创建一个新的对象存储库。如前所述,对象存储类似于关系数据库管理系统(RDBMS)中的表。您可以创建一个或多个对象存储。您可以在对象存储中创建、检索、更新和删除文档。

  3. 开始一个事务:在一个 IndexedDB 对象存储上作为一个事务执行动作。它使您能够保持一致的状态。例如,当对 IndexedDB 数据库执行操作时,用户可能会关闭浏览器。因为动作是在事务的上下文中执行的,所以如果动作没有完成,则事务被中止。事务确保错误或边缘情况不会使数据库处于不一致或不可恢复的状态。

  4. 执行 CRUD :与任何数据库一样,您可以在 IndexedDB 中创建、检索、更新或删除文档。

考虑下面的 Web Arcade 用例,利用 IndexedDB。

在前面的章节中,你已经看到了一个显示棋盘游戏列表的页面。考虑用游戏细节页面来支持一个新的用例。图 7-1 详细描述了一个游戏的所有信息。在游戏描述的下面,显示了一个用户评论列表和一个允许用户添加新评论的表单。当用户输入新的注释并提交表单时,您将这些数据发送到远程 HTTP 服务。理想情况下,该服务将用户评论保存在永久性存储/数据库中,如 MongoDB、Oracle 或 Microsoft SQL Server。考虑到服务器端代码不在本书讨论范围内,我们将保持简单。在下一章中,代码示例展示了一个将用户评论存储在文件中的服务。

图 7-1 显示了带有当前评论列表和允许用户提交新评论的表单的页面部分。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-1

在游戏详情页面上列出并提交评论

“提交”操作会创建新的注释。服务端点是一个 HTTP POST 方法。如前所述,Web Arcade 支持 HTTP GET 调用的离线访问。想象一下,当用户输入评论并点击提交时失去连接。典型的 web 应用会返回一个错误或类似“无法显示页面”的消息 Web Arcade 旨在对网络连接的丢失具有弹性。因此,Web Arcade 缓存用户评论,并在用户返回应用时与服务器端服务同步。

索引的 Angular 服务 b

通过运行以下命令创建新服务:

ng generate service common/idb-storage-access

该命令在目录src/app/common *中创建了一个名为IdbStorageAccessService的新服务。*该服务用于抽象访问 IndexedDB 的代码语句。它是一个中央服务,使用浏览器 API 与 IndexedDB 集成。在初始化期间,该服务执行一次性活动,如创建新的 IndexedDB 存储或打开数据库(如果它已经存在)。见清单 7-1 。

01: @Injectable()
02: export class IdbStorageAccessService {
03:
04:   idb = this.windowObj.indexedDB;
05:
06:   constructor(private windowObj: Window) {
07:   }
08:
09:   init() {
10:     let request = this.idb
11:       .open('web-arcade', 1);
12:
13:     request.onsuccess = (evt:any) => {
14:       console.log("Open Success", evt);
15:     };
16:
17:     request.onerror = (error: any) => {
18:       console.error("Error opening IndexedDB", error);
19:     }
20:   }
21:
22: }
23:

Listing 7-1Initialize IndexedDB with the IdbStorageAccessService

Note

默认情况下,ng generate service命令在根级别提供服务。在 Web Arcade 应用的上下文中,您可能希望删除第 1 行的provideIn: 'root'语句。只需离开inject()装饰器,如第一行所示。

这将在下一节连同清单 7-2 一起详细解释。

考虑以下解释:

  • 第 4 行创建了类变量idb(indexed db 的缩写)。它被设置为全局窗口对象上的indexedDB实例。indexedDB对象有一个 API 来帮助打开或创建一个新的 IndexedDB。第 4 行在初始化IdbStorageAccessService时运行,类似于构造函数。

Note

注意,全局窗口对象是通过一个Window服务来访问的。参见第 6 行的构造函数。它注入窗口服务。实例变量被命名为windowObjWindow服务在AppModule提供。

  • 关于初始化服务的init()函数,请参见第 9 行到第 20 行。

  • 参见对idb对象运行open()函数的第 10 行和第 11 行。如果用户第一次在浏览器上打开应用,它会创建一个新的数据库。

    1. 第一个参数是数据库的名称web-arcade

    2. 第二个参数(值 1)指定数据库的版本。可以想象,应用的新更新会导致 IndexedDB 结构的变化。IndexedDB API 使您能够随着版本的变化升级数据库。

要返回一个用户,数据库已经创建好,并且可以在浏览器上使用。open()函数试图打开数据库。它返回IDBOpenDBRequest对象的一个对象。

图 7-2 显示了一个新创建的 IndexedDB web-arcade。这张图片是用谷歌浏览器的开发工具拍摄的。包括 Firefox 和 Microsoft Edge 在内的所有主流浏览器都为开发者提供了类似的功能。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-2

Google Chrome 开发工具中的 IndexedDB

几乎所有的 IndexedDB APIs 都是异步的。像 open 这样的操作不会尝试立即完成操作。您指定一个回调函数,该函数在完成操作后被调用。可以想象,打开操作可能成功,也可能出错。因此,为每个结果定义一个回调函数,onsuccessonerror *。*见清单 7-1 第 13-15 行和第 17-19 行。目前,您只需在控制台上打印结果(第 14 和 18 行)。我们将在接下来的代码片段中进一步增强对结果的处理。

什么时候调用init()函数?这是 Angular 服务的方法之一。您可以在组件中调用它,这意味着只有当您加载(或导航)到组件时,IndexedDB 才会被初始化。另一方面,像 Web Arcade 这样的应用高度依赖于 IndexedDB。您可能需要利用来自多个组件的服务。该服务需要完成初始化,并为 CRUD 操作做好准备。因此,在主模块AppModule启动时,将服务和应用一起初始化是一个好主意。考虑将 7-2 上市。

03: import { NgModule, APP_INITIALIZER } from '@angular/core';
15: import { IdbStorageAccessService } from './common/idb-storage-access.service';
18:
19: @NgModule({
20:   declarations: [
21:     AppComponent,
25:   ],
26:   imports: [
27:     BrowserModule,
40:   ],
41:   providers: [
42:     IdbStorageAccessService,
43:     {
44:       provide: APP_INITIALIZER,
45:       useFactory: (svc: IdbStorageAccessService) => () => svc.init(),
46:       deps: [IdbStorageAccessService], multi: true
47:     }
48:   ],
49:   bootstrap: [AppComponent]
50: })
51: export class AppModule { }
52:

Listing 7-2Initialize IndexedDB with IdbStorageAccessService

考虑以下解释:

  • 参见第 42 至 48 行。块中的第一行(第 42 行)提供了一个新创建的IDBStorageAccessService *。*我们为什么需要它?如您所见,我们没有在根级别提供服务。我们删除了IdbStorageAccessService(清单 7-1 )中的代码行provideIn: 'root'

  • 参见第 43 行到第 47 行,它们提供了APP_INITIALIZER并使用了调用init()的工厂函数。

  • 总之,我们在模块级提供并初始化了IdbStorageService。在本例中,您在AppModule中完成。它可能是任何模块。

    它在浏览器上创建和/或打开Web-Arcade IndexedDB。它使数据库为进一步的操作(如 CRUD)做好准备。这段代码消除了将服务注入组件(或另一个服务)并调用init()函数的需要。服务随着AppModule一起初始化。

正在创建对象存储

虽然数据库是 IndexedDB 中的最高级别,但它可以有一个或多个对象存储。您为数据库中存储每个对象提供一个唯一的名称。对象存储是保存数据的容器。在当前的 Web Arcade 示例中,您将看到如何保存 JSON 对象。为了便于理解,对象存储类似于关系数据库中的表。

使用“onupgradeneeded”事件

创建或打开 IndexedDB 后,会触发一个名为onupgradeneeded的事件。您提供了一个回调函数,当该事件发生时,浏览器将调用该函数。对于新数据库,回调函数是创建对象存储的好地方。对于预先存在的数据库,如果需要升级,您可以在此处执行设计更改。例如,您可以创建新的对象存储,删除未使用的对象存储,并通过删除和重新创建来修改现有的对象存储。考虑上市 7-3 。

01: init() {
02:     let request = this.idb
03:         .open('web-arcade', 1);
04:
05:     request.onsuccess = (evt: any) => {
06:         console.log("Open Success", evt);
07:     };
08:
09:     request.onerror = (error: any) => {
10:         console.error("Error opening IndexedDB", error);
11:     }
12:
13:     request.onupgradeneeded = function (event: any) {
14:         console.log("version upgrade event triggered");
15:         let dbRef = event.target.result;
16:         dbRef
17:             .createObjectStore("gameComments", { autoIncrement: true });
18:     };
19: }

Listing 7-3onupgradeneeded Event Callback

考虑以下解释:

  • 注意,代码片段重复了清单 7-1 中的init()函数。除了onsuccessonerror回调之外,还包括一个名为onupgradeneeded的事件处理程序。参见第 13 行到第 18 行。

  • 该事件作为参数提供给函数回调。

  • 你可以访问一个对象上事件目标的 IndexedDB 的引用,即target

  • 使用db引用创建一个对象存储。在本例中,您将对象存储命名为gameComments *。*如前所述,如果用户失去连接,您可以使用 IndexedDB 和对象存储来缓存用户评论。

对象存储以键值对的形式保存数据。正如您将在接下来的几节中看到的,数据是使用键来检索的。它是唯一标识存储在 IndexedDB 中的值的主键。以下是创建键的两个选项(用于对象存储中存储的值)。这是在创建对象存储时决定的。参见清单 7-3 中的第 17 行。注意createObjectStore()函数的第二个参数。您可以指定以下两个选项之一:

  • 自动递增 : IndexedDB 管理密钥。它为对象存储中添加的每个新对象创建一个数值和增量。

    dbRef.createObjectStore("gameComments", {
    autoIncrement: true });
    
    
  • Key path :在正在添加的 JSON 对象中指定一个 Key path。因为键值是显式提供的,所以请确保提供唯一的值。重复值会导致插入失败。

    A field called commentId is provided as a keypath. If used, ensure you provide a unique value for commentId.

    dbRef.createObjectStore("gameComments", {
    keypath: 'commentId' });
    
    

Note

只能为 JavaScript 对象提供键路径。因此,创建带有键路径的对象存储会限制它只能存储 JavaScript 对象。但是,使用自动增量,考虑到键是由 IndexedDB 管理的,您可以存储任何类型的对象,包括基本类型。

参见图 7-3 中新创建的gameComments对象库。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-3

gameComments 对象存储和示例值

创建索引

定义对象存储时,可以创建附加索引,这些索引也可以作为唯一性约束。该索引应用于对象存储中持久化的 JavaScript 对象中的字段。考虑下面的代码片段。它解释了对象存储引用上的createIndex API。

objectStoreReference.createIndex('indexName', 'keyPath', {parms})

考虑以下解释:

  • Index name:第一个参数是索引名(任意)。

  • Key path:第二个参数keypath,指定需要在给定的字段上创建索引。

  • Params:您可以为创建索引指定以下参数:

    1. unique:这在 keypath 提供的字段上创建了一个唯一性约束。

    2. multiEntry:应用于数组。

如果为 true,则约束确保数组中的每个值都是唯一的。为数组中的每个元素的索引添加一个条目。

如果为 false,索引将为整个数组添加一个条目。唯一性是在数组对象级别维护的。

gameComments对象存储中,假设每个评论都有一个 ID。要确保 ID 是唯一的,请添加一个索引。考虑上市 7-4 。

1: request.onupgradeneeded = function(event: any){
2:     console.log("version upgrade event triggered");
3:     let dbRef = event.target.result;
4:     let objStore = dbRef
5:       .createObjectStore("gameComments", { autoIncrement: true })
6:
7:     let idxCommentId = objStore.createIndex('IdxCommentId', 'commentId', {unique: true})
8:   };

Listing 7-4Create Index IdxCommentId for the Comment ID

注意,第 7 行使用对象存储引用objStore创建了一个索引。该索引被命名为IdxCommentId。该索引被添加到commentId字段。您可以看到参数unique被设置为 true,这确保了commentId对于每条记录都是不同的。图 7-4 展示了具有新索引的对象存储。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-4

对象存储上的索引 IdxCommentId

浏览器支持

图 7-5 描述了浏览器对全局indexedDB对象(windowObj.indexedDB)的支持。请注意,这些数据是在 Mozilla 网站的 https://developer.mozilla.org/en-US/docs/Web/API/indexedDB 捕获的。对于 web 技术来说,它是一个可靠的开源平台。Mozilla 是开放网络的倡导者,也是包括 Firefox 浏览器在内的安全免费互联网技术的先驱。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-5

window.indexedDB 浏览器支持

另请参考 CanIUse.com,它是浏览器兼容性数据的可靠来源。对于 IndexedDB,使用 URL https://caniuse.com/indexeddb

指数化的局限性 b

虽然 IndexedDB 为浏览器中的客户端持久化和查询提供了一个很好的解决方案,但了解以下限制很重要:

  • 它不支持国际化排序,因此对非英语字符串进行排序可能会很棘手。很少有语言对字符串的排序不同于英语。在撰写本章时,IndexedDB 和所有浏览器都不完全支持本地化排序。如果这个特性很重要,您可能必须从数据库中检索数据,并编写额外的自定义代码来排序。

  • 还不支持全文搜索。

  • IndexedDB 不能被视为数据的真实来源。这是临时存储。在以下情况下,数据可能会丢失或被清除:

    1. 用户重置浏览器或手动清除数据库。

    2. 用户在 Google Chrome 匿名窗口或私人浏览会话(在其他浏览器上)中启动应用。由于浏览器窗口关闭,考虑到这是一个私人会话,数据库将被删除。

    3. 永久存储的磁盘配额是根据一些因素计算的,包括可用磁盘空间、设置、设备平台等。应用可能超出了配额限制,进一步的持久化失败。

    4. 各种情况,包括损坏的数据库、由不兼容的更改导致的数据库升级错误等。

摘要

本章提供了对 IndexedDB 的基本理解,它运行在浏览器的客户端。JavaScript 提供了一个本地 API 来处理 IndexedDB。大多数现代浏览器都支持它。

本章还解释了如何使用AppModule初始化 Angular 服务。在初始化过程中,您为 Web Arcade 创建或打开 IndexedDB 商店。如果用户第一次在浏览器上访问应用,您将创建一个新的 IndexedDB 存储。如果已经存在,则打开预先存在的数据库。

接下来,本章解释了如何使用onupgradeneeded函数回调来创建对象存储和索引。这些是用户首次访问应用时的一次性活动。

Exercise

  • 为创建新游戏创建一个额外的对象存储。加载应用(或 Angular 模块)时执行操作。

  • 创建对象存储以使用指定的 ID 作为键(主)。不要使用自动增量。

  • 在游戏标题上创建一个额外的索引。确保它是唯一的。

八、创建实体用例

在处理数据时,您从数据检索开始。您使用远程 HTTP 服务进行 GET 调用,并在 Angular 应用的屏幕上显示数据。您创建了缓存包括数据调用在内的资源的能力。随着您的进展,应用需要创建、更新和删除数据。在支持脱机功能的应用中,创建、更新和删除操作是复杂的。

本章建立了处理此类场景的用例。它详细描述了如何构建执行创建操作的 Angular 组件和服务。该示例可以很容易地升级为编辑和删除操作。前一章介绍了在浏览器中持久化数据的 IndexedDB。它通常用于管理缓存。本章描述的用例有助于充分利用 IndexedDB 实现。随着本书的深入,下一章将详细介绍如何使用 IndexedDB 执行离线操作,因此您需要理解我们将在本章构建的用例。

在 Web Arcade 中,创建、更新和删除操作在游戏详细信息页面上执行。页面将调用远程 HTTP 服务来保存数据。本章首先解释了前面提到的动作的 HTTP 方法。接下来,它详细介绍了如何创建组件。它为列表组件(显示棋盘游戏列表)和详细信息页面之间的导航引入了 Angular 路由。接下来,它详细描述了我们在开发用例时在游戏细节页面上构建的特性。最后,本章详细介绍了如何构建模拟服务器端服务来支持 Angular 应用。

网络街机:游戏详情页面

游戏详细信息页面显示所选游戏的详细信息。我们使用这个页面来展示一个如何将离线操作与远程 HTTP 服务同步的例子。

当调用远程 HTTP 服务时,HTTP 方法定义要执行的期望动作。考虑以下最常用的 HTTP 方法:

  • GET 检索数据。例如,它检索棋盘游戏列表。

  • POST 提交或创建实体。例如,它创建一个服务来创建一个棋盘游戏,或者在一个实现 POST 方法的游戏上添加用户评论。

  • 放置替换或完全更新实体。例如,考虑一个棋盘游戏实体,该实体具有游戏 ID、游戏标题、游戏描述、具有关于游戏的综合细节的网络链接、起源等字段。对于给定的游戏 ID,使用 PUT 方法替换所有字段。即使一些字段没有改变,您也可以在使用 PUT 方法时再次提供相同的值。

  • PATCH 替换或更新实体上的一些字段。在前面的示例中,考虑只更新原点字段。开发一个带有补丁的 HTTP 服务来更新实体上的 origin 字段。

  • 删除移除或删除实体。

Note

除了前面的 HTTP 方法,还有其他较少使用的 HTTP 方法,包括 HEAD、CONNECT、OPTIONS 和 TRACE。

到目前为止,提供离线访问来获取服务调用。本章使用 IndexedDB 来缓存和同步 POST 服务调用。您可以在其余的 HTTP 方法上使用类似的实现。

在前面的章节中,您创建了一个组件来显示游戏列表。在本章中,您将更新示例,以便通过单击选择游戏来导航到详细信息页面。见图 8-1 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-1

导航至游戏详情页面

游戏详细信息页面有游戏的描述和其他详细信息。它列出了所有用户的评论。它在底部提供了一个提交新评论的表单。参见图 8-2 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-2

游戏详细信息页面上的字段

离线场景

用户可以提交评论。在线时,该服务调用 HTTP 服务来发布新的评论。但是,如果脱机,请使用 IndexedDB 临时保存注释。重新上线后,将评论发布到远程服务。

为游戏细节创建组件

运行以下 Angular CLI 命令创建新组件:

ng g c game-details

在接下来的几节中,您将更新组件以显示游戏细节。但是,该游戏是在早期的游戏列表组件中选择的。游戏细节组件如何知道所选择的游戏?请记住,当用户从棋盘游戏列表中选择时,您将导航到游戏详细信息。列表组件在 URL 中提供选择的游戏 ID 作为query param。考虑以下带有游戏 ID 参数的 URL:

http://localhost:4200/details?gameId=1

选择途径

Angular routing 使 Angular 应用能够利用 URL(查看浏览器中的地址栏)并动态加载内容。您将组件映射到 URL 中的路径。该组件在用户导航到相应路径时加载。

请记住,当您使用 Angular CLI 为 Web Arcade 创建新应用时,路由已经设置好了。这包括一个用于配置自定义路径和 URL 的AppRoutingModule。更新路由配置,以在导航到详细信息页面时首先显示游戏列表组件和游戏详细信息组件(如前面提到的 URL 所示)。考虑app-routing.module.ts中的路由配置,如清单 8-1 所示。

06: const routes: Routes = [{
07:   path: "home",
08:   component: BoardGamesComponent
09: }, {
10:   path: "details",
11:   component: GameDetailsComponent
12: }, {
13:   path: "",
14:   redirectTo: "/home",
15:   pathMatch: "full"
16: }];
17:
18: @NgModule({
19:   imports: [RouterModule.forRoot(routes)],
20:   exports: [RouterModule]
21: })
22: export class AppRoutingModule { }
23:

Listing 8-1Route Configuration

注意,棋盘游戏组件被配置为使用路径/home加载,游戏细节组件被配置为使用路径/details加载,例如http://localhost:4200/homehttp://localhost:4200/details

组件在 HTML 模板中的router-outlet处加载。记住,AppComponent是根组件。更新路由器出口,以便在用户导航到相应的 URL(路径)时加载前面提到的组件。请考虑以下简短片段:

1: <div class="container align-center">
2:     <router-outlet></router-outlet>
3: </div>

导航到游戏详情页面

接下来,更新列表组件(BoardGamesComponent)以导航到详细信息页面。编辑组件的 HTML 模板(src/app/components/board-games/board-games.component.html)。见清单 8-2 。

01: <mat-toolbar color="primary">
02:     <mat-toolbar-row>Game List</mat-toolbar-row>
03: </mat-toolbar>
04: <div>
05:     <ng-container *ngFor="let game of (games | async)?.boardGames">
06:         <a (click)="gameSelected(game)">
07:             <mat-card>
08:                 <mat-card-header>
09:                     <h1>
10:                         {{game.title}}
11:                     </h1>
12:                 </mat-card-header>
13:                 <mat-card-content>
14:                     <span>{{game.alternateNames}}</span>
15:                     <div>{{game.origin}}</div>
16:                     <div>{{game.description}}</div>
17:                 </mat-card-content>
18:             </mat-card>
19:         </a>
20:     </ng-container>
21: </div>

Listing 8-2Board Games Component Template

考虑以下解释:

  • 请参见第 6 行和第 19 行。每个游戏(卡片)都包含在一个超级链接元素<a></a>中。

  • 注意,第 5 行使用ngFor指令遍历游戏列表。变量game代表迭代中的一个游戏。

  • 在第 6 行,点击事件由gameSelected()函数处理。注意游戏变量是作为参数传入的。这是当前迭代中包含游戏数据的变量。

gameSelected函数(在游戏细节组件的 TypeScript 文件中定义)导航到游戏细节页面,如清单 8-3 所示。

01:
02: export class BoardGamesComponent implements OnInit {
03:
04:   constructor(private router: Router) { }
05:
06:   gameSelected(game: BoardGamesEntity){
07:    this.router.navigate(['/details'], {queryParams: {gameId: game.gameId}})
08:   }
09:
10: }

Listing 8-3Navigate to the Details Page

考虑以下解释:

  • 第 4 行注入了一个路由器服务实例。

  • 第 7 行使用一个路由器实例导航到详细信息页面。

  • 注意,提供了一个查询参数游戏 ID。游戏对象是从模板传入的。参见之前的清单 8-2 。

  • 在当前列表组件(BoardGamesComponent)中选择的game-id将用于游戏详情组件。它检索所选游戏的完整细节。

接下来,从游戏细节组件中的 URL 检索游戏 ID,如清单 8-4 所示。

01: import { Component, OnInit } from '@angular/core';
02: import { ActivatedRoute } from '@angular/router';
03:
04: @Component({
05:   selector: 'wade-game-details',
06:   templateUrl: './game-details.component.html',
07:   styleUrls: ['./game-details.component.sass']
08: })
09: export class GameDetailsComponent implements OnInit {
10:   game: BoardGamesEntity;
      commentsObservable = new Observable<CommentsEntity[]>();
11:   constructor(private router: ActivatedRoute, private gamesSvc: GamesService) { }
12:
13:   ngOnInit(): void {
14:     this.router
15:       .queryParams
16:       .subscribe( r =>
17:         this.getGameById(r['gameId']));
18:   }
19:
20:   private getGameById(gameId: number){
21:     this.gamesSvc.getGameById(gameId).subscribe(
22:       (res: BoardGamesEntity) => {
23:         this.game = res;
24:         this.getComments(res?.gameId);
25:       });
26:   }
27: }

28:
29: private getComments(gameId: number){
30:     this.commentsObservable = this.gamesSvc.getComments(gameId);
31: }
32:

Listing 8-4Retrieve Game ID from Query Params

考虑以下解释:

  • 该示例使用ActivatedRoute服务读取 URL 中的查询参数。

  • 第 2 行从 Angular 的路由器模块导入了ActivatedRoute服务。接下来,第 11 行将服务注入到组件中。服务实例被命名为router

  • 参见第 13 行和第 18 行之间的ngOnInit()功能。该函数在构造函数之后和组件初始化期间被调用。

  • 请参见第 14 行和第 17 行之间的代码。注意,代码使用了router.queryParamsqueryParams是可观测的。订阅它以访问查询参数。

  • queryParams订阅的结果被命名为r。以结果字段的形式访问游戏 ID,r['gameId']。现在,你可以访问BoardGamesComponent提供的游戏 ID。

  • 将游戏 ID 作为函数参数传递给私有函数getGameById()。第 20 到 27 行定义了这个函数。

  • getGameById()函数调用另一个同名函数getGameById(),该函数被定义为GamesService的一部分。它返回一个可观察对象,订阅它会从 HTTP 服务返回结果。远程 HTTP 服务通过GameId提供游戏详情。

  • 在第 23 行中,您将来自 HTTP 服务的结果设置到组件上的一个游戏对象上,该对象在 HTML 模板中使用。

  • HTML 模板向用户显示游戏细节。

  • 接下来,调用私有函数getComments(),该函数检索对给定棋盘游戏的评论。参见第 29 行到第 31 行。它调用GameService实例上的getComments()函数,该函数从远程 HTTP 服务获取数据。

  • 将来自 HTTP 服务的结果设置到组件上的一个commentsObservable对象上,该对象在 HTML 模板中使用。HTML 模板显示了注释。

总之,清单 8-4 检索游戏细节和注释,并将它们设置在一个类变量上。在第 23 行,类别字段game已经选择了游戏标题、描述等。接下来,在第 30 行,类字段commentsObservable有一个注释列表。这些是不同用户对所选游戏的评论。接下来,查看呈现游戏细节和评论的 HTML 模板代码。考虑上市 8-5 。

01:
02: <!-- Toolbar to provide a title-->
03: <mat-toolbar [color]="toolbarColor">
04:     <h1>Game Details</h1>
05: </mat-toolbar>
06:
07:
08: <!-- This section shows game title and description-->
09: <div *ngIf="game">
10:     <h2>{{game.title}}</h2>
11:     <div>{{game.description}}</div>
12: </div>
13:
14:
15: <!-- Following section shows comments made by users -->
16: <div>
17:     <strong>
18:         Comments
19:     </strong>
20:     <hr />
21:     <mat-card *ngFor="let comment of commentsObservable | async">
22:         <mat-card-header>
23:             <strong>
24:                 {{comment.title}}
25:             </strong>
26:         </mat-card-header>
27:         <mat-card-content>
28:             <div>{{comment.comments}}</div>
29:             <div><span>{{comment.userName}}</span> <span class="date">{{comment.timeCommented | date}}</span></div>
30:         </mat-card-content>
31:     </mat-card>
32: </div>

Listing 8-5Game Details Component HTML Template

考虑以下解释:

  • 第 10 行和第 11 行显示了游戏标题和描述。第 9 行检查游戏对象是否被定义(用一个ngIf指令)。这是为了避免在从服务获取游戏数据之前组件出错。可以想象,当组件第一次加载时,服务调用仍在进行中。游戏标题、描述和其他字段尚不可用。从服务中检索后,ngIf条件变为真,并显示数据。

  • 见第 21 行。它遍历注释。第 24 行显示了注释标题。第 28 和 29 行显示了评论描述、用户名和评论时间戳。

  • 参见清单 8-4 。commentsObservable属于Observable类型。因此,清单 8-5 中的第 27 行使用了| async

  • 请注意以下 HTML 样式决定:

    • 清单使用 Angular Material 的工具栏组件(mat-toolbar)来显示标题。请参见第 3 行到第 5 行。

    • 每条评论都显示在一张有角的材料卡片上。参见第 21 至 31 行的组件mat-cardmat-card-headermat-card-content

清单 8-4 使用服务中的两个函数:getGameById()getComments().可以想象,Angular service 函数调用远程 HTTP 服务来获取数据。

记住,我们开发了模拟服务来演示远程 HTTP 服务功能。你为棋盘游戏返回了模拟 JSON。对于前面的两个函数,getGameById()getComments(),您将扩展 Node.js Express 服务。这将在本章后面的“模拟 HTTP 服务的更新”一节中介绍

Note

真实世界的服务与 Oracle、Microsoft SQL Server 或 MongoDB 等主流数据库集成,并在其中创建、检索和更新数据。这超出了本书的范围。为了确保代码示例的功能,我们创建了模拟服务。

然而,正如您在前面的代码示例中看到的,这些组件并不直接与远程 HTTP 服务集成。您使用一个 Angular 服务,它使用其他服务从组件中抽象出这个功能。组件纯粹关注应用的表示逻辑。

记住,您创建了一个名为GamesService的服务,用于封装检索游戏数据的代码。接下来,更新服务以包含之前的两个函数getGamesById()getComments(),如清单 8-6 所示。

01: @Injectable({
02:     providedIn: 'root'
03:   })
04:   export class GamesService {
05:
06:     constructor(private httpClient: HttpClient) { }
07:
08:     getGameById(gameId: number): Observable<BoardGamesEntity>{
09:       return this
10:         .httpClient
11:         .get<BoardGamesEntity>(environment.boardGamesByIdServiceUrl,{
12:           params: {gameId}
13:         });
14:     }
15:
16:     getComments(gameId: number): Observable<CommentsEntity[]>{
17:         return this
18:           .httpClient
19:           .get<CommentsEntity[]>(environment.commentsServiceUrl,{
20:             params: {gameId}
21:           });
22:       }
23:
24: }

Listing 8-6Game Service Invoking Remote HTTP Services

考虑以下解释:

  • 第 6 行注入了HttpClient服务。它是 Angular 提供的一种开箱即用的服务,可以进行 HTTP 调用。

  • 请参见第 10 行和第 18 行。这些函数使用HttpClient实例httpClient.来调用远程 HTTP 服务。

  • 这两个函数都使用 GET HTTP 方法。第一个参数是端点 URL。

  • 建议配置 URL(而不是在应用中对它们进行硬编码)。因此,URL 在环境文件中被更新。环境文件见清单 8-7 。

  • 注意,这两个函数都需要gameId作为参数。请参见第 8 行和第 16 行。

  • 游戏作为查询参数传递给远程 HTTP 服务。请参见第 12 行和第 20 行。

  • 请注意,getGameById()返回了一个类型为BoardGamesEntity (Observable<BoardGamesEntity>)的可观察对象。远程服务应该返回一个符合BoardGamesEntity中指定的接口契约的 JSON 响应。接口定义见清单 8-8 (a)。

  • getComments()返回一个CommentsEntity ( Observable<CommentsEntity>)类型的可观察值。由于从服务中检索到多个注释,所以它是一个数组。远程服务应该返回一个符合CommentsEntity中指定的接口契约的 JSON 响应。接口定义见清单 8-8 (b)。

  • 远程服务调用返回一个可观察的,因为它们是异步的。浏览器一调用数据,服务就不会立即返回数据。代码不会等到结果返回。因此,一旦远程服务中的数据可用,就会调用订户回调函数。

1: export interface CommentsEntity {
2:     title: string;
3:     comments: string;
4:     timeCommented: string;
5:     gameId: number;
6:     userName:string;
7: }

Listing 8-8(b)TypeScript Interface CommentsEntity

01: export interface BoardGamesEntity {
02:     gameId: number;
03:     age: string;
04:     link: string;
05:     title: string;
06:     origin: string;
07:     players: string;
08:     description: string;
09:     alternateNames: string;
10: }

Listing 8-8(a)TypeScript Interface BoardGamesEntity

08: export const environment = {
09:   boardGameServiceUrl: `/api/board-games`,
10:   commentsServiceUrl: '/api/board-games/comments',
11:   boardGamesByIdServiceUrl: '/api/board-games/gameById',
12:   production: false,
13: };
14:

Listing 8-7Environment File with Additional Endpoints

Note

两个文件中需要 URL:src/environments/environment.tssrc/environments/environment.prod.tsenvironment.ts文件用于开发构建(例如,yarn start)。environment.prod.ts文件用于生产构建(例如yarn buildng build)。

添加注释

参见图 8-2 。请注意数据表单的最后一部分添加了注释。它使用户能够添加关于棋盘游戏的评论。到目前为止,您主要从事数据检索工作。这是一个创建实体的例子,即一个评论实体。如前所述,您使用 HTTP POST 方法在后端系统中创建一个实体。

考虑清单 8-9 ,它显示了 Add Comment HTML 模板。

01: <div>
02:     <mat-form-field>
03:         <mat-label>Your name</mat-label>
04:         <input matInput type="text" placeholder="Please provide your name" (change)="updateName($event)">
05:     </mat-form-field>
06: </div>
07:
08: <div>
09:     <mat-form-field>
10:         <mat-label>Comment Title</mat-label>
11:         <input matInput type="text" placeholder="Please provide a title for the comment" (change)="updateTitle($event)">
12:     </mat-form-field>
13: </div>
14:
15: <div>
16:     <mat-form-field>
17:         <mat-label>Comment</mat-label>
18:         <textarea name="comment" id="comment" placeholder="Write your comment here" (change)="updateComments($event)" matInput cols="30" rows="10"></textarea>
19:     </mat-form-field>
20: </div>
21:
22: <button mat-raised-button color="primary" (click)="submitComment()">Submit</button>

Listing 8-9Add Comment HTML Template

考虑以下解释:

  • 注意第 1 行到第 20 行。他们为用户名、标题和评论详细信息创建表单字段。

  • 该列表使用材料设计组件和指令。第 4、11 和 18 行分别对元素输入和文本区域使用matInput。这些 Angular 材料元素需要材料设计输入模块。参见清单 8-10 ,第 1 行和第 8 行。

  • mat-form-field组件封装了表单字段和标签。组件mat-label显示了表单字段的标签。

  • 第 4、11 和 18 行使用与函数updateName()updateTitle()updateComments().绑定的变更事件数据,清单 8-11 将表单字段的值设置为组件中的一个变量。每当表单域发生更改(用户键入值)时,change 事件就会发生。

  • 在清单 8-9 中,注意第 22 行 HTML 模板中的点击事件数据绑定。当用户点击按钮时,TypeScript 函数submitComments()被调用。

01: import { MatInputModule } from '@angular/material/input';
02:
03: @NgModule({
04:   declarations: [
05:     AppComponent,
06:   ],
07:   imports: [
08:     MatInputModule,
09:     BrowserAnimationsModule
10:   ],
11:   bootstrap: [AppComponent]
12: })
13: export class AppModule { }
14:

Listing 8-10Import Angular Material Input Module

考虑为组件的 TypeScript 代码列出 8-11 。它包括“注释”表单中的“更改事件处理程序”和“单击事件处理程序”。

01: import { Component, OnInit } from '@angular/core';
02: import { MatSnackBar } from '@angular/material/snack-bar';
03: import { GamesService } from 'src/app/common/games.service';
04:
05: @Component({
06:   selector: 'wade-game-details',
07:   templateUrl: './game-details.component.html',
08:   styleUrls: ['./game-details.component.sass']
09: })
10: export class GameDetailsComponent implements OnInit {
11:
12:   name: string = "";
13:   title: string = "";
14:   comments: string = "";
15:
16:   constructor( private gamesSvc: GamesService,
17:     private snackbar: MatSnackBar) { }
18:
19:   updateName(event: any){
20:     this.name = event.target.value;
21:   }
22:
23:   updateTitle(event: any){
24:     this.title = event.target.value;
25:   }
26:
27:   updateComments(event: any){
28:     this.comments = event.target.value;
29:   }
30:
31:   submitComment(){
32:       this
33:         .gamesSvc
34:         .addComments(this.title, this.name, this.comments, this.game.gameId)
35:         .subscribe( (res) => {
36:           this.snackbar.open('Add comment successful', 'Close');
37:         });
38:   }
39:
40: }

Listing 8-11Comments Form Handlers

考虑以下解释:

  • 参见第 19 至 29 行的功能updateName()updateTitle()updateComments()。请记住,它们是在表单字段的变更事件中调用的。注意函数定义使用了event.target.value。事件的目标指向表单域(DOM 元素)。该值返回用户键入的数据。

  • 这些值被设置为类变量name(用户名)、title(评论标题)和comments(评论描述)。

  • 提交按钮的点击事件是绑定到submitComment()函数的数据。请参见第 32 至 38 行。注意,它调用了服务GameService实例(gameSvc)上的addComments()函数。在第 16 行,GameService被注入到组件中使用。

  • 注意,服务函数需要一个参数列表,包括用户名、标题和描述。先前捕获的值(使用 change 事件处理程序)被传递到服务函数中。

  • addComments()调用服务器端 HTTP 服务。如果添加注释动作成功,将调用可观察对象的成功回调。它显示一条成功消息,提供关于添加评论操作的反馈。

清单 8-12 显示了GameService的实现。清单主要关注addComments()动作。

01: import { Injectable } from '@angular/core';
02: import { HttpClient } from '@angular/common/http';
03: import { environment } from 'src/environments/environment';
04:
05:
06: @Injectable({
07:   providedIn: 'root'
08: })
09: export class GamesService {
10:
11:   constructor(private httpClient: HttpClient) { }
12:
13:   addComments(title: string, userName: string, comments: string, gameId: number, timeCommented = new Date()){
14:     return this
15:       .httpClient
16:       .post(environment.commentsServiceUrl, [{
17:         title,
18:         userName,
19:         timeCommented,
20:         comments,
21:         gameId
22:       }]);
23:   }
24: }

Listing 8-12GameService Implementation

考虑以下解释:

  • 第 11 行注入了HttpClient服务。这是 Angular 提供的现成服务,用于进行 HTTP 调用。

  • 在第 14 行和第 22 行,函数使用了名为httpClient.HttpClient实例,这调用了远程 HTTP 服务。

  • 在第 16 行,注意您正在进行一个 HTTP POST 调用。第一个参数是服务 URL。考虑到 URL 是一个配置工件,它在环境文件中被更新。

  • 第二个参数是 POST 方法的请求体。参见图 8-5 了解这些值如何在网络上转换为请求体。

Note

一个评论 URL 用于两个操作,检索和创建评论。RESTful 服务使用 HTTP 方法 GET 进行检索。对于创建操作,POST HTTP 方法使用相同的 URL。

模拟 HTTP 服务的更新

新组件需要来自远程 HTTP 服务的附加数据和特性。本节详细介绍了对模拟服务的更改。在实际应用中,这些服务和功能是通过查询和更新数据库来开发的。由于这超出了本书的范围,我们将开发模拟服务。

按 ID 过滤游戏详情

游戏细节组件一次需要一个游戏细节。请记住,在前面的章节中,您开发了一个返回所有棋盘游戏的服务。本节详细介绍了如何通过 ID 检索游戏数据。

记住,我们使用mock-data/board-games.js来表示所有棋盘游戏相关的端点。添加一个新的端点,它通过 ID 检索游戏。命名为/gameById,如清单 8-13 所示。

1: var express = require('express');
2: var router = express.Router();
3: var dataset = require('../data/board-games.json');
4:
5: router.get('/gameById', function(req, res, next){
6:     res.setHeader('Content-Type', 'application/json');
7:     res.send(dataset
          .boardGames
          .find( i => +i.gameId === +req.query.gameId));
8: });

Listing 8-13Filter Game by an ID

考虑以下解释:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-3

通过 ID 过滤游戏

  • 第 7 行通过游戏 ID 过滤棋盘游戏。语句dataset .boardGames.find( i => +i.gameId === +req.query.gameId)返回给定 ID 的游戏细节。通常,我们期望一个游戏有一个 ID。在另一个不同的场景中,如果您预期不止一个结果,请使用filter()函数而不是find()

  • 来自find()函数的结果作为参数传递给响应对象上的send()函数(变量名res)。这将结果返回给客户端(浏览器)。见图 8-3 。

    • 见第 3 行。模拟服务从数据目录中的模拟 JSON 对象检索棋盘游戏列表。

    • 见第 5 行。这个过滤器端点的 HTTP 方法是 GET。

    • 见第 6 行。响应内容类型设置为 JSON,这是 Angular 服务和组件的现成格式。

Note

注意第 7 行的+号。这是 JavaScript 中一种将字符串大小写转换为数字的方法。

正在检索注释

游戏详情页面列出评论,如图 8-2 所示。注意游戏描述下面的评论列表。本节详细介绍了如何创建一个模拟服务器端服务来检索注释。

该服务返回对给定游戏的评论。它使用游戏 ID 的查询参数。考虑一个示例 URL,http://localhost:3000/api/board-games/comments?gameId=1。见图 8-4 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-4

注释端点

真实世界的服务与数据库相集成,以有效地存储和查询数据。如前所述,它是一个模拟服务。因此,它从文件中读取注释。考虑在mock_sevices/routes/board-games.js中列出 8-14 。board-games.js文件是合适的,因为它包含了所有与棋盘游戏相关的端点。评论在一个棋盘游戏上。

01: var fs = require('fs');
02: var express = require('express');
03: var router = express.Router();
04:
05: router.get('/comments', function(req, res){
06:     fs.readFile("data/comments.json", {encoding: 'utf-8'},  function(err, data){
07:         let comments = [];
08:         if(err){
09:             return console.log("error reading from the file", err);
10:         }
11:         res.setHeader('Content-Type', 'application/json');
12:         comments = JSON.parse(data);
13:         comments = Object.values(comments).filter( i => {
14:
15:             return +i.gameId === +req.query.gameId
16:         });
17:         res.send(comments);
18:     });
19: });

Listing 8-14An Endpoint to Retrieve Comments

考虑以下解释:

  • 第 5 行创建了一个端点,它响应一个 HTTP 方法 GET。快速路由器实例上的get()功能使您能够创建端点。

  • 第 6 行从磁盘上的一个文件中检索当前的注释列表,data/comments.json

  • fs模块上的readFile()功能(用于“文件系统”)是异步的。您提供了一个回调函数,当 API 成功读取一个文件或错误时调用该函数。在清单 8-14 中,注意第 6 行和第 18 行之间的回调函数。

  • 第一个参数err表示错误,第二个参数data包含文件的内容。请参见第 8 行到第 10 行。如果返回一个错误,它将被记录下来,并将控制返回到函数之外。

  • 假设读取文件时没有错误,文件内容包括属于系统中所有游戏的完整评论列表。该服务预计只返回给定游戏的评论。因此,你通过游戏 ID 过滤评论。参见第 13 到 16 行的代码,它创建了一个过滤器。在 JavaScript 数组对象上定义了filter()函数。第 15 行的谓词测试数组中的每一项。返回带有给定游戏 ID 的评论。

  • 参见第 17 行,该行用过滤后的注释响应客户机(例如,浏览器)。

添加注释

游戏详情页面允许用户评论,如图 8-2 所示。该表单包含用户名、标题和详细评论字段。本节详细介绍了如何创建一个模拟服务器端服务来保存注释。

添加注释是通过创建操作完成的。您正在创建一个新的注释实体。请记住,POST 方法适用于创建实体。POST 方法有一个请求体,其中包含一个由 Angular 应用创建的注释列表(由用户键入)。参见图 8-5 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-5

创建注释端点

考虑在mock_sevices/routes/board-games.js中列出 8-15 。board-games.js文件是合适的,因为它包含了所有与棋盘游戏相关的端点。用户在评论桌游。

01: var fs = require('fs');
02: var express = require('express');
03: var router = express.Router();
04:
05: router.post('/comments', function(req, res){
06:     let commentsData = [];
07:     try{
08:         fs.readFile("data/comments.json", {encoding: 'utf-8'},  function(err, data){
09:             if(err){
10:                 return console.log("error reading from the file", err);
11:             }
12:             commentsData = commentsData.concat(JSON.parse(data));
13:             commentsData = commentsData.concat(req.body);
14:
15:             fs.writeFile("data/comments.json", JSON.stringify(commentsData), function(err){
16:                 if(err){
17:                     return console.log("error writing to file", err);
18:                 }
19:                 console.log("file saved");
20:             });
21:         });
22:         res.send({
23:             status: 'success'
24:         });
25:     }catch(err){
26:         console.log('err2', err);
27:         res.sendStatus(200);
28:     }
29: });

Listing 8-15A POST Endpoint to Create Comments

考虑以下解释:

  • 第 5 行创建了一个端点,它响应 HTTP POST 方法。快速路由器实例上的post()功能使您能够创建端点。

  • 端点需要将新的注释附加到当前的注释列表。文件data/comments.json有一个当前注释列表的数组。

  • fs模块上的readFile()功能是异步的。您提供了一个回调函数,当 API 成功读取一个文件或错误时调用该函数。在清单 8-15 中,注意第 8 到 21 行的回调函数。

  • 第一个参数err表示错误,第二个参数data包含文件内容。请参见第 9 行到第 11 行。如果返回一个错误,它将被记录下来,并将控制返回到函数之外。

  • 假设读取文件没有错误,第 12 行将文件中的注释添加到一个名为commentsData的局部变量中。

  • 接下来,第 13 行将请求对象上的新注释列表连接到commentsData变量。如前所述,POST 方法有一个请求体。它包括一个由 Angular 应用提供的注释列表。

  • 注释的综合列表被写回到文件中。参见第 15 行,它将整个注释列表写入文件。

摘要

本章基于 Web Arcade 使用案例。这对理解我们将在下一章构建的离线函数至关重要。到目前为止,在处理数据时,您执行了数据检索和缓存。本章为创建、更新和删除场景建立了一个用例。

Exercise

  • 游戏详情页面只显示一个棋盘游戏的标题和描述。然而,模拟服务和 TypeScript 接口包括许多附加字段,包括来源、别名、推荐的玩家数量等。包括游戏详细信息页面上的附加字段。

  • 如果操作成功,添加注释功能会显示一条 Snackbar 组件消息(参见清单 8-11 ,第 36 行)。该示例不显示错误消息。更新代码示例以显示 Snackbar 组件错误警报。

  • 在游戏详细信息页面上实现一个后退按钮,以导航回列表屏幕。

九、离线创建数据

在本书的前面,我们开始在 Angular 应用中集成 IndexedDB。我们还建立了一个离线创建数据的用例,即用户评论。假设一个用户在一个游戏详情页面上,并试图添加评论。但是,用户无法访问网络。Web Arcade 应用具有弹性,因为它将评论临时保存在客户端设备/浏览器上。当用户重新上线时,应用会在线同步评论。

本章详细阐述了如何离线创建数据。它以识别应用是在线还是离线的指令开始。您使用状态来确定如何访问服务器端服务或使用本地 IndexedDB 存储。接下来,本章详细介绍了如何在脱机状态下向 IndexedDB 添加注释。它详细说明了如何向用户提供应用脱机但数据被临时保存的反馈。

然后,本章介绍了一旦应用恢复在线,如何将离线注释与服务器端服务同步。记住,服务器端数据库和服务是数据的真实来源。IndexedDB 是临时的,为用户提供无缝体验。

在线和离线添加评论

前一章描述了如何添加注释。提交操作调用服务器端 HTTP 端点。如果设备离线并失去网络连接,典型的 web 应用会显示一个错误。一旦联机,用户可能必须重试该操作。如前所述,Web Arcade 使用 IndexedDB,临时保存数据,并在远程服务可用时进行同步。

用 Getter 识别联机/脱机状态

要识别设备(和浏览器)是否在线,请使用 navigator 对象上的 JavaScript API。它是window对象的只读属性。字段onLine返回当前状态,如果在线则为真,如果离线则为假。

谷歌 Chrome 上的开发者工具提供了一个降低网速的选项。这有助于应用评估其性能和用户体验。见图 9-1 。这些工具将onLine字段值打印在导航器对象上。请注意,浏览器窗口是离线节流的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-1

Google Chrome 开发者工具,在控制台上打印在线状态

Note

您可以在自己选择的浏览器上运行类似的命令。图 9-1 显示的是谷歌 Chrome,是任意选择的。

记住,我们创建了一个名为IdbStorageAccessService的服务来封装对 IndexedDB 的访问。联机/脱机状态决定了可以访问 IndexedDB 的组件。因此,您应该包含代码行来确定服务中的在线/离线状态。

Window服务注入IdbStorageAccessService,如清单 9-1 第 3 行所示。

1: @Injectable()
2: export class IdbStorageAccessService {
3:   constructor(
          private windowObj: Window) {
4:     // this.create();
5:   }
6: }

Listing 9-1Inject the Window Service

确保提供了Window服务。关于 Web Arcade 应用,请参见清单 9-2 ,第 10 行和第 15 行。您为Window服务提供了一个全局变量window,如第 14 行所示。这提供了对有用属性的访问,如documentnavigator等。

01: @NgModule({
02:     declarations: [
03:       AppComponent,
04:       // ...
05:     ],
06:     imports: [
07:       BrowserModule,
08:       // ...
09:     ],
10:     providers: [
11:       IdbStorageAccessService,
12:       {
13:         provide: Window,
14:         useValue: window
15:       }
16:     ],
17:     bootstrap: [AppComponent]
18:   })
19:   export class AppModule { }

Listing 9-2Provide the Window Service

IdbStorageAccessService *中创建一个名为IsOnline的 getter 函数。*服务实例可以使用IsOnline字段来获取浏览器的状态。代码在服务中被抽象。见清单 9-3 。

1: get IsOnline(){
2:     return this.windowObj.navigator.onLine;
3: }

Listing 9-3IsOnline Getter as Part of IdbStorageAccessService

添加在线/离线事件监听器

当应用联机或脱机时,您可能会遇到需要执行某个操作的情况。窗口对象(以及窗口服务)提供事件onlineoffline。初始化时将这些事件添加到IdbStorageAccessService。事件处理程序回调函数在事件发生时被调用。

清单 9-4 在浏览器控制台上打印一条包含事件数据的消息。您可以在事件触发时执行操作。具体参见第 8 至 11 行和第 13 至 16 行。

01: @Injectable()
02: export class IdbStorageAccessService {
03:
04:   constructor(private windowObj: Window) {
05:   }
06:
07:   init() {
08:     this.windowObj.addEventListener("online", (event) => {
09:       console.log("application is online", event);
10:       // Perform an action when online
11:     });
12:
13:     this.windowObj.addEventListener('offline', (event)=> {
14:         console.log("application is offline", event)
15:         // Perform an action when offline
16:     });
17:   }
18: }

Listing 9-4Online and Offline Events

图 9-2 显示了结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-2

线上和线下活动

向索引添加注释 b

记住,当需要时,我们打算在 IndexedDB 中缓存注释。考虑到IdbStorageAccessService从应用的其余部分抽象出访问数据库的任务,增加服务并添加一个在 IndexedDB 中缓存注释的功能。但是在我们开始之前,清单 9-5 显示了到目前为止服务的快速回顾。

01: @Injectable()
02: export class IdbStorageAccessService {
03:
04:   idb = this.windowObj.indexedDB;
05:
06:   constructor(private windowObj: Window) {
07:   }
08:
09:   init() {
10:
11:     let request = this.idb.open('web-arcade', 1);
12:
13:     request.onsuccess = (evt:any) => {
14:       console.log("Open Success", evt);
15:
16:     };
17:
18:     request.onerror = (error: any) => {
19:       console.error("Error opening IndexedDB", error);
20:     }
21:
22:     request.onupgradeneeded = function(event: any){
23:       let dbRef = event.target.result;
24:       let objStore = dbRef
25:         .createObjectStore("gameComments", { autoIncrement: true })
26:
27:       let idxCommentId = objStore.createIndex('IdxCommentId', 'commentId', {unique: true})
28:     };
29:
30:     this.windowObj.addEventListener("online", (event) => {
31:       console.log("application is online", event);
32:       // Peform an action when online
33:     });
34:
35:     this.windowObj.addEventListener('offline', (event) => {
36:         console.log("application is offline", event)
37:         // Perform an action when offline
38:     });
39:
40:   }
41: }

Listing 9-5IdbStorageAccessService

到目前为止,该服务创建了对 IndexedDB 的引用,打开了一个新的数据库,并创建了一个对象存储和一个索引。考虑下面对清单 9-5 的详细解释:

  • 在第 4 行,在一个类变量上设置了一个 IndexedDB 引用,即idb

  • 接下来,在init()函数(它初始化服务并出现在第 11 行)中,对idb对象运行open()函数。它返回IDBOpenDBRequest对象的一个对象。

  • 如果这是用户第一次在浏览器上打开应用,它会创建一个新的数据库。

    1. 第一个参数是数据库的名称web-arcade

    2. 第二个参数(值为 1)指定数据库的版本。可以想象,应用的新更新会导致 IndexedDB 结构的变化。IndexedDB API 使您能够随着版本的变化升级数据库。

对于再次访问的用户,数据库已经创建好,并且可以在浏览器上使用。open()函数试图打开数据库。

  1. IndexedDB APIs 是异步的。第 11 行的打开操作没有完成。您为成功和失败场景提供了一个函数回调。它们作为打开操作的结果被调用。

    1. 注意第 13 到 16 行的onsuccess()函数回调,如果打开数据库操作成功,就会调用这个函数。

    2. 如果 open database 动作失败,则调用第 18 到 20 行的onerror()函数回调。

    3. open 函数调用返回IDBOpenDBRequest *。*之前的回调函数onsuccessonerror被提供给这个返回的对象。

  2. 参见第 22 到 28 行的代码,其中onupgradeneeded在创建或打开 IndexedDB 后被触发。您提供了一个回调函数,当此事件发生时,浏览器会调用该函数。onupgradeneeded事件的意义是什么?

    1. 对于新数据库,回调函数是创建对象存储的好地方。在当前用例中,您创建一个对象存储来保存游戏评论。你给它取名gameComments

    2. 对于预先存在的数据库,如果需要升级,您可以在此处执行设计更改。

  3. 最后,在第 30 到 38 行,查看当浏览器联机/脱机时,联机和脱机事件的函数回调。

Angular 服务IdbStorageAccessService需要对数据库web-arcade的引用。你用它来创建一个交易。使用 IndexedDB,您需要一个事务来执行创建、检索、更新和删除(CRUD)操作。第 11 行的语句,this.idb.open('web-arcade',1)函数调用,试图打开一个数据库,即web-arcade。如果成功,您可以访问数据库引用作为onsuccess()函数回调的一部分。考虑清单 9-6 。

01: @Injectable()
02: export class IdbStorageAccessService {
03:
04:   idb = this.windowObj.indexedDB;
05:   indexedDb: IDBDatabase;
06:   init() {
07:
08:     let request = this.idb.open('web-arcade', 1);
09:
10:     request.onsuccess = (evt:any) => {
11:       console.log("Open Success", evt);
12:       this.indexedDb = evt?.target?.result;
13:     };
14:   }
15: }

Listing 9-6Access the web-arcade Database Reference

考虑以下解释:

  • 见第 5 行。indexedDB是一个可以跨服务访问的类变量。

  • 在成功打开web-arcade数据库时会分配一个值,如第 12 行所示。数据库实例在event(event.target.result)对象的target属性的result变量中可用。

接下来,添加一个函数在 IndexedDB 中创建注释。这将创建一个 IndexedDB 事务,访问对象存储,并添加一条新记录。考虑列出 9-7 。

01: addComment(title: string, userName: string, comments: string, gameId: number, timeCommented = new Date()){
02:     let transaction = this.indexedDb
03:       .transaction("gameComments", "readwrite");
04:
05:       transaction.objectStore("gameComments")
06:         .add(
07:           {
08:             title,
09:             userName,
10:             timeCommented,
11:             comments,
12:             gameId,
13:             commentId: new Date().getTime()
14:           }
15:         )
16:
17:
18:       transaction.oncomplete = (evt) => console.log("add comment transaction complete", evt);
19:       transaction.onerror = (err) => console.log("add comment transaction errored out", err);
20:
21:   }

Listing 9-7Add a New Record in IndexedDB

考虑以下解释:

  • 接下来,对 IndexedDB 执行添加记录操作。使用事务对象访问需要执行添加操作的对象存储。参见第 5 行,它使用了objectStore函数来访问对象store()

  • 请参见第 6 行和第 15 行。您存储了一个 JavaScript 对象,包括评论标题、用户名、评论时间、评论描述、添加评论的游戏 ID 和唯一的评论 ID。为了确保唯一性,可以使用时间值。您可以使用任何唯一的值。

  • 正如您在 IndexedDB 中看到的,数据库操作是异步的。add()函数不会立即添加记录。它最终调用一个成功或错误回调函数。事务具有以下回调函数:

    1. 成功时调用。见第 18 行。它在控制台上打印状态。

    2. onerror:出错时调用。见第 19 行。

  • 首先,用类变量indexedDB创建一个新的事务(在清单 9-6 中创建)。参见第 3 行的事务函数。它需要两个参数:

    1. 需要在其中创建事务的一个或多个对象存储。在这种情况下,您在对象存储库gameComments上创建一个事务。

    2. 指定交易模式,readwrite。IndexedDB 支持三种交易模式,即readonlyreadwriteversionchange、*。*可以想象,readonly帮助检索操作,readwrite帮助创建/更新/删除操作。然而,versionchange模式有助于在 IndexedDB 上创建和删除对象存储。

图 9-3 显示了 IndexedDB 中的一条记录。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-3

索引中的新纪录 b

添加评论的用户体验

记得上一章提到过,UserDetailsComponent通过调用名为addComments *的GameService函数来添加注释。*这将调用服务器端 POST 调用来添加注释。如果应用脱机,它将出错。您向用户显示一个错误反馈,并请求用户重试。

在这一章中,如果浏览器离线,你已经完成了在 IndexedDB 中缓存注释的后台工作。接下来,更新组件以检查应用是在线还是离线,并调用相应的服务函数。考虑清单 9-8 中的代码片段,它来自GameDetailsComponent ( app/components/game-details/game-details.component.ts)。

01: @Component({ /* ... */ })
02: export class GameDetailsComponent implements OnInit {
03:
04:     constructor(private idbSvc: IdbStorageAccessService,
05:         private gamesSvc: GamesService,
07:         private snackbar: MatSnackBar,
08:         private router: ActivatedRoute) { }
09:
10:     submitComment() {
11:         if (this.idbSvc.IsOnline) {
12:             this
13:                 .gamesSvc
14:                 .addComments(/* provide comment fields */)
15:                 .subscribe((res) => {
16:
17:                     this.snackbar.open('Add comment successful', 'Close');
18:                 });
19:         } else {
20:             this.idbSvc.addComment(this.title, this.name, this.comments, this.game.gameId);
21:             this.snackbar.open('Application is offline. We saved it temporarily', 'Close');
22:         }
23:     }
24: }

Listing 9-8Add a Comment in the Game Details Component

考虑以下解释:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-4

指示应用脱机的 Snackbar 组件警报

  • 请注意,在第 21 行,您显示了一条 Snackbar 组件消息,表明应用处于脱机状态。图 9-4 显示结果。

  • 一开始,注射IdbStorageAccessService。见第 4 行。服务实例被命名为idbSvc

  • 第 11 行检查应用是否在线。注意,您使用了在清单 9-3 中创建的IsOnline getter。

    1. 如果为真,继续调用游戏服务函数,addComments()。它调用服务器端服务。

    2. 如果离线,使用IdbStorageAccessService函数addComment(),将注释添加到 IndexedDB。参见清单 9-7 中的实现。

将离线注释与服务器同步

当应用离线时,您可以使用 IndexedDB 将浏览器中的注释缓存在持久存储中。最终,一旦应用重新上线,当用户再次启动应用时,评论需要与服务器端同步。这一节详细介绍了识别应用在线并同步注释记录的实现。

当浏览器获得或失去连接时,window对象上的两个事件onlineoffline被触发。IdbStorageAccessService服务包括onlineoffline事件的事件处理程序。参见清单 9-4 。

接下来,更新在线事件处理程序。考虑以下步骤来将数据与服务器端数据库同步。当应用重新联机时,您需要执行以下操作:

  1. 从 IndexedDB 中检索所有缓存的注释。

  2. 调用服务器端 HTTP 服务,该服务为用户评论更新主数据库。

  3. 最后,清空缓存。删除与远程服务同步的注释。

让我们从前面列表中的第一步开始,从 IndexedDB 中检索所有缓存的注释。下一节详细介绍了从 IndexedDB 检索数据的各种选项和可用的 API。

从 IndexedDB 中检索数据

IndexedDB 提供了以下用于检索数据的 API:

  • getAll():检索对象存储中的所有记录

如前所述,CRUD 操作在事务范围内运行。因此,您将在对象存储上创建一个只读事务(考虑它是一个数据检索操作)。调用getAll() API,它返回IDBRequest,如清单 9-9 所示。

IDBRequest对象上,提供onsuccessonerror回调函数定义。如您所知,几乎所有的 IndexedDB 操作都是异步的。使用getAll()的数据检索不会立即发生。它回调提供的回调函数。

1: let request = this.indexedDb
2: .transaction("gameComments", "readonly")
3: .objectStore("gameComments")
4: .getAll();
5:
6: request.onsuccess = resObject => console.log('getAll results', resObject);
7: request.onerror = err => console.error('Error reading data', err);

Listing 9-9Using getAll()

结果如图 9-5 所示。注意清单 9-9 中第 6 行的成功处理程序。结果变量命名为resObject *。*结果记录可以在resObject ( resObject.target.result)的target属性的result对象上获得。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-5

getAll()结果

  • get(key):按键检索记录。get()函数在对象存储上运行。

类似于getAll(),在对象存储上为get()创建一个只读事务。get() API 返回IDBRequest,如清单 9-10 所示。

处理结果或错误的其余代码是相同的。在IDBRequest对象上,提供onsuccessonerror回调函数定义。如您所知,几乎所有的 IndexedDB 操作都是异步的。使用get()的数据检索不会立即发生。它回调提供的回调函数。

1: let request = this.indexedDb
2:  .transaction("gameComments", "readonly")
3:  .objectStore("gameComments")
4:  .get(30);
5:
6: request.onsuccess = resultObject => console.log('get() results', resultObject);
7: request.onerror = err => console.error('Error reading data', err);

Listing 9-10Using get()

注意清单 9-10 中第 6 行的成功处理程序。结果变量命名为resultObject *。*结果记录在resultObject ( resultObject.target.result)的target属性的结果对象上可用。

  • 光标允许你遍历结果。它允许您一次处理一个记录。我们为注释用例选择了这个选项。它提供了在从 IndexedDB 读取数据时转换数据格式的灵活性。另外两个 API,getAll()get(),需要一个额外的代码循环来转换数据。

如前所述,CRUD 操作在事务范围内运行。因此,您将在对象存储上创建一个只读事务(考虑它是一个数据检索操作)。调用openCursor() API,它返回IDBRequest

同样,处理结果或错误的代码保持不变。在IDBRequest对象上,提供onsuccessonerror回调函数定义。与openCursor()的数据检索是异步的,它调用上述的onsuccessonerror回调函数。

创建一个新的私有函数来检索缓存的注释记录。提供任意名称getAllCachedComments()。在IdbStorageAccessService 中添加清单 9-11 所示的私有函数。

01: private getAllCachedComments() {
02:     return new Promise(
03:       (resolve, reject) => {
04:         let results: Array<{
05:           key: number,
06:           value: any
07:         }> = [];
08:
09:         let query = this.indexedDb
10:           .transaction("gameComments", "readonly")
11:           .objectStore("gameComments")
12:           .openCursor();
13:
14:           query.onsuccess = function (evt: any) {
15:
16:             let gameCommentsCursor = evt?.target?.result;
17:             if(gameCommentsCursor){
18:               results.push({
19:                 key: gameCommentsCursor.primaryKey,
20:                 value: gameCommentsCursor.value
21:               });
22:               gameCommentsCursor.continue();
23:             } else {
24:               resolve(results);
25:             }
26:           };
27:
28:           query.onerror = function (error: any){
29:             reject(error);
30:           };
31:
32:       });
33:   }

Listing 9-11Retrieve Cached Comments from IndexedDB

考虑以下解释:

  • 该函数创建并返回一个承诺。见第 2 行。考虑到数据检索是异步的,您不能立即从getAllCachedComments()函数返回注释记录。一旦游标从 IndexedDB 中检索完数据,该承诺就会得到解决。

  • 第 9 行和第 12 行创建一个只读事务,访问对象存储库gameComments,并打开一个游标。该语句返回一个IDBRequest对象,该对象被分配给一个局部变量query

  • 记住,如果光标能够从对象存储中检索数据,就会调用onsuccess回调。否则,调用onerror回调(第 28 和 30 行)。

  • 参见第 14 至 26 行中定义的onsuccesscallback()

  • event.target.result访问结果。见第 16 行。

Note

evt?.target?.result中的?.语法执行空检查。如果一个属性未定义,它将返回 null,而不是抛出一个错误并使整个函数工作流崩溃。前面的语句可能返回结果或 null。

  • 如果结果已定义,则将数据转换为键值对格式。将对象添加到名为result的局部变量中。

  • 记住,光标一次只作用于单个注释记录(不像get()getAll())。要将光标移动到下一条记录,请对查询对象调用 continue 函数。记住,query对象是由openCursor()返回的IDBRequest对象。

  • 第 17 行的if条件产生一个真值,直到游标中的所有记录都用完。

  • 如果为 false,当整个数据集(注释记录)被检索并添加到局部变量result时,解析承诺。调用函数使用从getAllCachedComments() 成功解析的结果。

这完成了前面描述的三个步骤中的第一步,如下所示:

  1. 从 IndexedDB 中检索所有缓存的评论。

    接下来,让我们继续另外两个步骤:

  2. 调用服务器端 HTTP 服务,该服务为用户评论更新主数据库。

  3. 最后,清空缓存。删除与远程服务同步的注释。

在服务器端批量更新注释

用户可能在应用脱机时添加了多个注释。建议在一次通话中上传所有评论。服务器端 HTTP POST 端点/comments接受一组注释。

记住,Angular 服务GameService ( src/app/common/game.service.ts)封装了所有游戏相关的服务调用。添加一个新函数,该函数接受一组注释并进行 HTTP POST 调用。与早期的服务调用类似,新函数使用一个HttpClient对象进行 post 调用。新功能addBulkComments见清单 9-12 (功能名称随意)。请参见第 9 行和第 18 行。

02: @Injectable({
03:   providedIn: 'root'
04: })
05: export class GamesService {
06:
07:   constructor(private httpClient: HttpClient) { }
08:
09:   addBulkComments(comments: Array<{title: string,
10:     userName: string,
11:     comments: string,
12:     gameId: number,
13:     timeCommented: Date}>){
14:     return this
15:       .httpClient
16:       .post(environment.commentsServiceUrl, comments);
17:
18:   }
19: }

Listing 9-12Add Bulk Comments

Note

函数addBulkComments()使用匿名数据类型作为参数。注释变量的类型是Array<{title: string, userName: string, comments: string, gameId: number, timeCommented: Date}>。突出显示的类型没有名称。您可以对一次性数据类型使用这种技术。

您可以选择创建一个新实体并使用它。

服务函数现在是可用的,但是还没有被调用。但是,您可以使用服务功能来批量更新缓存的注释。在我们开始使用这个函数之前,考虑添加一个函数来删除。

这也完成了第二步。现在,您有了从 IndexedDB 中检索缓存注释的代码,并调用服务器端服务来同步离线注释。

  1. 从 IndexedDB 中检索所有缓存的评论。

  2. 调用服务器端 HTTP 服务,该服务为用户评论更新主数据库。

  3. 最后,清空缓存。删除与远程服务同步的注释。

接下来,添加代码来清理 IndexedDB。

从索引中删除数据 b

IndexedDB 为从 IndexedDB 中删除数据提供了以下 API:

  • delete():删除对象存储中的记录。这将通过记录 ID 选择要删除的记录。

如前所述,CRUD 操作在事务范围内运行。因此,您将在对象存储上创建一个读写事务。调用getAll() API,它返回IDBRequest

IDBRequest对象上,提供onsuccessonerror回调函数定义。如前所述,几乎所有的 IndexedDB 操作都是异步的。删除操作不会立即发生。它回调提供的回调函数,如清单 9-13 所示。请注意,它返回一个承诺。如果删除操作成功,则承诺得到解决。见第 10 行。如果失败了,这个承诺就被拒绝了。见第 14 行。

01: deleteComment(recordId: number){
02:     return new Promise( (resolve, reject) => {
03:       let deleteQuery = this.indexedDb
04:             .transaction("gameComments", "readwrite")
05:             .objectStore("gameComments")
06:             .delete(recordId);
07:
08:       deleteQuery.onsuccess = (evt) => {
09:         console.log("delete successful", evt);
10:         resolve(true);
11:       }
12:       deleteQuery.onerror = (error) => {
13:         console.log("delete successful", error);
14:         reject(error);
15:       }
16:     });
17:   }

Listing 9-13Using delete()

IdbStorageAccessService中包含之前的功能。记住,这个服务封装了所有与 IndexedDB 相关的动作。现在,您已经有了同步脱机评论的所有三个步骤的代码。

  1. 从 IndexedDB 中检索所有缓存的评论。

  2. 调用一个服务器端 HTTP 服务,它为用户评论更新主数据库。

  3. 最后,清空缓存。删除与远程服务同步的注释。

请注意,这些服务功能是可用的,但是当应用重新联机时,它们还没有被触发。在本章的前面,服务IdbStorageAccessService包括一个用于online事件的事件处理程序。当应用重新联机时,将调用它。更新此事件处理程序以同步脱机注释。考虑在IdbStorageAccessService更新清单 9-14 。

01: this.windowObj.addEventListener("online", (event) => {
02:     this.getAllCachedComments()
03:     .then((result: any) => {
04:       if (Array.isArray(result)) {
05:         let r = this.transformCommentDataStructure(result);
06:         this
07:           .gameSvc
08:           .addBulkComments(r)
09:           .subscribe(
10:             () => {
11:               this.deleteSynchronizedComments(result);
12:             },
13:             () => ({/* error handler  */})
14:           );
15:       }
16:     });
17:   });

Listing 9-14The Online Event Handler

考虑以下解释:

  • 首先,检索所有缓存的注释。参见第 2 行,它调用了getAllCachedComments()服务函数。参见清单 9-11 查看从 IndexedDB 中检索缓存的注释。

  • 该函数返回一个承诺。解决承诺后,您可以从 IndexedDB 访问注释记录。您使用这些数据在后端添加注释,同步服务器端服务和数据库。

  • 在调用服务器端服务之前,将注释记录转换为请求对象结构。您遍历所有的注释,并根据服务器端服务的要求更改字段名称。

    1. 清单 9-15 定义了一个名为transformCommentDataStructure() *的私有函数。*注意从 IndexedDB 对象存储中获得的注释数组上的forEach()。注释被转换并添加到一个本地变量comments *。*这是在函数结束时返回的。
  • 接下来调用GameService函数addBulkComments(),该函数又调用服务器端服务。要查看addBulkComments()功能,请参见清单 9-12 。

  • 记住,函数addBulkComments()返回一个可观察值。你订阅了可观察的,它有成功和失败的处理器。成功处理程序指示注释被添加/与服务器端同步。因此,现在可以删除 IndexedDB 中缓存的注释。

  • 调用被定义为服务IdbStorageAccessService的一部分的私有函数deleteSynchronizedComments()。它遍历每个注释记录,并从本地数据库中删除注释。关于deleteSynchronizedComments()功能的定义,参见清单 9-16 。

    1. 注意,forEach循环使用了一个匿名类型和一个键值对。见第 3 行(r: {key: number; value: any})。它定义了注释数据的预期结构。

    2. deleteComment()按 ID 删除每条记录。要再次查看该功能,请参见清单 9-13 。

1: private deleteSynchronizedComments(result: Array<any>){
2:     result
3:       ?.forEach( (r: {key: number; value: any}) => this.deleteComment(r.key));
4:   }

Listing 9-16Delete Synchronized Comments

01: private transformCommentDataStructure(result: Array<any>){
02:     let comments: any[] = [];
03:     result?.forEach( (r: {key: number; value: any}) => {
04:         comments.push({
05:           title: r.value.title,
06:           userName: r.value.userName,
07:           comments: r.value.comments,
08:           gameId: r.value.gameId,
09:           timeCommented: new Date()
10:         });
11:     });
12:     return comments ;
13: }

Listing 9-15Transform Comments Data

现在,您已经将离线评论与服务器端同步了。参见清单 9-17 ,其中包括用于处理在线事件的事件处理程序和编排同步步骤的私有函数。

01: @Injectable()
02: export class IdbStorageAccessService {
03:
04:   idb = this.windowObj.indexedDB;
05:   indexedDb: IDBDatabase;
06:
07:   constructor(private gameSvc: GamesService, private windowObj: Window) {
08:   }
09:
10:   init() {
11:     let request = this.idb
12:       .open('web-arcade', 1);
13:
14:     request.onsuccess = (evt:any) => {
15:       this.indexedDb = evt?.target?.result;
16:     };
17:
18:     request.onupgradeneeded = function(event: any){
19:         // Create object store for game comments
20:     };
21:
22:     this.windowObj.addEventListener("online", (event) => {
23:       console.log("application is online", event);
24:       this.getAllCachedComments()
25:       .then((result: any) => {
26:         if (Array.isArray(result)) {
27:           let r = this.transformCommentDataStructure(result);
28:           this
29:             .gameSvc

30:             .addBulkComments(r)
31:             .subscribe(
32:               () => {
33:                 this.deleteSynchronizedComments(result);
34:               },
35:               () => ({/* error handler  */})
36:             );
37:         }
38:       });
39:     });
40:
41:     this.windowObj.addEventListener('offline', (event) => console.log("application is offline", event));
42:
43:   }
44:
45:   private deleteSynchronizedComments(result: Array<any>){
46:     result?.forEach( (r: {key: number; value: any}) => {
47:       this.deleteComment(r.key);
48:     });
49:   }
50:
51:   private transformCommentDataStructure(result: Array<any>){
52:       let comments: any[] = [];
53:       result?.forEach( (r: {key: number; value: any}) => {
54:           comments.push({
55:             title: r.value.title,
56:             userName: r.value.userName,
57:             comments: r.value.comments,
58:             gameId: r.value.gameId,
59:             timeCommented: new Date()
60:           });
61:       });
62:       return comments ;
63:   }

64:
65:   deleteComment(recordId: number){
66:     // Code in the listing 9-13
67:   }
68:
69:   private getAllCachedComments() {
70:     // Code in the listing 9-11
71:   }
72:
73: }

Listing 9-17Synchronized Comments with Online Event Handler

更新索引数据库中的数据

IndexedDB 为更新 IndexedDB 中的数据提供了以下 API:

  • put():更新对象存储中的记录。这将通过记录 ID 选择要更新的记录。

如前所述,CRUD 操作在事务范围内运行。因此,您将在对象存储上创建一个读写事务。调用put() API,它返回IDBRequest

IDBRequest对象上,提供onsuccessonerror回调函数定义。如前所述,几乎所有的 IndexedDB 操作都是异步的。使用put()的数据检索不会立即发生。它回调提供的回调函数,如清单 9-18 所示。

01: updateComment(recordId: number, updatedRecord: CommentEntity){
02:     /* let updatedRecord = {
03:         commentId: 1633432589457,
04:         comments: "New comment data",
05:         gameId: 1,
06:         timeCommented: 'Tue Oct 05 2021 16:46:29 GMT+0530 (India Standard Time)',
07:         title: "New Title",
08:         userName: "kotaru"
09:     } */
10:
11:     let update = this.indexedDb
12:         .transaction("gameComments", "readwrite")
13:         .objectStore("gameComments")
14:         .put(updatedRecord, recordId);
15:
16:     update.onsuccess = (evt) => {
17:         console.log("Update successful", evt);
18:     }
19:     update.onerror = (error) => {
20:         console.log("Update failed", error);
21:     }
22: }

Listing 9-18Update Records in IndexedDB

考虑以下解释:

  • 您创建了一个新函数来更新注释。想象一个允许用户编辑评论的表单。前面的函数可以执行此操作。

    注意当前用例不包括编辑评论用例。前面的函数用于演示 IndexedDB 上的put() API。

  • 注意第 2 行和第 9 行之间的注释代码行。这为更新的注释数据提供了一个任意的结构。然而,调用函数在一个updatedRecord变量中提供了更新的注释。

  • 见第 14 行。put 函数有两个参数。

    1. updatedRecord:这是替换当前对象的新对象。

    2. recordId:标识第二个参数recordId要更新的记录。

摘要

本章提供了向 IndexedDB 添加记录的详细说明。在带有游戏细节页面的 Web Arcade 用例中,应用允许用户离线添加评论。数据临时缓存在 IndexedDB 中,最终与服务器端服务同步。

Exercise

  • 您已经看到了如何使用put() API 来更新 IndexedDB 中的记录。添加编辑评论的功能。如果应用脱机,提供在 IndexedDB 中临时保存编辑内容的能力。

  • 注意,deleteComment()函数一次删除一条记录。提供错误处理以识别和纠正故障。

  • 当应用脱机时,提供一个可视指示器。您可以选择更改工具栏和标题的颜色。

十、将 Dexie.js 用于 IndexedDB

到目前为止,您已经看到了在客户端使用数据库的用例及实现。您了解并实现了 IndexedDB。浏览器 API 使您能够创建数据库,执行创建/检索/更新/删除(CRUD)操作。这些功能是浏览器自带的。所有主流浏览器的最新版本都支持 IndexedDB。然而,可以说,IndexedDB API 是复杂的。一个普通的开发者可能需要一个简化的版本。

Dexie.js 是 IndexedDB 的包装器。这是一个简单易用的库,可安装在您的应用中。它是一个拥有 Apache 2.0 许可的开源存储库。许可证允许商业使用、修改、分发、专利使用和私人使用。但是,它在商标使用方面有限制,并且没有责任和担保。在将该库用于您可能正在开发的业务应用时,更好地理解该协议。

本章是对 Dexie.js 的介绍。它概述了 Web Arcade 使用案例参数中的库。首先是在 Web Arcade 应用中安装 Dexie.js 的说明。接下来,它详细介绍了如何在 TypeScript 文件中使用该库。您将创建一个新的类和一个服务,使用 Dexie.js 将数据访问逻辑封装到 IndexedDB 中。最后,本章在 IndexedDB 的基础上列出了一些额外的库和包装器。

安装 Dexie.js

安装 Dexie 包。

npm i -S dexie
or
yarn add dexie

Note

命令npm i -S dexienpm install --save dexie的简称。

-S--save是将 Dexie 添加到 Web Arcade 包中的选项。一个条目将被添加到package.json。这将确保将来的安装包括 Dexie。

Yarn 不需要这个选项。它是含蓄的;它总是会将包添加到 Web Arcade。

网络商场数据库

创建封装 Web Arcade IndexedDB 连接的 TypeScript 类。使用此类通过 Dexie API 访问 IndexedDB 数据库web-arcade。运行此命令创建一个 TypeScript 类:

ng generate class common/web-arcade-db

使用类WebArcadeDb指定要创建和连接的 IndexedDB 数据库。您还将使用该类来定义对象存储、索引等。将清单 10-1 中所示的代码添加到新的类WebArcadeDb中。

01: import { Dexie } from 'dexie';
02: import { CommentsEntity } from './board-games-entity';
03:
04: const WEB_ARCADE_DB_NAME = 'web-arcade-dexie';
05: const OBJECT_STORE_GAME_COMMENTS = 'gameComments';
06: export class WebArcadeDb extends Dexie {
07:     comments: Dexie.Table<CommentsEntity>;
08:
09:     constructor() {
10:       super(WEB_ARCADE_DB_NAME);
11:       this.version(1.0).stores({
12:         gameComments: '++IdxCommentId,timeCommented,userName'
13:       });
14:       this.comments = this.table(OBJECT_STORE_GAME_COMMENTS);
15:      }
16:   }

Listing 10-1A TypeScript Class for the Web Arcade DB

考虑以下解释:

  • 第 6 行创建了一个新的 TypeScript 类,WebArcadeDb。它扩展了Dexie类,该类提供了许多开箱即用的特性,包括打开数据库、创建商店等。

  • 注意第 1 行的Dexie类是从dexie ES6 模块(Dexie 库的一部分)导入的。

  • 向超类提供web-arcade数据库名称。参见第 10 行,构造函数的第一行。在这个代码示例中,TypeScript 类WebArcadeDb专用于一个 IndexedDB,web-arcade。数据库名在第 4 行被赋给一个常量。它在打开数据库连接时使用。

对象存储/表

考虑下面的解释,它详细说明了如何在第 11 行和第 14 行之间使用stores()table()API:

  • 构造函数还定义了对象存储结构。在当前示例中,它创建了一个名为gameComments的商店。请参见第 5 行中的字符串值。您可以通过在 JSON 对象中包含额外的字段来创建额外的对象存储。它作为参数传递给stores()函数。

  • gameComments对象存储定义了两个字段,IdxCommentIdtimeCommented

  • 你在主键上加前缀(或后缀)++。此字段唯一标识每个注释。对于添加到对象存储中的每个记录,它都会自动递增。

  • 对象存储包括一个或多个字段。在这个例子中,对象存储包括两个字段:timeCommenteduserName *。*该语句使用列出的字段创建对象存储。

  • 在将记录插入对象存储时,您可能会包括更多字段。然而,索引只在用stores() API 指定的字段上创建(第 12 行)。查询仅限于使用对象存储索引的字段。因此,包括您将来可能会查询的任何字段。

  • 注意,stores 函数在一个version() API 中,它为 Web Arcade IndexedDB 定义了一个版本。正如您将在下一节中看到的,您可以创建数据库的附加版本并进行升级。

  • Dexie 使用名为Table的 TypeScript 类来引用对象存储。参见第 7 行的类变量注释。您创建了类型为Table ( Dexie.Table)的变量。

  • 注意传递给Table类*的泛型类型CommentsEntity。*类变量comments被限制在接口CommentsEntity中。记住,评论实体包括与用户评论相关的所有字段。在src/app/common/comments-entity.ts重访CommentsEntity。参见清单 10-2 。

  • 接下来,请参见第 14 行。this.table()函数返回一个对象存储引用。table()函数继承自父类。注意,您为table()函数提供了一个对象存储名称。它使用该名称返回特定的对象存储,例如一个gameComments对象存储。

  • 返回的对象存储被设置为类变量comments。在WebArcadeDb实例上访问这个变量指的是对象存储库gameComments。例如,webArcadeDbObject.comments指的是gameComments对象存储。

1: export interface CommentsEntity {
2:     IdxCommentId?: number;
3:     title: string;
4:     comments: string;
5:     timeCommented: string;
6:     gameId: number;
7:     userName:string;
8: }

Listing 10-2Comments Entity

索引数据库版本

随着应用的发展,预测数据库的变化。IndexedDB 支持版本在升级之间转换。Dexie 使用底层 API,并提供一种干净的方式来版本化 IndexedDB。

清单 10-3 用一个对象存储和三个字段(一个主键和两个索引)创建了web-arcade数据库。见第 12 行。假设您需要在索引中添加一个额外的字段gameId,并为棋盘游戏评论创建一个新的对象存储。

在对数据库进行更改之前,请增加版本号。考虑将其更新到 1.1。

Note

在版本号 1.0 中,小数点前的数字称为主版本。小数点后的数字是的小版本。顾名思义,如果数据库结构有重大变化,请考虑更新主版本号。对于单个字段、索引或对象存储的次要添加,请更新次要版本。

接下来,为gameId添加一个新的索引。包括一个名为boardGameComments的新对象存储,带有一个主键commentId。考虑将 10-3 上市。结果见图 10-1 。这是一个使用 Google Chrome 开发者工具的 IndexedDB 视图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-1

新的对象存储,版本 11 (1.1)中的索引

1: this.version(1.1).stores({
2:     gameComments: '++IdxCommentId,timeCommented, userName, gameId',
3:     boardGameComments: '++commentId'
4:   });

Listing 10-3Upgrade Web Arcade to a New Version

接下来,考虑一个场景,您需要删除一个对象存储和一个索引。考虑移除用户名上的索引并删除boardGameComments对象存储。请遵循以下说明:

  1. 更新版本号。考虑用 1.2。这将在 IndexedDB 上转换为 12。

  2. 将要删除的对象存储设置为空。在当前示例中,将boardGameComments设置为空。参见清单 10-4 中的第 3 行。

  3. 要对对象存储进行更改,请在数据库对象上使用upgrade() API。在当前的例子中,我们在对象存储库gameComments上删除了一个名为userName的索引,并提供了一个回调函数。函数参数是数据库的引用变量。考虑将 10-4 上市。

  • 第 8 行删除了comments对象上的用户。修改对象库gameComments?? 时获得comments引用。记住,Dexie 的表类(和实例)指的是一个对象存储。
1: this.version(1.2).stores({
2:     gameComments: '++IdxCommentId,timeCommented, userName, gameId',
3:     boardGameComments: null
4:   }).upgrade( idb =>
5:     idb.table(OBJECT_STORE_GAME_COMMENTS)
6:       .toCollection()
7:       .modify( comments => {
8:         delete comments.userName;
9:       }) );

Listing 10-4Remove Object Store and Index

与 Web-Arcade IndexedDB 连接

记住创造IdbStorageAccessService的思维过程。它从应用的其余部分抽象出 IndexedDB API。如果您选择使用 Dexie 而不是本机浏览器 API,请遵循类似的方法并创建一个服务。运行以下命令创建服务。为服务提供任意名称dexie-storage-access

ng g s common/dexie-storage-access

Note

命令ng g s common/dexie-storage-accessng generate service common/dexie-storage-access 的简称。

g-生成

s- service

IdbStorageAccessService类似,在应用启动时初始化DexieStorageAccessService。在初始化代码中包含一个init()函数。使用 Angular 的APP_INITIALIZER并将其包含在AppModule中。考虑清单 10-5 。参见第 11 行到第 16 行。注意,应用初始化器调用了init()函数(第 13 行)。

01: @NgModule({
02:     declarations: [
03:       AppComponent,
04:       /* More declarations go here */
05:     ],
06:     imports: [
07:       BrowserModule,
08:       /* additional imports go here */
09:     ],
10:     providers: [
11:       {
12:         provide: APP_INITIALIZER,
13:         useFactory: (svc: DexieStorageAccessService) => () => svc.init(),
14:         deps: [DexieStorageAccessService],
15:         multi: true
16:       }
17:       /* More providers go here */
18:     ],
19:     bootstrap: [AppComponent]
20:   })
21:   export class AppModule { }

Listing 10-5Initialize DexieStorageAccessService at Application Startup

正在初始化 IndexedDB

DexieStorageAccessService使用WebArcadeDB类的实例初始化 IndexedDB(在清单 10-3 中创建)。使用open()函数,如果数据库已经存在,它会打开一个到数据库的连接。如果没有,它将创建一个新的数据库并打开连接。考虑清单 10-6 。

01: import { Injectable } from '@angular/core';
02: import { WebArcadeDb } from 'src/app/common/web-arcade-db';
03: import { CommentsEntity } from 'src/app/common/comments-entity';
04:
05: @Injectable({
06:   providedIn: 'root'
07: })
08: export class DexieStorageAccessService {
09:   webArcadeDb = new WebArcadeDb();
10:   constructor() {}
11:   init(){
12:     this.webArcadeDb
13:     .open()
14:     .catch(err => console.log("Dexie, error opening DB"));
15:   }
16: }

Listing 10-6Dexie Storage Access Service

考虑以下解释:

  • 创建一个新的类级实例WebArcadeDb并实例化。它封装了 Web Arcade IndexedDB。参见清单 10-9 中的第 9 行。

  • 请记住,您在APP_INITIALIZER的帮助下从 app 模块调用了init()函数。注意第 11 到 15 行的定义。这通过调用 IndexedDB 上的open()来初始化。如前所述,它为 Web Arcade 创建一个数据库,如果它不存在的话。它将打开到 IndexedDB 的连接。

  • 初始化后,IndexedDB 对包括 CRUD 在内的数据库操作开放。

  • open 函数返回一个承诺。如果失败了,这个承诺就被拒绝了。注意第 14 行。这是承诺被拒绝时的错误处理语句。在当前示例中,您将消息和错误记录到浏览器控制台。

处理

在事务中包含数据库操作是很重要的。事务确保所有封闭的操作都是原子的,即作为单个单元执行。要么执行所有操作,要么不执行任何操作。这有助于确保数据的一致性。

在一个示例中,假设您正在将数据从对象存储 1 传输到对象存储 2。您从对象存储 1 中读取并删除了数据。假设用户在对对象存储 2 的更新完成之前关闭了浏览器。如果没有事务,数据就会丢失。如果在将数据添加到对象存储 2 之前出现故障,事务确保从对象存储 1 的删除被恢复。这确保了数据不会丢失。

在一个WebArcadeDb对象上创建一个事务,如清单 10-7 所示。

1: this.webArcadeDb.transaction("rw",
2:     this.webArcadeDb.comments,
3:     () => {
4:
5:     })

Listing 10-7Create a Transaction

考虑以下解释:

  • 见第 1 行。在WebArcadeDb对象的实例上创建一个事务。它是DexieStorageAccessService上的类级变量。

  • 交易函数的第一个参数是交易模式。两个值是可能的。

    • Read:第一个参数的值为r。该事务只能执行读取操作。

    • Read-Write:第一个参数的值为rw。参见清单 10-10 中的第 1 行。该事务可以执行读写操作。

  • 第二个参数是对象存储引用。见第 2 行。comments字段指向对象存储器gameComments。参见清单 10-3 中的第 14 行。

  • 您可以在一个事务中包含多个对象存储。

  • 最后一个参数是函数回调。它包括对数据库执行创建、检索、更新或删除操作的代码。

增加

记住,到目前为止,您创建了一个名为gameComments的对象存储。清单 10-8 向对象存储中添加一条记录。

01: addComment(title: string, userName: string, comments: string, gameId: number, timeCommented = new Date()){
02:     this.webArcadeDb
03:       .comments
04:       .add({
05:         title,
06:         userName,
07:         timeCommented: `${timeCommented.getMonth()}/${timeCommented.getDate()}/${timeCommented.getFullYear()}`,
08:         comments,
09:         gameId,
10:       })
11:       .then( id => console.log(`Comment added successfully. Comment Id is ${id}`))
12:       .catch( error => console.log(error))
13:       ;
14: }

Listing 10-8Add a Comment Record to the Object Store

考虑以下解释:

  • 见第 2 行。您使用了一个WebArcadeDb对象的实例。它是DexieStorageAccessService上的类级变量。

  • add()函数将一条记录插入到对象存储中(第 4 行)。该记录包括各种注释字段,包括标题、用户名、注释日期和时间、注释描述以及添加了注释的游戏的 ID。

  • add()函数返回一个承诺。如果添加操作成功,则承诺得到解决。参见第 11 行,它将注释 ID(主键)记录到浏览器控制台。如果添加操作失败,承诺将被拒绝。第 12 行的catch()函数运行,它在浏览器控制台上打印错误信息。

删除

使用数据库对象webArcadeDb执行删除操作。调用数据库上的delete() API。它需要一个注释 ID,主键作为输入参数。考虑上市 10-9 。

1: deleteComment(id: number){
2:     return this.webArcadeDb
3:       .comments
4:       .delete(id)
5:       .then( id => console.log(`Comment deleted successfully.`))
6:       .catch(err => console.error("Error deleting", err));
7: }
8:

Listing 10-9Delete a Comment Record in the Object Store

考虑以下解释:

  • 见第 2 行。您使用了一个WebArcadeDb对象的实例。它是DexieStorageAccessService上的类级变量。

  • delete()函数从对象存储中删除一条记录(第 4 行)。要删除的记录由注释 ID(一个主键)标识。

  • delete()函数返回一个承诺。如果删除操作成功,则承诺得到解决。请参见第 5 行,该行向浏览器控制台记录了一条成功消息。如果删除操作失败,承诺将被拒绝。第 6 行的catch()函数运行,它在浏览器控制台上打印错误信息。

更新

使用数据库对象webArcadeDb执行更新操作。调用数据库上的update() API。它需要一个注释 ID,这是主键,作为第一个输入参数。它使用注释 ID 选择要更新的记录。它使用一个带有键路径和要更新的新值的对象。考虑上市 10-10 。

1: updateComment(commentId: number, newTitle: string, newComments: string){
2:     this.webArcadeDb
3:       .comments
4:       .update(commentId, {title: newTitle, comments: newComments})
5:       .then( result => console.log(`Comment updated successfully. Updated record ID is ${result}`))
6:       .catch(error => console.error("Error updating", error));
7:   }

Listing 10-10Update a Comment Record

考虑以下解释:

  • 见第 2 行。您使用了一个WebArcadeDb对象的实例。它是DexieStorageAccessService上的类级变量。

  • update()函数更新对象存储上的记录(第 4 行)。要更新的记录由注释 ID(一个主键)标识。第二个参数是一个对象,包含要更新的值的键值对。请注意,在对象存储中,一个键标识记录中要更新的字段。

  • 例如,下面的代码片段用注释 ID 1(主键)更新一条记录。接下来的两个参数分别是新的标题和描述。

    this.updateComment(1, "New title", "new comment description");

图 10-2 显示了结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-2

更新结果

  • update()函数返回一个承诺。如果更新操作成功,则承诺得到解决。请参见第 5 行,该行向浏览器控制台记录了一条成功消息。如果更新操作失败,承诺将被拒绝。第 6 行的catch()函数运行,它在浏览器控制台上打印错误信息。

Note

update()函数更新对象存储中记录的特定字段。要替换整个对象,请使用put()

恢复

Dexie 提供了一个全面的函数列表,用于从 IndexedDB 中查询和检索数据。请考虑以下几点:

  • get(id):使用 ID/主键选择对象存储中的记录。ID 作为参数传入。get()函数返回一个承诺。成功获取后,then()回调函数返回结果。

  • bulkGet([id1, id2, id3]):选择对象库中的多条记录。id 作为参数传入。bulkGet()函数返回一个承诺。在一次成功的 get 中,then()回调函数返回结果。

  • where({keyPath1:value, keyPath2: value…, keyPath: value}):根据keyPath指定的字段和给定值过滤记录。

  • each(functionCallback):遍历对象库中的对象。API 异步调用提供的函数回调。考虑将 10-11 上市。

1: getAllCachedComments(){
2:     this.webArcadeDb
3:       .comments
4:       .each( (entity: CommentsEntity) => {
5:         console.log(entity);
6:       })
7:       .catch(error => console.error("Error updating", error));
8: }
9:

Listing 10-11Get All Cached Comments from the gameComments Object Store

考虑以下解释:

  • 第 2 行使用了一个WebArcadeDb对象的实例。它是DexieStorageAccessService上的类级变量。

  • 第 4 行遍历gameComments对象存储中的每条记录。

  • 回调函数使用类型为CommentsEntity的参数。当回调被异步调用时,局限于CommentsEntity接口的数据将被返回。

  • 第 5 行将实体打印到浏览器控制台。

更多选项

在本书中,您已经看到了浏览器原生支持的 IndexedDB API。本章介绍了 Dexie.js,这是一个旨在简化数据库访问的包装器。

以下是几个额外的选项供您考虑。虽然实现细节超出了本书的范围,但是可以考虑阅读并进一步了解这些库。所有这些库都在底层使用 IndexedDB。

  • 本地饲料:提供简单的 API 和函数。API 类似于本地存储。在不支持 IndexedDB 的传统浏览器上,Local feed 提供了一个 polyfill。它能够回退到 WebSQL 或本地存储。它是一个开源库,拥有 Apache 2.0 许可。

  • ZangoDB :这提供了一个简单易用的 API,模仿 MongoDB。该库使用 IndexedDB。包装器描述了一个用于过滤、排序、聚合等的简单 API。这是一个拥有 MIT 许可的开源库。

  • JS Store :为 IndexedDB 提供类似 API 的结构化查询语言(SQL)。它在一个类似于传统 SQL 的易于理解的 API 中提供了 IndexedDB 提供的所有功能。这是一个拥有 MIT 许可的开源库。

  • PouchDB :这提供了一个 API 来同步客户端的离线数据和 CouchDB。对于使用 CouchDB 服务器端后端的应用来说,这是一个非常有用的库。它是一个开源库,拥有 Apache 2.0 许可。

摘要

本章介绍了 Dexie.js。它在 Web Arcade 使用案例的参数内提供了对库的基本理解。它列出了将 Dexie.js NPM 软件包安装到 web arcade 应用的说明。

此外,本章在 IndexedDB 的基础上列出了一些额外的库和包装器。虽然实现细节超出了本书的范围,但它列出了名称和一行代码介绍以供进一步学习。

Exercise

  • 更新游戏细节组件,以便在应用离线时使用 Dexie 存储访问服务来缓存评论。

  • 更新 online 事件,以便在应用重新联机时使用 Dexie storage access 服务来检索记录。与服务器端服务集成,使用 Dexie.js API 同步数据和删除本地记录。

  • 提供使用 Dexie 存储访问服务更新注释的能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值