随着现代大型项目的复杂度越来越高,渲染一个WEB页面需要越来越多的数据。在多次打开渲染的过程中,很多数据是重复的,并不会经常更新,所以这部分数据需要浏览器缓存起来,以缓解网络压力,提高页面打开速度。IndexedDB存储方案比较在IndexedDB推出之前,已经有一些浏览器数据存储方案的实现,比如cookies、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版本控制机制,扩展了数据库版本的概念。同时统一数据库只保留唯一的最新版本,低于该版本的tab会触发upgradeneeded事件升级版本库。修改数据库结构的操作(如增删表、索引等)只能通过升级数据库版本来完成。ObjectStoreIndexedDB用来存储数据集的单位是ObjectStore,相当于关系数据库中的一张表或者非关系数据库的集合。TransactionTransaction相当于一个原子操作。如果事务发生错误,则整个事务中执行的所有功能都不会生效。这样数据库就可以保证数据的一致性,提高业务的可靠性。IndexedDB的一大特点是事务化,所有的数据操作都必须包裹在事务中。IndexedDB的层次关系是:请求->事务->数据库。我们也可以利用这个关系链进行错误处理的事件委托,从而集中错误捕获逻辑处理。IndexedDBAPI的原生使用IndexedDB的API相对复杂。由于不是本文的重点,这里就不展开了。如果对原生API感兴趣,可以参考MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API。由于原生API的异步过程使用了监听回调机制,在现代项目中使用不是很方便。一般来说,推荐使用Promise封装一个外层,更适合现代项目的使用场景。建立IndexedDB版本自动控制方案的思路可以从使用文档中得知。IDBFactory.open方法用于打开数据库连接。它传入数据库名和版本号version这两个参数来执行后面的步骤,并在相应的时间段触发Hook进行指定的回调。当指定的数据库已经存在时,等待versionchange操作完成。如果计划删除数据库,请等待删除完成。如果现有数据库版本高于给定版本,则中止操作并返回错误。如果存在现有数据库版本并且该版本低于给定版本,则触发版本更改操作。如果数据库不存在,则创建具有指定名称的数据库,将版本号设置为给定版本,如果没有给出版本号,则为1。创建数据库连接。从这里可以看出,这个方法既有创建数据库的功能,也有建立数据库连接的功能,和我们常用的数据库的操作不太一致,所以用起来会有点奇怪。其实IndexedDB的设计初衷和推荐用法就是让我们在代码中硬编码version的版本号,从而在触发的versionchange事件中根据版本号给出明确的响应。constopenRequest:IDBOpenDBRequest=this.dbFactory.open(this.dbName,version);openRequest.onupgradeneeded=(e)=>{versionChangeCb(e);if(e.oldVersion<1){constobjectStore=db.createObjectStore('test_objectStore');}elseif(e.oldVersion===1){...}else{...}};这与我们熟悉的数据库认知不一致。有时,我们希望IndexedDB在项目执行过程中像浏览器本地搭建的普通数据库一样进行任意表的增删改查。我们不想关心当前最新的版本号,希望能够自动控制版本。现有的IndexedDB能力对于这样的使用场景变得非常困难。因为在不知道当前最新版本号的情况下是不可能打开最新版本的数据库的,在打开数据库获取数据库实例之前是不可能获取到当前数据库的最新版本的!这就形成了一个僵局。我们必须在本地记录当前数据库的最新版本,以便下次打开表时直接读取。理清了处理思路之后,接下来就是具体的实现环节了。要在本地访问最新版本的数据库,首先要解决的是在本地存储版本号。本地访问的方式有很多种,之前也简单介绍过各种本地存储方案。这里,考虑到最大的兼容性,我们使用固定版本的IndexedDB数据库。(localStorage等存储解决方案也适合这里)privategetDBLatestVersion(dbName:string):Promise{returnnewPromise(async(resolve,reject)=>{constopenRequest:IDBOpenDBRequest=this.dbFactory.open('DBVersion',1);openRequest.onerror=()=>{reject(INDEXEDDB_ERROR.OPEN_FAILED);};openRequest.onsuccess=()=>{constdb=openRequest.result;constobjectStore=db.transaction(['version'],'readonly').objectStore('version');constrequest=objectStore.get(dbName);//如果没有找到,应该是一个新的数据库request.onerror=function(){resolve(0);};request.onsuccess=function(){if(request.result?.version){resolve(request.result.version);}else{resolve(0);}};};openRequest.onupgradeneeded=()=>{constdb=openRequest.result;constobjectStore=db.createObjectStore('version',{keyPath:'dbName',});objectStore.createIndex('dbName','dbName',{unique:true});objectStore.createIndex('version','version',{unique:false});};});}privateupdateDBLatestVersion(dbName:string,newVersion:number){returnnewPromise(async(resolve,reject)=>{constopenRequest:IDBOpenDBRequest=this.dbFactory.open('DBVersion',1);openRequest.onerror=()=>{reject(INDEXEDDB_ERROR.OPEN_FAILED);};openRequest.onsuccess=()=>{constdb=openRequest.result;constobjectStore=db.transaction(['version'],'readwrite').objectStore('版本');//更新数据库版本字段constupdateRequest=objectStore.put({dbName,version:newVersion});updateRequest.onerror=拒绝;updateRequest.onsuccess=resolve;};openRequest.onupgradeneeded=()=>{constdb=openRequest.result;constobjectStore=db.createObjectStore('version',{keyPath:'dbName',});objectStore.createIndex('dbName','dbName',{unique:true});对象存储。createIndex('version','version',{unique:false});};});}这里,dbName和version这两个字段用于存储和映射每个数据库及其最新版本。这里需要注意的是,如果这里找不到数据库的名字,说明数据库应该正在创建中,这也是正常的。根据建表方式的要求,返回0。建立数据库连接为了像普通数据库一样操作,我们首先需要拆分IndexedDB.openAPI的两个功能,即建立连接和添加新表。我们先来看连接建立部分。privategetDBConnection(version?:number):Promise{if(this.hasDBOpened&&this.db){returnPromise.resolve(this.db);}}constopenRequest:IDBOpenDBRequest=this.dbFactory.open(this.dbName,version||this.dbVersion);returnnewPromise((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);};});}publicclose(){if(this.db){this.db.close();}this.db=null;this.hasDBOpened=false;}这个逻辑挺好理解的,获取最新版本号后打开数据库,输入高于或低于当前版本抛出错误的目的是保证该方法只执行打开连接的操作。要断开连接,请使用IDBDatabase.close方法并重置标志。增删表创建新表的逻辑是在打开数据库之前获取当前数据库的最新版本,然后在此基础上+1。这是为了保证onupgradeneeded事件被触发,从而在这里更新数据库版本和创建新表。手术。由于版本号是unsignedlonglong类型,所以不要用浮点数来记录它的版本,否则会被强行取整。publiccreateTable(options:{tableName:string;objectStoreOptions}):Promise{if(this.hasDBOpened)this.close();const{tableName,createIndexParamsArr,primaryKey}=options;returnnewPromise(async(resolve,reject)=>{constversion=awaitthis.getDBLatestVersion(this.dbName);constnewVersion=version+1;constopenRequest: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();拒绝(INDEXEDDB_ERROR.OPEN_FAILED);};openRequest.onblocked=()=>{this.close();};});}删表也是同理publicdeleteTable(tableName:string):Promise{if(this.hasDBOpened)this.close();returnnewPromise(async(resolve,reject)=>{constversion=awaitthis.getDBLatestVersion(this.dbName);constnewVersion=version+1;constopenRequest:IDBOpenDBRequest=this.dbFactory.open(this.dbName,newVersion)复制代码;openRequest.onupgradeneeded=()=>{//版本更新this.dbVersion=newVersion;this.updateDBLatestVersion(this.dbName,newVersion);constdb=openRequest.result;if(db.objectStoreNames.contains(tableName)){db.deleteObje对象ctStore(表名);解决(数据库);}else{拒绝(INDEXEDDB_ERROR.CAN_NOT_FIND_TABLE);}};openRequest.onsuccess=()=>{this.db=openRequest.result;解决(openRequest.result);};openRequest.onerror=()=>{this.close();拒绝(INDEXEDDB_ERROR.OPEN_FAILED);};});至此,一个可以实现自动版本控制的IndexedDBpromise就可以实现了。当然,下一步就是对表进行增删改查,作为promise进行,支持批量增删改查,索引和主键查询,多条件查询等,可以打包成一个完整可用的库。由于与本题无关,就不贴代码了,有兴趣的可以自行实现。