Web API存储-客户端存储方案
# Web API存储-客户端存储方案
客户端存储允许Web应用在浏览器中保存数据,实现离线访问、状态持久化等功能。本文将详细介绍localStorage、sessionStorage和Cookie三种主要的客户端存储方案。
# 一、Web Storage概述
Web Storage包括localStorage和sessionStorage,提供了简单的键值对存储API。
# 1.1 localStorage vs sessionStorage
| 特性 | localStorage | sessionStorage |
|---|---|---|
| 生命周期 | 永久(除非手动清除) | 标签页关闭后清除 |
| 作用域 | 同源下所有标签页共享 | 仅当前标签页 |
| 存储容量 | 通常5-10MB | 通常5-10MB |
| API | 相同 | 相同 |
# 1.2 基本使用
// localStorage和sessionStorage API完全相同
// 存储数据
localStorage.setItem('username', '张三');
localStorage.setItem('age', '25');
// 读取数据
const username = localStorage.getItem('username'); // "张三"
const age = localStorage.getItem('age'); // "25"
// 删除数据
localStorage.removeItem('age');
// 清空所有数据
localStorage.clear();
// 获取键名
const key = localStorage.key(0); // 获取第一个键名
// 获取存储项数量
const length = localStorage.length;
// 简写方式(不推荐,可能与方法名冲突)
localStorage.username = '李四';
const name = localStorage.username;
delete localStorage.username;
# 二、localStorage详解
# 2.1 存储和读取数据
// 存储字符串
localStorage.setItem('name', '张三');
// 存储数字(会转为字符串)
localStorage.setItem('age', 25);
console.log(typeof localStorage.getItem('age')); // "string"
// 存储对象(需要序列化)
const user = { name: '张三', age: 25 };
localStorage.setItem('user', JSON.stringify(user));
// 读取对象(需要反序列化)
const storedUser = JSON.parse(localStorage.getItem('user'));
console.log(storedUser.name); // "张三"
// 存储数组
const list = ['苹果', '香蕉', '橙子'];
localStorage.setItem('fruits', JSON.stringify(list));
// 读取数组
const storedList = JSON.parse(localStorage.getItem('fruits'));
console.log(storedList[0]); // "苹果"
// 存储日期(需要特殊处理)
const now = new Date();
localStorage.setItem('timestamp', now.toISOString());
// 读取日期
const storedDate = new Date(localStorage.getItem('timestamp'));
console.log(storedDate);
# 2.2 封装Storage工具类
class Storage {
// 设置数据(自动序列化)
static set(key, value, expire = null) {
const data = {
value: value,
expire: expire ? Date.now() + expire : null
};
localStorage.setItem(key, JSON.stringify(data));
}
// 获取数据(自动反序列化,检查过期)
static get(key) {
const item = localStorage.getItem(key);
if (!item) {
return null;
}
try {
const data = JSON.parse(item);
// 检查是否过期
if (data.expire && Date.now() > data.expire) {
localStorage.removeItem(key);
return null;
}
return data.value;
} catch (err) {
// 如果不是JSON格式,直接返回原值
return item;
}
}
// 删除数据
static remove(key) {
localStorage.removeItem(key);
}
// 清空所有数据
static clear() {
localStorage.clear();
}
// 获取所有键
static keys() {
return Object.keys(localStorage);
}
// 获取所有数据
static getAll() {
const result = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
result[key] = this.get(key);
}
return result;
}
// 批量设置
static setMultiple(obj) {
Object.entries(obj).forEach(([key, value]) => {
this.set(key, value);
});
}
// 检查是否存在
static has(key) {
return localStorage.getItem(key) !== null;
}
// 获取存储大小(字节)
static getSize() {
let size = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
size += key.length + localStorage.getItem(key).length;
}
}
return size;
}
}
// 使用示例
Storage.set('user', { name: '张三', age: 25 });
Storage.set('token', 'abc123', 3600000); // 1小时后过期
const user = Storage.get('user');
console.log(user.name); // "张三"
Storage.setMultiple({
theme: 'dark',
language: 'zh-CN',
sidebar: 'collapsed'
});
console.log(Storage.getAll());
console.log('存储大小:', Storage.getSize(), '字节');
# 2.3 监听Storage事件
// 监听storage变化(仅在其他标签页修改时触发)
window.addEventListener('storage', function(e) {
console.log('Storage改变:');
console.log('键名:', e.key);
console.log('旧值:', e.oldValue);
console.log('新值:', e.newValue);
console.log('URL:', e.url);
console.log('Storage对象:', e.storageArea);
});
// 实际应用:多标签页同步
window.addEventListener('storage', function(e) {
if (e.key === 'theme') {
// 当其他标签页修改主题时,同步更新
applyTheme(e.newValue);
}
if (e.key === 'user' && e.newValue === null) {
// 其他标签页登出,当前页面也登出
logout();
}
});
// 注意:storage事件不会在当前标签页触发
// 如果需要在当前页触发,需要手动派发自定义事件
function setItemWithEvent(key, value) {
const oldValue = localStorage.getItem(key);
localStorage.setItem(key, value);
// 手动触发自定义事件
window.dispatchEvent(new CustomEvent('localstorage-change', {
detail: { key, oldValue, newValue: value }
}));
}
# 三、sessionStorage详解
# 3.1 基本使用
// API与localStorage完全相同
sessionStorage.setItem('tempData', 'temporary');
const temp = sessionStorage.getItem('tempData');
sessionStorage.removeItem('tempData');
sessionStorage.clear();
# 3.2 使用场景
// 1. 表单数据临时保存
function saveFormData() {
const formData = {
name: document.getElementById('name').value,
email: document.getElementById('email').value
};
sessionStorage.setItem('formDraft', JSON.stringify(formData));
}
// 恢复表单数据
function restoreFormData() {
const draft = sessionStorage.getItem('formDraft');
if (draft) {
const data = JSON.parse(draft);
document.getElementById('name').value = data.name;
document.getElementById('email').value = data.email;
}
}
// 表单提交成功后清除
function onFormSubmitSuccess() {
sessionStorage.removeItem('formDraft');
}
// 2. 页面状态保存
// 保存滚动位置
window.addEventListener('scroll', () => {
sessionStorage.setItem('scrollPosition', window.scrollY);
});
// 恢复滚动位置
window.addEventListener('load', () => {
const scrollPos = sessionStorage.getItem('scrollPosition');
if (scrollPos) {
window.scrollTo(0, parseInt(scrollPos));
}
});
// 3. 向导步骤数据
const wizard = {
setStep(step, data) {
sessionStorage.setItem(`wizard_step${step}`, JSON.stringify(data));
},
getStep(step) {
const data = sessionStorage.getItem(`wizard_step${step}`);
return data ? JSON.parse(data) : null;
},
clear() {
const keys = Object.keys(sessionStorage);
keys.forEach(key => {
if (key.startsWith('wizard_')) {
sessionStorage.removeItem(key);
}
});
}
};
# 四、Cookie详解
# 4.1 Cookie基础
// 设置Cookie(最简单方式)
document.cookie = 'username=张三';
// 设置Cookie(带过期时间)
const expires = new Date();
expires.setDate(expires.getDate() + 7); // 7天后过期
document.cookie = `username=张三; expires=${expires.toUTCString()}`;
// 设置Cookie(带路径和域)
document.cookie = 'username=张三; path=/; domain=.example.com';
// 设置Cookie(完整选项)
document.cookie = 'token=abc123; max-age=3600; path=/; secure; samesite=strict';
// 读取Cookie
console.log(document.cookie); // "username=张三; token=abc123; ..."
# 4.2 Cookie属性
| 属性 | 说明 | 示例 |
|---|---|---|
expires | 过期时间(GMT格式) | expires=Wed, 21 Oct 2024 07:28:00 GMT |
max-age | 有效期(秒) | max-age=3600 |
path | 路径 | path=/ |
domain | 域名 | domain=.example.com |
secure | 仅HTTPS传输 | secure |
httpOnly | 仅服务端访问(JS无法访问) | httpOnly |
samesite | 跨站请求限制 | samesite=strict/lax/none |
# 4.3 Cookie工具类
class Cookie {
// 设置Cookie
static set(name, value, options = {}) {
let cookieStr = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
// 过期时间
if (options.expires) {
if (typeof options.expires === 'number') {
const date = new Date();
date.setTime(date.getTime() + options.expires * 1000);
options.expires = date;
}
cookieStr += `; expires=${options.expires.toUTCString()}`;
}
// max-age(优先级高于expires)
if (options.maxAge) {
cookieStr += `; max-age=${options.maxAge}`;
}
// 路径
cookieStr += `; path=${options.path || '/'}`;
// 域名
if (options.domain) {
cookieStr += `; domain=${options.domain}`;
}
// secure
if (options.secure) {
cookieStr += '; secure';
}
// samesite
if (options.sameSite) {
cookieStr += `; samesite=${options.sameSite}`;
}
document.cookie = cookieStr;
}
// 获取Cookie
static get(name) {
const cookies = document.cookie.split('; ');
for (const cookie of cookies) {
const [key, value] = cookie.split('=');
if (decodeURIComponent(key) === name) {
return decodeURIComponent(value);
}
}
return null;
}
// 获取所有Cookie
static getAll() {
const cookies = {};
const items = document.cookie.split('; ');
items.forEach(item => {
const [key, value] = item.split('=');
cookies[decodeURIComponent(key)] = decodeURIComponent(value);
});
return cookies;
}
// 删除Cookie
static remove(name, options = {}) {
this.set(name, '', {
...options,
maxAge: -1
});
}
// 检查Cookie是否存在
static has(name) {
return this.get(name) !== null;
}
}
// 使用示例
Cookie.set('username', '张三', { maxAge: 3600 }); // 1小时
Cookie.set('token', 'abc123', {
maxAge: 7 * 24 * 3600, // 7天
secure: true,
sameSite: 'strict'
});
console.log(Cookie.get('username')); // "张三"
console.log(Cookie.getAll());
Cookie.remove('username');
# 4.4 SameSite属性详解
// Strict:完全禁止第三方Cookie
Cookie.set('session', 'xxx', { sameSite: 'strict' });
// 跨站点请求时不会发送此Cookie
// Lax(默认):允许部分第三方Cookie
Cookie.set('session', 'xxx', { sameSite: 'lax' });
// 导航到目标网址的GET请求会发送Cookie
// 跨站点的POST请求不会发送
// None:允许第三方Cookie(必须配合secure)
Cookie.set('tracking', 'xxx', {
sameSite: 'none',
secure: true // 必须为HTTPS
});
# 五、存储方案对比
# 5.1 三种方案对比
| 特性 | localStorage | sessionStorage | Cookie |
|---|---|---|---|
| 容量 | 5-10MB | 5-10MB | 4KB |
| 生命周期 | 永久 | 标签页关闭 | 可设置过期时间 |
| 作用域 | 同源所有标签页 | 当前标签页 | 可设置path和domain |
| HTTP请求 | 不发送 | 不发送 | 自动发送 |
| API | 简单 | 简单 | 复杂 |
| 兼容性 | IE8+ | IE8+ | 所有浏览器 |
| 用途 | 本地数据存储 | 临时数据存储 | 服务端会话、跨域 |
# 5.2 选择建议
// 使用localStorage:
// - 用户偏好设置
// - 本地缓存数据
// - 购物车(离线可用)
// - 草稿保存
// 使用sessionStorage:
// - 表单临时数据
// - 页面状态
// - 向导步骤数据
// - 一次性登录
// 使用Cookie:
// - 身份认证令牌
// - 跨子域共享数据
// - 需要服务端访问的数据
// - CSRF令牌
# 六、存储限制与配额
# 6.1 检测存储容量
// 检测localStorage可用空间
function getStorageSize() {
let size = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
size += key.length + localStorage.getItem(key).length;
}
}
return size;
}
// 测试最大容量
function testStorageLimit() {
let testKey = 'storageTest';
let data = '0123456789';
let size = 0;
try {
while (true) {
localStorage.setItem(testKey, data);
data += data;
size += data.length;
}
} catch (e) {
console.log('最大容量约为:', size, '字节');
localStorage.removeItem(testKey);
}
}
// 检查是否超出配额
function checkQuota() {
if (navigator.storage && navigator.storage.estimate) {
navigator.storage.estimate().then(estimate => {
console.log('已用空间:', estimate.usage);
console.log('总配额:', estimate.quota);
console.log('使用率:', (estimate.usage / estimate.quota * 100).toFixed(2) + '%');
});
}
}
# 6.2 处理存储超限
function safeSetItem(key, value) {
try {
localStorage.setItem(key, value);
return true;
} catch (e) {
if (e.name === 'QuotaExceededError') {
console.error('存储空间不足');
// 策略1:清理过期数据
clearExpiredData();
// 策略2:删除最旧的数据
removeOldestItem();
// 策略3:通知用户
alert('存储空间不足,已清理部分数据');
// 重试
try {
localStorage.setItem(key, value);
return true;
} catch (e) {
return false;
}
}
return false;
}
}
function clearExpiredData() {
const now = Date.now();
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
try {
const data = JSON.parse(localStorage.getItem(key));
if (data.expire && now > data.expire) {
localStorage.removeItem(key);
}
} catch (e) {
// 忽略无法解析的数据
}
}
}
}
function removeOldestItem() {
let oldestKey = null;
let oldestTime = Infinity;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
try {
const data = JSON.parse(localStorage.getItem(key));
if (data.timestamp && data.timestamp < oldestTime) {
oldestTime = data.timestamp;
oldestKey = key;
}
} catch (e) {
// 忽略无法解析的数据
}
}
}
if (oldestKey) {
localStorage.removeItem(oldestKey);
}
}
# 七、安全性考虑
# 7.1 XSS攻击防护
// ✗ 危险:直接存储用户输入
const userInput = getUserInput();
localStorage.setItem('data', userInput);
// ✓ 安全:清理用户输入
function sanitizeInput(input) {
const div = document.createElement('div');
div.textContent = input;
return div.innerHTML;
}
const safeInput = sanitizeInput(getUserInput());
localStorage.setItem('data', safeInput);
// ✗ 危险:直接使用存储的HTML
const html = localStorage.getItem('data');
element.innerHTML = html;
// ✓ 安全:使用textContent
const text = localStorage.getItem('data');
element.textContent = text;
# 7.2 敏感数据处理
// ✗ 避免:明文存储敏感信息
localStorage.setItem('password', 'myPassword123');
localStorage.setItem('creditCard', '1234-5678-9012-3456');
// ✓ 推荐:服务端存储敏感信息,客户端只存储token
localStorage.setItem('token', 'encrypted-token');
// 如果必须在客户端存储,考虑加密
class SecureStorage {
static encrypt(text, key) {
// 使用加密库(如CryptoJS)
// return CryptoJS.AES.encrypt(text, key).toString();
}
static decrypt(ciphertext, key) {
// return CryptoJS.AES.decrypt(ciphertext, key).toString(CryptoJS.enc.Utf8);
}
static set(key, value, encryptionKey) {
const encrypted = this.encrypt(JSON.stringify(value), encryptionKey);
localStorage.setItem(key, encrypted);
}
static get(key, encryptionKey) {
const encrypted = localStorage.getItem(key);
if (!encrypted) return null;
const decrypted = this.decrypt(encrypted, encryptionKey);
return JSON.parse(decrypted);
}
}
# 7.3 Cookie安全
// 敏感Cookie应设置HttpOnly(服务端设置)
// Set-Cookie: sessionId=xxx; HttpOnly; Secure; SameSite=Strict
// 客户端设置安全Cookie
Cookie.set('token', 'xxx', {
secure: true, // 仅HTTPS
sameSite: 'strict', // 防止CSRF
maxAge: 3600 // 限制有效期
});
// 避免在Cookie中存储敏感信息
// Cookie会在每次请求中发送,容易被拦截
# 八、综合示例
# 九、最佳实践
# 9.1 数据存储
- 使用JSON序列化存储复杂对象
- 添加版本号,方便数据迁移
- 为数据添加时间戳,便于清理
- 使用命名空间避免冲突
# 9.2 性能优化
- 避免频繁读写Storage
- 批量操作时使用缓存
- 大数据使用IndexedDB
- 定期清理过期数据
# 9.3 安全性
- 不存储敏感信息
- 清理用户输入
- Cookie使用secure和httpOnly
- 考虑数据加密
# 十、总结
客户端存储提供了多种数据持久化方案:
- localStorage:永久存储,适合本地数据缓存
- sessionStorage:临时存储,适合页面状态
- Cookie:小容量存储,适合服务端会话
合理选择存储方案,可以提升用户体验,实现离线功能和状态持久化。
祝你变得更强!
编辑 (opens new window)
上次更新: 2025/11/28