深入浅出 HTML5 localStorage —— 构建持久化的本地 Tapas 列表

156 阅读12分钟

引言

在现代Web开发中,localStorage是HTML5引入的一个非常实用的功能,它允许我们在用户的浏览器中存储数据,并且这些数据即使在页面刷新或关闭浏览器后仍然存在。这对于构建需要保存用户偏好设置、缓存数据或实现简单的离线功能的Web应用来说非常有用。今天,我们将通过一个具体的例子——创建一个可以添加和持久化西班牙小吃(Tapas)项目的列表来深入了解localStorage的使用。


什么是 localStorage

localStorage 是一种Web存储机制,它为每个原点(即协议+域名+端口)提供了一定大小的空间(通常大约5MB),用来存储键值对形式的数据。与sessionStorage不同的是,localStorage中的数据没有过期时间,除非被手动清除或覆盖,否则将一直保留在用户的设备上。

以下是localStorage在浏览器中的位置以及页面展示刷新也不会改变localStorage中的值

image.png


使用 localStorage 构建 Tapas 列表

我们先来看看这个项目的基本结构:

<div class="wrapper">
    <h2>LOCAL TAPAS</h2>
    <ul class="plates">
        <li>Loading Taps...</li>
    </ul>
    <form class="add-items">
        <input type="text" name="item" placeholder="Item Name" required>
        <input type="submit" value="+ Add Item">
    </form>
</div>

这段代码定义了一个简单的界面,包括一个标题、一个用于显示Tapas项目的无序列表以及一个用于添加新项目的表单。

接下来,让我们看看如何利用localStorage来保存和加载这些Tapas项目:

const addItems = document.querySelector('.add-items'); // 获取表单元素
const itemsList = document.querySelector('.plates'); // 获取列表元素
let items = JSON.parse(localStorage.getItem('items')) || []; // 尝试从 localStorage 中获取已有的 Tapas 列表

// 添加新 Tapas 项目
function addItem(event) {
    event.preventDefault(); // 阻止表单默认提交行为
    
    const text = (this.querySelector('[name=item]')).value.trim(); // 获取用户输入的文本
    
    if (!text) return; // 如果输入为空,则不执行任何操作
    
    const item = { text, done: false }; // 创建一个新的 Tapas 项目对象
    
    items.push(item); // 将新项目添加到列表中
    populateList(items, itemsList); // 更新页面上的列表视图
    localStorage.setItem('items', JSON.stringify(items)); // 将更新后的列表保存回 localStorage
    this.reset(); // 重置表单以便于下一次输入
}

// 渲染 Tapas 列表
function populateList(plates, platesList) {
    platesList.innerHTML = plates.map((plate, i) => `
        <li>
            <input type="checkbox" data-index=${i} id="item${i}" ${plate.done ? 'checked' : ''}>
            <label for="item${i}">${plate.text}</label>
        </li>
    `).join(''); // 使用 map 方法遍历数组并生成列表项,最后用 join 方法连接成字符串设置为列表的内容
}

addItems.addEventListener('submit', addItem); // 给表单添加提交事件监听器
populateList(items, itemsList); // 页面加载完成后初始化列表视图

在这段JavaScript代码中,我们做了以下几件事:

  1. 读取已有数据:尝试从localStorage中读取名为items的数据。如果不存在这样的数据,则初始化为空数组。
  2. 处理表单提交:当用户提交表单时,阻止默认行为,收集用户输入的信息,创建一个新的Tapas项目,并将其添加到items数组中。
  3. 更新UI:调用populateList函数,根据当前的items数组动态地渲染列表。
  4. 保存数据:每次添加新的Tapas项目后,都将最新的items数组转换为JSON格式并保存到localStorage中,确保数据的持久性。

下面是代码的详细解释:

1.初始阶段

 const addItems = document.querySelector('.add-items'); // form
            const itemsList = document.querySelector('.plates'); // ul
            const items = JSON.parse(localStorage.getItem('items')) || [];

如果你是一个新手小白看到document应该会很懵逼,下面让我简要介绍一下document

document 是浏览器环境中全局对象 window 的一个属性,它表示当前网页的文档(Document)对象。document 对象是Web页面与JavaScript之间的接口,允许开发者访问和操作HTML或XML文档的内容、结构和样式。它是DOM(文档对象模型,Document Object Model)的核心部分,提供了对页面中所有元素的访问和控制能力。

document的主要功能

  • 访问和修改内容:通过document可以获取页面中的文本内容、属性值等,并且能够动态地添加、删除或更改HTML内容。
  • 查找元素:提供了一系列方法用于选择页面上的元素,如getElementById()getElementsByClassName()getElementsByTagName()querySelector()querySelectorAll()等。
  • 创建和操作节点:可以创建新的HTML元素(如createElement())、文本节点(如createTextNode()),还可以插入、移除或替换这些节点。
  • 事件处理:为页面元素添加事件监听器,使得可以在特定事件发生时执行JavaScript代码,比如点击按钮、提交表单等。
  • 样式操作:直接修改元素的CSS样式,或者通过类名来应用预定义的样式规则。

既然已经知道document的用法让我们逐行详细解释每一部分的作用。

1. const addItems = document.querySelector('.add-items'); // form

这行代码使用了document.querySelector()方法来选择页面中具有类名.add-items的第一个元素,并将其赋值给常量addItems。根据HTML结构,.add-items是一个表单元素(<form>),用于添加新的Tapas项目。querySelector()返回的是一个DOM元素对象,因此addItems现在引用的就是这个表单。通过这种方式获取到的DOM元素可以用来监听用户事件(如提交表单)或操作该元素的属性和样式。

2. const itemsList = document.querySelector('.plates'); // ul

同样的,这里也是使用document.querySelector()来选择具有类名.plates的第一个元素,并将它赋值给常量itemsList。在这个例子中,.plates对应的是一个无序列表(<ul>),它用于展示所有的Tapas项目。itemsList变量现在引用的就是这个列表元素,之后可以通过它来动态地添加、移除或更新列表项。

3. const items = JSON.parse(localStorage.getItem('items')) || [];

这一行稍微复杂一些,它涉及到了浏览器提供的localStorage API以及JavaScript的对象解析:

  • localStorage.getItem('items') :

    • 这个方法尝试从浏览器的localStorage中读取名为'items'的数据项。localStorage是一种持久化的存储机制,允许网页在用户的浏览器中保存数据,即使浏览器关闭后这些数据依然存在。
  • JSON.parse(...) :

    • localStorage只能存储字符串形式的数据,因此当我们想要存储复杂的数据结构(如数组或对象)时,通常需要先将它们转换成JSON格式(即字符串)。这里我们使用JSON.parse()方法来解析从localStorage中取出的字符串,将其转换回原始的JavaScript对象或数组。
  • || [] :

    • 这是一个简写的逻辑运算符表达式。如果JSON.parse(localStorage.getItem('items'))的结果是nullundefined(即没有找到对应的存储项),那么整个表达式的值就会是右边的空数组[]。这样做是为了确保items始终是一个数组,即使之前从未向localStorage中添加过任何数据。

总结来说,这三行代码完成了以下任务:

  1. 获取页面上的表单元素并将其存储在addItems变量中,以便稍后处理用户输入。
  2. 获取用于显示Tapas项目的无序列表元素,并将其存储在itemsList变量中,方便后续对列表内容的操作。
  3. 尝试从localStorage中读取已有的Tapas项目数据,并将其解析为一个JavaScript数组;如果没有找到相关数据,则初始化为空数组。这个数组被存储在items变量中,作为应用程序内部的数据源。

通过这样的初始化,我们可以确保应用启动时能够正确加载已有数据,并准备好接收用户的新输入。


2.populateList 函数

function populateList(plates, platesList) {
    // 更新列表的 HTML 内容
    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: 这是一个数组,其中包含了多个对象,每个对象代表一个Tapas项目。每个对象通常至少包含两个属性:

    • text: Tapas项目的名称或描述。
    • done: 一个布尔值,表示该项目是否已完成(例如,用户是否已经勾选了对应的复选框)。
  • platesList: 这是一个DOM元素引用,指向页面上的一个无序列表(<ul>元素),用于展示所有的Tapas项目。

主要步骤

  1. 遍历 plates 数组

     

    使用 Array.prototype.map() 方法来遍历 plates 数组。对于数组中的每一个项目(即每个 plate),都会执行一次提供的回调函数。这个回调函数接收两个参数:当前项 (plate) 和它的索引 (i)。

  2. 创建 HTML 字符串

     

    map 的回调函数内部,我们构建了一个模板字符串(使用反引号 ` 包围),它定义了单个 <li> 元素的HTML结构。这里有几个关键点:

    • 模板字符串: 模板字符串允许我们在多行上编写字符串,并且可以方便地嵌入表达式。在本例中,我们使用 ${} 语法将变量和表达式的值插入到字符串中。

    • 复选框 (<input type="checkbox">) :

      • type="checkbox": 定义这是一个复选框输入。
      • data-index="${i}": 设置自定义属性 data-index,存储该Tapas项目在 plates 数组中的位置。这有助于后续处理,比如更新特定项目的完成状态。
      • id="item${i}": 给每个复选框分配一个唯一的ID,以便与标签关联。
      • ${plate.done ? 'checked' : ''}: 如果 plate.done 为真,则添加 checked 属性,使复选框默认被选中;否则不添加此属性。
    • 标签 (<label>)

      • for="item${i}"for 属性应该与相关联的复选框的 id 相匹配,这样点击标签也会触发复选框的状态变化。
      • ${plate.text}: 显示Tapas项目的名称或描述。
  3. 拼接成完整的 HTML 字符串

     

    map 方法返回的是一个新的数组,其中每个元素都是一个 <li> 元素的HTML字符串。接下来,我们调用 .join('') 方法将这些字符串连接起来,形成一个单一的、连续的HTML字符串。这样做是为了确保最终的结果没有多余的分隔符。

  4. 更新 DOM

     

    最后一步是将生成的HTML字符串赋值给 platesList.innerHTML。这会直接替换掉 platesList 元素内的所有现有内容。浏览器会解析这段HTML字符串,并将其转化为实际的DOM节点,从而更新页面上的Tapas列表。

实际效果

populateList 被调用时,它会根据 plates 数组的内容动态生成一系列 <li> 元素,并将它们添加到 platesList 中。如果 plates 数组为空,那么 platesList 将不会有任何子元素。反之,如果有多个项目,那么每个项目都会对应一个 <li> 元素,包含一个复选框和一个标签,显示项目的名称或描述。

这种方法不仅简化了DOM操作,还使得代码更加简洁易读,同时也提高了性能,因为一次性更新整个列表比逐个添加元素要高效得多。


3.addItem 函数

 function addItem(event) {
                event.preventDefault();
                //console.log(this, event.target);
                // 性能
                const text = (this.querySelector('[name=item]')).value.trim();
                // 数据
                const item = {
                    text, // es6 简写  等于 text:text
                    done: false
                }

                items.push(item);
                populateList(items, itemsList)
                localStorage.setItem('items', JSON.stringify(items))// 存储数据 5MB左右的空间
                this.reset();
            }

在解释这段代码前让我简单介绍event

在JavaScript中,event 参数是事件对象的一个实例,它包含了关于触发事件的详细信息。当某个事件(如点击、提交表单、按键等)被触发时,浏览器会自动创建这个对象,并将其传递给事件处理函数。通过访问 event 对象的属性和方法,开发者可以获取更多关于事件的信息并控制其行为。

事件对象 (event) 的主要用途

  1. 阻止默认行为:使用 event.preventDefault() 方法可以阻止元素的默认行为。
  2. 停止事件传播:使用 event.stopPropagation() 或 event.stopImmediatePropagation() 方法可以阻止事件冒泡或捕获阶段继续传播。
  3. 获取触发事件的目标元素:通过 event.target 属性可以找到实际触发事件的DOM元素。
  4. 了解事件类型event.type 属性返回触发事件的类型(如 "click""submit" 等)。
  5. 获取键盘或鼠标事件的细节:对于键盘事件(如 keydown),可以使用 event.key 或 event.code 获取按下的键;对于鼠标事件(如 click),可以使用 event.clientX 和 event.clientY 获取鼠标位置。

详细分解

1. event.preventDefault();

  • 作用:阻止表单的默认提交行为(即页面刷新)。当用户点击提交按钮时,默认情况下浏览器会尝试将表单数据发送到服务器,并且通常会导致页面重新加载。我们不希望这种情况发生,因为我们希望通过JavaScript动态更新页面内容。
  • 为什么重要:如果不调用 preventDefault(),每次用户提交表单后,页面都会重新加载,导致所有之前的状态丢失。通过防止默认行为,我们可以保持当前页面状态不变,同时执行自定义的JavaScript逻辑。

2. console.log(this, event.target);

  • 调试信息:打印当前函数上下文 (this) 和触发事件的目标元素 (event.target)。这有助于开发者了解函数是在什么环境下被执行的,以及哪个具体元素触发了该事件。
  • this 的含义:在事件处理程序中,this 指向的是绑定事件监听器的元素,在这里是表单 (<form> 元素)。
  • event.target 的含义:指向实际触发事件的元素。在表单提交的情况下,通常是提交按钮或回车键触发的事件。

3. const text = (this.querySelector('[name=item]')).value.trim();

  • 获取用户输入:使用 querySelector 方法找到表单中的 [name=item] 输入框,并读取其值。trim() 方法用于移除字符串两端的空白字符,以确保不会因为多余的空格而导致问题。
  • querySelector 方法:这是一个强大的选择器方法,可以用来查找匹配指定CSS选择器的第一个元素。这里我们使用它来定位表单内的特定输入字段。
  • value 属性:获取输入框当前的值,即用户输入的文本。
  • trim() 方法:移除字符串两端的空白字符,保证输入的整洁性。

4. const item = { text, done: false };

  • 创建新项目对象:这里我们创建了一个包含两个属性的对象:

    • text: 用户输入的内容。
    • done: 布尔值,表示该项目是否已完成,默认为 false
  • ES6 简写语法text 等同于 text: text,这是ES6引入的一种简化形式,使得代码更加简洁易读。

5. items.push(item);

  • 添加新项目:将新创建的 item 对象推入 items 数组中。这意味着我们的本地数据存储已经包含了这个新的Tapas项目。
  • push 方法:数组的一个方法,用于向数组末尾添加一个或多个元素。

6. populateList(items, itemsList);

  • 更新UI:调用 populateList 函数来根据最新的 items 数组生成HTML,并更新页面上的无序列表 (<ul>)。这一步确保了用户界面与内部数据保持同步。

  • 参数说明

    • items: 包含所有Tapas项目的数组。
    • itemsList: 页面上用于展示这些项目的 <ul> 元素。

7. localStorage.setItem('items', JSON.stringify(items));

  • 持久化存储:将更新后的 items 数组转换成JSON格式的字符串,并保存到浏览器的 localStorage 中。这样即使用户关闭或刷新页面,数据也不会丢失。
  • JSON.stringify 方法:将JavaScript对象或数组转换为JSON字符串,以便可以存储在 localStorage 中,因为 localStorage 只能存储字符串类型的数据。
  • localStorage.setItem 方法:用于设置 localStorage 中的键值对。第一个参数是要存储的数据项的名称(键),第二个参数是要存储的数据(值)。

8. this.reset();

  • 重置表单:调用 reset 方法清除表单中的所有字段,使用户能够方便地继续添加新的Tapas项目,而无需手动删除之前的输入。
  • reset 方法:表单元素的一个方法,用于将表单恢复到初始状态,即将所有输入字段清空。

实际效果

每当用户填写表单并点击提交按钮时,addItem 函数会被触发:

  1. 阻止默认的表单提交行为,防止页面刷新。
  2. 打印调试信息,帮助开发者理解当前的执行环境。
  3. 获取用户输入的内容,去除多余空格。
  4. 创建一个新的Tapas项目对象,包含用户输入和完成状态。
  5. 将新项目添加到 items 数组中,更新内部数据结构。
  6. 调用 populateList 函数,根据最新的 items 数组更新页面上的列表显示。
  7. 将更新后的 items 数组保存到 localStorage 中,实现数据的持久化。
  8. 清空表单,准备接收下一个用户的输入。

通过这种方式,addItem 函数不仅实现了从收集用户输入到更新内部数据结构,再到持久化存储和用户界面刷新的完整流程,还确保了良好的用户体验和数据管理能力。


4.函数调用

addItems.addEventListener('submit', addItem)
populateList(items, itemsList)

1. addItems.addEventListener('submit', addItem)

这行代码为表单元素 (<form>) 添加了一个事件监听器,用于监听“提交”(submit)事件,并指定当该事件触发时应执行的函数——addItem

解析

  • addEventListener 方法

    • 这是一个DOM元素的方法,用于注册一个或多个事件类型上的事件处理器。
  • 参数解释

    • 第一个参数 'submit':表示要监听的事件类型。在这个例子中,我们监听的是表单的提交事件。
    • 第二个参数 addItem:这是当事件发生时应该调用的回调函数。每当用户点击表单中的提交按钮或者按下回车键时,addItem 函数就会被调用。

实际效果

当你在浏览器中运行这段代码时:

  • 每当用户填写表单并尝试提交时(例如通过点击提交按钮或按下回车键),addItem 函数会被触发。
  • addItem 函数负责收集用户输入的数据、创建新的Tapas项目对象、更新内部数据存储,并确保页面上的列表得到相应的刷新。
  • 由于我们在 addItem 函数中使用了 event.preventDefault(),所以表单不会按照默认行为提交,而是通过JavaScript动态地更新页面内容。

2. populateList(items, itemsList)

这行代码调用了 populateList 函数,目的是在页面初次加载时根据已经存在的 items 数据来渲染 Tapas 列表。

解析

  • populateList 函数

    • 这个函数负责根据传入的数据生成HTML,并将这些HTML插入到指定的DOM元素中。它接受两个参数:

      • plates:这是一个数组,包含了所有需要显示的 Tapas 项目。
      • platesList:这是一个 DOM 元素引用,指向页面上的一个无序列表(<ul> 元素),用于展示所有的 Tapas 项目。
  • 参数传递

    • items:这是从 localStorage 中读取的 Tapas 项目数组,或者是如果 localStorage 中没有数据时的空数组。
    • itemsList:这是之前通过 document.querySelector('.plates') 获取到的 <ul> 元素。

实际效果

当你在浏览器中运行这段代码时:

  • 页面加载完成后,populateList(items, itemsList) 立即被调用。
  • 它会根据 items 数组中的数据动态生成一系列 <li> 元素,并将它们添加到 itemsList 中。
  • 如果 items 数组中有任何项目,那么这些项目将会立即显示在页面上;如果没有,则列表为空。
  • 这一步确保了即使用户刷新页面,之前保存的 Tapas 项目仍然会显示出来,提供了良好的用户体验。

结语

通过上述代码解析,我们深入了解了如何利用JavaScript、HTML和localStorage构建一个交互式的Tapas项目管理器。从监听表单提交事件到动态更新页面内容,再到确保数据的持久化存储,每一个步骤都体现了现代Web开发中的最佳实践。addItems.addEventListener('submit', addItem)populateList(items, itemsList) 这两行代码虽然简洁,却承载着实现用户交互与数据展示的核心逻辑。

希望这篇文章能够帮助你掌握这些关键技术点,并启发你在自己的项目中应用类似的模式。无论是创建待办事项列表、购物车功能,还是其他需要保存用户输入的应用场景,这些方法都能为你提供坚实的基础。

如果你在学习过程中遇到了挑战或有任何疑问,请不要犹豫,随时回来查阅本文或继续探索更多资源。编程是一个不断学习和实践的过程,每一次尝试都是进步的机会。感谢你的阅读,期待你在未来的开发旅程中创造出更多精彩的作品!