JavaScript 客户端存储学习笔记

客户端存储

cookie

HTTP cookie通常也叫做cookie,最初用于在客户端储存绘画信息;这个规范要求服务器在响应HTTP请求时,通过发送Set-Cookie HTTP头部包含会话信息

名和值在发送时都会经过URL编码;浏览器会储存这些会话信息,并在之后的每个请求中都会通过HTTP头部cookie再将它们发回服务器

这些发送回服务器的额外信息可用于唯一标识发送请求到客户端

限制

cookie是与特定域绑定的;设置cookie后,它会与请求一起发送到创建它的域;这样可以保证cookie中储存的信息只对被认可的接收者开放,不会被其它域访问

浏览器会对储存在客户端机器上的cookie加以限制,cookie也不会占用太多磁盘空间

一般要遵守以下大致的限制,就不会在任何浏览器中碰到问题:

​ 不超过300个cookie

​ 每个cookie不超过4096字节

​ 每个域不超过20个cookie

​ 每个域不超过81920字节

每个浏览器对每个域能设置的cookie数量有限制;如果超出了单个域最大容量,浏览器就会删除之前的cookie,但是一般不要超出限制

浏览器也会限制cookie的大小,大多数浏览器的限制是不超过4096字节,上下可以有一个字节的误差;为保证兼容,最好不要超过4095字节,这个限制适用于一个域所有cookie而不是单个cookie

如果创建的cookie超过最大限制,则该cookie会静默删除

cookie的构成

​ 名称:唯一标识cookie的名称;不区分大小写,但是服务器可能区分,所以可以在实践中区分大小写

​ 值:储存在cookie里的字符串值;这个值必须经过URL编码

​ 域:cookie有效的域;发送到这个域所有请求都会包含对应的cookie;如果不包含子域(如.coding.net),则cookie会对所有子域都生效

​ 路径:请求URL中包含这个路径才会把cookie发送到服务器

​ 过期时间:表示何时删除cookie的时间戳;这个值是GMT格式;默认情况下浏览器会话结束后会立即删除;cookie可以保存在机器上,即使关闭浏览器;把过期时间设置为过去的时间会立即删除cookie

​ 安全标志:设置之后,只在使用SSL安全连接的情况下才会把cookie发送到服务器

这些参数在Set-Cookie头部中使用分号加空格隔开,例如:

Set-Cookie: name=value; expires=Mon, 22-Jan-07 11:11:11 GMT; domain=.wrox.com

安全标志secure是cookie中唯一的非名/值对,只需要一个secure就可以了,例如:

Set-Cookie: name=value; domain=.wrox.com; path=/; secure

JavaScript中的cookie

在js中处理cookie比较麻烦,只有BOM的document.cookie属性可以访问

使用该接口获取值时,document.cookie返回包含页面所有有效cookie的字符串,以分号分隔,例如:

name1=value1;name2=value2

所有名和值都是URL编码的,因此必须使用decodeURIComponent()解码

在设置值时,通过document.cookie设置新的cookie字符串;设置cookie不会覆盖之前存在的任何cookie,除非设置了已有的cookie;格式和Set-Cookie头格式一样

所有的参数中,只有名称和值是必要的,例如:document.cookie = "name=lsn";;这个例子中的cookie会在客户端向服务器发送请求时都被带上,浏览器关闭动时候立即删除,虽然这个例子不需要编码任何字符,但是最好使用encodeURIComponent()对名称和值进行编码,例如:document.cookie = encodeURIComponent("name") + "=" + encodeURIComponent("lsn");

js中的cookie操作很麻烦,一般通过一个辅助函数进行操作,有增、查、删操作,详情看红宝书p754

子cookie

为绕过浏览器对每个域cookie数的限制,有些开发者提出了子cookie的概念;是在单个cookie中储存的小块数据,本质上是使用cookie的值在单个cookie中存储多个名/值对,例如:

name=name1=value1&name2=value2&name3=value3

这样便可以储存更多的cookie,但是操作子cookie,就得先取得cookie,操作函数需要进一步简化,详情查看红宝书p754

使用cookie的注意事项

还可以设置HTTP-only的cookie,可以在浏览器设置,也能在服务器设置,但是只能在服务器上获取;js无法获取这种cookie的值

cookie不是储存大量数据的理想方式

不要在cookie中储存重要或铭感信息,因为cookie数据不是保存在安全的环境中,因此任何人都有可能获得

Web Storage

Web Storage规范最新版本是第二版,这一规范主要有两个目标:

​ 提供在cookie之外的存储会话数据的途径

​ 提供跨会话持久化存储大量数据的机制

这一版本定义了两个对象:localStorage、sessionStorage;前者是永久储存机制,后者是跨会话储存机制;这两种机制提供了不受页面刷新影响的储存数据的方式;这两个对象都在window上实现了

Storage类型

该类型用于保存名/值对数据,直至储存空间上限(浏览器决定);该类型实例还增加了以下方法:

​ clear():删除所有值(Firefox未实现)

​ getItem(name):取得给定name的值

​ key(index):取得给定数值位置的名称

​ removeItem(name):删除给定name的名/值对

​ setItem(name, value):设置给定name值

因为每个数据项都作为属性存储在该对象上,所以可以使用点或方括号操作符访问这些属性;通过length属性可以确定Storage对象中保存了多少名/值对

Storage类型只能存储字符串,非字符串数据会在存储之前会自动转换为字符串,这个转换在取值时不能撤销

sessionStorage对象

该对象只存储会话数据,这意味着数据只会存储到浏览器关闭,不受页面刷新影响,可以在浏览器崩溃并重启后恢复(取决于浏览器)

本地文件不能使用,只能由最初存储数据的页面使用,在多页应用程序中用处有限

该对象是Storage的实例,所以能用Storage上的方法

所有现代浏览器在实现存储写入时都使用了同步阻塞方式,因此数据会被立即提交到储存;通过Web Storage写入到任何数据都可以立即被读取;老版IE在这个写入方式上又有区别(详情看红宝书p760)

可以通过length或for-in遍历sessionStorage

可以使用delete直接删除对象属性,或者使用removeItem方法删除

localStorage对象

在修订的HTML5规范里,localStorage对象取代了globalStorage,作为在客户端持久存储数据的机制;要访问同一个localStorage对象,页面必须来自同一个域(子域不可以)、在相同的端口上使用相同的协议

该对象也是Storage实例,可以使用Storage上有的方法

存储事件

每当Storage对象发生变化时,会在文档上触发storage事件,使用属性或者Storage实例方法时都会触发

该事件event对象有4个属性:

​ domain:存储变化对应的域

​ key:被设置或删除的键

​ newValue:键被设置的新值,若键被删除则为null

​ oldValue:键变化之前的值

可以使用DOM Level 2方法添加事件处理程序

对于sessionStorage和localStorage上任何改变都会触发,但是storage事件不会区分两者

限制

具体的限制取决于浏览器;一般来说,客户端数据的大小限制是按照每个源来设置的,每个源有固定大小的数据存储空间

大多数会限制为每个源5MB

IndexedDB

Indexed DataBase API,是浏览器中存储结构化数据的一个方案

IndexedDB背后的思想是创造一套API,方便js对象的存储和获取,同时也支持查询和搜索

IndexedDB的设计几乎完全是异步的;绝大多数IndexedDB要求添加onerror和onsuccess事件处理程序来确定输出

数据库

IndexedDB是类似于MySQL或Web SQL Database的数据库;但是IndexedDB使用的是对象存储而不是表格保存数据

IndexedDB数据库就是在一个公共命名空间下的一组对象储存,类似于NoSQL风格的实现

使用indexedDB.open()方法,传入一个要打开的数据库名和版本;如果数据库已存在,则会发送一个打开它的请求,如果不存在,则会发送创建并打开这个数据库的请求;该方法返回一个IDBRequest实例,可以在这个实例上添加onerror和onsuccess事件处理程序

let db,
    request,
    version = 1;
request = indexedDB.open('admin', version);
request.onerror = (event) => {
    alert(event.target.errorCode);
}
request.onsuccess = (event) => {
    db = event.target.result;
}

打开数据库时要指定版本,这个版本号会转换为unsigned long long数值,所以要使用整数,不要使用浮点数

成功打开后所有与该数据库相关的操作都要通过db对象本身来进行;打开错误可以通过errorCode访问错误码

对象储存

数据库版本决定了数据库模式,包括数据中的对象存储和这些对象存储的结构;如果数据库不存在,open()操作会创建一个新数据库,然后出发upgradeneeded事件;可以为这个事件设置处理程序,并在程序中创建数据库模式;如果数据库存在,而你指定了一个升级版的版本号,则会立即触发upgradeneeded事件,然后更新数据库模式

下面例子演示了如何创建对象存储:

request.onupgradeneeded = (event) => {
    const db = event.target.result;
    
    //删除当前objectStore,测试的时候可以这样做
    //但是这样会删除已有数据
    if (db.objectStoreNames.contains("users")) {
        db.deleteObjectStore("users");
    }
    
    db.createObjectStore("users", { keyPath: "username" });
}

这里keyPath参数表示作为键的存储对象的属性名

事务

创建了对象存储后,剩下的所有操作都是通过事务完成的;事务要通过调用数据库对象的transaction()方法创建;任何时候想要读取或修改数据,都要通过事务把所有修改操作组织起来

最简单的情况:

let transaction = db.transaction();
//不指定参数,则会对数据库中所有对象存储只有读取权限

let t = db.transaction('users');
//这样可以确保事务期间只加载users对象存储信息

let t = db.transaction(['users', 'anotherStore']);
//这样可以访问多个对象存储
//上述三种方式都只读

//访问模式通过第二个参数修改,有三个模式:“readonly”、“readwrite”、“versionchange”
let t = db.transaction("users", "readwrite");

有了事务的引用,就可以用objectStore()方法并传入对象存储的名称以访问特定的对象存储

然后使用add()和put()方法添加和更新对象,get()方法取得对象,delete()方法删除对象,clear()方法删除所有对象;get()和delete()方法都接收对象键作为参数,这五个请求都创建新的请求对象

const t = db.transaction("users"),
      store = transaction.objectStore("users"),
      request = store.get("007");
request.onerror = (event) => alert("error");
request.onsuccess = (event) => alert(event.target.result.firstName);

一个事务可以完成多个请求,所以事务本身也有事件处理程序:onerror、oncomplete

t.onerror = (event) => {
    //事务被取消
};
t.oncomplete = (event) => {
    //整个事务成功完成
}

不能通过oncomplete事件处理程序的event访问get()请求返回的任何数据

插入对象

拿到对象存储的引用后就可以使用add()和put()写入数据了;这两个方法都接受一个参数,及要存储的对象,并把对象保存到对象存储

当对象存储中存在同名键时,add()会报错,put()会重写该对象;在其它时候这两个方法没有区别

每次调用这两个方法都会创建对象存储的更新请求;可以把请求对象保存起来,然后用onerror和onsuccess事件处理程序验证是否成功

let request,
    requests = [];
for (let user of users) {
    request = store.add(user);
    request.onerror = (event) => {
        //...
    };
    request.onsuccess = (event) => {
        //...
    };
    requests.push(request);
}

创建并填充数据后,可以查询对象存储了

通过游标查询

使用事务可以通过一个已知键取得一条记录;如果想取得多条数据,则需要在事务中创建一个游标;游标是一个指向结果集的指针

游标不会先收集所有数据,而是指向第一个结果,并且在接到指令前不会主动查找下一条数据

需要在对象储存上调用openCursor()方法创建游标,方法返回一个一个请求,必须为它添加onsuccess和onerror事件处理程序

const t = db.transaction("users"),
      store = t.objectStore("users"),
      request = store.openCursor();
request.onsuccess = (event) => {
    //...
};
request.onerror = (event) => {
    //...
};

调用onsuccess事件处理程序,可以通过event.target.result访问对象储存中下一条记录,这个属性中保存着IDBCursor实例或null;这个IDBCursor实例有几个属性:

​ direction:字符串常量,表示游标的前进方向以及是否应该遍历所有重复的值;有如下值:NEXT(“next”)、NEXTUNIQUE(“nextunique”)、PREV(“prev”)、PREVUNIQUE(“prevunique”)

​ key:对象的键

​ value:实际的对象

​ primaryKey:游标使用的键;可能是对象键或者索引键

游标可用于更新个别记录,update()方法使用指定的对象更新当前游标对应的值,该方法回创建一个新的请求,可以通过onsuccess和onerror事件处理程序观察结果

request.onsuccess = (event) => {
    const cursor = event.target.result;
    let value,
        updateRequest;
    
    if (cursor) {//永远要检查
        value = cursor.value;
        value.password = "magic";
        updateRequest = cursor.update(value);
        updateRequest.onsuccess = () => {
            //...
        };
        updateRequest.onerror = () => {
            //...
        };
    }
}

也可以调用delete()来删除游标位置的记录,与update一样,也会创建一个请求

如果事务没有修改对象存储的权限,update()和delete()都会抛出错误

如果要访问新记录可以使用两个方法:

​ continue(key):移动到结果集中下一条记录;key是可选的,如果没有指定,游标就移动到下一条记录;如果指定了,则移动到指定的键

​ advance(count):游标向前移动指定的count条记录

这两个方法都会让游标重用相同的请求,因此也会重用onsuccess和onerror处理程序

键范围

使用键范围可以让游标更容易管理;键范围对应IDBKeyRange实例;有四种方法指定键范围:

​ only(key):传入想要获取的键,这个方法和get()差不多

​ lowerBound(key, flag):定义结果集的下限,表示游标从key位置开始,直到最后;可选参数flag是一个Boolean值,表示是否跳过游标位置key,true表示从key后面一条记录开始,不传或传false则相反

​ upperBound(key, flaf):定义结果集的上限,表示游标从开头位置开始,直到key位置;可选参数flag是一个Boolean值,表示是否包含游标位置key,true表示到key前面一条记录,不传或传false则相反

​ bound(lowerKey, upperKey, lowerFlag, upperFlag):结合上述两个方法的方法,同时定义上限和下限

将定义好后的范围传给openCursor()方法,就可以获得该范围的游标

const store = db.transaction("users").objectStore('users'),      range = IDBKeyRange.bound("007", "ace"),      request = store.openCursor(range);
设置游标方向

openCursor()方法实际上接收两个参数:IDBKeyRange实例或null(传入null则默认键范围是所有值)、方向字符串(默认为”next“)

如果需要跳过重复的项,可以在第二个参数传入“nextunique”

如果需要反向移动的游标,从最后一项开始向第一项移动,需要传“prev”或“prevunique”作为第二个参数;这样子会使得continue()和advance()在对象储存中反向移动游标

索引

对于某些数据集,可能需要为对象储存创建多个键

要创建新的索引,首先要取得对象储存的引用,调用createIndex()方法:

const index = store.createIndex("username", "username", { unique: true });

createIndex()方法的第一个参数是索引的名称,第二个参数是索引属性的名称,第三个参数是包含键unique的options对象;该方法返回IDBIndex实例,在储存对象上调用一个index()方法也可以得到同一个实例

let index = store.index("username");

索引非常像对象存储,可以在索引上调用openCursor()方法创建新游标,创建的游标和在对象储存上调用openCursor()创建的游标完全一样;不过result.key属性中保存的是索引键,而不是主键

let index = store.index("username");let request = index.openCursor();

使用openKeyCursor()方法也可以在索引上创建特殊游标,只返回每条记录的主键;这个方法接收参数与openCursor()一样;不同之处在于event.result.key是索引键,event.result.value是主键而不是整个记录

可以使用get()方法并传入索引键通过索引取得单条记录,这会创建一个请求

如果想只取得给定索引键的主键,可以使用getKey()方法,这会创建一个新请求,但result.value等于主键而不是整个记录

IDBIndex对象有下列属性:

​ name:索引的名称

​ keyPath:调用createIndex()时传入的属性路径

​ objectStore:索引对应的对象储存

​ unique:表示索引键是否唯一的布尔值

对象存储自身也有一个indexNames属性,保存着与之相关索引的名称

在存储对象上调用deleteIndex()方法并传入索引的名称可以删除索引

并发问题

IndexDB虽然是异步API,但仍存在并发问题

两个不同的浏览器标签页同时打开同一个网页,有可能出现一个尝试升级数据库而另一个尚未就绪的情形;有问题的操作是设置数据库为新版本,而版本变化只能在浏览器只有一个标签页使用数据库时才能完成

第一次打开数据库时,添加onversionchange事件处理程序非常重要,另一个同源标签页将数据库打开到新版本时,将执行此回调;对这个事件最好的回应是立即关闭数据库,以便完成版本升级

let request, db;request = indexDB.open("admin", 1);request.onsuccess = (event) => {    db = event.target.result;    db.onversionchange = () => db.close();};

应该在每次成功打开数据库之后都指定onversionchange事件处理程序,这个事件可能会被其它标签页触发

限制

IndexedDB很多限制实际上与Web Storage一样;IndexedDB数据库是与页面源绑定的,因此不能跨域共享

其次,每个源都有可以储存的空间限制

不同浏览器有不同区别,详情请看红宝书p771

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Samuel_luo。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值