JS Advance --- 数据存储

254 阅读11分钟

WebStorage

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

WebStorage主要提供了一种机制,可以让浏览器提供一种比cookie更直观的key、value存储方式

localStorage: 本地存储,提供的是一种永久性的存储方法,在关闭掉网页重新打开时,存储的内容依然保留

sessionStorage: 会话存储,提供的是本次会话的存储,在关闭掉会话时,存储的内容会被清除(在浏览器中,我们认为一个标签页会建立一个会话,如果一个标签页关闭了,那么我们会认为对应的会话也就相应的结束了)

注意:

  1. 每一个域名下都有自己的localStoragesessionStorage

  2. 不同域下的localStoragesessionStorage是不共通的

常见属性和方法

localStoragesessionStorage的属性和方法的使用是一致的

这里以sessionStorage为例

// setItem(key, value) --- 设置值
// key --- 属性名
// value --- 值(如果值的类型不是字符串,那么会将这个值先转换为字符串形式以后再进行存储)
sessionStorage.setItem('name', 'Klaus')
sessionStorage.setItem('age', 24)

// 获取值 --- 没有获取到对应value的时候,值为null
console.log(sessionStorage.getItem('name'))

ITsed2.png

存储的key在浏览器中会按照字典排序进行编排

此时age对应的index为0,name对应的index为1

注意: 是字典排序的顺序,不是插入值的先后顺序

console.log(sessionStorage.key(0)) // => age
console.log(sessionStorage.key(1)) // => name
// 使用length属性 查看存储的storage的长度
// 只读属性 不可以修改
onsole.log(sessionStorage.length) // => 2
// 移除具体的key值
sessionStorage.remove('name')

// 全部清空
sessionStorage.clear()

简单封装

class Cache {
  constructor(isLocal = true) {
    this.type = isLocal ? localStorage : sessionStorage
  }

  setItem(key, value) {
    if (!value || value instanceof Function) {
      throw new TypeError('value的类型不正确')
    }

    this.type.setItem(key, JSON.stringify(value))
  }

  getItem(key) {
    return JSON.parse(this.type.getItem(key))
  }

  removeItem(key) {
    this.type.removeItem(key)
  }

  clear() {
    this.type.clear()
  }
}

export const localCache = new Cache()
export const sessionCache = new Cache(false)

IndexedDB

我们能看到DB这个词,就说明它其实是一种数据库(Database),通常情况下在服务器端比较常见

在实际的开发中,大量的数据都是存储在数据库的,客户端主要是请求这些数据并且展示

有时候我们可能会存储一些简单的数据到本地(浏览器中),比如token、用户名、密码、用户信息等,比较少存储大量的数 据,如果确实有大量的数据需要存储,这个时候可以选择使用IndexedDB

IndexedDB是一种底层的API,用于在客户端存储大量的结构化数据。

  • 它是一种事务型数据库系统,是一种基于JavaScript面向对象数据库,有点类似于NoSQL(非关系型数据库)
  • IndexDB本身就是基于事务的,我们只需要指定数据库模式,打开与数据库的连接,然后检索和更新一系列事务即可
  • 相比较只能存储字符串的storage,使用IndexDB进行大量数据的存储和检索,其执行效率有着显著的提高

特点

  1. indexedDB是事务型数据库

在执行数据库操作的时候,对于某些业务要求,一系列操作必须全部执行,而不能仅执行一部分

例如转账,张三向李四转100块,在数据库中是两个操作

  1. 张三 账号减去100块
  2. 李四账户添加100块

这两个操作必须全部执行,或者,由于某些原因,如果第一条语句成功,第二条语句失败,就必须全部撤销。

这种把多条语句作为一个整体进行操作的功能,被称为数据库事务

数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。

如果事务失败,那么效果就和没有执行这些数据库操作一样,不会对数据库数据有任何改动。

所以可以认为数据库事务操作的最小单元

在indexedDB中所有操作都是一个个的事务

  1. indexedDB操作类似于noSQL,但并不是noSQL

    noSQL操作的数据格式是JSON,而indexedDB操作的数据格式就是纯JS对象

  2. indexedDB每次操作之后需要手动刷新数据库

    默认情况下,数据库中数据改变后,浏览器显示的数据库数据并不会改变,需要手动刷新后才可以看到改变后的最新数据

  3. indexedDB的操作全部都是异步的

    因为indexedDB是浏览器端数据库,而对于数据库的所有操作都是相对比较耗时的。所以indexedDB的所有API都被设计为是异步的,从而避免阻塞主线程的执行。因此indexedDB的所有操作都是在对应的回调函数中进行操作的

创建数据库

// 打开数据库(建立数据库连接 --- 如果存在直接建立连接,如果不存在,直接新建)
// 参数1: 数据库名称
// 参数2: 版本号,如果数据库已经存在,那么默认值就是当前数据库的版本号
//               如果数据库是新建的,那么版本号的默认值就是1

// 返回值 数据库链接对象
const dbRequest = indexedDB.open('foo')

// 监听数据库链接失败回调
dbRequest.onerror = () => console.log('error')

// 监听数据库链接成功回调 
//    --- indexedDB的所有操作都是异步的,所以需要在onsuccess回调中进行数据库的相关操作
// event.target.result 就是我们所需要的数据库对象
dbRequest.onsuccess = event => console.log(event.target.result)

// 在新建数据库或数据库版本升级的时候会回调的函数
// event.target.result 就是我们所需要的数据库对象
dbRequest.onupgradeneeded = event => {
  const db = event.target.result
  // createObjectStore方法用来创建数据存储对象 --- 其实就是数据库中的数据表
  // keyPath对应的就是数据库中每一条记录中的主键
  db.createObjectStore('users', { keyPath: 'id' })
}

新增数据

const dbRequest = indexedDB.open('foo')

dbRequest.onerror = () => console.log('error')

dbRequest.onsuccess = event => {
  const db = event.target.result

  class User {
    constructor(id, name, age) {
      this.id = id
      this.name = name
      this.age = age
    }
  }

  const users = [
    new User(100, "Klaus", 18),
    new User(100, "Alex", 40),
    new User(100, "Steven", 30),
  ]

  // IndexedDB是事务型数据库,所有操作是基于事务
  // 参数1: 需要操作的数据表,如果需要操作多个的时候,可以传递数组作为对应的参数
  // 参数2: 对数据表的操作方式 一般传入readwrite即可
  const transaction = db.transaction('users', 'readwrite')

  // 创建数据操作处理对象 --- 参数是这个操作对象实际需要操作的那个数据表
  const store = transaction.objectStore('users')

  // 添加数据
  for (const user of users) {
    // 添加记录
    const request = store.add(user)

    // 监听某一条记录添加是否成功
    request.onsuccess = () => console.log(`${user.name} 添加成功`)

    // 监听插入失败的回调
    // 因为indexedDB是事务型数据库,所以对其所有的操作都是一个个的事务
    // 如果某一个记录插入失败了,那么之前所有成功插入的记录都会被回滚
    request.onerror = request => console.log(request.target.error)
  }

  // 监听事务是否被操作成功
  store.oncomplete = () => console.log('数据全部插入完成')
}

dbRequest.onupgradeneeded = event => {
  const db = event.target.result
  db.createObjectStore('users', { keyPath: 'id' })
}

查询数据

const dbRequest = indexedDB.open('foo')

dbRequest.onerror = () => console.log('error')

dbRequest.onsuccess = event => {
  const db = event.target.result

  const transaction = db.transaction('users', 'readwrite')
  const store = transaction.objectStore('users')

  // 查询方式1 --- 通过主键进行查找
  store.get(102).onsuccess = request => console.log(request.target.result)

  // 查询方式2 === 使用数据库游标进行遍历
  // 新建或成功移动查询游标后会执行的回调
  store.openCursor().onsuccess = request => {
    const cursor = request.target.result

    if (cursor) {
      console.log(cursor.key, cursor.value)

      // 游标下移,移动成功后,会自动再次执行onsuccess方法
      cursor.continue()
    } else {
      console.log("数据遍历完毕")
    }
  }
}

dbRequest.onupgradeneeded = event => {
  const db = event.target.result
  db.createObjectStore('users', { keyPath: 'id' })
}

修改数据

indexedDB的查询操作是依赖于查询操作的

也就是需要先将数据查讯到后再去修改对应的数据

const dbRequest = indexedDB.open('foo')

dbRequest.onerror = () => console.log('error')

dbRequest.onsuccess = event => {
  const db = event.target.result

  const transaction = db.transaction('users', 'readwrite')
  const store = transaction.objectStore('users')

  store.openCursor().onsuccess = request => {
    const cursor = request.target.result

    if (cursor) {
      if (cursor.key === 102) {
        // 取出数据
        const value = cursor.value
        value.age = 28
        // 更新数据
        cursor.update(value)
      } else {
        cursor.continue()
      }
    }
  }

}

dbRequest.onupgradeneeded = event => {
  const db = event.target.result
  db.createObjectStore('users', { keyPath: 'id' })
}

删除数据

const dbRequest = indexedDB.open('foo')

dbRequest.onerror = () => console.log('error')

dbRequest.onsuccess = event => {
  const db = event.target.result

  const transaction = db.transaction('users', 'readwrite')
  const store = transaction.objectStore('users')

  store.openCursor().onsuccess = request => {
    const cursor = request.target.result

    if (cursor) {
      if (cursor.key === 102) {
        // 删除数据
        cursor.delete()
      } else {
        cursor.continue()
      }
    }
  }

}

dbRequest.onupgradeneeded = event => {
  const db = event.target.result
  db.createObjectStore('users', { keyPath: 'id' })
}

Cookie

Cookie(复数形态Cookies),又称为“小甜饼”。类型为“小型文本文件,某些网站为了辨别用户身份而存储在用户本地终端(Client Side)上的数据。

  • cookie一般不是在浏览器端手动设置的,而是由服务器主动向浏览器进行设置的
  • 浏览器会在特定的情况下携带上cookie来发送请求,我们可以通过cookie来获取一些信息

Cookie总是保存在客户端中,按在客户端中的存储位置,Cookie可以分为内存Cookie和硬盘Cookie

内存Cookie:

  • 由浏览器维护,保存在内存中,浏览器关闭时Cookie就会消失,其存在时间是短暂的
  • 没有设置过期时间或者过期时间不为0或者负数的cookie,默认情况下cookie是内存cookie,在关闭浏览器时会自动删除

硬盘Cookie:

  • 保存在硬盘中,有一个过期时间,用户手动清理或者过期时间到时,才会被清理
  • 有设置过期时间,并且过期时间不为0或者负数的cookie,是硬盘cookie,需要手动或者到期时,才会删除

IacpFt.png

服务器通过在response header头中设置set-cookie字段来向浏览器存储对应的cookie

IacvyR.png

浏览器会在请求对应路径的时候,会自动在request header中添加上上次服务器传递过来的cookie

常见属性

  1. cookie的生命周期:

默认情况下的cookie是内存cookie,也称之为会话cookie,也就是在浏览器关闭时会自动被删除;

我们可以通过设置expires或者max-age来设置过期的时间:

  • expires:设置的是Date.toUTCString(),设置格式是;expires=date-in-GMTString-format
  • max-age:设置过期的秒钟,;max-age=max-age-in-seconds (例如一年为606024*365)
  1. cookie的作用域:(允许cookie发送给哪些URL)
  • Domain:指定哪些主机可以接受cookie
    • 如果不指定,那么默认是 origin,不包括子域名
    • 如果指定Domain,则包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如developer.mozilla.org)
  • Path:指定主机下哪些路径可以接受cookie
    • 如果不指定,默认值是/,即同域名下的所有路径都需要传递对应的cookies
    • 设置 Path=/docs,则以下地址都会匹配,则匹配/docs及其子路由
      • 如 /docs, /docs/Web/,/docs/Web/HTTP等
// 输出所有同作用域下的cookie
console.log(document.cookie)  // => name=Klaus; age=24 --- 多个cookie之间使用分号+空格进行分割

// 新增cookie
// 如果需要设置多个cookie的值,那么就需要调用多个document.cookie来进行设置
document.cookie="name=Klaus" // key值为name value值为Klaus

// 移除cookie
// 注意: 我们只能使用JS去移除那些httpOnly属性为false的cookie
// 如果一个cookie的httpOnly属性的值是true,那么就意味着该cookie只能由服务器通过请求和响应的方式来进行添加和移除
// 是无法通过JS代码来删除httpOnly属性的值是true的cookie的

// 1. 设置max-age的值为0或负数
document.cookie="name=Klaus; max-age=0;"

// 2. 设置expires时间是一个已过期的时间
// 注意: expoires属性的值必须是一个GMT格式的时间,可以通过new Date().toGMTString()来获取对应的值
document.cookie="name=Klaus; expires=Sat, 13 Nov 2020 07:23:42 GMT"

存在的问题

  1. cookie的传输是明文的,虽然我们可以将传输数据加密后再进行传输

  2. cookie具有大小限制,最多存放4kb的内容

  3. 只要符合同作用域的路径,在进行请求的时候,浏览器都会加上对应的cookies,无论服务器是否需要这些cookies

  4. 使用cookies来存储服务器发送给客户端的数据,只适用于浏览器

    而现如今客户端并不只有浏览器,而有Android,IOS,小程序等等

    并不是所有的客户端都支持cookie这种形式存取数据的,所以服务器端需要准备多套处理方案

所以目前更流行的一种方式是使用token来替换cookie

在登录后等操作后,服务器会返回我们对应的具有时效性的token

我们在请求数据的时候,可以主动决定是否需要传递对应的token给服务器

我们一般可以将token放置到请求头或请求体中,从而将对应的数据传递给服务器