在现代 Web 开发中,前端开发者常常需要在浏览器中持久化存储一些用户数据。比如用户的偏好设置、表单草稿、或者像本文要实现的------一个简单的待办事项(To-Do List)应用。HTML5 提供了一个非常实用的 API:localStorage,它允许我们在用户的浏览器中以键值对的形式永久保存数据。
本文将通过一个完整的示例,带你了解如何使用 localStorage 实现一个可持久化存储的 To-Do 列表,并结合 JavaScript 的函数式编程思想和事件委托机制,写出结构清晰、易于维护的代码。
什么是 localStorage?
localStorage 是 Web Storage API 的一部分,用于在浏览器中长期存储数据。它的特点包括:
- 持久性:除非用户手动清除,否则数据不会过期。
- 键值对存储:只能存储字符串类型的数据(key 和 value 都是字符串)。
- 同源策略 :只有同协议、同域名、同端口的页面才能共享同一个
localStorage。
由于 localStorage 只能存字符串,因此我们通常会配合 JSON.stringify() 和 JSON.parse() 来存储和读取对象或数组。
项目结构概览
我们的目标是实现一个简单的"Tapas"(小食)清单,用户可以:
- 添加新的 Tapas 项;
- 勾选已完成的项;
- 页面刷新后数据依然保留。
HTML 结构如下:
javascript
<div class="wrapper">
<h2>LOCAL TAPAS</h2>
<ul class="plates">
<li>Loading Tapas...</li>
</ul>
<form class="add-items">
<input type="text" placeholder="Item Name" required name="item">
<input type="submit" value="+ Add Item">
</form>
</div>
核心逻辑集中在 <script> 标签中,下面我们逐步拆解。
初始化数据与渲染列表
首先,我们从 localStorage 中读取已有的待办事项:
ini
const items = JSON.parse(localStorage.getItem('todos')) || [];
如果 localStorage 中没有 'todos' 这个 key,就返回一个空数组作为默认值。
接着,我们封装一个 populateList 函数,用于将数据渲染到页面上:
ini
function populateList(plates = [], platesList) {
platesList.innerHTML = plates.map((plate, i) => {
return `
<li>
<input type="checkbox" data-index=${i} id="item${i}" ${plate.done ? 'checked' : ''} />
<label for="item${i}">${plate.text}</label>
</li>
`;
}).join('');
}
这个函数接收两个参数:
plates:待办事项数组;platesList:要渲染到的 DOM 元素(即<ul class="plates">)。
通过 map 方法生成每个列表项的 HTML 字符串,并用 join('') 拼接成完整内容,最后赋值给 innerHTML。这种方式避免了频繁操作 DOM,性能更优。
调用一次即可完成初始渲染:
scss
populateList(items, itemsList);
添加新事项
当用户在表单中输入内容并点击"+ Add Item"时,会触发 submit 事件。我们通过事件监听器处理:
arduino
addItems.addEventListener('submit', addItem);
addItem 函数的核心逻辑如下:
javascript
function addItem(e) {
e.preventDefault(); // 阻止表单默认提交(页面跳转)
const text = this.querySelector('[name=item]').value.trim();
//通过属性选择器 拿到表单中name为item的input中的值 并去除左右空格 这里其实也运用到包装类的概念
//因为拿到的value是字符串 简单数据类型 但是可以调用方法.trim()
const item = {
text,
done: false
//通过done来控制勾选框的选中 一开始设置为false
};
items.push(item);//把输入的item添加到items
localStorage.setItem('todos', JSON.stringify(items)); // 持久化存储
populateList(items, itemsList); // 重新渲染
this.reset(); // 清空表单内容 优化用户体验
}
这里有几个关键点:
- 使用
e.preventDefault()防止页面刷新; - 通过
this(指向表单元素)获取输入框的值; - 构造新对象
{ text, done: false }并加入数组; - 调用
localStorage.setItem()将整个数组转为字符串后保存; - 重新渲染列表并清空表单。
勾选/取消勾选事项(事件委托)
如果为每个复选框单独绑定事件,效率低下且难以维护。因此我们采用事件委托 :将点击事件绑定在父容器 <ul class="plates"> 上,通过判断点击目标是否为 input 来决定是否处理。
ini
itemsList.addEventListener('click', toggleDone);
function toggleDone(e) {
const el = e.target;
//指定 当点击的为input时 才进行下面操作
if (el === 'INPUT') {
const index = el.dataset.index;
items[index].done = !items[index].done;
//通过done来控制勾选 点击取反
localStorage.setItem('todos', JSON.stringify(items));
//点击后修改了item中的done值 再次进行本地存储
populateList(items, itemsList);//再次渲染
}
}
每次切换状态后,我们同样更新 localStorage 并重新渲染整个列表,确保视图与数据一致。
为什么使用函数式封装?
文中提到:"拒绝流程式代码,超过10行就封装函数"。这其实是一种良好的编程习惯:
- 提高可读性 :
populateList、addItem等函数名清晰表达了意图; - 便于复用:渲染逻辑被抽离,可在多处调用;
- 降低耦合:数据操作与 DOM 操作分离,逻辑更清晰;
- 易于测试与调试:每个函数职责单一。
此外,默认参数(如 plates = [])和解构赋值(如 { text, done })也体现了 ES6 的简洁语法优势。
总结
通过这个小小的"Local Tapas"项目,我们不仅实现了基本的增删改查(CRUD)功能,更重要的是掌握了以下核心知识点:
localStorage的基本用法 :存储字符串、配合JSON处理复杂数据;- 事件委托:高效处理动态生成元素的交互;
- 函数式编程思想:封装细节、关注输入输出;
- DOM 操作优化:批量更新而非逐个操作。
虽然这个应用很简单,但它展示了现代前端开发中"数据驱动视图"的典型模式:数据变化 → 更新存储 → 重新渲染。这种模式也是 React、Vue 等框架的思想基础。
你可以在此基础上继续扩展,比如:
- 添加删除按钮;
- 支持编辑事项;
- 按完成状态过滤;
- 加入动画效果等。
但无论如何,记住:好的代码始于清晰的结构和合理的抽象 。而 localStorage,正是你构建离线优先(Offline-First)应用的第一步。
本文代码已通过实际测试,可直接运行。欢迎在评论区交流你的改进想法!