第 9 节:本地存储 — 数据不丢的前端缓存

第 9 节:本地存储 --- 数据不丢的前端缓存

前面的章节我们陆续掌握了 DOM 操作、事件处理、表单校验,已经能够构建出功能完整的交互界面。但所有这些数据(学生列表、用户输入)都存在 JavaScript 的内存变量里------页面一刷新,一切归零 。要让数据在浏览器端持久化,我们需要学习浏览器提供的本地存储能力。这节的内容就像是给你的前端应用配了一个极简的"数据库",它的 API 只有几个方法,但足以解决绝大部分的轻量级持久化需求。

本节将解决三个核心问题

  1. 如何把数据存到浏览器里,刷新后依然能读出来?
  2. 为什么存对象之前必须 JSON.stringify,取出来之后必须 JSON.parse
  3. 如何封装一个 store.js,让存储操作安全、可维护,并处理容量上限和异常?

1. 两种浏览器存储:localStoragesessionStorage

浏览器提供了两个极其简单的键值对存储对象,它们的 API 完全相同,唯一的区别在于数据的生命周期

1.1 基本 CRUD

javascript 复制代码
// 存一条数据(键值对)
localStorage.setItem('username', '张三');

// 读取
localStorage.getItem('username');   // "张三"

// 删除一条
localStorage.removeItem('username');

// 清空当前域名下所有数据
localStorage.clear();

这两个对象都是按域名隔离 的,不同域名的数据互不相通,并且只能存储字符串

1.2 localStorage vs sessionStorage

特性 localStorage sessionStorage
生命周期 永久,直到被主动删除 仅在当前标签页会话期间存在
页面刷新 数据保留 数据保留
关闭标签页/浏览器 数据保留 数据立即清空
跨标签页共享 同域名下共享 不共享(每个标签页独立)
典型用途 记住登录状态、用户偏好、缓存列表 表单草稿、临时跳转参数

选择策略 :问自己"关掉这个标签页后,这条数据还应该存在吗?"应该 → localStorage;不该 → sessionStorage


2. 核心痛点:只能存字符串 ------ JSON.stringify / JSON.parse

所有存入 localStorage 的值都会被隐式转换为字符串,如果你直接存放对象或数组,得到的将是毫无用处的 "[object Object]"

javascript 复制代码
const student = { name: '张三', score: 92 };
localStorage.setItem('stu', student);
localStorage.getItem('stu');  // "[object Object]"  ❌ 完全无法使用

解决方法是手动序列化与反序列化 ,使用 JSON.stringifyJSON.parse

javascript 复制代码
// 存:对象 → JSON 字符串
localStorage.setItem('students', JSON.stringify(list));

// 取:JSON 字符串 → 对象
const raw = localStorage.getItem('students');
const list = raw === null ? [] : JSON.parse(raw);

后端对照 :这就是你每天都在做的序列化 / 反序列化------Jackson 的 writeValueAsStringreadValue,只不过换成了浏览器原生 API。


3. 封装 store.js:打造自己的前端数据仓库

直接裸写 localStorage 会带来三个问题:键名容易冲突、序列化逻辑散落各处、没有任何异常保护。最佳实践是将其封装成一个类似 DAO 的工具模块。

3.1 设计目标

  • 统一添加域名前缀,避免键名冲突。
  • 自动处理 JSON.stringify / parse,外部无需关心。
  • 读取不存在的数据时,返回默认值 而不是 null
  • 内部使用 try-catch 兜底,避免存储失败时整个页面崩溃。

3.2 核心实现

javascript 复制代码
(function(global) {
  'use strict';
  const PREFIX = 'app_';   // 自定义前缀

  // 拼装完整键名
  function fullKey(key) {
    return PREFIX + key;
  }

  // 保存:支持任意类型,失败返回 false
  function save(key, value) {
    try {
      localStorage.setItem(fullKey(key), JSON.stringify(value));
      return true;
    } catch (e) {
      console.error('[store] 保存失败', key, e);
      return false;
    }
  }

  // 读取:自动反序列化,不存在时返回默认值
  function load(key, defaultValue) {
    try {
      const raw = localStorage.getItem(fullKey(key));
      if (raw === null) return defaultValue;
      return JSON.parse(raw);
    } catch (e) {
      console.error('[store] 读取失败', key, e);
      return defaultValue;
    }
  }

  // 删除
  function remove(key) {
    try {
      localStorage.removeItem(fullKey(key));
      return true;
    } catch (e) {
      console.error('[store] 删除失败', key, e);
      return false;
    }
  }

  global.Store = { save, load, remove };
})(window);

使用示例:

javascript 复制代码
// 保存学生数组
Store.save('students', students);

// 读取(若不存在则返回空数组)
const list = Store.load('students', []);

// 删除
Store.remove('students');

4. 容量限制与异常场景

localStorage 的存储上限通常在 5MB 左右(因浏览器而异)。一旦超出配额,setItem 会抛出 QuotaExceededError;此外,在某些隐私模式下浏览器可能直接禁用存储,也会导致写入失败。

这些异常是你无法控制的,因此所有存储操作都应该被 try-catch 包裹 ,这正是 store.js 中每个方法都内置异常捕获的原因。当 save 返回 false 时,你可以友好地提醒用户清理缓存,而不是让整个页面业务逻辑中断。

核心原则:本地存储只适合存放轻量级数据(JSON 配置、小型列表),不要用它缓存大文件或图片的 base64 数据。


5. 综合实战:学生列表持久化

结合前几节的知识,我们可以构建一个完整的学生管理界面:页面刷新后,之前新增或删除的记录依然存在。

设计思路

  • 使用一个内存数组 students 作为当前数据源。
  • 页面加载时,通过 Store.load('students', DEFAULT_LIST) 初始化。
  • 每次对数组进行增、删、改后,立即调用 Store.save 将全量数据写回存储
  • 渲染函数负责根据数组生成 DOM(第6节),事件委托处理删除(第7节),表单校验保证输入合法(第8节)。

核心代码示例

javascript 复制代码
const DEFAULT_LIST = [
  { id: 'S001', name: '张三', score: 92 },
  { id: 'S002', name: '李四', score: 78 },
];

let students = Store.load('students', DEFAULT_LIST);

// 渲染表格(使用第6节的 createElement + textContent 模式)
function render() {
  // ...清空 tbody,遍历 students 生成 tr
}

// 新增学生(表单 submit 事件,已调用 e.preventDefault)
function onAdd(e) {
  e.preventDefault();
  // 从表单取值、校验(第8节)
  students.push({ id: newId, name: newName, score: 0 });
  Store.save('students', students);
  render();
}

// 删除学生(事件委托在 tbody,结合 closest 和 dataset)
tbody.addEventListener('click', (e) => {
  const delBtn = e.target.closest('[data-action="delete"]');
  if (!delBtn) return;
  const id = delBtn.closest('tr').dataset.id;
  students = students.filter(s => s.id !== id);   // 第4节 filter
  Store.save('students', students);
  render();
});

// 页面启动时首次渲染
render();

关键思想 :前端的数据持久化遵循"读-改-写 "模式------load(读)→ 修改内存数组 → save(写回)→ 重新渲染 UI。这和后端 JPA 的"查询实体 → 修改属性 → repository.save()"逻辑完全同构。


6. 小结

核心知识点 一句话解释
存储 API setItemgetItemremoveItemclear,只能存字符串
对象序列化 存用 JSON.stringify,取用 JSON.parse,不可省略
生命周期 需要跨会话保留 → localStorage;仅当前标签页 → sessionStorage
封装 store.js 统一前缀、自动序列化、默认值、异常保护,对外暴露 save/load/remove
容量与异常 上限约 5MB,必须 try-catch,切勿存放大文件

掌握了本地存储,你的前端应用就能拥有"记忆",刷新、关闭后数据依然留存。

相关推荐
泯泷14 小时前
那段文字是谁删的?Yjs 14 正式版之前,一套删除归属方案的实现与边界
前端·javascript·算法
hold?fish:palm14 小时前
34 合并K个升序链表
javascript·算法·链表
HanhahnaH15 小时前
Redis单线程和Tair多线程架构设计对比
分布式·缓存
野槐16 小时前
前端项目优化(有了我,会发生...)
前端
幸运小圣17 小时前
PointerEvent 常用属性与事件整理【JavaScript】
开发语言·javascript·ecmascript
aa小小17 小时前
【无标题】
前端
计算机魔术师18 小时前
还在单线程跟 AI 聊天?Claude Code 并行多会话让你一个人干三个人的活
前端
志尊宝18 小时前
Vue3 零基础每日笔记(038):亲手封装 useDebounceFn 与 useThrottleFn——高频事件的流量阀
前端·javascript·vue·html·vue3
JavaGuide18 小时前
对标 MinIO!全新一代分布式文件系统正式发布
前端·后端
风骏时光牛马18 小时前
AI驱动自动化业务工作流搭建实践
前端