前端存储方案全面对比:localStorage、sessionStorage、cookies与IndexedDB
引言
在前端开发中,数据存储是一个常见需求。无论是用户偏好设置、表单数据暂存、还是应用状态维护,我们都需要在客户端保存一定的数据。浏览器提供了多种存储方案,每种都有其独特的特性和适用场景。本文将全面对比四种主要的浏览器存储技术:localStorage、sessionStorage、cookies和IndexedDB,从使用方式、适用场景到性能限制进行详细分析,并提供标准化的封装方案,帮助你在项目中做出合适的选择。
各存储方案基本介绍
1. localStorage
localStorage 提供了一种持久化的键值对存储机制,数据没有过期时间,除非被手动清除,否则将一直存在。
2. sessionStorage
sessionStorage 类似于 localStorage,但数据仅在当前会话期间有效,关闭标签页或浏览器后数据将被清除。
3. Cookies
Cookies 是最早的客户端存储方案,主要用于维持服务端会话状态,数据会在每次 HTTP 请求中发送到服务器。
4. IndexedDB
IndexedDB 是一个低级API,提供了客户端存储大量结构化数据的能力,并能够高性能检索这些数据,支持事务操作。
各存储方案的封装
1. localStorage 封装
/**
* localStorage 操作工具类
* 封装常用的 localStorage 操作,并提供数据类型保持功能
*/
class LocalStorageUtil {
/**
* 设置 localStorage 项
* @param {string} key - 存储的键名
* @param {any} value - 存储的值,会自动序列化
* @param {number} [expireTime] - 可选的过期时间(毫秒),不设置则永久存储
*/
static set(key, value, expireTime) {
const data = {
value: value,
type: typeof value,
// 如果设置了过期时间,则记录到期时间戳
expire: expireTime ? new Date().getTime() + expireTime : null
};
// 将对象转为 JSON 字符串存储
localStorage.setItem(key, JSON.stringify(data));
}
/**
* 获取 localStorage 项
* @param {string} key - 要获取的键名
* @returns {any} 返回存储的值,如果已过期或不存在则返回 null
*/
static get(key) {
const dataStr = localStorage.getItem(key);
// 如果数据不存在,直接返回 null
if (!dataStr) return null;
try {
const data = JSON.parse(dataStr);
// 检查是否设置了过期时间且已过期
if (data.expire && data.expire < new Date().getTime()) {
// 过期则删除并返回 null
this.remove(key);
return null;
}
// 根据存储时的类型还原数据
if (data.type === 'number') return Number(data.value);
if (data.type === 'boolean') return Boolean(data.value);
return data.value;
} catch (e) {
// 解析JSON出错,返回原始字符串
console.error(`解析localStorage数据出错: ${e.message}`);
return dataStr;
}
}
/**
* 删除 localStorage 项
* @param {string} key - 要删除的键名
*/
static remove(key) {
localStorage.removeItem(key);
}
/**
* 清空所有 localStorage 数据
*/
static clear() {
localStorage.clear();
}
/**
* 获取localStorage中所有的键名
* @returns {Array<string>} 键名数组
*/
static keys() {
return Object.keys(localStorage);
}
/**
* 检查键是否存在且未过期
* @param {string} key - 要检查的键名
* @returns {boolean} 如果键存在且未过期返回true
*/
static has(key) {
return this.get(key) !== null;
}
}
2. sessionStorage 封装
/**
* sessionStorage 操作工具类
* 封装常用的 sessionStorage 操作,并提供数据类型保持功能
*/
class SessionStorageUtil {
/**
* 设置 sessionStorage 项
* @param {string} key - 存储的键名
* @param {any} value - 存储的值,会自动序列化
*/
static set(key, value) {
const data = {
value: value,
type: typeof value
};
// 将对象转为 JSON 字符串存储
sessionStorage.setItem(key, JSON.stringify(data));
}
/**
* 获取 sessionStorage 项
* @param {string} key - 要获取的键名
* @returns {any} 返回存储的值,如果不存在则返回 null
*/
static get(key) {
const dataStr = sessionStorage.getItem(key);
// 如果数据不存在,直接返回 null
if (!dataStr) return null;
try {
const data = JSON.parse(dataStr);
// 根据存储时的类型还原数据
if (data.type === 'number') return Number(data.value);
if (data.type === 'boolean') return Boolean(data.value);
return data.value;
} catch (e) {
// 解析JSON出错,返回原始字符串
console.error(`解析sessionStorage数据出错: ${e.message}`);
return dataStr;
}
}
/**
* 删除 sessionStorage 项
* @param {string} key - 要删除的键名
*/
static remove(key) {
sessionStorage.removeItem(key);
}
/**
* 清空所有 sessionStorage 数据
*/
static clear() {
sessionStorage.clear();
}
/**
* 获取sessionStorage中所有的键名
* @returns {Array<string>} 键名数组
*/
static keys() {
return Object.keys(sessionStorage);
}
/**
* 检查键是否存在
* @param {string} key - 要检查的键名
* @returns {boolean} 如果键存在返回true
*/
static has(key) {
return this.get(key) !== null;
}
}
3. Cookies 封装
/**
* Cookies 操作工具类
* 封装cookie的读写删等操作,提供更友好的API
*/
class CookieUtil {
/**
* 设置 cookie
* @param {string} name - cookie名称
* @param {string} value - cookie值
* @param {Object} [options] - cookie选项
* @param {number} [options.days] - 过期天数
* @param {string} [options.path] - cookie路径
* @param {string} [options.domain] - cookie域
* @param {boolean} [options.secure] - 是否仅通过HTTPS发送
* @param {string} [options.sameSite] - SameSite属性 ('strict', 'lax', 'none')
*/
static set(name, value, options = {}) {
// 构建cookie值
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
// 设置过期时间
if (options.days !== undefined) {
const expireDate = new Date();
expireDate.setDate(expireDate.getDate() + options.days);
cookie += `; expires=${expireDate.toUTCString()}`;
}
// 设置路径
if (options.path) {
cookie += `; path=${options.path}`;
} else {
cookie += '; path=/'; // 默认路径
}
// 设置域
if (options.domain) {
cookie += `; domain=${options.domain}`;
}
// 设置安全标志
if (options.secure) {
cookie += '; secure';
}
// 设置SameSite
if (options.sameSite) {
cookie += `; SameSite=${options.sameSite}`;
}
// 设置httpOnly (注意:JavaScript无法设置或读取HttpOnly cookie)
if (options.httpOnly) {
cookie += '; HttpOnly';
}
// 写入cookie
document.cookie = cookie;
}
/**
* 获取指定名称的cookie值
* @param {string} name - cookie名称
* @returns {string|null} 如果找到返回cookie值,否则返回null
*/
static get(name) {
const cookies = document.cookie.split(';');
const encodedName = encodeURIComponent(name);
for (let i = 0; i < cookies.length; i++) {
let cookie = cookies[i].trim();
// 检查这个cookie是否是我们要找的
if (cookie.indexOf(encodedName + '=') === 0) {
// 返回解码后的值
return decodeURIComponent(
cookie.substring(encodedName.length + 1, cookie.length)
);
}
}
return null;
}
/**
* 删除指定的cookie
* @param {string} name - 要删除的cookie名称
* @param {Object} [options] - cookie选项
* @param {string} [options.path] - cookie路径
* @param {string} [options.domain] - cookie域
*/
static remove(name, options = {}) {
// 设置过期时间为过去的日期来删除cookie
const deleteOptions = {
...options,
days: -1 // 设置为过去的日期
};
this.set(name, '', deleteOptions);
}
/**
* 检查cookie是否存在
* @param {string} name - cookie名称
* @returns {boolean} 如果cookie存在返回true
*/
static has(name) {
return this.get(name) !== null;
}
/**
* 获取所有cookie名称
* @returns {Array<string>} cookie名称数组
*/
static keys() {
const result = [];
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie) {
const equalPos = cookie.indexOf('=');
if (equalPos > 0) {
result.push(decodeURIComponent(cookie.substring(0, equalPos)));
}
}
}
return result;
}
}
4. IndexedDB 封装
/**
* IndexedDB 操作工具类
* 提供简化的 IndexedDB 异步操作接口
*/
class IndexedDBUtil {
/**
* 初始化数据库
* @param {string} dbName - 数据库名称
* @param {number} version - 数据库版本
* @param {Function} upgradeCallback - 升级回调函数,用于创建对象仓库
* @returns {Promise<IDBDatabase>} 返回数据库连接
*/
static async openDB(dbName, version, upgradeCallback) {
return new Promise((resolve, reject) => {
// 打开数据库连接
const request = indexedDB.open(dbName, version);
// 数据库打开成功
request.onsuccess = (event) => {
const db = event.target.result;
resolve(db);
};
// 数据库打开失败
request.onerror = (event) => {
reject(`数据库打开失败: ${event.target.error}`);
};
// 数据库升级事件
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 调用升级回调来创建对象仓库
if (upgradeCallback && typeof upgradeCallback === 'function') {
upgradeCallback(db, event);
}
};
});
}
/**
* 添加数据到对象仓库
* @param {IDBDatabase} db - 数据库连接
* @param {string} storeName - 对象仓库名称
* @param {any} data - 要添加的数据
* @param {string} [keyPath] - 可选的键路径,用于更新数据
* @returns {Promise<any>} 返回添加操作的结果
*/
static async add(db, storeName, data, keyPath = null) {
return new Promise((resolve, reject) => {
// 创建事务
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
// 添加或放置数据
let request;
if (keyPath && data[keyPath]) {
// 使用put方法时,如果存在具有相同键的记录,将更新该记录
request = store.put(data);
} else {
// 使用add方法时,如果存在具有相同键的记录,将抛出错误
request = store.add(data);
}
// 请求成功
request.onsuccess = (event) => {
resolve(event.target.result);
};
// 请求失败
request.onerror = (event) => {
reject(`数据添加失败: ${event.target.error}`);
};
});
}
/**
* 获取对象仓库中的数据
* @param {IDBDatabase} db - 数据库连接
* @param {string} storeName - 对象仓库名称
* @param {string|number} key - 要获取的记录的键
* @returns {Promise<any>} 返回请求的数据
*/
static async get(db, storeName, key) {
return new Promise((resolve, reject) => {
// 创建事务
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
// 获取数据
const request = store.get(key);
// 请求成功
request.onsuccess = (event) => {
resolve(event.target.result);
};
// 请求失败
request.onerror = (event) => {
reject(`数据获取失败: ${event.target.error}`);
};
});
}
/**
* 获取对象仓库中的所有数据
* @param {IDBDatabase} db - 数据库连接
* @param {string} storeName - 对象仓库名称
* @returns {Promise<Array>} 返回仓库中的所有数据
*/
static async getAll(db, storeName) {
return new Promise((resolve, reject) => {
// 创建事务
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
// 获取所有数据
const request = store.getAll();
// 请求成功
request.onsuccess = (event) => {
resolve(event.target.result);
};
// 请求失败
request.onerror = (event) => {
reject(`获取所有数据失败: ${event.target.error}`);
};
});
}
/**
* 删除对象仓库中的数据
* @param {IDBDatabase} db - 数据库连接
* @param {string} storeName - 对象仓库名称
* @param {string|number} key - 要删除的记录的键
* @returns {Promise<void>} 返回删除操作的结果
*/
static async delete(db, storeName, key) {
return new Promise((resolve, reject) => {
// 创建事务
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
// 删除数据
const request = store.delete(key);
// 请求成功
request.onsuccess = (event) => {
resolve();
};
// 请求失败
request.onerror = (event) => {
reject(`数据删除失败: ${event.target.error}`);
};
});
}
/**
* 清空对象仓库
* @param {IDBDatabase} db - 数据库连接
* @param {string} storeName - 对象仓库名称
* @returns {Promise<void>} 返回清空操作的结果
*/
static async clear(db, storeName) {
return new Promise((resolve, reject) => {
// 创建事务
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
// 清空仓库
const request = store.clear();
// 请求成功
request.onsuccess = (event) => {
resolve();
};
// 请求失败
request.onerror = (event) => {
reject(`清空仓库失败: ${event.target.error}`);
};
});
}
/**
* 使用索引查询数据
* @param {IDBDatabase} db - 数据库连接
* @param {string} storeName - 对象仓库名称
* @param {string} indexName - 索引名称
* @param {any} value - 要查询的值
* @returns {Promise<Array>} 返回查询结果
*/
static async getByIndex(db, storeName, indexName, value) {
return new Promise((resolve, reject) => {
// 创建事务
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const index = store.index(indexName);
// 使用索引查询
const request = index.getAll(value);
// 请求成功
request.onsuccess = (event) => {
resolve(event.target.result);
};
// 请求失败
request.onerror = (event) => {
reject(`索引查询失败: ${event.target.error}`);
};
});
}
/**
* 关闭数据库连接
* @param {IDBDatabase} db - 数据库连接
*/
static closeDB(db) {
if (db) {
db.close();
}
}
/**
* 删除数据库
* @param {string} dbName - 数据库名称
* @returns {Promise<void>} 返回删除操作的结果
*/
static async deleteDB(dbName) {
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(dbName);
request.onsuccess = () => {
resolve();
};
request.onerror = (event) => {
reject(`删除数据库失败: ${event.target.error}`);
};
});
}
}
实际使用案例
1. localStorage 使用示例
// 设置一个带过期时间的用户偏好设置(7天后过期)
LocalStorageUtil.set('userTheme', 'dark', 7 * 24 * 60 * 60 * 1000);
LocalStorageUtil.set('fontSize', 16, 7 * 24 * 60 * 60 * 1000);
// 获取用户主题设置
const theme = LocalStorageUtil.get('userTheme');
if (theme) {
console.log(`用户主题: ${theme}`);
// 应用主题设置
document.body.classList.add(theme);
} else {
console.log('用户主题设置已过期或未设置');
// 使用默认主题
document.body.classList.add('light');
}
// 检查某个设置是否存在
if (LocalStorageUtil.has('fontSize')) {
const fontSize = LocalStorageUtil.get('fontSize');
document.body.style.fontSize = `${fontSize}px`;
}
// 删除某个设置
LocalStorageUtil.remove('temporarySetting');
// 查看所有存储的键
console.log('本地存储的所有键:', LocalStorageUtil.keys());
2. sessionStorage 使用示例
// 存储多步骤表单的临时数据
SessionStorageUtil.set('formStep1', {
name: 'John Doe',
email: 'john@example.com'
});
// 在下一个页面获取表单数据
const formData = SessionStorageUtil.get('formStep1');
if (formData) {
console.log('表单数据:', formData);
// 填充表单字段
document.getElementById('name').value = formData.name;
document.getElementById('email').value = formData.email;
}
// 表单提交后清除临时数据
document.getElementById('submitForm').addEventListener('click', () => {
// 处理表单提交...
SessionStorageUtil.remove('formStep1');
console.log('表单数据已清除');
});
// 检查会话中是否有未完成的表单
if (SessionStorageUtil.has('formStep1')) {
// 显示"继续填写"按钮
document.getElementById('continueBtn').style.display = 'block';
}
3. Cookies 使用示例
// 设置一个基本的cookie(30天过期)
CookieUtil.set('visited', 'true', { days: 30 });
// 设置一个安全的cookie
CookieUtil.set('authToken', 'xyz123', {
days: 7,
path: '/',
secure: true,
sameSite: 'strict'
});
// 读取cookie
const hasVisited = CookieUtil.get('visited');
if (hasVisited) {
console.log('欢迎回来!');
} else {
console.log('欢迎首次访问!');
}
// 检查认证状态
if (CookieUtil.has('authToken')) {
const token = CookieUtil.get('authToken');
console.log(`用户已认证,令牌: ${token}`);
} else {
console.log('用户未认证');
// 重定向到登录页
}
// 删除cookie(注销)
document.getElementById('logoutBtn').addEventListener('click', () => {
CookieUtil.remove('authToken', { path: '/' });
console.log('用户已注销');
// 重定向到登录页
});
// 获取所有cookie名称
console.log('所有cookie:', CookieUtil.keys());
4. IndexedDB 使用示例
// 创建和初始化数据库
async function initDatabase() {
try {
// 打开数据库,如果不存在则创建
const db = await IndexedDBUtil.openDB('TodoApp', 1, (db, event) => {
// 创建一个对象仓库(在数据库升级时调用)
if (!db.objectStoreNames.contains('tasks')) {
const taskStore = db.createObjectStore('tasks', { keyPath: 'id', autoIncrement: true });
// 创建索引以便按状态查询
taskStore.createIndex('status', 'status', { unique: false });
// 创建索引以便按优先级查询
taskStore.createIndex('priority', 'priority', { unique: false });
console.log('任务仓库创建成功');
}
});
console.log('数据库初始化成功');
return db;
} catch (error) {
console.error('数据库初始化失败:', error);
return null;
}
}
// 添加任务
async function addTask(db, taskData) {
try {
// 添加带时间戳的任务
const task = {
...taskData,
createdAt: new Date().toISOString(),
status: taskData.status || 'pending'
};
const id = await IndexedDBUtil.add(db, 'tasks', task);
console.log(`任务添加成功,ID: ${id}`);
return id;
} catch (error) {
console.error('添加任务失败:', error);
return null;
}
}
// 获取所有任务
async function getAllTasks(db) {
try {
const tasks = await IndexedDBUtil.getAll(db, 'tasks');
console.log(`获取了 ${tasks.length} 个任务`);
return tasks;
} catch (error) {
console.error('获取任务失败:', error);
return [];
}
}
// 按状态获取任务
async function getTasksByStatus(db, status) {
try {
const tasks = await IndexedDBUtil.getByIndex(db, 'tasks', 'status', status);
console.log(`获取了 ${tasks.length} 个${status}状态的任务`);
return tasks;
} catch (error) {
console.error('按状态获取任务失败:', error);
return [];
}
}
// 更新任务
async function updateTask(db, task) {
try {
await IndexedDBUtil.add(db, 'tasks', task, 'id');
console.log(`任务 ${task.id} 更新成功`);
return true;
} catch (error) {
console.error('更新任务失败:', error);
return false;
}
}
// 删除任务
async function deleteTask(db, taskId) {
try {
await IndexedDBUtil.delete(db, 'tasks', taskId);
console.log(`任务 ${taskId} 删除成功`);
return true;
} catch (error) {
console.error('删除任务失败:', error);
return false;
}
}
// 使用示例
async function todoAppExample() {
// 初始化数据库
const db = await initDatabase();
if (!db) return;
// 添加一些任务
const task1Id = await addTask(db, {
title: '完成前端存储文章',
description: '编写关于浏览器存储方案的博客文章',
priority: 'high'
});
await addTask(db, {
title: '购买杂货',
description: '购买本周所需的食品和日用品',
priority: 'medium'
});
await addTask(db, {
title: '练习弹吉他',
description: '练习新学的曲子',
priority: 'low',
status: 'completed'
});
// 获取所有任务
const allTasks = await getAllTasks(db);
console.log('所有任务:', allTasks);
// 获取完成的任务
const completedTasks = await getTasksByStatus(db, 'completed');
console.log('已完成任务:', completedTasks);
// 更新任务状态
if (task1Id) {
const task = await IndexedDBUtil.get(db, 'tasks', task1Id);
if (task) {
task.status = 'completed';
await updateTask(db, task);
}
}
// 关闭数据库连接
IndexedDBUtil.closeDB(db);
}
// 运行示例
todoAppExample();
存储方案详细对比
存储容量
存储方式 | 存储限制 | 备注 |
---|---|---|
localStorage | 通常为5-10 MB | 不同浏览器有所差异,永久存储 |
sessionStorage | 通常为5-10 MB | 与localStorage类似,但仅存在于会话期间 |
Cookies | 4 KB | 单个cookie通常限制4KB,每个域总数约为50个 |
IndexedDB | 通常为250MB-无限制 | 不同浏览器实现不同,但一般非常大 |
数据生命周期
存储方式 | 数据持久性 | 过期机制 |
---|---|---|
localStorage | 永久 | 除非手动清除,否则会一直保存 |
sessionStorage | 会话期间 | 关闭标签页后自动清除 |
Cookies | 可配置 | 可设置过期时间,默认为会话结束 |
IndexedDB | 永久 | 除非手动清除,否则会一直保存 |
数据传输
存储方式 | 是否自动发送到服务器 | 影响请求性能 |
---|---|---|
localStorage | 否 | 无 |
sessionStorage | 否 | 无 |
Cookies | 是,每次HTTP请求都会带上 | 是,增加请求头大小 |
IndexedDB | 否 | 无 |
操作方式
存储方式 | API类型 | 同步/异步 | 复杂度 |
---|---|---|---|
localStorage | 简单键值对 | 同步 | 低 |
sessionStorage | 简单键值对 | 同步 | 低 |
Cookies | 字符串解析 | 同步 | 中 |
IndexedDB | 对象仓库 | 异步 | 高 |
数据类型支持
存储方式 | 直接支持的数据类型 | 可通过序列化支持复杂类型 |
---|---|---|
localStorage | 字符串 | 是(JSON) |
sessionStorage | 字符串 | 是(JSON) |
Cookies | 字符串 | 有限制(由于大小限制) |
IndexedDB | 几乎所有JavaScript类型 | 是,原生支持对象 |
安全特性
存储方式 | 同源策略 | 可设置HttpOnly | 可设置Secure标志 |
---|---|---|---|
localStorage | 是 | 否 | 否 |
sessionStorage | 是(且仅限同标签页) | 否 | 否 |
Cookies | 是 | 是 | 是 |
IndexedDB | 是 | 否 | 否 |
选择正确的存储方案
何时使用 localStorage
- 存储用户偏好设置(如主题、字体大小等)
- 保存不敏感的应用状态
- 缓存不频繁更改的数据
- 需要在浏览器会话之间保持数据
- 存储量较小(<5MB)的数据
何时使用 sessionStorage
- 多步骤表单的临时数据存储
- 会话特定的用户设置
- 页面间导航时需保留的状态数据
- 临时缓存当前会话的查询结果
- 一次性使用的敏感数据(会话结束即清除)
何时使用 Cookies
- 需要服务端访问的数据(如认证令牌)
- 跟踪用户会话状态
- 记住用户登录状态
- 需要在HTTP请求中自动发送的数据
- 需要设置域、路径、过期时间等细粒度控制的场景
何时使用 IndexedDB
- 存储大量结构化数据(如客户端数据库)
- 离线应用需要持久化的数据
- 需要高性能查询和索引的数据
- 存储二进制文件或大型对象
- 需要事务支持的数据操作
最佳实践与注意事项
localStorage 和 sessionStorage 注意事项
- 不要存储敏感信息,因为这些数据以明文形式存储
- 存储前始终序列化对象,取出时解析
- 避免频繁写入大量数据,可能影响性能
- 考虑为数据添加元信息,如创建时间、版本号等
- 实现数据过期机制(尤其是localStorage)
// 为localStorage添加过期机制的例子
function setWithExpiry(key, value, ttl) {
const now = new Date();
const item = {
value: value,
expiry: now.getTime() + ttl,
};
localStorage.setItem(key, JSON.stringify(item));
}
function getWithExpiry(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) return null;
const item = JSON.parse(itemStr);
const now = new Date();
if (now.getTime() > item.expiry) {
localStorage.removeItem(key);
return null;
}
return item.value;
}
Cookies 最佳实践
- 总是设置合适的过期时间
- 使用HttpOnly标志保护敏感cookie(如身份验证相关)
- 使用Secure标志确保cookie只通过HTTPS发送
- 设置适当的SameSite属性(防止CSRF攻击)
- 最小化cookie数量和大小,减少网络负担
// 安全的cookie设置示例
document.cookie = "authToken=abc123; max-age=86400; path=/; secure; HttpOnly; SameSite=Strict";
IndexedDB 最佳实践
- 使用事务确保数据完整性
- 创建适当的索引以优化查询
- 处理版本升级场景
- 实现错误处理机制
- 考虑使用库简化操作(如Dexie.js、localForage)
// 使用事务确保数据完整性
function transferFunds(db, fromAccount, toAccount, amount) {
return new Promise((resolve, reject) => {
const tx = db.transaction(['accounts'], 'readwrite');
const store = tx.objectStore('accounts');
// 获取源账户
const fromRequest = store.get(fromAccount);
fromRequest.onsuccess = () => {
const fromData = fromRequest.result;
if (fromData.balance < amount) {
return reject(new Error('余额不足'));
}
// 更新源账户
fromData.balance -= amount;
store.put(fromData);
// 获取目标账户
const toRequest = store.get(toAccount);
toRequest.onsuccess = () => {
const toData = toRequest.result;
// 更新目标账户
toData.balance += amount;
store.put(toData);
};
};
// 事务完成
tx.oncomplete = () => {
resolve(true);
};
// 事务错误
tx.onerror = (event) => {
reject(event.target.error);
};
});
}
总结
浏览器提供了多种客户端存储方案,每种都有其独特的特性和适用场景:
-
localStorage:适用于需要长期保存的小到中等大小的数据,如用户偏好设置、主题选择等。API简单,但限于同步操作和字符串存储。
-
sessionStorage:与localStorage类似,但数据仅在会话期间有效,适合临时数据存储,如多步骤表单的中间状态。
-
Cookies:最古老的客户端存储机制,主要优势是可以随HTTP请求发送到服务器,适用于需要服务端访问的数据,如认证状态。但大小受限且会增加请求负担。
-
IndexedDB:功能最强大的客户端存储方案,支持大量结构化数据存储和高性能查询,适合离线应用和需要客户端数据库的场景。但API较复杂,学习曲线较陡。
在实际项目中,应根据具体需求选择合适的存储方案,有时甚至需要结合多种存储技术:
- 用户偏好和UI状态可以使用localStorage
- 表单临时数据可以使用sessionStorage
- 身份验证令牌可以使用安全的cookies
- 大型结构化数据可以使用IndexedDB
无论选择哪种存储方案,始终记住客户端存储的安全限制,避免存储敏感数据,并实现适当的过期策略。通过本文提供的封装工具类,可以更加方便、安全地使用这些存储技术,提升前端应用的用户体验和性能。