引言

在前端开发中,数据存储是一个常见需求。无论是用户偏好设置、表单数据暂存、还是应用状态维护,我们都需要在客户端保存一定的数据。浏览器提供了多种存储方案,每种都有其独特的特性和适用场景。本文将全面对比四种主要的浏览器存储技术: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);
    };
  });
}

总结

浏览器提供了多种客户端存储方案,每种都有其独特的特性和适用场景:

  1. localStorage:适用于需要长期保存的小到中等大小的数据,如用户偏好设置、主题选择等。API简单,但限于同步操作和字符串存储。

  2. sessionStorage:与localStorage类似,但数据仅在会话期间有效,适合临时数据存储,如多步骤表单的中间状态。

  3. Cookies:最古老的客户端存储机制,主要优势是可以随HTTP请求发送到服务器,适用于需要服务端访问的数据,如认证状态。但大小受限且会增加请求负担。

  4. IndexedDB:功能最强大的客户端存储方案,支持大量结构化数据存储和高性能查询,适合离线应用和需要客户端数据库的场景。但API较复杂,学习曲线较陡。

在实际项目中,应根据具体需求选择合适的存储方案,有时甚至需要结合多种存储技术:

  • 用户偏好和UI状态可以使用localStorage
  • 表单临时数据可以使用sessionStorage
  • 身份验证令牌可以使用安全的cookies
  • 大型结构化数据可以使用IndexedDB

无论选择哪种存储方案,始终记住客户端存储的安全限制,避免存储敏感数据,并实现适当的过期策略。通过本文提供的封装工具类,可以更加方便、安全地使用这些存储技术,提升前端应用的用户体验和性能。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐