浏览器中存储数据
Cookie
直接存储在浏览器中的小数据,通常由Web 服务器使用响应 Set-Cookie HTTP-header 设置,然后浏览器使用 Cookie HTTP-header 将它们自动添加到(几乎)每个对相同域的请求中。
作用:
- 登录后,服务器在响应中使用
Set-CookieHTTP-header 来设置具有唯一“会话标识符(session identifier)”的 cookie - 下次当请求被发送到同一个域时,浏览器会使用
CookieHTTP-header 通过网络发送 cookie - 帮助服务器知道是谁发起了请求
读取
从文档中获取cookie
// _ym_uid=1759117014973370134; _ym_d=1759117014; __stripe_mid=96461b27-f155-4efd-9179-f6555e80e6611896f5
// 格式为:name1=value1;name2=value2;
console.info( document.cookie );
写入
虽然可以对cookie进行写入,但本质并非数据属性而是访问器,赋值操作会进行内部处理(即仅允许进行追加,不允许覆盖写)
document.cookie = "user=John"; // 只会更新或者新增名称为 user 的 cookie
console.info(document.cookie); // 展示所有 cookie
// _ym_uid=1759117014973370134; _ym_d=1759117014; __stripe_mid=96461b27-f155-4efd-9179-f6555e80e6611896f5;user=John
cookie允许任何字符串,因此为避免异常情况,应当进行编码处理
// 特殊字符(空格),需要编码
let name = "my name";
let value = "John Smith"
// 将 cookie 编码为 my%20name=John%20Smith
document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);
// 同时设置多个cookie,使用 ; 进行分隔
document.cookie = "user=John; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT"
注意:
encodeURIComponent编码后的name=value对,大小不能超过 4KB- 每个域的 cookie 总数不得超过 20+ 左右,具体限制取决于浏览器
额外属性
path
path=/mypath
路径前缀必须为绝对路径,只有path指定的路径以及子路径才能访问对应的cookie。通常设置为根目录/
如果一个 cookie 带有
path=/admin设置,那么该 cookie 在/admin和/admin/something下都是可见的,但是在/home或/adminpage下不可见
domain
domain=site.com
此属性控制哪些域可以访问这些cookie,实际中存在限制,无法手动设置任何域。
因此每个域的cookie都是隔离的。
默认下子域与主域的cookie也是隔离的,但允许进行设置:
// 在 site.com
// 使 cookie 可以被在任何子域 *.site.com 访问:
document.cookie = "user=John; domain=site.com"
// 之后
// 在 forum.site.com即可访问主域的cookie
console.info(document.cookie);
expires、max-age
session cookie:未设置expires、max-age两个参数的cookie,关闭浏览器后就会消失。
仅设置其中一个即可使cookie在特定时间失效:
expires:定义了浏览器会自动清除该 cookie 的时间,日期必须完全采用 GMT 时区的这种格式,如果设置为过去的时间,则 cookie 会被删除// 当前时间 +1 天 let date = new Date(Date.now() + 86400e3); date = date.toUTCString(); document.cookie = "user=John; expires=" + date;max-age:expires的替代选项,指明了 cookie 的过期时间距离当前时间的秒数,将其设置为 0 或负数,则 cookie 会被删除// cookie 会在一小时后失效 document.cookie = "user=John; max-age=3600"; // 删除 cookie(让它立即过期) document.cookie = "user=John; max-age=0";
secure
标记此cookie只能通过HTTPS传输,只在https://site.com下可见,在http://site.com下不可见,但默认情况下cookie仅基于域而言,只要同在site.com即可见
// 设置 cookie secure(只在 HTTPS 环境下可访问)
document.cookie = "user=John; secure";
samesite
本意是为了防止 XSRF(跨网站请求伪造)攻击。
XSRF:在site1.com(恶意网站)上存在一个表单请求指向site2.com(正常网站),由于表单设置的请求为site2.com,因此会携带此域名下的cookie,从而完成请求提交,为避免此情况,site2.com的表单提交都会携带XSRF token进行服务器校验,而site1.com无法获取到或者生成此token,因此请求无法获取数据。
samesite=strict:非当前域下的任何操作都不会发送cookie等同于直接设置 samesite
但也就意味着如何用户自己从笔记中访问网址则也不会发送cookie,因此设计时可以通过cookie1进行身份认证,cookie2进行数据更改操作的认证
samesite=lax:当从外部来到网站,则禁止浏览器发送 cookie,但是增加了一个例外- HTTP 方法是“安全的”(例如 GET 方法,而不是 POST,仅进行数据获取,不进行修改)
- 该操作执行顶级导航(更改浏览器地址栏中的 URL),即
top==window,意味着此页面并非运行在iframe中
httpOnly
Web 服务器使用 Set-Cookie header 来设置 cookie。并且,它可以设置 httpOnly 选项,这个选项禁止任何 JavaScript 访问 cookie。使用 document.cookie 看不到此类 cookie,也无法对此类 cookie 进行操作。
Cookie自定义函数
- 使用正则获取
// 返回具有给定 name 的 cookie, // 如果没找到,则返回 undefined function getCookie(name) { let matches = document.cookie.match(new RegExp( "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)" )); return matches ? decodeURIComponent(matches[1]) : undefined; } - 添加方法,可以自定义默认值属性,如默认path=/
function setCookie(name, value, options = {}) { options = { path: '/', // 如果需要,可以在这里添加其他默认值 ...options }; if (options.expires instanceof Date) { options.expires = options.expires.toUTCString(); } let updatedCookie = encodeURIComponent(name) + "=" + encodeURIComponent(value); for (let optionKey in options) { updatedCookie += "; " + optionKey; let optionValue = options[optionKey]; if (optionValue !== true) { updatedCookie += "=" + optionValue; } } document.cookie = updatedCookie; } - 删除方法:设置一个负的过期时间来调用它
function deleteCookie(name) { setCookie(name, "", { 'max-age': -1 }) }
第三方cookie
如果 cookie 是由用户所访问的页面的域以外的域通过 HTTP 响应头(如 Set-Cookie)设置的,则称为第三方 cookie。
典型场景:
- 用户访问
site.com,该页面加载了来自ads.com的资源(如<img src="https://ads.com/banner.png">)。 ads.com在响应中通过Set-Cookie: id=1234设置 cookie。- 此 cookie 属于
ads.com域,仅在向ads.com发起请求时自动携带。
跟踪机制:
当用户随后访问另一个也嵌入了 ads.com 资源的网站(如 other.com),浏览器仍会向 ads.com 发送之前设置的 cookie,使 ads.com 能跨站识别同一用户,实现跨站跟踪。
用途与争议:
第三方 cookie 常用于广告定向和用户行为分析,但由于隐私问题,越来越多的浏览器限制或默认阻止其使用:
- Safari:完全禁止第三方 cookie。
- Firefox:内置黑名单,阻止已知跟踪器的第三方 cookie。
- Chrome 等:提供隐私设置选项,允许用户禁用第三方 cookie。
重要澄清:
通过 <script> 加载的第三方脚本(如 Google Analytics)若使用 document.cookie 设置 cookie,该 cookie 仍属于当前页面的主域,不是第三方 cookie。只有通过第三方域的 HTTP 响应头(如图片、iframe、XHR/fetch 到第三方域并接收 Set-Cookie)设置的 cookie 才被视为第三方 cookie。
Storage
localStorage
localStorage 是一种持久化的客户端存储机制,具有以下特点:
- 持久性:数据不会因页面刷新、关闭标签页、重启浏览器甚至操作系统而丢失。
- 作用域:绑定到源(协议 + 域名 + 端口),同源的所有窗口和标签页共享同一个
localStorage。 - 容量大:通常支持至少 5MB 的存储空间(远大于 cookie 的 4KB),且不会随 HTTP 请求自动发送到服务器,避免带宽浪费。
- 仅限客户端操作:完全通过 JavaScript 控制,服务器无法直接读写。
方法示例:
setItem(key, value):存储键/值对(值会被自动转换为字符串)
localStorage.setItem('theme', 'dark');
localStorage.setItem('count', 42); // 数字转为 "42"
getItem(key):根据键获取对应的值,若不存在则返回 null
const theme = localStorage.getItem('theme'); // "dark"
const count = localStorage.getItem('count'); // "42"
removeItem(key):删除指定键及其对应的值
localStorage.removeItem('theme'); // 删除 theme 键
clear():清空所有存储的数据
localStorage.clear(); // 所有数据被移除
key(index):返回指定索引位置的键名(索引从 0 开始)
// 假设 localStorage 中有两项:['name', 'age']
const firstKey = localStorage.key(0); // "name"
length:返回当前存储中键值对的数量(只读属性)
console.log(localStorage.length); // 例如:2
可遍历性:虽然 localStorage 不是可迭代对象,但可通过 length 和 key() 或 Object.keys() 遍历
// 方法一:使用 length + key()
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
console.log(key, localStorage.getItem(key));
}
// 方法二:使用 Object.keys()
Object.keys(localStorage).forEach(key => {
console.log(key, localStorage.getItem(key));
});
注意:键和值必须是字符串。存储对象应使用
JSON.stringify(),读取时用JSON.parse()。
sessionStorage
sessionStorage 提供临时的会话级存储,特性如下:
- 生命周期:仅在当前浏览器标签页有效。页面刷新后数据保留,但关闭标签页后数据被清除。
- 作用域:绑定到源 + 浏览上下文(标签页),不同标签页即使访问同一 URL 也拥有独立的
sessionStorage。 - API 与
localStorage完全一致,仅生命周期和作用范围不同。
方法示例:
setItem(key, value):在当前会话中保存数据
sessionStorage.setItem('tempToken', 'abc123');
sessionStorage.setItem('step', '3');
getItem(key):读取当前会话中的数据
const token = sessionStorage.getItem('tempToken'); // "abc123"
const step = sessionStorage.getItem('step'); // "3"
removeItem(key):删除某项会话数据
sessionStorage.removeItem('tempToken');
clear():清除当前标签页的所有会话数据
sessionStorage.clear();
key(index):获取指定索引的键名
const firstKey = sessionStorage.key(0); // 例如 "step"
length:返回当前会话中存储项的数量
console.log(sessionStorage.length); // 例如:1
典型使用场景:
// 表单草稿自动保存(刷新后恢复)
document.getElementById('message').addEventListener('input', (e) => {
sessionStorage.setItem('draft', e.target.value);
});
// 页面加载时恢复草稿
window.addEventListener('load', () => {
const draft = sessionStorage.getItem('draft');
if (draft) {
document.getElementById('message').value = draft;
}
});
注意:在新标签页中打开同一页面时,
sessionStorage是全新的,不会继承原标签页的数据。
Storage事件
当 localStorage 或 sessionStorage 中的数据被修改时,会触发 storage 事件。该事件不会在执行修改的当前窗口中触发,而是在其他同源且能访问该存储的窗口或标签页中触发,常用于跨标签页通信。
事件对象属性:
key:被修改的键名(若调用clear(),则为null)oldValue:修改前的值(新增项时为null)newValue:修改后的值(删除项时为null)url:执行修改操作的文档 URLstorageArea:被修改的存储对象(即localStorage或sessionStorage)
使用示例:
// 在所有同源窗口中监听 storage 事件(除自身外)
window.addEventListener('storage', (event) => {
console.log('存储发生变化:');
console.log('key:', event.key);
console.log('old value:', event.oldValue);
console.log('new value:', event.newValue);
console.log('from URL:', event.url);
console.log('storage area:', event.storageArea === localStorage ? 'localStorage' : 'sessionStorage');
});
// 在另一个同源窗口中执行以下代码,将触发上述监听器
localStorage.setItem('message', 'Hello from another tab!');
典型应用场景:
- 用户在标签页 A 登出,通知其他标签页清除 UI 状态
- 多标签页协同编辑时同步状态
- 实现简单的跨标签页消息广播
注意:
sessionStorage的storage事件仅在同一标签页内的同源 iframe之间可能触发(因不同标签页的sessionStorage完全隔离)。storage事件是浏览器原生机制,兼容性良好;对于更复杂的通信需求,可考虑 BroadcastChannel API(但需注意其浏览器支持情况)。
IndexedDB
IndexedDB 是一个浏览器内建的数据库,比 localStorage 强大得多。它支持多种类型的键,可以存储几乎任何类型的值(包括对象和二进制数据),具备事务可靠性、键范围查询、索引等功能,并能存储远超 localStorage 的数据量(通常数百 MB 甚至更多)。它主要适用于离线应用,常与 Service Worker 等技术结合使用。
IndexedDB 原生基于事件模型,但也可通过 Promise 包装器(如 idb)使用 async/await,提升开发体验。
打开数据库
使用 indexedDB.open(name, version) 打开或创建数据库:
let openRequest = indexedDB.open("booksDb", 1);
name:数据库名称(字符串)version:正整数版本号(默认为 1)
返回一个请求对象,需监听以下事件:
success:数据库打开成功,openRequest.result 为数据库对象(IDBDatabase)
openRequest.onsuccess = function(event) {
let db = openRequest.result;
console.log("数据库已打开:", db);
// 可在此执行后续操作
};
error:打开失败
openRequest.onerror = function(event) {
console.error("打开数据库失败:", openRequest.error);
};
upgradeneeded:数据库不存在(版本为 0)或本地版本低于指定版本,用于初始化或升级结构
openRequest.onupgradeneeded = function(event) {
let db = openRequest.result;
let oldVersion = event.oldVersion;
let newVersion = event.newVersion ?? 1;
if (oldVersion < 1) {
// 初始化数据库结构
db.createObjectStore('books', { keyPath: 'id' });
}
if (oldVersion < 2) {
// 升级到版本 2:添加索引
let books = transaction.objectStore('books');
books.createIndex('price_idx', 'price', { unique: false });
}
};
注意:
upgradeneeded是唯一允许修改数据库结构(如创建对象库、索引)的时机。
版本控制机制:
当新版本号高于当前本地版本时,触发 upgradeneeded。若多个标签页同时访问,旧连接应监听 versionchange 并主动关闭:
db.onversionchange = function() {
db.close();
alert("数据库已更新,请刷新页面");
};
对象库(Object Store)
对象库是 IndexedDB 中存储数据的基本单位,类似于关系型数据库中的“表”。
- 每个值必须有唯一键(key),类型可为数字、字符串、日期、二进制或数组
- 键可通过以下方式指定:
- 显式提供(调用
add/put时传入) - 使用对象属性作为键(
keyPath) - 自动生成递增键(
autoIncrement: true)
- 显式提供(调用
创建对象库(仅在 upgradeneeded 中允许):
db.createObjectStore('books', {keyPath: 'id'});
// 或
db.createObjectStore('books', {autoIncrement: true});
删除对象库:
db.deleteObjectStore('books');
事务(Transaction)
所有数据操作必须在事务中进行。事务保证操作的原子性(全部成功或全部回滚)。
启动事务:
let transaction = db.transaction("books", "readwrite");
- 第一参数:对象库名(或数组)
- 第二参数:事务类型
"readonly"(默认):允许多个并发读"readwrite":独占写锁,阻塞其他写事务"versionchange":仅在upgradeneeded中自动创建,可修改结构
执行操作:
let books = transaction.objectStore("books");
let book = { id: 'js', price: 10 };
let request = books.add(book);
request.onsuccess = () => console.log("添加成功", request.result); // 返回键
request.onerror = () => console.error("错误", request.error);
方法对比:
add(value, [key]):插入新记录,若键已存在则失败(ConstraintError)put(value, [key]):插入或覆盖现有记录
事务自动提交:
事务在所有请求完成且微任务队列清空后自动提交。不能在事务中插入宏任务(如 fetch、setTimeout),否则后续操作会因事务已关闭而失败。
可监听 transaction.oncomplete 或 transaction.onabort 跟踪事务状态。
搜索
通过键搜索
支持精确键或键范围(IDBKeyRange):
// 精确查询
books.get('js');
// 范围查询
books.getAll(IDBKeyRange.bound('css', 'html')); // 'css' ≤ id ≤ 'html'
books.getAll(IDBKeyRange.upperBound('html', true)); // id < 'html'
books.getAll(); // 获取所有
常用搜索方法示例:
get(query):获取第一个匹配项的值
books.get('js').onsuccess = (e) => {
console.log(e.target.result); // { id: 'js', price: 10 }
};
getAll([query], [count]):获取所有匹配项的值(可限制数量)
books.getAll(IDBKeyRange.lowerBound(5), 2).onsuccess = (e) => {
console.log(e.target.result); // 最多 2 本价格 ≥5 的书
};
getKey(query):获取第一个匹配项的主键
books.getKey(IDBKeyRange.only('js')).onsuccess = (e) => {
console.log(e.target.result); // 'js'
};
getAllKeys([query], [count]):获取所有匹配项的主键
books.getAllKeys().onsuccess = (e) => {
console.log(e.target.result); // ['js', 'css', 'html']
};
count([query]):统计匹配项数量
books.count(IDBKeyRange.bound('a', 'z')).onsuccess = (e) => {
console.log("字母开头的书籍数量:", e.target.result);
};
结果按键排序(因内部有序存储)。
通过字段搜索(使用索引)
若需按非键字段(如 price)查询,需先创建索引:
// 在 upgradeneeded 中
let books = db.createObjectStore('books', {keyPath: 'id'});
books.createIndex('price_idx', 'price', {unique: false});
name:索引名keyPath:要索引的对象属性路径options:unique: true:禁止重复值multiEntry: true:若字段是数组,为每个元素建索引项
使用索引查询:
let priceIndex = books.index("price_idx");
priceIndex.getAll(10); // 价格为 10 的所有书
priceIndex.getAll(IDBKeyRange.upperBound(5)); // 价格 ≤ 5 的书
索引结果按索引字段值排序。
从存储中删除
- 按主键删除:
books.delete('js') - 按索引字段删除:先查主键,再删除
priceIndex.getKey(5).onsuccess = (e) => {
let id = e.target.result;
if (id !== undefined) books.delete(id);
};
- 清空整个对象库:
books.clear()
光标(Cursors)
当数据量过大无法一次性加载(如 getAll 内存不足)时,使用光标逐条遍历。
基本用法:
let request = books.openCursor();
request.onsuccess = function() {
let cursor = request.result;
if (cursor) {
console.log(cursor.key, cursor.value); // 键和值
cursor.continue(); // 移至下一条
} else {
console.log("遍历完成");
}
};
方向选项:
"next"(默认):升序"prev":降序"nextunique"/"prevunique":跳过重复键(仅用于索引光标)
索引光标:
在索引上打开光标时,cursor.key 为索引键(如价格),cursor.primaryKey 为主键(如书 ID):
let request = priceIndex.openCursor(IDBKeyRange.upperBound(5));
request.onsuccess = function() {
let cursor = request.result;
if (cursor) {
console.log("价格:", cursor.key);
console.log("书 ID:", cursor.primaryKey);
cursor.continue();
}
};
Promise 包装器
推荐使用 idb 库简化异步操作:
import { openDB } from 'https://cdn.jsdelivr.net/npm/idb@7/+esm';
const db = await openDB('booksDb', 1, {
upgrade(db) {
db.createObjectStore('books', { keyPath: 'id' });
}
});
// 添加数据
const tx = db.transaction('books', 'readwrite');
await tx.store.add({ id: 'js', price: 10 });
await tx.done; // 等待事务完成
优势:
- 使用
async/await避免回调嵌套 - 自动处理事务生命周期(通过
tx.done) - 错误通过
try...catch捕获
注意:即使使用包装器,仍需遵守“事务中不能插入宏任务”的规则。
总结
IndexedDB 是功能强大的客户端数据库,适用于需要存储大量结构化数据的场景。核心概念包括:
- 数据库:通过版本控制管理结构变更
- 对象库:存储数据的容器,需在
upgradeneeded中定义 - 事务:所有读写操作的载体,分
readonly/readwrite类型 - 索引:支持高效字段查询
- 光标:处理大数据集的流式遍历
- Promise 包装器:显著提升开发体验
虽然 API 较复杂,但结合现代工具(如 idb),可高效构建离线优先的 Web 应用。