前言 2021 年,如果你的前端应用,需要在浏览器上保存数据,有三个主流方案可以选择:
Cookie
:上古时代就已存在,但能应用的业务场景非常有限LocalStorage
:使用简单灵活,但是容量只有 10Mb,且不适合储存结构化数据IndexedDB
:算得上真正意义上的数据库,但坑异常多,使用麻烦,古老的 API 设计放在现代前端工程中总有种格格不入的感觉我在大三的时候,曾经用 IndexedDB
写过一个背单词 App,当时就有把 IndexedDB
封装一遍的想法,但是由于学业紧张,后来就搁置了
最近,我终于有了空闲时间,于是捡起了当年的想法,开始尝试用 TypeScript
把 IndexedDB
封装一遍,把坑一个个填上,做成一个开发者友好的库,并开源出来,上传至 npm
拍脑袋后,我决定把这个项目命名为 Godb.js
Godb.js
的出现,让你即使你不了解浏览器数据库 IndexedDB,也能把它用的行云流水,从而把关注点放到业务上面去
毕竟要用好 IndexedDB,你需要翻无数遍 MDN,而 Godb
替你吃透了 MDN,从而让你把 IndexedDB 用的更好的同时,操作还更简单了
当前项目处于 Alpha 阶段(版本 0.3.x),意味着之后随时可能会有 breaking changes,在正式版(1.0.0 及以后)发布之前,不要把这个项目用到任何严肃的场景下
项目GitHub:https://github.com/chenstarx/Godb.js
如果觉得不错的话就点个 Star 吧~
项目完整文档与官网正在紧张开发中,现阶段可以通过下面的 demo 来尝鲜
首先需要安装,这里默认你使用了 webpack、gulp 等打包工具,或在 vue、react 等项目中
npm install godb
在第一个正式版发布后,还会提供 CDN 的引入方式,敬请期待~
操作非常简单,增、删、改、查各只需要一行代码:
import Godb from 'godb';
const testDB = new Godb('testDB');
const user = testDB.table('user');
const data = {
name: 'luke',
age: 22
};
user.add(data) // 增
.then(id => user.get(id)) // 查,等价于 user.get({ id: id })
.then(luke => user.put({ ...luke, age: 23 })) // 改
.then(id => user.delete(id)); // 删
这里注意增删改查四个方法在 Promise.then
的返回值:
Table.get()
返回的是完整数据Table.add()
和 Table.put()
返回的是 id
(也可以返回完整数据,评论区留言讨论吧~)Table.delete()
不返回数据(返回 undefined
)第二点需要注意的就是,put(obj)
方法中的 obj
需要包含 id
,否则就等价于 add(obj)
上面的 demo 中,get
得到的 luke
对象包含 id
,因此是修改操作
之后会引入一个 update
方法来改进这个问题
也可以一次性添加多条数据:
const data = [
{
name: 'luke',
age: 22
},
{
name: 'elaine',
age: 23
}
];
user.addMany(data)
.then(() => user.consoleTable());
addMany(data)
方法:
data
的顺序添加data
顺序一致之所以单独写个 addMany
,而不在 add
里加一个判断数组的逻辑,是因为用户想要的可能就是添加一个数组到数据库
注意:addMany
和 add
不要同步调用,如果在 addMany
正在执行时调用 add
,可能会导致数据库里的顺序不符合预期,请在 addMany
的回调完成后再调用 add
Table.consoleTable()
这里用了一个 Table.consoleTable()
的方法,它会在浏览器的控制台打印出下面的内容:
这里的 (index) 就是 id
虽然 chrome 开发者工具内就能看到表内所有数据,但这个方法好处是可以在需要的时候打印出数据,方便 debug
注意:这个方法是异步的,因为需要在数据库里把数据库取出来;异步意味着紧接在它后面的代码,可能会在打印出结果之前执行,如果不希望出现这种情况,使用 await
或 Promise.then
即可
Table.find()
如果你想在数据库中查找数据,还可以使用 Table.find()
方法:
const data = [
{
name: 'luke',
age: 22
},
{
name: 'elaine',
age: 23
}
];
user.addMany(data)
.then(() => {
user.find((item) => {
return item.age > 22;
})
.then((data) => console.log(data)) // { name: 'luke', age: 23 }
});
Table.find(fn)
接受一个函数 fn
作为参数,这个函数的返回值为 true
和 false
这个方法在内部会从头遍历整个表(使用 IndexedDB 的 Cursor),然后把每一次的结果放进 fn
执行,如果 fn
的返回值为 true
(也可以是 1
这样的等价于 true
的值),就返回当前的结果,停止遍历
这个方法只会返回第一个满足条件的值,如果需要返回所有满足条件的值,请使用 Table.findAll()
,用法与 Table.find()
一致,但是会返回一个数组,包含所有满足条件的值
如果你希望数据库的结构更严格一点,也可以添加 schema
import Godb from 'godb';
// 定义数据库结构
const schema = {
// user 表:
user: {
// user 表的字段:
name: {
type: String,
unique: true // 指定 name 字段在表里唯一
},
age: Number
}
}
const testDB = new Godb('testDB', schema);
const user = testDB.table('user');
const luke1 = {
name: 'luke'
age: 22
};
const luke2 = {
name: 'luke'
age: 19
};
user.add(luke1) // 没问题
.then(() => user.get({ name: 'luke' })) // 定义schema后,就可以用 id 以外的字段获取到数据了
.then(() => user.add(luke2)) // 报错,name 重复了
如上面的例子
get()
中可以使用 id
以外的字段搜索了,否则只能传入 id
user.name
这一项是唯一的,因此无法添加重复的 name
Table.get() 与 Table.find() 区别
get()
使用数据库索引搜索,性能更高,而 find()
是遍历全表,使用上灵活
关于 schema:
部分同学或许会发现,上面定义 schema
的方式有点眼熟,没错,正是参考了 mongoose
age: Number
type
,也指明这个字段是不是唯一的(unique: true
),之后会添加更多可选属性,如用来指定字段默认值的 default
,和指向别的表的索引 ref
不定义 Schema 时,Godb
使用起来就像 MongoDB 一样,可以灵活添加数据;区别是 Mongodb 中,每条数据的唯一标识符是 _id
,而 Godb
是 id
虽然这样做的问题是,IndexedDB 毕竟还是结构化的,用户使用不规范的话(如每次添加的数据结构都不一样),久而久之可能会使得数据库的字段特别多,且不同数据中没用到的字段都是空的,导致浪费,影响性能
定义 Schema 后,Godb
使用起来就像 MySQL 一样,如果添加 Schema 没有的字段,或者是字段类型不符合定义,会报错(在写文档的时候还没有实现这个功能,即使 Schema 不符合也能加,下个版本会安排上)
因此推荐在项目中,定义好 schema
,这样不管是维护性上,还是性能上,都要更胜一筹
另一个使用 await 的 CRUD demo:
import Godb from 'godb';
const schema = {
user: {
name: {
type: String,
unique: true
},
age: Number
}
};
const db = new Godb('testDB', schema);
const user = db.table('user');
crud();
async function crud() {
// 增:
await user.addMany([
{
name: 'luke',
age: 22
},
{
name: 'elaine',
age: 23
}
]);
console.log('add user: luke');
// await 非必须,这里是为了防止打印顺序不出错
await user.consoleTable();
// 查:
const luke = await user.get({ name: 'luke' });
// const luke = await user.get(2); // 等价于:
// const luke = await user.get({ id: 2 });
// 改:
luke.age = 23;
await user.put(luke);
console.log('update: set luke.age to 23');
await user.consoleTable();
// 删:
await user.delete({ name: 'luke' });
console.log('delete user: luke');
await user.consoleTable();
}
上面这段 demo,会在控制台打印出下面的内容:
因为「连接数据库」和「连接表」这两个操作是异步的,在设计之初,曾经有两个 API 方案,区别在于:要不要把这两个操作,做为异步 API 提供给用户
这里讨论的不是「API 如何命名」这样的细节,而是「API 的使用方式」,因为这会直接影响到用户使用 Godb
时的业务代码编写方式
以连接数据库 -> 添加一条数据的过程为例
设计一:提供异步特性
GitHub 上大多数开源的 IndexedDB 封装库都是这么做的
import Godb from 'godb';
// 连接数据库是异步的
Godb.open('testDB')
.then(testDB => testDB.table('user')) // 连接表也需要异步
.then(user => {
user.add({
name: 'luke',
age: 22
});
});
});
这样的优点是,工作流程一目了然,毕竟对数据库的操作,要放在连接数据库之后
但是,这种设计不适合工程化的前端项目!
因为,所有增删改查等操作,都需要用户,手动放到连接完成的异步回调之后,否则无法知道操作时有没有连上数据库和表
导致每次需要操作数据库时,都要先打开数据库一遍数据库,才能继续
即使你预先定义一个全局的连接,你在之后想要使用它时,如果不包一层 Promise,是无法确定数据库和表,在使用时有没有连接上的
以 Vue 为例,如果你在全局环境(比如 Vuex)定义了一个连接:
import Godb from 'godb';
new Vuex.Store({
state: {
godb: await Godb.open('testDB') // 不加 await 返回的就是 Promise 了
}
});
这样,在 Vue 的任何一个组件中,我们都能访问到 Godb
实例
问题来了,在你的组件中,如果你想在组件初始化时,比如 created
和 mounted
这样的钩子函数中(React 中就是 ComponentDidMount
),去访问数据库:
new Vue({
mounted() {
const godb = this.$store.state.godb; // 从全局环境取出连接
godb.table('user')
.then(user => {
user.add({
name: 'luke',
age: 22
}); // user is undefined!
});
}
});
你会发现,如果这个组件在 App 初始化时就被加载,在组件 mounted
函数触发时,本地数据库可能根本就没有连接上!(连接数据库这样的操作,最典型的执行场景就是在组件加载时)
解决办法是,在每一个需要操作数据库的地方,都定义一个连接:
import Godb from 'godb';
new Vue({
mounted() {
Godb.open('testDB')
.then(testDB => testDB.table('user'))
.then(user => {
user.add({
name: 'luke',
age: 22
});
});
}
});
这样不仅代码又臭又长,性能低下(每次操作都需要先连接),在需要连接本地数据库的组件多了后,维护起来更是一场噩梦
简而言之,就是这个方案,在工程化前端的不同组件中,需要在每次操作之前,都连一遍数据库,否则无法确保组件加载时,已经连接上了 IndexedDB
设计二:隐藏连接的异步特性
我最终采用了这个方案,对开发者而言,甚至感觉不到「连接数据库」和「连接表」这两个操作是异步的
const testDB = new Godb('testDB');
const user = testDB.table('user');
user.add({
name: 'luke',
age: 22
}).then(id => console.log(id));
这样使用上非常自然,开发者并不需要关心操作时有没有连上数据库和表,只需要在操作后的回调内写好自己的逻辑就可以
但是,这个方案的缺点就是开发起来比较麻烦(嘿嘿,麻烦自己,方便用户)
因为 new Codb('testDB')
内部的连接数据库的操作,实际上是异步的(因为 IndexedDB 的原生 API 就是异步的设计)
在连接数据库的操作发出去后,即使还没连接上,下面的 testDB.table('user')
和 user.add()
也会先开始执行
也就是说,之后的「获取 user 表」 和 「添加一条数据」实际上会先于「连上数据库」这个过程执行,如果实现该 API 设计时未处理这个问题,上面的示例代码肯定会报错
而要处理这个问题,我用到了下面两个方法:
add()
),先拿到数据库的连接,再进行操作具体而言,就是
Godb
的 class 中定义一个 getDB(callback)
,用来获取 IndexedDB 连接实例getDB
,在 callback
获取到 IndexedDB 的连接实例后再进行操作getDB
中使用一个队列,如果数据库还没连接上,就把 callback
放进队列,在连接上后,执行这个队列中的函数callback
执行即可在调用 getDB
时,可能有三种状态(其实还有个数据库已关闭的状态,这里不讨论):
第一种状态只在第一次执行 getDB
时触发,因为一旦尝试建立连接就进入下一个状态了;第一次执行被我放到了 Godb
类的构造函数中
第三种状态时,也就是已经连上数据库后,直接把连接实例传进 callback
执行即可
关键是处理第二种状态,此时正在连接数据库,但还未连上,无法进行增删改查:
const testDB = new Godb('testDB');
const user = testDB.table('user');
user.add({ name: 'luke' }); // 此时数据库正在连接,还未连上
user.add({ name: 'elaine' }); // 此时数据库正在连接,还未连上
testDB.onOpened = () => { // 数据库连接成功的回调
user.add({ name: 'lucas' }); // 此时已连接
}
上面的例子,头两个 add
操作时其实数据库并未连接上
那要如何操作,才能保证正常添加,并且 luke
和 elaine
在 lucas
进入数据库的顺序和代码一致呢?
答案是使用队列 Queue,把两个 add
操作加进队列,在连接成功时,按先进先出的顺序执行
这样,用户就不需要关心,操作时数据库是否已经连上了(注意增删改查有异步回调,在回调里可以知道是否操作成功),Godb
帮你在幕后做好了这一切
注意之所以使用 callback
而不是 Promise
,是因为 JS 中的回调既可以是异步的,也可以是同步的
而连接成功,已经有连接实例后,直接同步返回连接实例更好,没必要再使用异步
还是以 Vue 为例,如果我们在 Vuex(全局变量)中添加连接实例:
import Godb from 'godb';
new Vuex.Store({
state: {
godb: new Godb('testDB')
}
});
这样,在所有组件中,我们都可以使用同一个连接实例:
new Vue({
computed: {
// 把全局实例变为组件属性
godb() {
return this.$store.state.godb;
}
},
mounted() {
this.godb.table('user').add({
name: 'luke',
age: 22
}).then(id => console.log(id));
}
});
总结这个方案的优点:
缺点:Godb
开发更麻烦,不是简单把 IndexedDB 包一层 Promise 就行
因此,我最终采用了这个方案,毕竟麻烦我一个,方便你我他,优点远远盖过了缺点
如果对实现好奇的话,可以去阅读源码,当前只是实现了基本的 CRUD,源码暂时还不复杂
在把基本的 CRUD 完成后,我就写下了这篇文章,让大家来尝尝鲜
而接下来要做的事其实非常多,近期我会完成下面的开发:
Table.update()
:更好的更新数据的方案本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/PVCPpu-TJbwA0WG7h_U21w
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。