阅读视图

发现新文章,点击刷新页面。

使用 LocalStorage 实现本地待办事项(To-Do)列表

在现代 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 结构如下:

<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 中读取已有的待办事项:

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

如果 localStorage 中没有 'todos' 这个 key,就返回一个空数组作为默认值。

接着,我们封装一个 populateList 函数,用于将数据渲染到页面上:

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,性能更优。

调用一次即可完成初始渲染:

populateList(items, itemsList);

添加新事项

当用户在表单中输入内容并点击“+ Add Item”时,会触发 submit 事件。我们通过事件监听器处理:

addItems.addEventListener('submit', addItem);

addItem 函数的核心逻辑如下:

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 来决定是否处理。

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行就封装函数”。这其实是一种良好的编程习惯:

  • 提高可读性populateListaddItem 等函数名清晰表达了意图;
  • 便于复用:渲染逻辑被抽离,可在多处调用;
  • 降低耦合:数据操作与 DOM 操作分离,逻辑更清晰;
  • 易于测试与调试:每个函数职责单一。

此外,默认参数(如 plates = [])和解构赋值(如 { text, done })也体现了 ES6 的简洁语法优势。


总结

通过这个小小的“Local Tapas”项目,我们不仅实现了基本的增删改查(CRUD)功能,更重要的是掌握了以下核心知识点:

  1. localStorage 的基本用法:存储字符串、配合 JSON 处理复杂数据;
  2. 事件委托:高效处理动态生成元素的交互;
  3. 函数式编程思想:封装细节、关注输入输出;
  4. DOM 操作优化:批量更新而非逐个操作。

虽然这个应用很简单,但它展示了现代前端开发中“数据驱动视图”的典型模式:数据变化 → 更新存储 → 重新渲染。这种模式也是 React、Vue 等框架的思想基础。

你可以在此基础上继续扩展,比如:

  • 添加删除按钮;
  • 支持编辑事项;
  • 按完成状态过滤;
  • 加入动画效果等。

但无论如何,记住:好的代码始于清晰的结构和合理的抽象。而 localStorage,正是你构建离线优先(Offline-First)应用的第一步。


本文代码已通过实际测试,可直接运行。欢迎在评论区交流你的改进想法!

前端开发者也能玩转大模型:使用HTTP请求调用DeepSeek全记录

无需后端支持,纯前端技术栈也能集成人工智能大模型

作为一名前端开发者,我最近探索了一个有趣的技术方案:如何在前端项目中直接调用大型语言模型。在这个过程中,我发现了不少值得分享的经验和技巧,今天就带大家一步步实现前端调用DeepSeek大模型的全过程。

项目背景与初衷

传统上,调用AI大模型通常需要后端服务的支持,前端通过API与自己的服务器交互,再由服务器调用AI服务。这样做主要是出于安全考虑,特别是为了保护API密钥。但有时候,我们只是想快速原型验证,或者构建简单的个人项目,这时候如果能直接从前端调用大模型,会大大简化开发流程。

我决定尝试使用纯前端技术栈调用DeepSeek大模型,并记录下整个过程。

项目初始化:从零搭建

为了简化开发流程,我使用了 Trae(一种 AI 辅助开发工具)帮我快速初始化了一个基于 Vite 的前端项目。V

选择构建工具

我选择了Vite作为项目构建工具,它不仅速度快,而且内置了丰富的功能,特别是对环境变量的支持,这对保护API密钥至关重要。

npm create vite@latest frontend-llm-demo
cd frontend-llm-demo
npm install

项目结构设计

保持简洁的项目结构:

frontend-llm-demo/
├── index.html
├── styles/
│   └── style.css
├── scripts/
│   └── app.js
└── .env.local

其中,index.html 是主页面,app.js 负责发起 API 请求,而 .env.local 将用于存放敏感的 API Key。

核心实现:HTTP请求调用LLM

构建API请求

app.js中,我实现了完整的API调用逻辑:

// DeepSeek API端点
const endpoint = 'https://api.deepseek.com/chat/completions'

// 设置请求头
const headers = {
  'Content-Type': 'application/json',
  'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`
}

// 构建请求体
const payload = {
  model: 'deepseek-chat',
  messages: [
    {
      role: 'system',
      content: 'You are a helpful assistant.'
    },
    {
      role: 'user',
      content: '你好 DeepSeek'
    }
  ]
}

// 发送请求并处理响应
const response = await fetch(endpoint, {
  method: 'POST',
  headers: headers,
  body: JSON.stringify(payload)
})

const data = await response.json()
console.log(data)

// 将模型回复动态挂载到页面
document.getElementById('reply').textContent = data.choices[0].message.content

关键技术点解析

1. 请求方法选择

我使用了POST而非GET方法,原因有二:

  • POST请求更安全,参数不会暴露在URL中
  • 我们需要传递请求体,GET请求通常不包含请求体 而 LLM 调用需要传递复杂的 messages 结构,必须使用 POST

2. 请求头设置

请求头包含了两个关键信息:

  • Content-Type: application/json - 告诉服务器我们发送的是JSON格式数据
  • Authorization: Bearer ... - 用于身份验证的API密钥

3. 请求体构建

请求体是一个JSON对象,包含:

  • model - 指定要使用的模型版本
  • messages - 对话消息数组,包含角色和内容

4. 数据序列化

使用JSON.stringify()将JavaScript对象转换为JSON字符串,因为HTTP协议只能传输文本或二进制数据,不能直接传输对象。

安全考虑:保护API密钥

环境变量的重要性

直接将API密钥硬编码在前端代码中是极其危险的,任何用户都可以查看源代码获取密钥。为了解决这个问题,我使用了环境变量。

Vite环境变量配置

Vite使用特殊的import.meta.env对象来访问环境变量。以VITE_开头的变量会被嵌入到客户端代码中。

  1. 创建.env文件:
VITE_DEEPSEEK_API_KEY=your_api_key_here
  1. 在代码中访问:
const apiKey = import.meta.env.VITE_DEEPSEEK_API_KEY

注意:即使使用环境变量,前端代码中的API密钥仍然可能被有心人获取。对于生产环境,最佳实践仍然是使用后端服务作为代理。

异步处理:从Promise到async/await

传统的Promise链式调用

fetch(endpoint, options)
  .then(response => response.json())
  .then(data => {
    // 处理数据
  })
  .catch(error => {
    // 错误处理
  })

现代的async/await

我选择了async/await语法,因为它更简洁、更易读:

try {
  const response = await fetch(endpoint, options)
  const data = await response.json()
  // 处理数据
} catch (error) {
  // 错误处理
}

结果展示:动态更新DOM

获取到API响应后,需要将结果显示在页面上:

document.getElementById('reply').textContent = data.choices[0].message.content

这里使用了直接操作DOM的方式,在现代前端框架流行的今天,这种原生方法依然简单有效。

完整HTML结构

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="color-scheme" content="light dark">
  <title>前端调用大模型示例</title>
  <link rel="stylesheet" href="styles/style.css">
</head>
<body>
  <h1>Hello DeepSeek</h1>
  <div id="reply"></div>
  <script type="module" src="./scripts/app.js"></script>
</body>
</html>

开发心得与总结

通过这个项目,我获得了以下几点经验:

1. 前端调用LLM的可行性

事实证明,前端直接调用大模型API是完全可行的,这为快速原型开发和个人项目提供了便利。但需要注意安全性问题,不建议在生产环境中直接暴露API密钥。

2. HTTP请求的细节重要性

调用外部API时,请求的每个部分都很重要:

  • 正确的端点URL
  • 恰当的HTTP方法
  • 必要的请求头
  • 正确格式化的请求体

3. 现代JavaScript特性的价值

ES6+的特性让代码更加简洁:

  • 模板字符串
  • 箭头函数
  • 解构赋值
  • async/await

4. 构建工具的重要性

Vite等现代构建工具不仅提供开发服务器和打包功能,还解决了环境变量、模块化等工程化问题。

5. 安全意识的培养

即使是在个人项目中,也应该养成良好的安全习惯,使用环境变量管理敏感信息。

结语

前端技术日新月异,如今我们甚至可以直接在前端调用强大的人工智能模型。这个项目虽然简单,但展示了前端开发的强大能力和无限可能性。作为前端开发者,我们不应该将自己局限在传统的界面开发中,而应该积极探索前端技术的边界。

希望这篇文章能为想要在前端项目中集成AI能力的开发者提供一些参考和启发。前端的世界很精彩,让我们一起探索更多可能性!


注意:本文示例仅适用于开发和测试环境,生产环境中请务必通过后端服务调用第三方API,确保API密钥的安全性。

❌