JavaScript 前端存储的几种方式简单介绍

一般情况下,数据都是存在在后台服务器的数据库中.

比如常见的关系型数据库 : MySQL,Oracle ,MS SQL SERVER

非关系型数据库 : MongoDB,Redis

常规操作是,前端发送一个请求,后台接受请求,然后从数据库获取数据,最后返回给前端.

这里接受的前端数据存储,就和后端数据库没有一毛钱关系了.

它是存储在客户端,也就是用户浏览器中当中.

所谓的同源策略:

当然这些前端存储的分界点就是和浏览器当前域名绑定的.

不可能你在 zhihu.com 里写的数据能再 baidu.com 里访问到.


cookie

严格来将,cookie是和后端有关系的.

cookie 一般是由后端设置,并通过 response 流发送给前端.

并设置过期时间.

在过期时间结束之前,cookie 一直会存储在浏览器当中.

并且每次发送HTTP 请求时,都会携带在HTTP请求头中.


localStorage

localStorage 生命周期是永久的,这表示了,除非用户自己手动清除.

否则就一直存储在用户的浏览器当中.(关机,重启没用.写入到硬盘了)

localStorageAPI很简单.

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对象.拿出来直接就用.
不像前者,存数据要从对象到字符串.
取数据从字符串到对象.(当然也不是很复杂.主要是存储空间小了.)

  • 支持二进制存储

主要是可以存储 ArrayBufferBlob 对象.(但我没找到实际的场景和demo,无法做演示)


step 1 - indexedDB.open -- 打开或者创建数据库

上面齐刷刷的罗列的六条规则,其实对于初学者来说,没什么大用.初学的时候,不用太抠规则.
有时候,看别人写的博客,喜欢用一些比较官方的词句,是很专业,但对于大多数读者来说,
特别是初学者来说,特别的不友好.

indexedDB 使用的第一步就是打开或者创建数据库.
也就是标题里的 open 方法.

我这里说的是打开或者创建.一开始我还以为有一个 create 的方法,后来发现并没有.

indexedDB.open(dbName,version) 打开或者创建indexedDB数据库.

  • 第一个参数 dbName 很明显了,是数据库的名字.
  • 第二个参数 version 这个是数据库的版本. 第二个参数可传可不传(不传的时候默认为1)

还记得上一段时候的有关于 indexedDB 规则的第二条.
也就是 indexedDB 中,所有的操作都是异步的.

对于,open 方法,当然也不例外.

异步无非就是做不在主线程做这个事情,而是丢到事件循环了.
等啥时候,异步任务事情做完了,在通知主线程回调就好了.

image.png

看上述截图可以知道.

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'} 的方式确定主键,那我们还必须维护主键的唯一性.

结合上述的代码,我们在一个叫 mydbindexedDB 数据库里创建了一个 Persons 对象向仓储.
并将即将存储在Persons 对象仓储里的对象 Person 的 属性 id 作为主键.

image.png

墙裂推荐使用第二种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 了.

那和全局打开游标读取又有什么分别呢?

根据索引读取,可以排除一些,没有设置这些字段的存储对象.

你可能感兴趣的:(JavaScript 前端存储的几种方式简单介绍)