随着古代大型项目复杂度的晋升,渲染一个 WEB 页面须要的数据越来越多,在屡次关上并渲染的过程中,有许多数据都是反复并且不常更新的,因而这部分的数据须要通过浏览器缓存来缓解网络压力,同时晋升页面关上速度。
<!-- more -->
IndexedDB 的存储计划比拟
在 IndexedDB 推出以前,浏览器数据的存储计划就曾经有了一些实现,例如 cookie,localStorage 等等。
cookie 不必多说,每次都须要随着申请全副带给服务端,并且大小只有可怜的 4KB。cookie 用来做存储数据缓存必然会给网络申请带来更大的压力,因而在该种状况下不是一个适合的载体。
localStorage 作为一个 HTML5 规范,很适宜用来做存储数据的本地缓存,并且它可能在不同的标签页之间共享数据,一些网站利用这个特点可能实现一些神奇的操作。它的存储限度比 cookie 要大,依据浏览器的实现不同,大部分浏览器至多反对 5MB - 50MB 的存储。然而,因为 localStorage 的实现与 cookie 相似,存储格局只能为 key-value, 并且 value 只能为 string 类型。因而须要存储简单类型时,还必须得进行一次 JSON 的序列化转换。于此同时,localStorage 的读写是同步的,会阻塞主线程的执行,因而在存取简单类型或大数据量的缓存数据时,localStorage 并不是一个很适合的抉择。
为了解决 localStorage 存在的上述问题,W3C 提出了浏览器数据库 —— IndexedDB 规范。一个无大小限度的(个别只取决于硬盘容量)、异步的、反对存储任意类型数据的浏览器存储计划。
IndexedDB 的基本概念
要学习 IndexedDB 的应用,首先得理解它的一些外围概念。
数据库版本
和所有数据库一样,IndexedDB 也有 Database 的概念。每个同源策略下,都能够有多个数据库。
因为 IndexedDB 存在于客户端,数据存储在浏览器中。因而开发人员不能间接拜访它。因而 IndexedDB 有一个独特的 scheme 版本控制机制,引申进去数据库版本的概念。同一时间对立数据库只保留惟一且最新的版本,低于此版本的标签页会触发 upgradeneeded 事件降级版本库。批改数据库构造的操作(如增删表、索引等),只能通过降级数据库版本实现。
ObjectStore
IndexedDB 用来存储数据集的单位是 ObjectStore,相当于关系型数据库的表,或是非关系型数据库的汇合。
事务
事务相当于是一个原子操作,在一个事务中若呈现报错,整个事务之中执行的所有性能都不会失效。从而使得数据库可能保证数据一致性,晋升业务可靠性。
IndexedDB 的一大特点就是事务化,所有的数据操作都必须被包裹在事务之内执行。IndexedDB 的层级关系为:申请 -> 事务 -> 数据库,咱们也能够通过这个关系链来进行错误处理的事件委托,从而集中谬误捕捉逻辑解决。
IndexedDB API 的原生应用
IndexedDB 的 API 较为繁冗,因为并不是本文要讲的重点,在此不开展,对原生 API 感兴趣的能够参考一下 MDN 的文档:https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API。
因为原生 API 的异步过程采纳的是监听回调机制,在古代我的项目中应用起来不是很不便,一般来说举荐应用 Promise 的形式在内部封装一层,更可能贴合古代我的项目的应用场景。
建设版本自动控制的 IndexedDB
解决思路
从应用文档中能够晓得,IDBFactory.open 办法用于关上一个数据库连贯,它通过传入数据库名称以及版本号 version 两个参数,执行以下步骤,并在相应的期间触发指定回调的钩子。
- 指定数据库曾经存在时,期待 versionchange 操作实现。如果数据库已打算删除,那等着删除实现。
- 如果已有数据库版本高于给定的 version,停止操作并返回 Error。
- 如果已有数据库版本,且版本低于给定的 version,触发一个 versionchange 操作。
- 如果数据库不存在,创立指定名称的数据库,将版本号设置为给定版本,如果没有给定版本号,则设置为 1。
- 创立数据库连贯。
从这里能够看出,这个办法兼具了创立数据库与建设数据库连贯两个性能,这里与咱们罕用数据库的操作不太统一,因而应用起来会有些奇怪。
事实上,IndexedDB 的设计初衷及举荐用法是让咱们在代码中硬编码 version 这个版本号,从而在触发的 versionchange 事件中依据版本号不同给出确定的响应。
const openRequest: IDBOpenDBRequest = this.dbFactory.open(this.dbName, version); openRequest.onupgradeneeded = (e) => { versionChangeCb(e); if (e.oldVersion < 1) { const objectStore = db.createObjectStore('test_objectStore'); } else if(e.oldVersion === 1) { ... } else { ... } };
这与咱们对相熟的数据库认知是不统一的。有的时候,咱们心愿 IndexedDB 只像一个建设在浏览器本地的一般的数据库一样在我的项目执行时进行任意的增删表操作,并不想关怀以后最新的版本号是多少,心愿能自动控制版本。
而现有的IndexedDB能力对于这样的应用场景来说就变得十分艰巨。因为在不晓得以后最新版本号的状况下基本没法关上最新版本的数据库,并且,在不关上数据库失去数据库实例之前也没法获取以后数据库的最新版本!这就造成了一个死结,咱们必须在某个本地地位记录下以后数据库的最新版本,以便下次关上表时可能间接读取到。
理清了解决思路,接下来就是具体的实现环节。
本地存取某个数据库的最新版本
首先咱们须要解决的就是在本地存储版本号的问题。
本地存取的形式有很多,在之前也简略介绍过各种本地存储的解决方案。在这里,思考到最大的兼容性,应用的是多应用一个版本固定不变的IndexedDB数据库。(这里应用 localStorage 等存储计划也同样适合)
private getDBLatestVersion(dbName: string): Promise<number> { return new Promise(async (resolve, reject) => { const openRequest: IDBOpenDBRequest = this.dbFactory.open('DBVersion', 1); openRequest.onerror = () => { reject(INDEXEDDB_ERROR.OPEN_FAILED); }; openRequest.onsuccess = () => { const db = openRequest.result; const objectStore = db.transaction(['version'], 'readonly').objectStore('version'); const request = objectStore.get(dbName); // 找不到阐明应该是新建的数据库 request.onerror = function () { resolve(0); }; request.onsuccess = function () { if (request.result?.version) { resolve(request.result.version); } else { resolve(0); } }; }; openRequest.onupgradeneeded = () => { const db = openRequest.result; const objectStore = db.createObjectStore('version', { keyPath: 'dbName', }); objectStore.createIndex('dbName', 'dbName', { unique: true }); objectStore.createIndex('version', 'version', { unique: false }); }; }); } private updateDBLatestVersion(dbName: string, newVersion: number) { return new Promise(async (resolve, reject) => { const openRequest: IDBOpenDBRequest = this.dbFactory.open('DBVersion', 1); openRequest.onerror = () => { reject(INDEXEDDB_ERROR.OPEN_FAILED); }; openRequest.onsuccess = () => { const db = openRequest.result; const objectStore = db.transaction(['version'], 'readwrite').objectStore('version'); // 更新数据库版本字段 const updateRequest = objectStore.put({ dbName, version: newVersion }); updateRequest.onerror = reject; updateRequest.onsuccess = resolve; }; openRequest.onupgradeneeded = () => { const db = openRequest.result; const objectStore = db.createObjectStore('version', { keyPath: 'dbName', }); objectStore.createIndex('dbName', 'dbName', { unique: true }); objectStore.createIndex('version', 'version', { unique: false }); }; }); }
这里应用dbName与version两个字段来对每一个数据库以及其最新版本进行存储映射。这里须要留神的是,若是无奈在这里找到该数据库名称,阐明应该是数据库在新建过程中,也是失常状况,依据建表办法所需,返回0。
建设数据库连贯
为了像一般数据库一样操作,首先咱们须要拆分IndexedDB.open这个API的建设连贯和新增表这两个性能,先来看建连局部。
private getDBConnection(version?: number): Promise<IDBDatabase> { if (this.hasDBOpened && this.db) { return Promise.resolve(this.db); } const openRequest: IDBOpenDBRequest = this.dbFactory.open(this.dbName, version || this.dbVersion); return new Promise((resolve, reject) => { openRequest.onerror = () => { this.close(); reject(INDEXEDDB_ERROR.CONNECTION_FAILED); }; openRequest.onblocked = () => { this.close(); reject(INDEXEDDB_ERROR.CONNECTION_FAILED); }; openRequest.onsuccess = () => { this.db = openRequest.result; this.hasDBOpened = true; resolve(openRequest.result); }; // 此时会新建一个数据库,不正确的调用 openRequest.onupgradeneeded = () => { this.close(); reject(INDEXEDDB_ERROR.CONNECTION_FAILED); }; }); } public close() { if (this.db) { this.db.close(); } this.db = null; this.hasDBOpened = false; }
这块逻辑挺好了解,在获得最新版本号后关上数据库,并对高于或低于以后版本的输出均抛出报错。目标是为了确保该办法仅执行关上连贯的操作。
断开连接即应用 IDBDatabase.close 办法,并重置标记位即可。
增删表操作
新建表的逻辑为,再关上数据库前,先获取到以后数据库的最新版本,并在该根底上+1,这是为了确保触发onupgradeneeded事件,从而在这里进行更新数据库版本与创立新表的操作。
因为版本号是一个 unsigned long long 类型,因而不要应用浮点数来记录它的版本,否则会被强行取整。
public createTable(options: { tableName: string; objectStoreOptions }): Promise<IDBDatabase> { if (this.hasDBOpened) this.close(); const { tableName, createIndexParamsArr, primaryKey } = options; return new Promise(async (resolve, reject) => { const version = await this.getDBLatestVersion(this.dbName); const newVersion = version + 1; const openRequest: IDBOpenDBRequest = this.dbFactory.open(this.dbName, newVersion); openRequest.onupgradeneeded = () => { // 版本更新 this.dbVersion = newVersion; this.updateDBLatestVersion(this.dbName, newVersion); db.createObjectStore(tableName, objectStoreOptions); }; openRequest.onsuccess = () => { this.db = openRequest.result; this.hasDBOpened = true; resolve(openRequest.result); }; openRequest.onerror = () => { this.close(); reject(INDEXEDDB_ERROR.OPEN_FAILED); }; openRequest.onblocked = () => { this.close(); }; }); }
删表也是同理
public deleteTable(tableName: string): Promise<IDBDatabase> { if (this.hasDBOpened) this.close(); return new Promise(async (resolve, reject) => { const version = await this.getDBLatestVersion(this.dbName); const newVersion = version + 1; const openRequest: IDBOpenDBRequest = this.dbFactory.open(this.dbName, newVersion); openRequest.onupgradeneeded = () => { // 版本更新 this.dbVersion = newVersion; this.updateDBLatestVersion(this.dbName, newVersion); const db = openRequest.result; if (db.objectStoreNames.contains(tableName)) { db.deleteObjectStore(tableName); resolve(db); } else { reject(INDEXEDDB_ERROR.CAN_NOT_FIND_TABLE); } }; openRequest.onsuccess = () => { this.db = openRequest.result; resolve(openRequest.result); }; openRequest.onerror = () => { this.close(); reject(INDEXEDDB_ERROR.OPEN_FAILED); }; }); }
至此,就能实现一个可能进行主动版本控制的 IndexedDB promise 封装了。
当然,接下来还须要对表的增删改查进行promise化解决,并反对批量增删、索引与主键查问、多条件查问等等,就能封装成一个残缺可用的库了。因为跟本次主题无关,就不将代码贴上来了,感兴趣的能够本人实现一下。