前端 LocalStorage 实战:从入门到熟练,一篇就够了

前端 LocalStorage 实战:从入门到熟练,一篇就够了

你是不是还在用 localStorage.setItem / getItem 一通乱存?

存对象忘写 JSON.stringify 拿不到?存满 5M 怎么办?

这篇文章带你系统掌握 LocalStorage 的全部玩法,附带一个「笔记宝盒」完整项目。


一、LocalStorage 是什么?

localStorage 是浏览器提供的持久化存储 方案。它就像给每个网站分配了一个小抽屉,你把数据放进去,关闭浏览器、重启电脑,数据依然在,除非手动清除或代码删除。

与它对应的还有 sessionStorage(会话存储,关标签页就没了)、cookie(每次请求自动携带,容量小)。

核心特性

  • 容量约 5MB(不同浏览器略有差异)
  • 存储 键值对 ,键和值都必须是字符串
  • 同源策略:同一个协议、域名、端口才能共享
  • 同步 API,会阻塞 JS 执行(但数据量小,影响可忽略)
  • 永远不会过期(除非主动清除)

二、基础用法(5 分钟上手)

js 复制代码
// 1. 存储数据
localStorage.setItem('name', '张三');
localStorage.setItem('age', '25');

// 2. 读取数据
const name = localStorage.getItem('name');     // '张三'
const age = localStorage.getItem('age');       // '25'

// 3. 删除某一条
localStorage.removeItem('age');

// 4. 清空所有
localStorage.clear();

// 5. 获取 key 名(用于遍历)
const key = localStorage.key(0);  // 第0个键名

注意:存储数字、布尔、对象都必须转为字符串。

js 复制代码
// 存储对象
const user = { id: 1, name: '李四' };
localStorage.setItem('user', JSON.stringify(user));

// 读取对象
const raw = localStorage.getItem('user');
const userObj = JSON.parse(raw);

三、实战场景:一个「笔记宝盒」项目

光说没用,我们做一个完整的"笔记宝盒"应用,包含:添加笔记、删除笔记、编辑笔记、搜索笔记,所有数据存在 localStorage 里。

最终效果预览

  • 顶部输入框 + 添加按钮
  • 笔记列表显示标题、内容、操作按钮(编辑、删除)
  • 搜索框实时过滤
  • 关闭浏览器再打开,笔记还在

完整代码(复制即用)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>📒 笔记宝盒|LocalStorage 实战</title>
  <style>
    * { box-sizing: border-box; }
    body {
      background: #f5f7fb;
      font-family: system-ui, -apple-system, sans-serif;
      display: flex;
      justify-content: center;
      padding: 2rem;
    }
    .app {
      max-width: 800px;
      width: 100%;
      background: white;
      border-radius: 1.5rem;
      box-shadow: 0 8px 20px rgba(0,0,0,0.05);
      padding: 1.5rem;
    }
    h1 { margin-top: 0; font-size: 1.8rem; display: flex; align-items: center; gap: 8px; }
    .add-section { display: flex; gap: 12px; margin-bottom: 1.5rem; flex-wrap: wrap; }
    .add-section input, .add-section textarea {
      padding: 10px;
      border: 1px solid #e2e8f0;
      border-radius: 12px;
      font-size: 0.9rem;
    }
    .add-section input { flex: 2; }
    .add-section textarea { flex: 3; resize: vertical; }
    .add-section button {
      background: #3b82f6;
      color: white;
      border: none;
      border-radius: 12px;
      padding: 0 20px;
      cursor: pointer;
      font-weight: bold;
    }
    .search-box {
      width: 100%;
      padding: 10px;
      margin-bottom: 1.5rem;
      border: 1px solid #e2e8f0;
      border-radius: 40px;
      font-size: 0.9rem;
    }
    .note-card {
      background: #fefce8;
      border-left: 6px solid #eab308;
      border-radius: 12px;
      padding: 12px 16px;
      margin-bottom: 12px;
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
      transition: 0.2s;
    }
    .note-card:hover { background: #fff9db; }
    .note-info h3 { margin: 0 0 6px 0; font-size: 1.1rem; }
    .note-info p { margin: 0; color: #334155; font-size: 0.9rem; word-break: break-all; }
    .note-actions { display: flex; gap: 8px; }
    .note-actions button {
      background: none;
      border: none;
      cursor: pointer;
      font-size: 1.2rem;
      padding: 4px 8px;
      border-radius: 8px;
    }
    .edit-btn { color: #3b82f6; }
    .delete-btn { color: #ef4444; }
    .empty { text-align: center; color: #94a3b8; padding: 2rem; }
  </style>
</head>
<body>
<div class="app">
  <h1>📒 笔记宝盒 <span style="font-size:0.8rem;">LocalStorage 实战</span></h1>

  <div class="add-section">
    <input type="text" id="titleInput" placeholder="标题">
    <textarea id="contentInput" rows="1" placeholder="内容..."></textarea>
    <button id="addBtn">+ 添加笔记</button>
  </div>

  <input type="text" id="searchInput" class="search-box" placeholder="🔍 搜索标题或内容...">

  <div id="notesContainer"></div>
</div>

<script>
  // ---------- 存储 key ----------
  const STORAGE_KEY = 'note_box';

  // ---------- 读取笔记 ----------
  function loadNotes() {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return [];
    try {
      return JSON.parse(raw);
    } catch (e) {
      return [];
    }
  }

  // ---------- 保存笔记 ----------
  function saveNotes(notes) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
  }

  // ---------- 全局状态 ----------
  let notes = loadNotes();        // 笔记数组 [{ id, title, content }]
  let searchKeyword = '';

  // ---------- 渲染视图 ----------
  function render() {
    const container = document.getElementById('notesContainer');
    const keyword = searchKeyword.trim().toLowerCase();

    // 过滤笔记
    let filtered = notes;
    if (keyword) {
      filtered = notes.filter(note =>
        note.title.toLowerCase().includes(keyword) ||
        note.content.toLowerCase().includes(keyword)
      );
    }

    if (filtered.length === 0) {
      container.innerHTML = '<div class="empty">📭 暂无笔记,添加一条吧~</div>';
      return;
    }

    container.innerHTML = filtered.map(note => `
      <div class="note-card" data-id="${note.id}">
        <div class="note-info">
          <h3>${escapeHtml(note.title)}</h3>
          <p>${escapeHtml(note.content)}</p>
        </div>
        <div class="note-actions">
          <button class="edit-btn" data-action="edit">✏️</button>
          <button class="delete-btn" data-action="delete">🗑️</button>
        </div>
      </div>
    `).join('');
  }

  // 简单的防 XSS
  function escapeHtml(str) {
    if (!str) return '';
    return str.replace(/[&<>]/g, function(m) {
      if (m === '&') return '&amp;';
      if (m === '<') return '&lt;';
      if (m === '>') return '&gt;';
      return m;
    });
  }

  // ---------- 添加笔记 ----------
  function addNote() {
    const title = document.getElementById('titleInput').value.trim();
    const content = document.getElementById('contentInput').value.trim();
    if (!title && !content) {
      alert('请至少填写标题或内容');
      return;
    }
    const newNote = {
      id: Date.now(),
      title: title || '无标题',
      content: content || '无内容'
    };
    notes.push(newNote);
    saveNotes(notes);
    // 清空输入框
    document.getElementById('titleInput').value = '';
    document.getElementById('contentInput').value = '';
    render();
  }

  // ---------- 删除笔记 ----------
  function deleteNote(id) {
    notes = notes.filter(note => note.id !== id);
    saveNotes(notes);
    render();
  }

  // ---------- 编辑笔记(弹窗修改) ----------
  function editNote(id) {
    const note = notes.find(n => n.id === id);
    if (!note) return;
    const newTitle = prompt('修改标题', note.title);
    const newContent = prompt('修改内容', note.content);
    if (newTitle !== null) note.title = newTitle.trim() || '无标题';
    if (newContent !== null) note.content = newContent.trim() || '无内容';
    saveNotes(notes);
    render();
  }

  // ---------- 事件委托(处理删除/编辑) ----------
  function handleContainerClick(e) {
    const target = e.target;
    const action = target.dataset.action;
    const card = target.closest('.note-card');
    if (!card) return;
    const id = Number(card.dataset.id);

    if (action === 'delete') {
      if (confirm('确定删除这条笔记吗?')) deleteNote(id);
    } else if (action === 'edit') {
      editNote(id);
    }
  }

  // ---------- 搜索监听 ----------
  function handleSearchInput(e) {
    searchKeyword = e.target.value;
    render();
  }

  // ---------- 初始化 ----------
  function init() {
    render();
    document.getElementById('addBtn').addEventListener('click', addNote);
    document.getElementById('searchInput').addEventListener('input', handleSearchInput);
    document.getElementById('notesContainer').addEventListener('click', handleContainerClick);
    // 允许回车添加(可选)
    const inputs = ['titleInput', 'contentInput'];
    inputs.forEach(id => {
      document.getElementById(id).addEventListener('keypress', (e) => {
        if (e.key === 'Enter') addNote();
      });
    });
  }

  init();
</script>
</body>
</html>

四、代码中的 LocalStorage 知识点总结

  • 存储/读取localStorage.setItem(key, value) / getItem(key)
  • 存储对象 :必须 JSON.stringify,取出后 JSON.parse
  • 删除某条:不能直接删对象内部的某个字段,而是读取整个数组 → 修改数组 → 重新存回去
  • 初始化 :页面加载时从 localStorage 读取,没有就给空数组
  • 修改数据流 :修改 notes 数组 → saveNotesrender,单向数据流

五、进阶技巧与注意事项

1. 存储空间超出 5MB 怎么办?

使用 try...catch 捕获 QuotaExceededError,提示用户清理数据。

js 复制代码
try {
  localStorage.setItem('key', 'large string...');
} catch (e) {
  if (e.name === 'QuotaExceededError') {
    alert('存储已满,请删除部分数据');
  }
}

2. 监听 storage 事件(多标签页实时同步)

当你在一个标签页修改 localStorage其他同源标签页可以监听到变化,用于跨标签通信。

js 复制代码
window.addEventListener('storage', (e) => {
  console.log('key:', e.key, 'oldValue:', e.oldValue, 'newValue:', e.newValue);
  // 刷新页面数据,实现多标签实时同步
});

3. 封装一个带过期时间的存储工具

js 复制代码
function setWithExpiry(key, value, ttlMs) {
  const item = {
    value: value,
    expiry: Date.now() + ttlMs
  };
  localStorage.setItem(key, JSON.stringify(item));
}
function getWithExpiry(key) {
  const raw = localStorage.getItem(key);
  if (!raw) return null;
  const item = JSON.parse(raw);
  if (Date.now() > item.expiry) {
    localStorage.removeItem(key);
    return null;
  }
  return item.value;
}

4. LocalStorage vs sessionStorage vs IndexedDB

特性 localStorage sessionStorage IndexedDB
容量 ~5MB ~5MB 很大(几百MB)
生命周期 永久 标签页关闭即失 永久
API 同步性 同步 同步 异步(复杂)
适用场景 用户设置、草稿、小数据 表单临时缓存 大量结构化数据(如离线日志)

六、总结

LocalStorage 是前端本地存储的"入门砖",简单但够用。通过"笔记宝盒"这个项目,你学会了:

  • 基础 CRUD + 持久化
  • 存储对象数组的正确姿势
  • 搜索过滤 + 事件委托
  • 空间不足、多标签同步等进阶知识

现在你可以

  • 把登录状态、主题设置、用户偏好存在 LocalStorage
  • 做一个离线可用的便签本
  • 甚至做一个简单的 todo list 或日记本

快去动手改改笔记宝盒,给它加上"标签分类"或者"导出为 JSON"功能吧!遇到问题欢迎评论区交流~

相关推荐
hexu_blog1 小时前
前端vue后端java如何实现证件照功能
前端·javascript·vue.js
用户40189933422841 小时前
第 11 章 MCP 协议与集成
前端
Southern Wind1 小时前
谷记账——一个 Vue 3 批次记账 App
前端·javascript·vue.js
A923A2 小时前
【javaScript 原型精讲】
javascript·原型·原型链
卷帘依旧2 小时前
手写throttle
javascript
lzhdim2 小时前
SQL 入门 14:SQL 触发器与事件:自动化数据处理
linux·前端·数据库·sql·自动化
其实防守也摸鱼2 小时前
Sqlmap:选取sqli-labs中less-8进行sqlmap注入测试
前端·css·网络·安全·web安全·less·sqli-labs
伯远医学2 小时前
Nat. Methods | 邻近标记技术:活细胞中捕捉分子互作的新利器
java·开发语言·前端·javascript·人工智能·算法·eclipse
莪_幻尘2 小时前
一份 AGENTS.md,让 AI 代码规范率从 60% 飙升到 95%
前端·ai编程·cursor