第 9 节:本地存储 --- 数据不丢的前端缓存
前面的章节我们陆续掌握了 DOM 操作、事件处理、表单校验,已经能够构建出功能完整的交互界面。但所有这些数据(学生列表、用户输入)都存在 JavaScript 的内存变量里------页面一刷新,一切归零 。要让数据在浏览器端持久化,我们需要学习浏览器提供的本地存储能力。这节的内容就像是给你的前端应用配了一个极简的"数据库",它的 API 只有几个方法,但足以解决绝大部分的轻量级持久化需求。
本节将解决三个核心问题:
- 如何把数据存到浏览器里,刷新后依然能读出来?
- 为什么存对象之前必须
JSON.stringify,取出来之后必须JSON.parse? - 如何封装一个
store.js,让存储操作安全、可维护,并处理容量上限和异常?
1. 两种浏览器存储:localStorage 与 sessionStorage
浏览器提供了两个极其简单的键值对存储对象,它们的 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.stringify 和 JSON.parse:
javascript
// 存:对象 → JSON 字符串
localStorage.setItem('students', JSON.stringify(list));
// 取:JSON 字符串 → 对象
const raw = localStorage.getItem('students');
const list = raw === null ? [] : JSON.parse(raw);
后端对照 :这就是你每天都在做的序列化 / 反序列化------Jackson 的
writeValueAsString和readValue,只不过换成了浏览器原生 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 | setItem、getItem、removeItem、clear,只能存字符串 |
| 对象序列化 | 存用 JSON.stringify,取用 JSON.parse,不可省略 |
| 生命周期 | 需要跨会话保留 → localStorage;仅当前标签页 → sessionStorage |
封装 store.js |
统一前缀、自动序列化、默认值、异常保护,对外暴露 save/load/remove |
| 容量与异常 | 上限约 5MB,必须 try-catch,切勿存放大文件 |
掌握了本地存储,你的前端应用就能拥有"记忆",刷新、关闭后数据依然留存。