前端开发的客户端本地存储

在前端开发过程当中,为了与服务器更方便的交互或者提高用户体验,咱们都会在客户端(用户)本地保存一部分数据,好比cookie/localStorage/sessionStorage。在后端管理系统的前端,更是会涉及到一部分超大数据的请求,一个接口有时会达到5M甚至15M的程度,当这个接口数据并非常常更新时,咱们能够用两种方式,一种是分页请求+预加载+懒加载,另外一种就是本地存储+热更新。而因为第二种方式用户体验更优秀,即是我经常使用的方式。php

这篇文章的客户端本地存储,咱们主要讲到cookie/localStorage/sessionStorage/indexedDB四种技术。html

cookie

HTTP Cookie一般简称cookie,该标准用于浏览器存储会话信息,在发起HTTP请求时携带Cookie参数:前端

// Request Header 

GET /oss/index.php?r=api/jlog/collect HTTP/1.1
Host: gzhxy.baidu.com:8090
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36

···

Cookie: name=Leon; age=24

Cookie有一些限制:ajax

  • 它是以; (分号+一个空格)分割的键值对字符串,在网络传送时必须是URL编码的。
  • 绑定在固定域名下,不容许其它域名访问。
  • 有些浏览器有数量限制,在超出后删除顺序不统一,有些最近最少使用(LRU),有些随机删
  • 同一域名下总大小限制5KB,超出限制后静默失败

Cookie的参数构成:数据库

  • 名称:惟一肯定,不区分大小写(实际编写建议区分),必须URL编码
  • 值:字符串值,必须URL编码
  • 域:该cookie字段有效的域,能够为baidu.com域名,也能够为cdn.baidu.com子域名,缺省值为当前页面子域名
  • 路径:该cookie字段有效范围为指定域下的具体路径,如cdn.baidu.com/oss,那么其余路径就没法访问
  • 失效时间:cookie被删除的时间戳,缺省值为浏览器会话结束被删除,也能够手动设置,时间格式为GMT格式Wdy, DD-Mon-YYYY HH:MM:SS GMT,能够调用Date实例的toGMTString()方法转换,若是设置为过去的时间,该cookie被马上删除
  • 安全标志:为单词secure而非键值对,指定后,只有在SSL链接的时候才会被发送到服务器,也就是https协议

注意参数中只有名称和值才会被发送给服务器,其他的只是须要浏览器识别的命令式参数。后端

cookie的接口设置很是的不人性化,每每须要咱们对其操做进行封装才会方便使用。下面咱们对其进行增删查改。api

查看cookie:decodeURIComponent(document.cookie);浏览器

添加或修改cookie:缓存

// 须要改为本身须要的cookie和域名、路径以及是否为https
document.cookie = 'encodeURIComponent(name)=encodeURIComponent(Leon); expries=' + (new Date(Date.now() + 24*60*60*1000)).toGMTString() + '; path=oss; domain=cdn.baidu.com; secure';

删除cookie: document.cookie = 'name=Leon; expires=' + (new Date(0)).toGMTString();安全

具体的封装的方式网上有不少,能够去搜一搜,核心就是对cookie进行字符串检索和切分,以及将传入的函数参数最终转换为字符串。

Storage

因为cookie的大小限制和须要全量传递给服务器,在不少场景下并不适用,因此HTML5规范中出现了Storage对象,包含localStoragesessionStorage两种继承对象,属于window的属性。它提供了一般5M的大小空间来保存无需服务器交互的本地数据。

Storage的经常使用方法:

  • clear(): 清除全部值
  • getItem(name): 获取指定name的值
  • key(index): 得到对应索引值的键名
  • removeItem(name): 删除指定name的键值对
  • setItem(name, value): 为指定name设置对应的值

除了这些方法以外,Storage对象能够直接经过点语法或者方括号语法访问属性和操做属性,也能够经过delete关键字删除属性。该对象中的值均为字符串。

持久化数据localStorage一般保留到JS删除或者用户清除浏览器缓存。

会话数据sessionStorage保留到关闭浏览器。因为绑定会话窗口,因此不支持本地文件读写。另外在IE8中,该对象为异步读写,须要调用begin()commit()方法保证成功读取,再也不赘述。

Storage对象作任何的新增,修改或者删除操做,都会触发storage事件。该事件只支持在与服务端通讯时,一个页面修改,另外一个页面会触发该事件。

document.addEventListener('storage', function (e) {
    console.log(e);
});

该事件对象的主要属性:

  • domain: 发生变化的存储空间的域名
  • key:修改的键名
  • newValue:若是是设置值,则为新值;若是删除,则为null
  • oldValue:更改前的值

IndexedDB

拥有了Storage利器,已经能解决不少问题,可是一般5M的大小限制仍是会限制一部分场景,好比后台管理系统的接口数据很容易突破5M,这个时候就须要咱们的浏览器数据库IndexedDB了。其实在此以前,各家厂商主要推广的是Web SQL Database,不事后来被废弃了,虽然如今在部分平台上也能使用,但咱们不作介绍了。

IndexedDB数据库用于浏览器保存结构化数据,区别于传统数据库它保存的是对象,彻底采用事务类型,全部的操做被转换为请求的方式,因此咱们须要对每一步操做添加回调函数。

一个完整的实例为:

// 判断可否正确打开数据库,避免屡次检测
let dbOpened = false;
// 打开本地持久化数据库,默认版本为1
const request = indexedDB.open('jomocha');
// 当打开错误时
request.onerror = function(event){
    console.error('打开本地持久化数据错误', event);
    OSS.commonUI.showMsg('打开本地持久化数据库错误,试用功能,不影响使用,请联系zhaoshuaiqiang', 'error');
};
// 当数据库首次建立该版本时(首次建立或更新版本)
request.onupgradeneeded = function(event){
    const db = event.target.result;
    // 建立一个数据库存储对象,保存全部的维度项,分为name和list两个属性
    const objectStore = db.createObjectStore('dimensions', {
        keyPath: 'name'
    });
    // 定义存储对象的数据项属性
    objectStore.createIndex('name', 'name', {
        unique: true
    });
};
// 成功打开了数据库
request.onsuccess = function(event){
    dbOpened = true;
    const db = event.target.result;
    // 新建一个事务,包含oncomplete 和onerror句柄事件,缺省值为readonly,只读模式,可并行
    const transaction = db.transaction(['dimensions']);
    // 打开存储对象
    const objectStore = transaction.objectStore('dimensions');
    const request = objectStore.get('host');
    request.onsuccess = function (event) {
        // 第一次打开数据库时,确定没有数据,因此须要检测
        if (event.target.result) {
            JomoCha.data = event.target.result.list;
        }
    };
}

数据库

当咱们须要使用IndexedDB时,首先要调用indexedDB.open()方法打开数据库,若是该数据存在,则发起打开的请求,若是不存在,则发起建立并打开的请求。该方法会返回一个IDBRequest对象,能够在该对象上添加回调方法。具体的方法如示例中的最外层请求。

回调函数传入的事件属性event.target就指向该请求,即request。若是发生了错误,event.target.errorCode将会保存错误信息的错误码;若是成功,event.target.result就会保存一个数据库实例对象。

错误码列表(第二个开始省略前缀IDBDatabaseException.):

  • IDBDatabaseException.UNKNOWN_ERR(1):意外错误,没法归类
  • NON_TRANSIENT_ERR(2):操做不合法
  • NOT_FOUND_ERR(3):未发现要操做的数据库
  • CONSTRAINT_ERR(4):违反了数据库约束
  • DATA_ERR(5):提供给事务的数据不知足要求
  • NOT_ALLOWED_ERR(6):操做不合法
  • TRANSACTION_INACTIVE_ERR(7):试图重用已完成的事务
  • ABORT_ERR(8):请求中断,未完成
  • READ_ONLY_ERR(9):试图在只读模式下写入或修改数据
  • TIMEOUT_ERR(10):在有效时间内未完成操做
  • QUOTA_ERR(11):磁盘空间不足

对象存储空间(表)

成功打开数据库以后,咱们就能够打开对象存储空间了,你能够把它理解成数据库中的表,用于保存不一样的数据,如用户、交易、购物车等。下面的全部db表明成功回调中的数据库对象.

咱们先调用db.createObjectStore('dimensions', {keyPath:'name'});来建立一张表,这个dimensions为惟一表名,其中的keyPath属性指定该表的键名,后面存储的全部数据都必须拥有该属性。

看下写入数据库的实例:

// 每次同步更新最新数据
$.ajax({
    url: '?r=tools/api/hosts',
    success: function (data) {
        // 若是可以打开本地数据库,则保存
        if (dbOpened) {
            const request = indexedDB.open('jomocha');
            request.onsuccess = function(event){
                const db = event.target.result;
                // 新建一个事务,读写模式,不可并行
                const transaction = db.transaction(['dimensions'], 'readwrite');
                // 打开存储对象
                const objectStore = transaction.objectStore('dimensions');
                // 使用put方法,有则修改,没有则添加
                const request = objectStore.put({
                    name: 'host',
                    list: data.data
                });
            }
        }
    }
});

这里写入数据库的对象里就包含了name参数。咱们拿到数据使用add()put()方法添加数据,这两种方法区别在于遇到相同的键名存在时,add()报错,put()修改原有值。这两种方法依然为请求,能够对其指定onsuccess()onerror()事件处理回调,示例中省略了。

事务

在建立完成数据后,就能够对其进行查询了,indexedDB中全部读写操做都要经过事务,咱们调用db.transaction();方法来打开全部存储空间(表),也能够传入参数来控制咱们须要打开的表:

// 打开一张表
db.transaction('user');
// 打开多张表
db.transaction(['user', 'dimensions']);

该方法还接受第二个参数做为访问方式,包含3种:readonly(缺省值)、readwriteversionchange。最后一个比较特别,为在版本更新时使用,不能够与其余事务并发执行,容许任何操做,包括删除和建立索引。

如今经过事务咱们已经肯定了操做空间,接下来就能够获取具体的数据对象了,下面的transaction表明着上面的db.transaction()方法返回的事务。事务能够执行多个请求,自己也是一个请求,能够指定相应的oncomplete()onerror()事件处理回调。其中的oncomplete()事件不能拿到该事务中请求的数据。

使用transaction.objectStore('dimension');获取数据对象,而后就能够经过add/put/get/delete/clear方法进行增删查改,均为请求,须要设置onsuccess()onerror()回调。

游标遍历

咱们可使用get()方法检索出具体的单个对象,可是若是须要遍历,咱们就要使用到游标查询了,也就是指定范围,而后遍历数据。下面的objectStore指代上面transaction.objectStore()返回的数据对象集。

// 指定游标范围
const cursorRange = IDBKeyRange.bound('001', '100');
// 打开游标查询
const request = objectStore.openCursor(cursorRange);
request.onsuccess = function (event) {
    const cursor = event.target.result;
    // 必须检查cursor是否存在,若是该项存在,则为IDBCursor实例,不然为null
    if (cursor) {
        console.log(cursor.key + ': ' + cursor.value);
        cursor.continue();
    } else {
        console.log('遍历完成');
    }
}
request.onerror = function (event) {
    console.error('游标区间获取失败');
}

在游标遍历时,具体的数据会保存在event.target.result里,若是该项存在,则会为一个IDBCursor对象实例,不然为null。实例的属性:

  • direction:数值,表示游标的方向,next/nextunique/prev/prevunique,带有unique的会去重
  • key:数据对象键
  • value:数据对象值
  • primaryKey:游标当前的使用键值,后面会说

在遍历到游标中具体的每一项时,可使用update()delete()来修改,若是想要移动游标:

  • continue(key):存在key移动到指定项,不然下一项
  • advance(count):存在count移动指定项数,不然上一项

咱们经过IDBKeyRange对象来控制键范围,有4种方式指定:

  • only(key):只取得想要的键值对,等同于直接调用get(key)
  • lowerBound(key, true):第一个元素为key,若是第二个参数为true,从该项的下一项开始,缺省值为false
  • upperBound(key, true):最后一个元素为key,若是第二个参数为true,从该项的下一项开始,缺省值为false
  • bound(lowerkey, upperkey, true, true):前二者的结合,1和3对应lower,2和4对应upper

openCursor()方法接收的第一个参数为范围区间,若是为null,则默认所有范围;第二个参数为方向,为next/nextunique/prev/prevunique四个,带有unique的会去重

索引

可使用objectStore.createIndex('name', 'name', {unique: true});来建立索引,分别为索引名,索引的属性名,该属性值是否惟一。调用objectStore.delete('name');来删除索引。删除索引不会影响数据,因此没有回调函数。

若是咱们不想在主键上遍历游标或者获取数据,能够在数据集上取得新的索引列表:

// 直接切换索引
const index = objectStore('dimensions');
// 在该索引上进行游标遍历
request = index.openCursor();

若是咱们在非主键的游标中,想要去的主键值,调用index.getKey('007');

并发问题

数据库操做存在并发问题,但因为是异步,咱们不用担忧,可是若是存在新版本变动仍是会致使问题。因此打开数据库时指定onversionchange()处理事件,能够避免这个问题,在setVesion()时,若是触发onerror()表明已经打开了该数据库,没法如今更新版本,提示用户关闭其它网页,从新调用更新。

总结

若是须要与服务器实时交互,使用cookie,若是须要保存一些小信息字段,使用localStorage,若是只须要本次会话有效,使用sessionStorage,若是数据很大,使用indexedDB。使用什么技术跟业务场景匹配,可是技术仍是都要了解都要会,毕竟,巧妇难为无米之炊。

参考资料

  1. JavaScript高级程序设计 23章-离线应用与客户端存储
  2. storage事件使用:https://www.cnblogs.com/incon...
  3. IDBCursor对象:https://developer.mozilla.org...
相关文章
相关标签/搜索