前言
大家好,我是柒八九
。我们在网络拾遗之Http缓存文章中,从网络协议的视角介绍了网站「客户端缓存」 中的HTTP
缓存策略,并对「强缓存」和「协商缓存」做了较为详细的介绍。
而今天,这篇文章,打算介绍客户端缓存的另外一种类别 -- 本地缓存(也可以叫客户端存储)
还是老样子。赶紧上车。发车走起。
面试加油站
❝
- 存储在「客户端」上的
cookie
1. 「每个 cookie」 不超过 「4 KB」 2. 「每个域」不超过20
个 cookie Web Storage
的目的是解决通过「客户端存储不需要频繁发送回服务器的数据」时使用 cookie 的问题Web Storage
定义了「两个对象」:localStorage
和sessionStorage
。 1.localStorage
是「永久存储」机制 2.sessionStorage
是「跨会话的存储」机制 3. 「单个域」的大小为「5M」IndexedDB
是一个事务型数据库系统
❞
文章概要
- cookie
- Web Storage
- sessionStorage
- localStorage
- IndexDB
1. cookie
❝
HTTP cookie
通常也叫作cookie
,最初用于在「客户端存储会话信息」 ❞
这个规范要求「服务器」在「响应 HTTP 请求」时,通过发送 Set-Cookie
HTTP 「头部包」含会话信息。
例如,下面是包含这个头部的一个 HTTP「响应」:
代码语言:javascript复制HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Other-header: other-header-value
这个 HTTP 响应会设置一个「名」为"name",「值」为"value"的 cookie。名和值在发送时都会经过 「URL 编码」。
浏览器会「存储」这些会话信息,并在之后的「每个请求」中都会通过 HTTP 头部 cookie 再「将它们发回服务器」。
代码语言:javascript复制GET /index.js HTTP/1.1
Cookie: name=value
Other-header: other-header-value
限制
❝
cookie
是与「特定域」绑定的 ❞
设置 cookie
后,它会与请求一起发送到「创建它的域」,这样能够保证 cookie
中存储的信息只对被认可的接收者开放,不被其他域访问。
cookie 存储在「客户端」机器上,所以有很多针对安全性的限制
- 不超过 300 个 cookie
- 「每个 cookie」 不超过 「4 KB」
- 「每个域」不超过
20
个 cookie - 「每个域」不超过 「80KB」
「不同浏览器的针对每个域能设置多少cookie
有不同的限制」。
如果 cookie
总数超过了「单个域的上限」,浏览器就会删除之前设置的 cookie
。
如果创建的 cookie
超过「最大限制」,则该 cookie
会被「静默删除」。
cookie 的构成
cookie 在浏览器中是由以下参数构成的
- 「名称」
1. 「唯一标识」
cookie
的名称 2.「不区分大小写」 3. 必须经过 「URL 编码」 - 「值」: 1. 存储在 cookie 里的「字符串值」 2. 必须经过 「URL 编码」
- 「域」
1.
cookie
有效的域 2. 发送到这个域的「所有请求」都会包含对应的cookie
- 「过期时间」
1. 表示「何时删除」
cookie
的「时间戳」 2. 默认情况下,浏览器「会话结束」后会「删除所有 cookie」 3.这个值是GMT
格式(Wdy, DD-Mon-YYYY HH:MM:SS GMT),用于指定删除 cookie 的「具体时间」 4.把过期时间设置为「过去的时间」会立即删除cookie
- 「路径」
1. 「请求 URL」 中「包含这个路径」才会把
cookie
发送到服务器 - 「安全标志」
1. 设置之后,只在使用
SSL
安全连接的情况下才会把cookie
发送到服务器
这些参数在 Set-Cookie
头部中使用「分号加空格」隔开
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; expires=expiration_time;
path=domain_path; domain=domain_name; secure
Other-header: other-header-value
❝「安全标志」
secure
是 cookie 中「唯一的非名/值对」,只需一个 secure 就可以了 ❞
JS 中的 cookie
❝在 JS 中只有
BOM
的document.cookie
属性用于处理cookie
❞
document.cookie
返回包含页面中「所有有效」 cookie
的字符串(根据域、路径、过期时间和安全设置),以分号分隔。
所有「名和值」都是 「URL 编码」的,因此必须使用 decodeURIComponent()
解码。
在「设置值时」,可以通过 document.cookie
属性设置「新的」 cookie
字符串。这个字符串在被解析后会「添加到原有 cookie
中」。
❝设置 document.cookie 「不会覆盖之前存在的任何 cookie,除非设置了已有的cookie」 ❞
「设置」 cookie
的格式如下,与 Set-Cookie
头部的格式一样:
name=value; expires=expiration_time;
path=domain_path; domain=domain_name; secure
❝在所有这些参数中,「只有 cookie 的名称和值是必需」的 ❞
下面是个简单的例子:
代码语言:javascript复制document.cookie = encodeURIComponent("name") "="
encodeURIComponent("Nicholas");
创建一个名为name
,值为bcnz789
会话 cookie,这个 cookie 在「每次客户端向服务器发送请求时」都会被带上,在「浏览器关闭时就会被删除」。
子 cookie
为绕过浏览器对「每个域 cookie 数的限制」,有些开发者提出了「子 cookie」 的概念。
代码语言:javascript复制❝子 cookie 是在「单个 cookie 存储的小块数据」,本质上是使用 「cookie 的值」在「单个」 cookie 中存储「多个名/值对」 ❞
name=name1=value1&name2=value2&name3=value3&name4=value4&name5=value5
注意事项
有一种叫作 HTTP-only
的 cookie
。HTTP-only
可以在浏览器设置,也可以在服务器设置,但「只能在服务器上读取」,这是因为 「JS 无法取得这种 cookie 的值」。
因为「所有 cookie 都会作为请求头部由浏览器发送给服务器」,所以在 cookie
中保存大量信息可能会「影响特定域浏览器请求的性能」。保存的 cookie
越大,请求完成的「时间就越长」。即使浏览器对 cookie 大小有限制,最好还是尽可能「只通过 cookie 保存必要信息」,以避免性能问题。
Web Storage
❝
Web Storage
的目的是解决通过「客户端存储不需要频繁发送回服务器的数据」时使用 cookie 的问题 ❞
Web Storage
规范「最新的版本」是第 2 版,这一版规范主要有「两个目标」
- 提供「在
cookie
之外」的「存储会话数据」的途径 - 提供「跨会话持久化存储大量数据」的机制
Web Storage
的第 2 版定义了「两个对象」:localStorage
和 sessionStorage
。
localStorage
是「永久存储」机制sessionStorage
是「跨会话的存储」机制
这两种浏览器存储 API 提供了在「浏览器中不受页面刷新影响而存储数据」的两种方式。
Storage 类型
Storage
类型用于保存「名/值对」数据,直至存储空间上限(由浏览器决定)。
Storage 的实例增加了以下方法
clear()
:删除「所有值」;getItem(name)
:取得「给定」 name 的值。key(index)
:取得「给定数值位置」的名称。removeItem(name)
:删除「给定」 name 的名/值对。setItem(name, value)
:设置给定 name 的值。
通过 length
属性可以确定 Storage
对象中保存了多少名/值对。
sessionStorage 对象
❝
sessionStorage
对象「只存储会话数据」,这意味着数据「只会存储到浏览器关闭」 ❞
这跟浏览器关闭时会消失的「会话 cookie」 类似。
存储在 sessionStorage
中的数据「不受页面刷新影响」,可以在浏览器崩溃并重启后恢复。sessionStorage
对象与「服务器会话」紧密相关,所以在「运行本地文件时不能使用」。
因为 sessionStorage
对象是 Storage
的实例,所以可以通过使用 setItem()
方法或直接「给属性赋值」给它添加数据。
// 使用方法存储数据
sessionStorage.setItem("name", "wl");
// 使用属性存储数据
sessionStorage.name = "wl";
「所有」现代浏览器在「实现存储写入时」都使用了「同步阻塞方式」,因此数据会被「立即提交到存储」。也就是说
❝通过 Web Storage 写入的任何数据都可以「立即被读取」 ❞
对存在于 sessionStorage
上的数据,可以使用 getItem()
或直接访问属性名来取得。
// 使用方法取得数据
let name = sessionStorage.getItem("name");
// 使用属性取得数据
let name2 = sessionStorage.name;
也可以结合 sessionStorage
的 length
属性和 key()
方法遍历「所有的值」
let len = sessionStorage.length;
for (let i = 0; i < len; i ){
let key = sessionStorage.key(i);
let value = sessionStorage.getItem(key);
console.log(`${key}=`${value}`);
}
也可以使用 for-in
循环迭代 sessionStorage
的值:
for (let key in sessionStorage){
let value = sessionStorage.getItem(key);
console.log(`${key}=${value}`);
}
localStorage 对象
❝
localStorage
作为在「客户端持久存储数据」的机制 ❞
要访问「同一个」 localStorage
对象,页面「必须」来自
- 同一个域(子域不可以)
- 在相同的端口
- 使用相同的协议
❝「同源页面」,可以访问同一个
localStorage
❞
localStorage
是 Storage
的实例,所以可以像使用 sessionStorage
一样使用localStorage
// 使用方法存储数据
localStorage.setItem("name", "wl");
// 使用属性存储数据
localStorage.book = "bc";
// 使用方法取得数据
let name = localStorage.getItem("name");
// 使用属性取得数据
let book = localStorage.book;
两种存储方法的「区别在于」存储在 localStorage
中的数据会保留到「通过 JS 删除」或者「用户清除浏览器缓存」
localStorage
数据「不受页面刷新影响」,也不会因关闭窗口、标签页或重新启动浏览器而丢失
存储事件
❝每当
Storage
对象发生变化时,都会在「文档上」触发storage
事件 ❞
使用属性或 setItem()
设置值、使用 delete
或 removeItem()
删除值,以及每次调用 clear()
时都会触发这个事件。
这个事件的「事件对象」有如下 4 个属性。
domain
:存储变化对应的域key
:被设置或删除的键newValue
:键被设置的「新值」,若键被删除则为 null。oldValue
:键变化之前的值
代码监听 storage
事件:
window.addEventListener("storage",
(event) => alert('发生变化的域:${event.domain}'));
限制
一般来说,客户端数据的「大小限制」是按照「每个源」(协议、域和端口)来设置的,因此每个源有「固定大小的数据存储空间」。
❝大部分浏览器将
localStorage
和sessionStorage
限制为「每个源 5MB」 ❞
IndexedDB
❝
Indexed Database
API 简称IndexedDB
,是浏览器中存储「结构化数据」的一个方案 ❞
IndexedDB 的设计几乎完全是「异步的」。为此,大多数操作以「请求的形式」执行,这些请求会「异步执行」,产生成功的结果或错误。
数据库
❝
IndexedDB
是类似于MySQL
或Web SQL Database
的「数据库」 ❞
与传统数据库最大的「区别」在于,IndexedDB
使用「对象存储」而不是表格保存数据。IndexedDB
数据库就是在一个公共命名空间下的「一组对象存储」。
使用 IndexedDB
数据库的「第一步」是调用 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;
};
对象存储
建立了数据库连接之后,下一步就是「使用对象存储」。创建对象存储时「必须指定一个键」。
在 upgradeneeded
事件中设置对象存储信息。
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 如果存在则删除当前 objectStore。
if (db.objectStoreNames.contains("users")) {
db.deleteObjectStore("users");
}
// 设置对象存储,并指定usename为主键
db.createObjectStore("users", { keyPath: "username" });
};
事务
创建了对象存储之后,剩下的所有操作都是通过「事务」完成的。
事务要通过调用数据库对象的 transaction()
方法创建。「任何时候」,只要想要读取或修改数据,都要「通过事务把所有修改操作组织起来」。
let transaction = db.transaction("users");
保在事务期间只加载 users
对象存储的信息。(参数也可以是数组)。
有了「事务的引用」,就可以使用 objectStore()
方法并传入「对象存储的名称」以访问特定的对象存储。
add()/put()
:添加和更新对象get()
:获取对象delete()
:删除对象clear()
:删除所有对象
这 5 个方法都创建「新的请求对象」。
代码语言:javascript复制const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
request = store.get("007");
request.onerror = (event) => alert("没有该项数据");
request.onsuccess = (event) => alert(event.target.result.firstName);
「一个事务可以完成任意多个请求」,所以事务对象本身也有「事件处理程序」:
onerror
oncomplete
这两个事件可以用来获取「事务级的状态信息」:
代码语言:javascript复制transaction.onerror = (event) => {
// 整个事务被取消
};
transaction.oncomplete = (event) => {
// 整个事务成功完成
};
限制
IndexedDB
数据库是与页面源(协议、域和端口)绑定的,因此「信息不能跨域共享」 意味着www.wl.com
和bc.wl.com
会对应不同的数据存储- 「每个源」都有可以存储的空间限制
针对IndexDB的简化使用
- 在 Chrome 「正常模式」下:
- 在 Chrome 「隐身模式」下:固定 100MB 的大小
在官网提供了很多基于IndexDB
包装的库,隐藏了一些比较「啰嗦」的数据库实例化等操作。
然后「谁用了,都说好」。
其他不常见的数据持久化方案
WebSQL
用于存储较大量数据的缓存机制。
- 将数据以数据库二维表的形式存储在客户端
- 允许SQL语句的查询
- 让浏览器实现小型数据库存储功能
- 不是H5规范
核心方法
openDatabase()
transaction()
executeSql()
「已废弃并且被IndexDB
所替代」
Application Cache
允许浏览器通过manifest
配置文件在本地「有选择」的存储JS/CSS/图片等静态资源的文件级缓存机制
当页面「不是首次打开」,通过特定的manifest文件配置描述来选择本地Application Cache里的文件。
「已废弃并且被ServerWorkers
所替代」
在昨天的文章中,我们介绍了Service Worker
,而服务工作线程的一个主要能力是可以「通过编程方式实现真正的网络请求缓存机制」,其中利用CacheStorage
实现该功能。
后记
参考资料:JS高级程序设计第四版