当前位置: 首页 > Web前端 > HTML5

一行代码获取浏览器数据库IndexedDB

时间:2023-04-05 01:06:37 HTML5

前言大三备考GRE时,为了高效背单词,在宝贵的备考阶段花了5天时间,写了一个背单词的WebApp。当时的技术实现是基于IndexedDB,一个运行在浏览器上的数据库,用来存储用户信息,记忆词汇进度等。最近很少有比较空闲的时间,所以想重写一下自己当年写的App,把它做成一个正经的开源项目。当时回头看代码的时候突然想到:为什么IndexedDB的操作不能直接封装成一个库,然后发布到npm呢?那就干吧,前段时间刚接触Typescript,使用Typescript进行开发。这样做的好处是开发者可以在开发过程中得到类型提示,减少出现bug的概率。写这个库的主要目的是为了解决原来的IndexedDB:操作复杂繁琐,对开发者不友好没有现代的操作方式(Promise)即使只是简单的增删改查,很多必须提前写代码数据库表结构维护麻烦,不清楚。因此,我的设计目标(初始阶段)是:简单直观的API,任何人都可以快速上手。所有API都封装了Promise,增删改查一行代码,规范了表结构的定义。它易于维护,易于维护。此外,项目必须是轻量级的,并且有强大实用的API。决定将其命名为Godb.jsGodb.jsGodb.js的样子,即使你不知道浏览器数据库IndexedDB,也可以流畅的使用它,从而专注于业务。毕竟,你必须用好IndexedDB。你需要无数次的阅读MDN,而Godb已经为你彻底的理解了MDN,让你更好的使用IndexedDB,同时也让操作更简单。当前项目处于Alpha阶段(0.3.x版本),意味着可能会有更多的Breakingchanges,在正式版(1.0.0及以后)发布之前,不建议大家使用本项目任何严重的情况。除了封装IndexedDB,其实我对这个项目还有一个更大的想法。这里先剧透一下,等我实现了思路再分享给大家~ProjectGitHub:https://github.com/chenstarx/...觉得不错请点Star~PS:上述背单词的app也可以在我的GitHub主页上找到。近期打算用Vue3重写,同时介绍Godb项目的完整文档,官网正在紧锣密鼓的开发中。在此阶段,您可以使用以下方法演示要尝试安装,您需要先安装它。这里默认使用webpack、gulp等打包工具,或者在vue、react等项目中npminstallgodb。第一个正式版发布后,也会提供引入CDN的方式。敬请期待~简单操作非常简单,增删改查只需要一行代码:importGodbfrom'godb';consttestDB=newGodb('testDB');constuser=testDB。table('user');constdata={name:'luke',age:22};user.add(data)//add.then(id=>user.get(id))//检查,相当于user.get({id:id}).then(luke=>user.put({...luke,age:23}))//改变.then(id=>user.delete(id));//deletehereandaddDelete,修改并检查Promise.then中四个方法的返回值.then:get返回完整数据add和put返回数据id(也可以返回完整数据,在评论区留言评论区~)delete不返回数据(返回undefined)第二点需要注意的是put(obj)方法中的obj需要包含id,否则相当于add(obj)。上面的demo是因为get得到的luke是有id的,所以在修改操作之后会引入一个update方法来改善这个问题,也可以一次添加多条数据:constdata=[{name:'luke',年龄:22},{姓名:'elaine',年龄:23??}];user.addMany(数据).then(()=>user.consoleTable());addMany(data)方法:严格按照data的顺序添加返回的id数组,与data的顺序一致。之所以单独写addMany而不是在add中添加判断数组的逻辑是因为用户可能想要的是向数据库中添加一个数组注意:不要调用addMany并同步添加。如果在执行addMany时调用add,数据库中的顺序可能不符合预期。请在addMany回调完成后调用add。请在awaitaddMany()之后调用add()。如果不加await,表中add的数据会出现在addMany的第一条数据之后,第二条数据在以后的版本中。我会尝试改进这个操作体验,比如等待addMany完成。继续addTable.consoleTable()这里使用了一个Table.consoleTable()方法,会在浏览器控制台打印如下内容:(index)是id,虽然在chromedevelopertoolsdata中可以看到所有的表,但是这种方式的好处是可以在需要的地方打印出数据,方便调试。注意:该方法是异步的,因为需要从数据库中取出数据库;在打印出结果之前执行。如果您不希望这种情况发生,请使用await或Promise.then。schema如果希望数据库结构更严格,还可以添加schemaimportgodbfrom'godb';//定义数据库结构constschema={//usertable:user:{//usertable的字段:name:{type:String,unique:true//指定name字段在表中唯一},age:Number}}consttestDB=newGodb('testDB',schema);constuser=testDB.table('user');constluke1={name:'luke'age:22};constluke2={name:'luke'age:19};user.add(luke1)//没问题。then(()=>user.get({name:'luke'}))//定义schema后,可以使用id以外的字段获取数字根据.then(()=>user.add(luke2))//报错,name重复了上面例子中定义的schema,所以可以在get()中使用id以外的字段进行搜索,否则只能通过传入的id指定user.name,是唯一的,所以不能添加重名关于schema:可能有同学觉得上面定义schema的方式有点眼熟。是的,就是引用mongoose定义的数据库的字段的时候。可以只指定数据类型,比如上面的age:Number也可以使用对象,除了定义数据类型type外,还表示这个字段是否唯一(unique:true),然后再添加更多的可选属性,比如使用Default指定字段的默认值,当指向其他表的indexref没有定义Schema时,Godb可以像MongoDB一样使用,可以灵活的添加数据;不同的是,在Mongodb中,每条数据的唯一标识是_id,而Godb是id,虽然这样做的问题是IndexedDB毕竟还是结构化的。如果用户使用不规律(比如每次添加的数据结构都不一样),久而久之,数据库可能字段过多,不同数据未使用的字段全部为空,造成浪费,影响性能。定义Schema后,Godb就像MySQL一样工作。如果添加Schema中没有的字段,或者字段类型不符合定义,会报错(写文档的时候还没有实现这个功能,即使schema不符合要求,可以添加,下个版本会安排)所以建议在项目中定义schema,这样在可维护性和性能上会更好。另一个使用await的CRUD演示:importGodbfrom'godb';constschema={user:{name:{type:String,unique:true},age:Number}};constdb=newGodb('testDB',schema);constuser=db.table('user');crud();asyncfunctioncrud(){//添加:awaituser.addMany([{name:'卢克',年龄:22},{名字:'elaine',年龄:23??}]);console.log('添加用户:luke');//await不是必须的,这里是为了防止打印顺序错误awaituser.consoleTable();//检查:constluke=awaituser.get({name:'luke'});//constluke=awaituser.get(2);//相当于://constluke=awaituser.get({id:2});//改变:luke.age=23;等待user.put(卢克);console.log('更新:将luke.age设置为23');等待用户。控制台表();//删除:awaituser.delete({name:'luke'});console.log('删除用户:luke');awaituser.consoleTable();}上面的demo会在控件表中打印出如下内容:API设计由于“连接数据库”和“连接表”这两个操作是异步的,在一开始在设计中,有两个API解决方案。不同的是:是否结合这两个操作,作为异步API提供给用户,这里讨论的不是“如何命名API”等细节,而是“如何使用API??”,因为这会直接影响用户在使用godb连接数据库时编写业务代码的方式->add以一条数据的过程为例。设计一:提供异步特性。GitHub上的大多数开源IndexedDB包库都是这样做的。importGodbfrom'godb';//异步连接数据库Godb.open('testDB').then(testDB=>testDB.table('user'))//连接表也要异步.then(user=>{user.add({name:'luke',年龄:22});});});这样做的好处是工作流程一目了然。毕竟对数据库的操作应该放在连接数据库之后。但是,这种设计不适合工程前端项目!因为,所有的增删改查等操作都需要用户手动放在连接完成的异步回调之后,否则操作过程中无法知道数据库和表是否连接。导致每次需要操作数据库,都必须先打开数据库。,tocontinue即使预先定义了一个全局连接,后面要用到的时候,如果不包裹一层Promise,也无法判断数据库和表是否连接。以Vue为例,如果你在全局环境中定义了一个连接(比如Vuex):importGodbfrom'godb';newVuex.Store({state:{godb:awaitGodb.open('testDB')//Promise不等待就返回}});这样,在Vue的任意一个组件中,我们都可以访问到Godb实例问题。在你的组件中,如果你想初始化组件,比如created和mounted钩子函数(在React中是ComponentDidMount),访问数据库:newVue({mounted(){constgodb=this.$store.state.godb;//从全局环境中取出连接godb.table('user').then(user=>{user.add({name:'luke',age:22});//userisundefined!});}});你会发现如果在App初始化的时候加载这个组件,在组件挂载函数中会触发,本地数据库可能根本连不上!(连接数据库等操作最典型的执行场景是组件加载时)解决方法是在每个需要操作数据库的地方定义一个连接:importGodbfrom'godb';newVue({安装(){去db.open('testDB').then(testDB=>testDB.table('user')).then(user=>{user.add({name:'luke',age:22});});}});这样一来,不仅代码又臭又长,而且性能低下(每次操作都需要先连接),而且需要连接本地数据库的组件太多之后,维护就更麻烦了恶梦。总之就是这样解决,在工程前端的不同组件中,每次操作前都需要连接数据库,否则无法保证组件在加载时已经连接到IndexedDB设计2:隐藏连接的异步性对于开发者来说,他们甚至不觉得“连接数据库”和“连接表”这两个操作是异步的consttestDB=newGodb('testDB');constuser=testDB.table('user');用户。add({name:'luke',age:22}).then(id=>console.log(id));这样用起来很自然,开发者在运行过程中不需要关心是否连接了数据库和表。你只需要在操作后的回调中写上自己的逻辑即可。但是这种方案的缺点是开发起来比较麻烦(哎,麻烦自己了,方便用户)因为newCodb('testDB')里面连接数据库的操作其实是异步的(因为IndexedDB的原生API是异步设计的)。连接数据库的操作发出后,即使还没有连接上,下面的testDB.table('user')和user.add()也会先执行,也就是说后面的“getusertable”"和"添加一条数据"实际上会在"连接数据库"的过程之前执行。如果在实现API设计的时候不处理这个问题,上面的示例代码肯定会处理这个问题,我使用了下面两种方法:在每个需要连接数据库的操作中(比如add()),先获取数据库的连接,然后使用队列进行操作,将需要连接数据库的操作放入队列中,等待连接完成,再执行队列。具体来说,就是在Godb类中定义一个getDB(回调)获取IndexedDB连接实例。增删改查时调用getDB,回调获取到IndexedDB连接实例后进行操作。getDB中使用了一个队列。如果还没有连接数据库,则将回调放入队列中,连接后执行队列中的函数,连接完成后直接将IndexedDB连接实例传入回调中执行。在调用getDB时,可能会出现三种状态(其实还有一种数据库Closed状态,这里不讨论):刚刚初始化,还没有发起与IndexedDB的连接。它正在连接到IndexedDB,但尚未连接。这个时候已经有一个IndexedDB的连接实例了。第一个状态只在第一次执行。GetDB被触发,因为一旦尝试建立连接,就会进入下一个状态;我把第一次执行放在了godb类的构造函数的第三种状态,也就是你连接数据库后,直接将连接实例传入回调即可执行。关键是处理第二种状态。此时正在连接数据库,但还没有连接,无法进行增删改查:consttestDB=newGodb('testDB');constuser=testDB.table('user');user.add({name:'luke'});//此时数据库正在连接,尚未连接user.add({name:'elaine'});//此时数据库正在连接,还没有连接到testDB.onOpened=()=>{//数据库连接成功的回调user.add({name:'lucas'});//此时已经连接}在上面的例子中,前两个添加操作其实是如果没有连接数据库,如何保证正常添加,以及lucas中luke和elaine进入数据库的顺序与代码一致?答案是使用队列Queue,向队列中添加两个add操作。当连接成功后,会按照先进先出的顺序执行。这样用户在操作过程中就不需要关心数据库是否已经连接(注意有异步的增删改查回调,可以在回调中知道操作是否成功),godb帮助您在幕后完成这一切。注意,之所以使用回调而不是Promise,是因为JS中的回调既可以是异步的,也可以是同步的。连接成功。已经有connection实例后,最好直接同步返回connection实例。没必要使用异步或者以Vue为例。如果我们向Vuex添加一个连接实例(全局变量):importGodbfrom'godb';newVuex.Store({state:{godb:newGodb('testDB')}});这样,在所有组件中,我们可以使用同一个连接实例:newVue({computed:{//将全局实例改为组件属性godb(){returnthis.$store.state.godb;}},mounted(){this.godb.table('user').add({name:'luke',age:22}).then(id=>console.log(id));}});总结一下这个方案的优点:更高的性能(一个connection实例可以全局共享)更简洁的代码最重要的是,精神负担要低很多!缺点:godb开发比较麻烦,简单的用一层Promise包裹IndexedDB是不够的。因此,我最终采用了这个方案。毕竟是我麻烦,你我方便。优点远远大于缺点。如果你对实现感到好奇,你可以去阅读源代码。目前只实现了基本的CRUD。源码暂时不复杂。很多,我近期会完成以下开发:Table.find():FindfunctionTable.update():更新数据更好的解决方法全局错误处理,当前代码中throw的错误其实没有处理if它定义了Schema,然后在执行所有Table方法之前检查Schema。如果定义了Schema,确保数据库的结构与Schema一致。如果您有任何建议或意见,请在评论区留言。我会核实并阅读每一个反馈。如果你觉得这个项目有意思,欢迎点赞文章,欢迎到GitHub点个star~https://github.com/chenstarx/...