一般情况下,数据都是存在在后台服务器的数据库中.
比如常见的关系型数据库 : MySQL
,Oracle
,MS SQL SERVER
非关系型数据库 : MongoDB
,Redis
常规操作是,前端发送一个请求,后台接受请求,然后从数据库获取数据,最后返回给前端.
这里接受的前端数据存储,就和后端数据库没有一毛钱关系了.
它是存储在客户端,也就是用户浏览器中当中.
所谓的同源策略:
当然这些前端存储的分界点就是和浏览器当前域名绑定的.
不可能你在
zhihu.com
里写的数据能再baidu.com
里访问到.
cookie
严格来将,cookie是和后端有关系的.
cookie
一般是由后端设置,并通过 response
流发送给前端.
并设置过期时间.
在过期时间结束之前,cookie
一直会存储在浏览器当中.
并且每次发送HTTP
请求时,都会携带在HTTP
请求头中.
localStorage
localStorage
生命周期是永久的,这表示了,除非用户自己手动清除.
否则就一直存储在用户的浏览器当中.(关机,重启没用.写入到硬盘了)
localStorage 的API
很简单.
const db = window.localStorage
db.setItem('name','李四')
db.setItem('age',22)
console.log(db.getItem('name')) // '李四'
console.log(db.getItem('age')) // '22' 存进去的是数字,取出来是字符串了
db.removeItem('age')
console.log(db.getItem('age')) // 当取出一个不存在的数据是,返回 null
db.clear() // 删除所所有数据
console.log(db.getItem('name'))
如果存储的是对象,直接存储拿出来的是 [object object]
无法使用.
const obj = {xxxxx}
localStorage.setItem('key',JSON.stringify(obj))
const obj2 = JSON.parse(localStorage.getItem('key'))
sessionStorage
sessionStorage
用法和 localStorage
完全一致. 这里就不多做介绍了.
除了 seesionStorage
在当前域名下的所有选项卡都关闭之后,它就被清除.(仅仅被写入内存)
sessionStorage.setItem(key,value)
sessionStorage.getItem(key,value)
sessionStorage.removeItem(key)
sessionStorage.clear()
indexedDB
indexedDB
有以下几个特性
- 它是一个对象仓库.
里面存储的都是
js
对象.
- 异步性
所有的操作,包括上层的数据库打开,下层的数据操作等.
- 基于事务
所有的数据操作都是基于事务的,包括
add
,delete
,put
,get
全是基于事务操作.
- 同源限制
我觉得这是废话,如果不是同源限制的,那也太不安全和太扯淡了.
- 存储空间大
和
localStorage
以及sessionStorage
相比.
空间要大的多.一般来说不少于250MB.
而前者一般只有5M左右.
更好的一个好处是,它能够直接存储js
对象.拿出来直接就用.
不像前者,存数据要从对象到字符串.
取数据从字符串到对象.(当然也不是很复杂.主要是存储空间小了.)
- 支持二进制存储
主要是可以存储
ArrayBuffer
和Blob
对象.(但我没找到实际的场景和demo,无法做演示)
step 1 - indexedDB.open -- 打开或者创建数据库
上面齐刷刷的罗列的六条规则,其实对于初学者来说,没什么大用.初学的时候,不用太抠规则.
有时候,看别人写的博客,喜欢用一些比较官方的词句,是很专业,但对于大多数读者来说,
特别是初学者来说,特别的不友好.
indexedDB
使用的第一步就是打开或者创建数据库.
也就是标题里的 open
方法.
我这里说的是打开或者创建.一开始我还以为有一个
create
的方法,后来发现并没有.
indexedDB.open(dbName,version)
打开或者创建indexedDB
数据库.
- 第一个参数 dbName 很明显了,是数据库的名字.
- 第二个参数 version 这个是数据库的版本. 第二个参数可传可不传(不传的时候默认为1)
还记得上一段时候的有关于 indexedDB
规则的第二条.
也就是 indexedDB
中,所有的操作都是异步的.
对于,open
方法,当然也不例外.
异步无非就是做不在主线程做这个事情,而是丢到事件循环了.
等啥时候,异步任务事情做完了,在通知主线程回调就好了.
看上述截图可以知道.
open
方法,会有一个返回值.
类型为 IDBOpenRequest
..
const openReq = indexedDB.open('mydb',1)
openReq.onsuccess = (event) => {
console.log('数据库打开或者创建成功')
}
openReq.onerror = (event) => {
console.log('数据库打开或者创建失败')
}
openReq.onupgradeneeded = (event) => {
//??????
}
前两个事件都很好理解. onsuccess
& onerror
. 分别表示数据库创建或者打开成功或者失败.
第三个事件 onupgradeneeded
是个什么意思呢?
当数据库的版本号,也就是第二个参数
version
发生改变的时候,就会触发.
那为什么要改变version这个参数呢?
在之前接触的数据中,比如:
MYSQL
,MSSQLSERVER
,Oracle
这类关系型数据库等.
只要连接到了数据库服务器,并有相应的权限,改表结构是一件很自然的事情.
但是对于indexedDB
数据库则不一样.
每次修改存储对象的表结构的时候,必须重新传递一个新的version
.
然后会触发onupgradeneeded
这个事件.
然后必须在onupgradeneeded
这个事件的回调函数里修改或者定义表结构.
这样说可能会比较懵逼.
可以把这个过程想象成:
indexedDB 规定了,数据里的表结构写好了,就不要变了.
如果你要变,就给我新传递一个version
.
作为初学者的我很懵逼的onupgradeneeded事件
indexedDB.open('mydb')
-
version = null
: 如果没传version
.indexedDB
会把version
设置成1
: ==>null -> 1
,有版本变动. -> 触发onupgradeneeded
indexedDB.open('mydb',1)
-
version = 1
: 如果传了version
. 但是一开始数据库是没有version
的, 也是从null
===>1
===> 触发onupgradeneeded
indexedDB.open('mydb',1)
因为上一次
version=1
,这次还是传入version=1
,没有变动,所以不触发onupgradeneeded
step 2 - db.createObjectStore - 创建表结构
上一章节说过,表单的创建必须放在 onupgradeneeded
这个回调函数的事件里.
因为一开始我们使用open
打开或者创建数据库的时候
不论是哪一种打开方式
- indexedDB.open('mydb')
- indexedDB.open('mydb',1)
都会触发 onupgradeneeded
这个事件.
而 indexedDB
也规定了,表单的创建或者是更新都必须放在 onupgradeneeded
这个回调函数里.
接着在说 db.createObjectStore(storeName,primarykey) 创建对象存储,并设置主键.
-
storeName
: 存储对象的表的名字. -
primarykey
: 设置主键(必须要设置主键,会有两种设置主键的方式) - 返回值是一个当前对象仓储的对象.(这个方法是同步的,也不是所有操作都是异步的)
openReq.onupgradeneeded = (event) => {
// 版本号变化之后,拿到数据库
db = event.target.result
// 首先判断此表单是否在数据库中存在.
if (!db.objectStoreNames.contains('Persons')) {
let PersonStore = db.createObject('Persons', {keyPath: 'id'})
// db.createObject('Persons',{autoIncrement:true})
}
}
对于 indexedDB
中创建的表来说,必须要指定主键.因为后续的操作基本都依赖于主键.
主键的指定形式有两种.
db.createObject('Persons',{autoIncrement:true})
{autoIncrement:true}主键的指定形式表示了,让
indexedDB
自动帮我们维护主键信息.
等同于MY SQL
里的,主键自增.
db.createObject('Persons', {keyPath: 'id'})
{keyPath: 'id'} 这种主键的指定形式是告诉
indexedDB
.
我的Persons
表里的主键是使用我存储进去的这个person
对象里必须要有一个id
属性.
然后就用这个id
属性来做主键.
为什么要设置主键?
- 因为在
indexedDB
中,对Persons
表的绝大部分操作都必须使用到主键. - 并且如果是使用
{keyPath:'id'}
的方式确定主键,那我们还必须维护主键的唯一性.
结合上述的代码,我们在一个叫 mydb
的 indexedDB
数据库里创建了一个 Persons
对象向仓储.
并将即将存储在Persons
对象仓储里的对象 Person
的 属性 id
作为主键.
墙裂推荐使用第二种keyPath的方式设置主键.否则会带来很多操作上的麻烦.
step 3 - Persons 对象的增删查改
我上一步,通过 onupgradeneeded
回调函数,创建了存储对象的仓储.
并以同步的方式,拿到了此仓库对象.
openReq.onupgradeneeded = (event) => {
// 版本号变化之后,拿到数据库
db = event.target.result
// 首先判断此表单是否在数据库中存在.
if (!db.objectStoreNames.contains('Persons')) {
let PersonObject = db.createObject('Persons', {keyPath: 'id'})
}
}
还记得indexedDB
的几个特性里说的第三条:基于事务
所有的数据操作都是基于事务的,包括
add
,delete
,put
,get
全是基于事务操作.
所以,对 Persons
仓库的进行CURD
操作,都是基于事务.
且套路很固定.
- 首先使用
db
获取事务. - 根据事务获取对象需要操作的仓储对象.
添加数据
function add(obj) {
// 因为我们之前设置了.{keyPath:'id'} 主键设置依赖对象的id属性.如果待插入的对象没有这个属性,就不执行插入操作.
if (!obj.id) return
// 第一步:利用db拿到事务.
// ['Persons'] 告诉这个事务需要操作的仓储对象只有 `Persons`
// `readwrite` 我需要对 Persons 仓促进行读写操作.
let personTransaction = db.transaction(['Persons'],'readwrite')
// 第二步: 在拿到的包含对多个仓储操作的事务对象中,获取你要操作的那个仓储对象.
// 这里是 Persons
let PersonsStore = personTransaction.objectStore('Persons')
// 调用 add 方法.
PersonsStore.add(obj)
}
删除数据 - delete
还记得之前添加Persons
仓储时,设置的 keyPath
吗?
我们在删除操作的时候,就需要用到这个 keyPath
function delete(id) {
// 第一步:利用db拿到事务.
// ['Persons'] 告诉这个事务需要操作的仓储对象只有 `Persons`
// `readwrite` 我需要对 Persons 仓促进行读写操作.
let personTransaction = db.transaction(['Persons'],'readwrite')
// 第二步: 在拿到的包含对多个仓储操作的事务对象中,获取你要操作的那个仓储对象.
// 这里是 Persons
let PersonsStore = personTransaction.objectStore('Persons')
// 调用 delete 方法.
PersonsStore.delete(id)
}
修改数据 -- put
修改操作,同样也需要利用到我们之前设置的 keyPath.
所以,我们给 put
传递的对象中,一定要包含 id 属性.
这就分两种情况了:
-
id
的值在仓储中,存在.put = update -
id
的值不在仓储中存储在. put = insert
function put(obj) {
// 因为我们之前设置了.{keyPath:'id'} 主键设置依赖对象的id属性.如果待插入的对象没有这个属性,就不执行put操作..
if (!obj.id) return
// 第一步:利用db拿到事务.
// ['Persons'] 告诉这个事务需要操作的仓储对象只有 `Persons`
// `readwrite` 我需要对 Persons 仓促进行读写操作.
let personTransaction = db.transaction(['Persons'],'readwrite')
// 第二步: 在拿到的包含对多个仓储操作的事务对象中,获取你要操作的那个仓储对象.
// 这里是 Persons
let PersonsStore = personTransaction.objectStore('Persons')
// 调用 put 方法.
PersonsStore.put(obj)
}
查询数据 - get
同样的,查询数据,更加需要我们之前设置的 keyPath
function get(id) {
// 第一步:利用db拿到事务.
// ['Persons'] 告诉这个事务需要操作的仓储对象只有 `Persons`
// `readwrite` 我需要对 Persons 仓促进行读写操作.
let personTransaction = db.transaction(['Persons'],'readwrite')
// 第二步: 在拿到的包含对多个仓储操作的事务对象中,获取你要操作的那个仓储对象.
// 这里是 Persons
let PersonsStore = personTransaction.objectStore('Persons')
// 调用 get 方法.
PersonsStore.get(id)
}
对于单个对象的 CURD 操作.就是上述那个套路.
- 当前数据库通过事务获取需要对象仓储集合
(['Persons'])
以及设置操作权限('readwrite')
- 根据事务的
objectStore(storename)
方法来获取需要操作的具体的仓储对象 .
接着,不管是 add
delete
put
还是 get
上述代码功能只写了一半.
获取数据的过程仍然是异步的.
但会同步返回一个当前操作请求的对象.
对象里有2个回调. onsuccess
& onerror
function add(obj) {
// 因为我们之前设置了.
if (!obj.id) return
// 第一步:利用db拿到事务.
// ['Persons'] 告诉这个事务需要操作的仓储对象只有 `Persons`
// `readwrite` 我需要对 Persons 仓促进行读写操作.
let personTransaction = db.transaction(['Persons'], 'readwrite')
// 第二步: 在拿到的包含对多个仓储操作的事务对象中,获取你要操作的那个仓储对象.
// 这里是 Persons
let PersonsStore = personTransaction.objectStore('Persons')
// 调用 add 方法.并返回添加请求对象
let addReq = PersonsStore.add(obj)
// 在回调里判断此操作是否成功
addReq.onsuccess = (event) => {
const obj = event.target.result
console.log('数据添加成功')
}
addReq.onerror = (event) => {
console.log(event.target.error.message)
}
}
其他的套路都是一样的.
游标
上一节讲的add
,delete
,put
,get
.都是操作单个数据.
但最常见的场景是,如何读取多个数据,也就是获取一个数据列表,甚至是整个仓储数据.
这里就需要利用到游标.
代码如下:
function readAll() {
// 1. 根据db拿到事务.
let readAllTransaction = db.transaction(['Persons'],'readonly')
// 2. 根据事务拿到需要操作的 objectStore
let personStore = readAllTransaction.objectStore('Persons')
// 3. 利用 objectStore 打开游标
let readAllReq = personStore.openCursor()
// 4. 仍然是里用回调函数获取游标的读取数据.
readAllReq.onsuccess = (event) => {
let resultArr = []
//拿到当期那游标
let cursor = event.target.result
if (cursor) {
resultArr.push(cursor.value) // 游标的.value属性就是每一次游标读取出来的对象数据
cursor.continue() // 这一句很关键,是让游标指针指向下一个元素.
} else {
// 此时,数据就读取完毕了.
resultArr
}
}
readAllReq.onerror = (event) => {
console.log('游标读取失败')
}
}
关于创建仓储索引,并利用索引读取数据.
其实,我们在 onupgradeneeded
回调函数里,创建Persons仓储的时候.指定了此对象仓储的主键.
接着,也可以同时设置此仓储对象的索引.
代码如下:
openReq.onupgradeneeded = (event) => {
// 版本号变化之后,拿到数据库
db = event.target.result
// 首先判断此表单是否在数据库中存在.
if (!db.objectStoreNames.contains('Persons')) {
let PersonObject = db.createObject('Persons', {keyPath: 'id'})
// 创建索引
// 这个决定了存储对象中的 object.name 字段不能重复 且可以用索引搜索数据
personStore.createIndex('name', 'name', { unique: true })
// 这个决定了存储对象中的 object.email 字段不能重复 , 且可以用索引搜索数据
personStore.createIndex('email', 'email', { unique: true })
// 其实这也是在定义数据结构.也就是所谓的 dataSchema
personStore.createIndex('address', 'address', { unique: false })
}
}
personStore.createIndex('name', 'name', { unique: true })
- 第一个参数
name
当前索引的名字,可以随便写. - 第二个参数
name
是指存储在仓储中的对象,以那个键作为索引.这里的person.name
- 第三个参数是一个对象. 指明了当前索引的值是否可以在本仓储中重复.
创建索引的几个好处:
- 可以事先定义存储在内部的对象可能会包含哪些属性.
- 设置属性定义的唯一性(unique),保证数据的正确性
- 仓储也可以利用索引来读取数据.
之前说了一个 indexedDB
仓储对象 Persons
身上的 get
方法.
但是这个方法默认是里用主键来读取的.
但当我们设置了索引之后,就可以利用索引来读取数据了.
比如我们设置了
personStore.createIndex('name', 'name', { unique: true })
那么,我们可以指定,我们查询所依赖的字段信息是当前对象的 name
属性.
function fetchDataWithIndex(indexName,indexValue) {
// 1. 仍然是拿到事务
let personTransaction = db.transaction(['Persons'],'readonly')
// 2. 拿到需要操作的仓储对象.
let personObjectStore = personTransaction.objectStore('Persons')
// 3. 根据对应的indexName,创建索引查询对象.
let indexFetch = personObject.index(indexName)
// 4. 利用执行了name的索引查询对象查询.
let indexFetchReq = indexFetch.get(indexValue)
// 5.后面的套路和索引读取没有区别了.
indexFetchReq.onsuccess = (event) => {
event.target.result // 这个就是利用 name 索引读取到值为 indexValue 的数据.
}
indexFetchReq.onerror = (event) => {
event.target.error.message // 读取失败
}
}
细心的你可能发现了,如果是里用指定的索引查询.
但索引设置又没有设置成 {unique:true}
而是 {unique:false}
的话.
那么读取出来的数据可能会有多条.
function fetchDataListWithIndex(indexName) {
// 1. 仍然是拿到事务
let personTransaction = db.transaction(['Persons'],'readonly')
// 2. 拿到需要操作的仓储对象.
let personObjectStore = personTransaction.objectStore('Persons')
// 3. 根据对应的indexName,创建索引查询对象.
let indexFetch = personObject.index(indexName)
// 4. 利用执行了name的索引查询对象查询.
//let indexFetchReq = indexFetch.get(indexValue)
let indexFetchReq = indexFetch.openCursor() // 打开指定了indexName 的索引游标
// 5.后面的套路和索引读取没有区别了.
indexFetchReq.onsuccess = (event) => {
let resultArr = []
let cursor = event.target.result
if (cursor) {
resultArr.push(cursor.value)
cursor.continue()
} else {
resultArr // 这个就是利用指定索引读取出来的而结果.
}
}
indexFetchReq.onerror = (event) => {
event.target.error.message // 读取失败
}
}
但是又有一个问题.
这里不能指定 indexValue
了.
那和全局打开游标读取又有什么分别呢?
根据索引读取,可以排除一些,没有设置这些字段的存储对象.