美文网首页
JavaScript 前端存储的几种方式简单介绍

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

作者: 人话博客 | 来源:发表于2019-03-06 12:36 被阅读0次

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

    比如常见的关系型数据库 : 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 前端存储的几种方式简单介绍

          本文链接:https://www.haomeiwen.com/subject/zxstpqtx.html