Web存储

Web存储概述

Web存储(Web Storage)是HTML5提供的客户端数据存储机制,包括localStorage和sessionStorage两种方式。与传统的Cookie相比,Web存储提供了更大的存储空间和更简单的API。

东巴文(db-w.cn) 认为:Web存储让前端应用拥有了本地数据持久化能力,是构建现代Web应用的重要基础。

Web存储特点

localStorage与sessionStorage对比

特性 localStorage sessionStorage
生命周期 永久存储 会话期间(关闭浏览器后清除)
存储大小 约5MB 约5MB
作用域 同源窗口共享 仅当前窗口
存储位置 客户端 客户端
与服务器通信 不自动发送 不自动发送

Web存储与Cookie对比

特性 Web存储 Cookie
存储大小 5MB 4KB
HTTP请求 不自动发送 自动发送
API 简单易用 较复杂
过期时间 可设置或永久 必须设置
跨域 同源策略 可设置domain

东巴文点评:Web存储适合存储大量客户端数据,Cookie适合存储需要发送到服务器的少量数据。

localStorage

基本用法

// 存储数据
localStorage.setItem('username', '张三');
localStorage.setItem('age', '25');

// 读取数据
const username = localStorage.getItem('username');
const age = localStorage.getItem('age');

console.log(username); // 张三
console.log(age); // 25

// 删除数据
localStorage.removeItem('username');

// 清除所有数据
localStorage.clear();

属性访问方式

// 存储数据
localStorage.username = '张三';
localStorage.age = '25';

// 读取数据
console.log(localStorage.username); // 张三
console.log(localStorage.age); // 25

// 删除数据
delete localStorage.username;

存储对象

// 存储对象(需要JSON序列化)
const user = {
    name: '张三',
    age: 25,
    email: 'zhangsan@example.com'
};

localStorage.setItem('user', JSON.stringify(user));

// 读取对象(需要JSON反序列化)
const storedUser = JSON.parse(localStorage.getItem('user'));
console.log(storedUser.name); // 张三

东巴文点评:localStorage只能存储字符串,存储对象时必须使用JSON.stringify()序列化,读取时使用JSON.parse()反序列化。

存储数组

// 存储数组
const colors = ['red', 'green', 'blue'];
localStorage.setItem('colors', JSON.stringify(colors));

// 读取数组
const storedColors = JSON.parse(localStorage.getItem('colors'));
console.log(storedColors); // ['red', 'green', 'blue']

// 添加新元素
storedColors.push('yellow');
localStorage.setItem('colors', JSON.stringify(storedColors));

检查键是否存在

// 方法1:使用getItem
if (localStorage.getItem('username') !== null) {
    console.log('用户名存在');
}

// 方法2:使用hasOwnProperty
if (localStorage.hasOwnProperty('username')) {
    console.log('用户名存在');
}

// 方法3:使用in操作符
if ('username' in localStorage) {
    console.log('用户名存在');
}

获取所有键

// 存储多个数据
localStorage.setItem('name', '张三');
localStorage.setItem('age', '25');
localStorage.setItem('city', '北京');

// 获取键的数量
console.log(localStorage.length); // 3

// 遍历所有键
for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    const value = localStorage.getItem(key);
    console.log(`${key}: ${value}`);
}

// 使用Object.keys遍历
Object.keys(localStorage).forEach(key => {
    const value = localStorage.getItem(key);
    console.log(`${key}: ${value}`);
});

sessionStorage

基本用法

// 存储数据
sessionStorage.setItem('token', 'abc123');
sessionStorage.setItem('loginTime', Date.now().toString());

// 读取数据
const token = sessionStorage.getItem('token');
const loginTime = sessionStorage.getItem('loginTime');

console.log(token); // abc123
console.log(loginTime); // 时间戳

// 删除数据
sessionStorage.removeItem('token');

// 清除所有数据
sessionStorage.clear();

会话存储示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>sessionStorage示例</title>
</head>
<body>
    <h1>会话计数器</h1>
    <p>当前会话访问次数:<span id="count">0</span></p>
    
    <script>
        // 获取访问次数
        let count = parseInt(sessionStorage.getItem('visitCount')) || 0;
        
        // 增加访问次数
        count++;
        
        // 存储访问次数
        sessionStorage.setItem('visitCount', count.toString());
        
        // 显示访问次数
        document.getElementById('count').textContent = count;
    </script>
</body>
</html>

东巴文点评:sessionStorage适合存储会话期间的临时数据,如用户登录状态、表单数据等。

存储事件

storage事件

当localStorage或sessionStorage的数据发生变化时,会触发storage事件。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>storage事件示例</title>
</head>
<body>
    <h1>storage事件监听</h1>
    
    <script>
        // 监听storage事件
        window.addEventListener('storage', function(e) {
            console.log('键:', e.key);
            console.log('旧值:', e.oldValue);
            console.log('新值:', e.newValue);
            console.log('URL:', e.url);
            console.log('存储区域:', e.storageArea);
        });
    </script>
</body>
</html>

storage事件属性

属性 说明
key 发生变化的键
oldValue 旧值
newValue 新值
url 发生变化的页面URL
storageArea 存储区域对象

东巴文点评:storage事件只在同源的其他窗口中触发,当前窗口不会触发。

存储限制

存储大小检测

function getStorageSize() {
    let total = 0;
    
    for (let key in localStorage) {
        if (localStorage.hasOwnProperty(key)) {
            total += localStorage.getItem(key).length + key.length;
        }
    }
    
    // 转换为KB
    return (total * 2 / 1024).toFixed(2) + ' KB';
}

console.log('已使用存储空间:', getStorageSize());

存储空间测试

function testStorageLimit() {
    const testKey = 'test';
    const testValue = 'a';
    let data = '';
    
    try {
        // 构建测试数据
        for (let i = 0; i < 1024 * 1024; i++) {
            data += testValue;
            localStorage.setItem(testKey, data);
        }
    } catch (e) {
        console.log('存储空间已满');
        localStorage.removeItem(testKey);
    }
}

异常处理

function setLocalStorage(key, value) {
    try {
        localStorage.setItem(key, value);
        return true;
    } catch (e) {
        if (e.name === 'QuotaExceededError') {
            console.error('存储空间已满');
        } else {
            console.error('存储失败:', e.message);
        }
        return false;
    }
}

// 使用示例
if (setLocalStorage('username', '张三')) {
    console.log('存储成功');
} else {
    console.log('存储失败');
}

东巴文点评:在使用Web存储时,应该始终进行异常处理,避免因存储空间满或其他错误导致程序崩溃。

实际应用场景

1. 用户偏好设置

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>用户偏好设置</title>
    <style>
        body {
            transition: background-color 0.3s, color 0.3s;
        }
        
        body.dark {
            background-color: #333;
            color: #fff;
        }
        
        body.light {
            background-color: #fff;
            color: #333;
        }
    </style>
</head>
<body>
    <h1>用户偏好设置</h1>
    
    <label>
        <input type="checkbox" id="darkMode"> 深色模式
    </label>
    
    <script>
        // 加载用户偏好
        function loadPreferences() {
            const darkMode = localStorage.getItem('darkMode') === 'true';
            document.getElementById('darkMode').checked = darkMode;
            document.body.className = darkMode ? 'dark' : 'light';
        }
        
        // 保存用户偏好
        function savePreferences() {
            const darkMode = document.getElementById('darkMode').checked;
            localStorage.setItem('darkMode', darkMode.toString());
            document.body.className = darkMode ? 'dark' : 'light';
        }
        
        // 监听变化
        document.getElementById('darkMode').addEventListener('change', savePreferences);
        
        // 页面加载时加载偏好
        loadPreferences();
    </script>
</body>
</html>

2. 表单数据自动保存

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>表单自动保存</title>
</head>
<body>
    <h1>表单自动保存</h1>
    
    <form id="myForm">
        <div>
            <label>姓名:</label>
            <input type="text" id="name" name="name">
        </div>
        
        <div>
            <label>邮箱:</label>
            <input type="email" id="email" name="email">
        </div>
        
        <div>
            <label>留言:</label>
            <textarea id="message" name="message"></textarea>
        </div>
        
        <button type="submit">提交</button>
        <button type="button" id="clearBtn">清除保存的数据</button>
    </form>
    
    <script>
        const form = document.getElementById('myForm');
        
        // 加载保存的表单数据
        function loadFormData() {
            const savedData = localStorage.getItem('formData');
            if (savedData) {
                const data = JSON.parse(savedData);
                document.getElementById('name').value = data.name || '';
                document.getElementById('email').value = data.email || '';
                document.getElementById('message').value = data.message || '';
            }
        }
        
        // 保存表单数据
        function saveFormData() {
            const data = {
                name: document.getElementById('name').value,
                email: document.getElementById('email').value,
                message: document.getElementById('message').value
            };
            localStorage.setItem('formData', JSON.stringify(data));
        }
        
        // 监听表单输入
        form.addEventListener('input', saveFormData);
        
        // 提交表单
        form.addEventListener('submit', function(e) {
            e.preventDefault();
            console.log('表单提交成功');
            localStorage.removeItem('formData');
            form.reset();
        });
        
        // 清除保存的数据
        document.getElementById('clearBtn').addEventListener('click', function() {
            localStorage.removeItem('formData');
            form.reset();
        });
        
        // 页面加载时加载表单数据
        loadFormData();
    </script>
</body>
</html>

3. 购物车

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>购物车示例</title>
    <style>
        .cart {
            border: 1px solid #ccc;
            padding: 20px;
            margin: 20px 0;
        }
        
        .cart-item {
            display: flex;
            justify-content: space-between;
            padding: 10px 0;
            border-bottom: 1px solid #eee;
        }
        
        .product {
            display: inline-block;
            margin: 10px;
            padding: 10px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h1>购物车示例</h1>
    
    <div>
        <div class="product">
            <h3>商品A</h3>
            <p>价格:¥100</p>
            <button onclick="addToCart('商品A', 100)">加入购物车</button>
        </div>
        
        <div class="product">
            <h3>商品B</h3>
            <p>价格:¥200</p>
            <button onclick="addToCart('商品B', 200)">加入购物车</button>
        </div>
        
        <div class="product">
            <h3>商品C</h3>
            <p>价格:¥300</p>
            <button onclick="addToCart('商品C', 300)">加入购物车</button>
        </div>
    </div>
    
    <div class="cart">
        <h2>购物车</h2>
        <div id="cartItems"></div>
        <p>总计:<span id="totalPrice">¥0</span></p>
        <button onclick="clearCart()">清空购物车</button>
    </div>
    
    <script>
        // 获取购物车数据
        function getCart() {
            const cart = localStorage.getItem('cart');
            return cart ? JSON.parse(cart) : [];
        }
        
        // 保存购物车数据
        function saveCart(cart) {
            localStorage.setItem('cart', JSON.stringify(cart));
            renderCart();
        }
        
        // 添加商品到购物车
        function addToCart(name, price) {
            const cart = getCart();
            const existingItem = cart.find(item => item.name === name);
            
            if (existingItem) {
                existingItem.quantity++;
            } else {
                cart.push({ name, price, quantity: 1 });
            }
            
            saveCart(cart);
        }
        
        // 从购物车移除商品
        function removeFromCart(name) {
            const cart = getCart();
            const index = cart.findIndex(item => item.name === name);
            
            if (index > -1) {
                if (cart[index].quantity > 1) {
                    cart[index].quantity--;
                } else {
                    cart.splice(index, 1);
                }
            }
            
            saveCart(cart);
        }
        
        // 清空购物车
        function clearCart() {
            localStorage.removeItem('cart');
            renderCart();
        }
        
        // 渲染购物车
        function renderCart() {
            const cart = getCart();
            const cartItemsEl = document.getElementById('cartItems');
            const totalPriceEl = document.getElementById('totalPrice');
            
            let html = '';
            let total = 0;
            
            cart.forEach(item => {
                const itemTotal = item.price * item.quantity;
                total += itemTotal;
                
                html += `
                    <div class="cart-item">
                        <span>${item.name}</span>
                        <span>¥${item.price} x ${item.quantity}</span>
                        <span>¥${itemTotal}</span>
                        <button onclick="removeFromCart('${item.name}')">-</button>
                    </div>
                `;
            });
            
            cartItemsEl.innerHTML = html;
            totalPriceEl.textContent = '¥' + total;
        }
        
        // 页面加载时渲染购物车
        renderCart();
    </script>
</body>
</html>

东巴文点评:Web存储非常适合存储购物车、用户偏好、表单数据等客户端数据。

安全考虑

不要存储敏感信息

// 不推荐:存储敏感信息
localStorage.setItem('password', '123456');
localStorage.setItem('creditCard', '1234567890123456');

// 推荐:存储非敏感信息
localStorage.setItem('username', '张三');
localStorage.setItem('theme', 'dark');

数据加密

// 简单的加密示例(实际应用中应使用更强大的加密算法)
function encrypt(text, key) {
    let result = '';
    for (let i = 0; i < text.length; i++) {
        result += String.fromCharCode(text.charCodeAt(i) ^ key.charCodeAt(i % key.length));
    }
    return btoa(result);
}

function decrypt(text, key) {
    text = atob(text);
    let result = '';
    for (let i = 0; i < text.length; i++) {
        result += String.fromCharCode(text.charCodeAt(i) ^ key.charCodeAt(i % key.length));
    }
    return result;
}

// 存储加密数据
const sensitiveData = '敏感信息';
const encrypted = encrypt(sensitiveData, 'secret-key');
localStorage.setItem('data', encrypted);

// 读取加密数据
const decrypted = decrypt(localStorage.getItem('data'), 'secret-key');
console.log(decrypted); // 敏感信息

XSS防护

// 对存储的数据进行转义
function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// 存储转义后的数据
const userInput = '<script>alert("XSS")</script>';
localStorage.setItem('comment', escapeHtml(userInput));

// 读取时数据是安全的
const comment = localStorage.getItem('comment');
console.log(comment); // &lt;script&gt;alert("XSS")&lt;/script&gt;

东巴文点评:Web存储容易受到XSS攻击,不要存储敏感信息,必要时对数据进行加密。

封装工具类

class StorageUtil {
    constructor(storage = localStorage) {
        this.storage = storage;
    }
    
    // 设置数据
    set(key, value) {
        try {
            const data = JSON.stringify(value);
            this.storage.setItem(key, data);
            return true;
        } catch (e) {
            console.error('存储失败:', e.message);
            return false;
        }
    }
    
    // 获取数据
    get(key, defaultValue = null) {
        try {
            const data = this.storage.getItem(key);
            return data ? JSON.parse(data) : defaultValue;
        } catch (e) {
            console.error('读取失败:', e.message);
            return defaultValue;
        }
    }
    
    // 删除数据
    remove(key) {
        this.storage.removeItem(key);
    }
    
    // 清空数据
    clear() {
        this.storage.clear();
    }
    
    // 检查键是否存在
    has(key) {
        return this.storage.getItem(key) !== null;
    }
    
    // 获取所有键
    keys() {
        return Object.keys(this.storage);
    }
    
    // 获取存储大小
    size() {
        let total = 0;
        for (let key in this.storage) {
            if (this.storage.hasOwnProperty(key)) {
                total += this.storage.getItem(key).length + key.length;
            }
        }
        return (total * 2 / 1024).toFixed(2) + ' KB';
    }
}

// 使用示例
const storage = new StorageUtil();

// 存储对象
storage.set('user', { name: '张三', age: 25 });

// 读取对象
const user = storage.get('user');
console.log(user); // { name: '张三', age: 25 }

// 检查键是否存在
console.log(storage.has('user')); // true

// 获取存储大小
console.log(storage.size());

// 删除数据
storage.remove('user');

综合示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web存储示例 - 东巴文</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            transition: background-color 0.3s, color 0.3s;
        }
        
        body.dark {
            background-color: #333;
            color: #fff;
        }
        
        .section {
            margin: 30px 0;
            padding: 20px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
        
        .note-list {
            list-style: none;
            padding: 0;
        }
        
        .note-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px;
            margin: 10px 0;
            background: #f5f5f5;
            border-radius: 3px;
        }
        
        body.dark .note-item {
            background: #444;
        }
        
        button {
            padding: 8px 16px;
            margin: 5px;
            border: none;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border-radius: 3px;
            cursor: pointer;
        }
        
        button:hover {
            opacity: 0.9;
        }
        
        input, textarea {
            padding: 8px;
            margin: 5px 0;
            border: 1px solid #ddd;
            border-radius: 3px;
            width: 100%;
            box-sizing: border-box;
        }
        
        .stats {
            display: flex;
            gap: 20px;
            margin: 20px 0;
        }
        
        .stat-item {
            flex: 1;
            padding: 15px;
            background: #f5f5f5;
            border-radius: 5px;
            text-align: center;
        }
        
        body.dark .stat-item {
            background: #444;
        }
    </style>
</head>
<body>
    <h1>Web存储示例</h1>
    
    <div class="stats">
        <div class="stat-item">
            <h3>localStorage使用量</h3>
            <p id="localStorageSize">0 KB</p>
        </div>
        <div class="stat-item">
            <h3>sessionStorage使用量</h3>
            <p id="sessionStorageSize">0 KB</p>
        </div>
    </div>
    
    <div class="section">
        <h2>用户偏好设置</h2>
        <label>
            <input type="checkbox" id="darkMode"> 深色模式
        </label>
    </div>
    
    <div class="section">
        <h2>笔记管理</h2>
        <input type="text" id="noteInput" placeholder="输入笔记内容">
        <button onclick="addNote()">添加笔记</button>
        
        <ul class="note-list" id="noteList"></ul>
        
        <button onclick="clearNotes()">清空所有笔记</button>
    </div>
    
    <div class="section">
        <h2>会话计数器</h2>
        <p>当前会话访问次数:<span id="visitCount">0</span></p>
    </div>
    
    <script>
        // 工具类
        class StorageUtil {
            constructor(storage) {
                this.storage = storage;
            }
            
            set(key, value) {
                try {
                    this.storage.setItem(key, JSON.stringify(value));
                    return true;
                } catch (e) {
                    console.error('存储失败:', e.message);
                    return false;
                }
            }
            
            get(key, defaultValue = null) {
                try {
                    const data = this.storage.getItem(key);
                    return data ? JSON.parse(data) : defaultValue;
                } catch (e) {
                    console.error('读取失败:', e.message);
                    return defaultValue;
                }
            }
            
            remove(key) {
                this.storage.removeItem(key);
            }
            
            clear() {
                this.storage.clear();
            }
            
            size() {
                let total = 0;
                for (let key in this.storage) {
                    if (this.storage.hasOwnProperty(key)) {
                        total += this.storage.getItem(key).length + key.length;
                    }
                }
                return (total * 2 / 1024).toFixed(2) + ' KB';
            }
        }
        
        const localStore = new StorageUtil(localStorage);
        const sessionStore = new StorageUtil(sessionStorage);
        
        // 更新存储大小显示
        function updateStorageSize() {
            document.getElementById('localStorageSize').textContent = localStore.size();
            document.getElementById('sessionStorageSize').textContent = sessionStore.size();
        }
        
        // 深色模式
        function loadTheme() {
            const darkMode = localStore.get('darkMode', false);
            document.getElementById('darkMode').checked = darkMode;
            document.body.className = darkMode ? 'dark' : '';
        }
        
        function saveTheme() {
            const darkMode = document.getElementById('darkMode').checked;
            localStore.set('darkMode', darkMode);
            document.body.className = darkMode ? 'dark' : '';
            updateStorageSize();
        }
        
        document.getElementById('darkMode').addEventListener('change', saveTheme);
        
        // 笔记管理
        function loadNotes() {
            const notes = localStore.get('notes', []);
            const noteList = document.getElementById('noteList');
            
            noteList.innerHTML = notes.map((note, index) => `
                <li class="note-item">
                    <span>${note}</span>
                    <button onclick="deleteNote(${index})">删除</button>
                </li>
            `).join('');
        }
        
        function addNote() {
            const input = document.getElementById('noteInput');
            const note = input.value.trim();
            
            if (note) {
                const notes = localStore.get('notes', []);
                notes.push(note);
                localStore.set('notes', notes);
                input.value = '';
                loadNotes();
                updateStorageSize();
            }
        }
        
        function deleteNote(index) {
            const notes = localStore.get('notes', []);
            notes.splice(index, 1);
            localStore.set('notes', notes);
            loadNotes();
            updateStorageSize();
        }
        
        function clearNotes() {
            localStore.remove('notes');
            loadNotes();
            updateStorageSize();
        }
        
        // 会话计数器
        function updateVisitCount() {
            let count = sessionStore.get('visitCount', 0);
            count++;
            sessionStore.set('visitCount', count);
            document.getElementById('visitCount').textContent = count;
            updateStorageSize();
        }
        
        // 初始化
        loadTheme();
        loadNotes();
        updateVisitCount();
        updateStorageSize();
    </script>
</body>
</html>

最佳实践

1. 数据分类存储

// 推荐:使用前缀分类
localStorage.setItem('user:name', '张三');
localStorage.setItem('user:age', '25');
localStorage.setItem('settings:theme', 'dark');
localStorage.setItem('settings:language', 'zh-CN');

// 不推荐:无分类
localStorage.setItem('name', '张三');
localStorage.setItem('age', '25');
localStorage.setItem('theme', 'dark');
localStorage.setItem('language', 'zh-CN');

2. 错误处理

function safeSetItem(key, value) {
    try {
        localStorage.setItem(key, value);
        return true;
    } catch (e) {
        if (e.name === 'QuotaExceededError') {
            console.error('存储空间已满,请清理数据');
        }
        return false;
    }
}

3. 定期清理

// 清理过期数据
function cleanExpiredData() {
    const now = Date.now();
    const keys = Object.keys(localStorage);
    
    keys.forEach(key => {
        if (key.startsWith('temp:')) {
            const data = JSON.parse(localStorage.getItem(key));
            if (data.expires && data.expires < now) {
                localStorage.removeItem(key);
            }
        }
    });
}

// 设置带过期时间的数据
function setWithExpiry(key, value, expiryInMs) {
    const item = {
        value: value,
        expires: Date.now() + expiryInMs
    };
    localStorage.setItem(key, JSON.stringify(item));
}

// 获取带过期时间的数据
function getWithExpiry(key) {
    const itemStr = localStorage.getItem(key);
    if (!itemStr) return null;
    
    const item = JSON.parse(itemStr);
    if (item.expires && Date.now() > item.expires) {
        localStorage.removeItem(key);
        return null;
    }
    
    return item.value;
}

学习检验

知识点测试

问题1:localStorage和sessionStorage的主要区别是?

A. 存储大小不同 B. 生命周期不同 C. API不同 D. 存储位置不同

<details> <summary>点击查看答案</summary>

答案:B

东巴文解释:localStorage是永久存储,除非手动删除;sessionStorage是会话存储,关闭浏览器后自动清除。

</details>

问题2:以下哪个方法用于清除localStorage中的所有数据?

A. localStorage.removeItem() B. localStorage.deleteAll() C. localStorage.clear() D. localStorage.empty()

<details> <summary>点击查看答案</summary>

答案:C

东巴文解释localStorage.clear()方法用于清除localStorage中的所有数据。

</details>

实践任务

任务:创建一个待办事项列表,使用localStorage存储数据,实现添加、删除、标记完成等功能。

<details> <summary>点击查看参考答案</summary>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>待办事项列表</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .todo-item {
            display: flex;
            align-items: center;
            padding: 10px;
            margin: 10px 0;
            background: #f5f5f5;
            border-radius: 5px;
        }
        
        .todo-item.completed span {
            text-decoration: line-through;
            opacity: 0.5;
        }
        
        input[type="text"] {
            padding: 10px;
            font-size: 16px;
            border: 1px solid #ddd;
            border-radius: 5px;
            width: 70%;
        }
        
        button {
            padding: 10px 20px;
            margin-left: 10px;
            border: none;
            background: #667eea;
            color: white;
            border-radius: 5px;
            cursor: pointer;
        }
        
        button:hover {
            opacity: 0.9;
        }
    </style>
</head>
<body>
    <h1>待办事项列表</h1>
    
    <div>
        <input type="text" id="todoInput" placeholder="输入待办事项">
        <button onclick="addTodo()">添加</button>
    </div>
    
    <div id="todoList"></div>
    
    <script>
        let todos = JSON.parse(localStorage.getItem('todos')) || [];
        
        function saveTodos() {
            localStorage.setItem('todos', JSON.stringify(todos));
        }
        
        function renderTodos() {
            const list = document.getElementById('todoList');
            list.innerHTML = todos.map((todo, index) => `
                <div class="todo-item ${todo.completed ? 'completed' : ''}">
                    <input type="checkbox" ${todo.completed ? 'checked' : ''} 
                           onchange="toggleTodo(${index})">
                    <span style="flex: 1; margin: 0 10px;">${todo.text}</span>
                    <button onclick="deleteTodo(${index})">删除</button>
                </div>
            `).join('');
        }
        
        function addTodo() {
            const input = document.getElementById('todoInput');
            const text = input.value.trim();
            
            if (text) {
                todos.push({ text, completed: false });
                saveTodos();
                renderTodos();
                input.value = '';
            }
        }
        
        function toggleTodo(index) {
            todos[index].completed = !todos[index].completed;
            saveTodos();
            renderTodos();
        }
        
        function deleteTodo(index) {
            todos.splice(index, 1);
            saveTodos();
            renderTodos();
        }
        
        renderTodos();
    </script>
</body>
</html>
</details>

东巴文(db-w.cn) - 让编程学习更有趣、更高效!