1、cookie
- 用于在客户端存储会话信息。这个规范要求服务器在响应 HTTP 请求时,通过发送
Set-Cookie
HTTP 头部包含会话信息
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 再将它们发回服务器
GET /index.jsl HTTP/1.1
Cookie: name=value
Other-header: other-header-value
复制代码
1、限制
- 与特定域绑定
- 设置后会与请求一起发送到创建它的域,能保证cookie中信息只被认可的接受者开放。
- cookie能够占用的空间有限
- 不超过300个cookie
- 每个4kb(4096字节)
- 现在每个域基本无限制
2、cookie的构成
- cookie的参数
- 名称
- 唯一标识cookie的名称
- 不区分大小写
- cookie必须经过URL编码
- 值
- 存储在cookie中的字符串值
- 必须经过URL编码
- 域
- cookie有效的域
- 发送到这个域的请求都会包含对应cookie
- 可以包含或不包含子域
- 默认为设置cookie的域
- 路径
- 请求URL包含这个路径才会把cookie发送到服务器
- 过期时间
- 表示何时删除cookie的时间戳
- 默认浏览器会话结束后会删除所有cookie
- 安全标志
- 设置后只有在使用SSL安全连接的情况下才会把cookie发送到服务器
- 名称
- 在Set-Cookie头部中使用分号加空格隔开
...
Set-Cookie: name=value; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com
...
复制代码
- 安全标志source时cookie中唯一的非名值对,只需要一个source
...
Set-Cookie: name=value; domain=.wrox.com; path=/; secure
...
复制代码
3、JavaScript 中的cookie
-
document.cookie
返回页面所有有效的cookie的字符串,分号分隔 -
所有名和值都是 URL 编码的,因此必须使用
decodeURIComponent()
解码 -
设置值的时候,不会覆盖之前存在的任何cookie,除非设置了已有的cookie,设置cookie格式与Set-Cookie头部格式一样
name=value; expires=expiration_time; path=domain_path; domain=domain_name; secure 复制代码
-
只有cookie的名称和值是必需的
-
最好是使用 encodeURIComponent()对名称和值进行编码
-
要为创建的 cookie 指定额外的信息,只要像 Set-Cookie 头部一样直接在后面追加相同格式的字符串
-
CookieUtil 对象
class CookieUtil {
static get (name) {
let cookieName = `${encodeURIComponent(name)}=`
let cookieStart = document.cookie.indexOf(cookieName)
let cookieValue = null
if(cookieStart > -1) {
let cookieEnd = document.cookie.indexOf(";", cookieStart)
if(cookieEnd == -1) {
cookieEnd = document.cookie.length
}
cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd))
}
return cookieValue
}
static set (name, value, expires, path, domain, secure) {
let cookieText = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`
if(expires instanceOf Date) {
cookieText += `; expires=${expires}`
}
if(path) {
cookieText += `; path=${path}`
}
if(domain) {
cookieText += `; domain=${domain}`
}
if(secure) {
cookieText += "; secure"
}
document.cookie = cookieText
}
static unset(name, path, domain, secure) {
CookieUtil.set(name, "", new Data(0), path, domain, secure)
}
}
复制代码
4、子cookie
- 子 cookie 是在单个 cookie 存储的小块数据,本质上是使用 cookie 的值在单个 cookie 中存储多个名/值对。
name=name1=value1&name2=value2&name3=value3&name4=value4&name5=value5
复制代码
- 不用单独存储为自己的名/值对,能够在单域 cookie 数限制下存储更多的结构化数据。
class SubCookieUtil {
static get(name, subName) {
let subCookies = SubCookieUtil.getAll(name);
return subCookies ? subCookies[subName] : null;
}
static getAll(name) {
let cookieName = encodeURIComponent(name) + "=",
cookieStart = document.cookie.indexOf(cookieName),
cookieValue = null,
cookieEnd,
subCookies,
parts,
result = {};
if (cookieStart > -1) {
cookieEnd = document.cookie.indexOf(";", cookieStart);
if (cookieEnd == -1) {
cookieEnd = document.cookie.length;
}
cookieValue = document.cookie.substring(cookieStart +
cookieName.length, cookieEnd);
if (cookieValue.length > 0) {
subCookies = cookieValue.split("&");
for (let i = 0, len = subCookies.length; i < len; i++) {
parts = subCookies[i].split("=");
result[decodeURIComponent(parts[0])] =
decodeURIComponent(parts[1]);
}
return result;
}
}
return null;
}
static set(name, subName, value, expires, path, domain, secure) {
let subcookies = SubCookieUtil.getAll(name) || {};
subcookies[subName] = value;
SubCookieUtil.setAll(name, subcookies, expires, path, domain, secure);
}
static setAll(name, subcookies, expires, path, domain, secure) {
let cookieText = encodeURIComponent(name) + "=",
subcookieParts = new Array(),
subName;
for (subName in subcookies) {
if (subName.length > 0 && subcookies.hasOwnProperty(subName)) {
subcookieParts.push(
`${encodeURIComponent(subName)}=${encodeURIComponent(subcookies[subName])}`);
}
}
if (cookieParts.length > 0) {
cookieText += subcookieParts.join("&");
if (expires instanceof Date) {
cookieText += `; expires=${expires.toGMTString()}`;
}
if (path) {
cookieText += `; path=${path}`;
}
if (domain) {
cookieText += `; domain=${domain}`;
}
if (secure) {
cookieText += "; secure";
}
} else {
cookieText += `; expires=${(new Date(0)).toGMTString()}`;
}
document.cookie = cookieText;
}
static unset(name, subName, path, domain, secure) {
let subcookies = SubCookieUtil.getAll(name);
if (subcookies) {
delete subcookies[subName]; // 删除
SubCookieUtil.setAll(name, subcookies, null, path, domain, secure);
}
}
static unsetAll(name, path, domain, secure) {
SubCookieUtil.setAll(name, null, new Date(0), path, domain, secure);
}
};
复制代码
5、使用cookie注意事项
HTTP-only
的cookie,可以在浏览器和服务端设置,但只能在服务端获取。- 因为所有cookie都会作为请求头部由浏览器发送给服务器,所以在 cookie 中保存大量信息可能会影响特定域浏览器请求的性能。
- 最好还是尽可能只通过 cookie 保存必要信息,以避免性能问题。
2、Web Storage
- localStorage 永久存储机制,sessionStorage 跨会话存储机制。
1、Storage类型
- 用于保存名值对数据,直至上限。
clear()
删除所有值getItem(name)
取得给定name的值key(index)
取得给定数值位置的名称removeItem(name)
删除给定name 的名值对setItem(name, value)
设置给定name的值
- 只能存储字符串
2、sessionStorage对象
-
sessionStorage 对象只存储会话数据,这意味着数据只会存储到浏览器关闭。
-
存储在 sessionStorage 中的数据不受页面刷新影响,可以在浏览器崩溃并重启后恢复。
-
sessionStorage 对象与服务器会话紧密相关,所以在运行本地文件时不能使用。
-
存储在 sessionStorage 对象中的数据只能由最初存储数据的页面使用,在多页应用程序中的用处有限。
-
可以通过使用
setItem()
方法或直接给属 性赋值给它添加数据。// 使用方法存储数据 sessionStorage.setItem("name", "Nicholas"); // 使用属性存储数据 sessionStorage.book = "Professional JavaScript"; 复制代码
-
同步阻塞方式,数据会立即提交到存储。通过 Web Storage 写入的任何数据都可以立即被读取。
-
可以使用
getItem()
或直接访问属性名来取得// 使用方法取得数据 let name = sessionStorage.getItem("name"); // 使用属性取得数据 let book = sessionStorage.book; 复制代码
-
可以结合length属性或使用for-in遍历所有的值
-
可以使用 delete 操作符直接删除对象属性,也可以使用
removeItem()
方法// 使用 delete 删除值 delete sessionStorage.name; // 使用方法删除值 sessionStorage.removeItem("book"); 复制代码
-
sessionStorage 对象应该主要用于存储只在会话期间有效的小块数据
-
跨会话持久存储数据,可以使用localStorage
3、localStorage对象
- 在客户端会话持久存储数据的机制,访问同一个localStorage对象,页面必须来自同一个域(子域不行),在相同的端口上使用相同的协议。
- 方法和sessionStorage相同。
- 区别
- 存储在 localStorage 中的数据会保留到通过 JavaScript 删除或者用户清除浏览器缓存
- localStorage 数据不受页面刷新影响,也不会因关闭窗口、标签页或重新启动浏览器而丢失
4、存储事件
- 当Storage对象发生变化时,都会在文档上触发storage事件。
- 使用属性或 setItem()设置值、使用 delete 或 removeItem()删除值,以及每次调用 clear()时都会触发这个事件。
- 包含属性
- domain 存储变化对应的域
- key 被设置或删除的键
- newValue 键被设置的新值,若被删除则为null
- oldValue 键变化之前的值
window.addEventListener("storage", (event)=> {
//...
})
复制代码
5、限制
- 取决于特定浏览器,一般是按照每个源(协议、域和端口)设置的。
- 大多数限制为每个源5MB
3、IndexedDB
- 浏览器中存储结构化数据的一个方案
- IndexedDB 的设计几乎完全是异步的。这些请求会异步执行, 产生成功的结果或错误。
1、数据库
-
与传统数据库最大的区别在于, IndexedDB 使用对象存储而不是表格保存数据。
-
IndexedDB 数据库就是在一个公共命名空间下的一组对象存储,类似于 NoSQL 风格的实现
-
使用方式
- 调用
indexedDB.open()
传入一个要打开的数据库名称- 已存在,发送一个打开它的请求
- 不存在,发送创建并打开这个数据库的请求
- 返回IDBRequest实例
- 实例上添加onerror和onsuccess事件处理程序
let db, request, version = 1 request = indexDB.open("admin", version) request.onerror = (event) => { alert(`Failed to open ${event.target.errorCode}`) } request.onsuccess = (event) => { db = event.target.result } 复制代码
onsuccess
中event.target.result代表数据库实例onerror
中event.target.errorCode会存储表示问题的错误码
- 调用
2、对象存储
- 创建对象存储时必须指定一个键,必须全局唯一
- 如果数据库还不存在,
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 属性表示应该用作键的存储对象的属性名
复制代码
3、事务
- 创建数据库之后的操作都要通过事务来完成。
- 通过
transaction()
创建,任何时候只要想读取或修改数据,都要通过事务把所有修改操作组织起来。
let transaction = db.transaction()
复制代码
- 不指定参数的时候,对数据库所有对象存储有只读权限
- 具体的方式是指定一个或多个要访 问的对象存储的名称
let transaction = db.transaction("users");
let transaction = db.transaction(["users", "anotherStore"]);
复制代码
- 每个事务都以只读方式访问数据。要修改访问模式,可以传入第二个参数
readOnly
readwrite
versionchange
let transaction = db.transaction("users", "readwrite");
复制代码
- 使用
objectStore()
方法并传入对象存储的名称以访问特定的对象存储 add()
添加对象put()
更新对象get()
取得对象delete()
删除对象clear()
删除所有对象
const transaction = db.transaction("users");
const store = transaction.objectStore("users");
const request = store.get("007");
request.onerror = (event) => alert("Did not get the object!");
request.onsuccess = (event) => alert(event.target.result.firstName);
复制代码
- 为一个事务可以完成任意多个请求,所以事务对象本身也有事件处理程序
transaction.onerror = (event) => {
// 整个事务被取消
};
transaction.oncomplete = (event) => {
// 整个事务成功完成
};
复制代码
4、插入对象
add()
或put()
写入数据。- 这两个方法都接收一个参数, 即要存储的对象,并把对象保存到对象存储。
- 这两个方法只在对象存储中已存在同名的键时有区别。
add()
会导致错误,而put()
会简单地重写该对象。- 更简单地说,可以把 add()想象成插入 新值,而把 put()想象为更新值。
// 初始化
for (let user in users) {
store.add(user)
}
复制代码
// users 是一个用户数据的数组
let request,
requests = [];
for (let user of users) {
request = store.add(user);
request.onerror = () => {
// 处理错误
};
request.onsuccess = () => {
// 处理成功
};
requests.push(request);
}
复制代码
5、通过游标查询
-
事务可以通过一个已知键取得一条记录。
-
如果想取得多条数据,则需要在事务中创建一个游标
-
游标是一个指向结果集的指针。
-
与传统数据库查询不同,游标不会事先收集所有结果。相反,游标指向第一个结果,并在接到指令前不会主动查找下一条数据
-
用
openCursor()
方法创建游标。
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
request = store.openCursor();
request.onsuccess = (event) => {
// 处理成功
};
request.onerror = (event) => {
// 处理错误
};
复制代码
- event.target.result 访问对象存储中的下一条记录,保存着IDBCursor实例或null
- IDBCursor实例
direction
- 字符串常量,表示游标的前进方向以及是否应该遍历所有重复的值
- NEXT(“next”)
- NEXTUNIQUE(“nextunique”)
- PREV(“prev”)
- PREVUNIQUE(“prevunique”)
- 字符串常量,表示游标的前进方向以及是否应该遍历所有重复的值
key
- 对象的键
value
- 实际的对象
primaryKey
- 游标使用的键。可能是对象键或索引键
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) { // 永远要检查
console.log(`Key: ${cursor.key}, Value: ${JSON.stringify(cursor.value)}`);
}
};
复制代码
- 游标可用于更新个别记录。
update()
方法使用指定的对象更新当前游标对应的值
request.onsuccess = (event) => {
const cursor = event.target.result;
let value,
updateRequest;
if (cursor) { // 永远要检查
if (cursor.key == "foo") {
value = cursor.value; // 取得当前对象
value.password = "magic!"; // 更新密码
updateRequest = cursor.update(value); // 请求保存更新后的对象
updateRequest.onsuccess = () => {
// 处理成功
};
updateRequest.onerror = () => {
// 处理错误
};
}
}
};
复制代码
- 可以调用
delelte()
来删除游标位置的记录
request.onsuccess = (event) => {
const cursor = event.target.result;
let value,
deleteRequest;
if (cursor) { // 永远要检查
if (cursor.key == "foo") {
deleteRequest = cursor.delete(); // 请求删除对象
deleteRequest.onsuccess = () => {
// 处理成功
};
deleteRequest.onerror = () => {
// 处理错误
};
}
}
};
复制代码
- 如果事务没有修改对象存储的权限,
update()
和delete()
都会抛出错误 - 默认每个游标只会创建一个请求。要创建另一个请求,必须调用下列中的一个方法
continue(key)
- 移动到结果集中的下一条记录。
- 参数 key 是可选的。如果没有指定 key,游标就移动到下一条记录;
- 如果指定了,则游标移动到指定的键
advance(count)
- 游标向前移动指定的 count 条记录
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) { // 永远要检查
console.log(`Key: ${cursor.key}, Value: ${JSON.stringify(cursor.value)}`);
cursor.continue(); // 移动到下一条记录
} else {
console.log("Done!");
}
};
复制代码
6、键范围
-
用键范围(key range) 可以让游标更容易管理。
-
键范围对应 IDBKeyRange 的实例。有四种方式指定键范围,
-
only()
方法并传入想要获取的键- 保证只获取想要键的值,类似与直接访问对象存储并调用
get()
const onlyRange = IDBKeyRange.only("007") 复制代码
- 保证只获取想要键的值,类似与直接访问对象存储并调用
-
lowerBound()
定义结果集的下限。下限表示游标开始的位置。// 从"007"记录开始,直到最后 const lowerRange = IDBKeyRange.lowerBound("007"); // 从"007"的下一条记录开始,直到最后 const lowerRange = IDBKeyRange.lowerBound("007", true); 复制代码
-
upperBound()
定义结果集的上限,指定游标不会越过的记录。// 从头开始,到"ace"记录为止 const upperRange = IDBKeyRange.upperBound("ace"); // 从头开始,到"ace"的前一条记录为止 const upperRange = IDBKeyRange.upperBound("ace", true); 复制代码
-
使用
bound()
方法同时指定下限和上限- 这个方法接收四个参数:
- 下限的键
- 上限的键
- 可选的布尔值表示是否跳过下限
- 可选的布尔值表示是否跳过上限
// 从"007"记录开始,到"ace"记录停止 const boundRange = IDBKeyRange.bound("007", "ace"); // 从"007"的下一条记录开始,到"ace"记录停止 const boundRange = IDBKeyRange.bound("007", "ace", true); // 从"007"的下一条记录开始,到"ace"的前一条记录停止 const boundRange = IDBKeyRange.bound("007", "ace", true, true); // 从"007"记录开始,到"ace"的前一条记录停止 const boundRange = IDBKeyRange.bound("007", "ace", false, true); 复制代码
- 这个方法接收四个参数:
-
-
定义范围后,传给
openCursor()
方法,就可以得到位于该范围内的游标
const store = db.transaction("users").objectStore("users");
const range = IDBKeyRange.bound("007", "ace");
request = store.openCursor(range);
request.onsuccess = function (event) {
const cursor = event.target.result;
if (cursor) { // 永远要检查
console.log(`Key: ${cursor.key}, Value: ${JSON.stringify(cursor.value)}`);
cursor.continue(); // 移动到下一条记录
} else {
console.log("Done!");
}
};
复制代码
7、设置游标方向
openCursor()
可接收两个参数, IDBKeyRange 的实例和表示方向的字符串- 默认从对象存储的第一条记录开始,每次调用 continue()或 advance()都会向最后一条记录前进
- 方向的字符串
- next
- 默认方向
- nextunique
- 正向 对象存储中有重复的记录,需要游标跳过那些重复的项
- prev
- 反向移动的游标,从最后一项开始向第一项移动
- prevunique
- 反向 对象存储中有重复的记录,需要游标跳过那些重复的项
- next
8、索引
- 对某些数据集,可能需要为对象存储指定多个键
- 将用户 ID 作为主键,然后在用户名上创建索引
- 要创建新索引,首先要取得对象存储的引用,调用
createIndex()
const transaction = db.transaction("users");
const store = transaction.objectStore("users");
const index = store.createIndex("username", "username", { unique: true });
复制代码
- createIndex参数
- 索引名称
- 索性属性名称
- 包含键
unique
的options对象,表示这个键是否在所有记录中唯一
createIndex()
返回的是 IDBIndex 实例。在对象存储上调用index()
方法可以得到同一个实例
const transaction = db.transaction("users");
const store = transaction.objectStore("users")
const index = store.index("username");
复制代码
- 可以在索引上使用
openCursor()
方法创建新游标- 这个游标与在对象存储上调用
openCursor(
创建的游标完全一样 - 只是result.key属性保存的是索引键不是主键
- 这个游标与在对象存储上调用
const transaction = db.transaction("users");
const store = transaction.objectStore("users");
const index = store.index("username");
const request = index.openCursor();
request.onsuccess = (event) => {
// 处理成功
};
复制代码
openKeyCursor()
方法也可以在索引上创建特殊游标,只返回每条记录的主键。- 这个方法接收的 参数与 openCursor()一样。
- 不同在于,event.result.key 是索引键,且 event.result.value 是主键而不是整个记录。
const transaction = db.transaction("users");
const store = transaction.objectStore("users");
const index = store.index("username");
const request = index.openKeyCursor();
request.onsuccess = (event) => {
// 处理成功
// event.result.key 是索引键,event.result.value 是主键
};
复制代码
get()
方法传入索引键通过索引取得单条记录- 如果果想只取得给定索引键的主键,可以使用
getKey()
方法 - IDBIndex 对象的属性
- name 索引名称
- keyPath 调用
createIndex()
传入的属性路径 - objectStore 索引对应的对象存储
- unique 索引键是否唯一(布尔值)
- 对象存储自身也有一个 indexNames 属性,保存着与之相关索引的名称。
- 在对象存储上调用
deleteIndex()
方法并传入索引的名称可以删除索
9、并发问题
- 第一次打开数据库时,添加
onversionchange
事件处理程序非常重要。 - 另一个同源标签页将数据库打开到新版本时,将执行此回调。
- 对这个事件最好的回应是立即关闭数据库,以便完成版本升级。
let request, database;
request = indexedDB.open("admin", 1);
request.onsuccess = (event) => {
database = event.target.result;
database.onversionchange = () => database.close();
};
复制代码
10、限制
- 信息不能跨域共享
- 每个源都有可以存储的空间限制