pin_drop当前位置:知识文库 ❯ 图文

HTML5 API:HTML5 LocalStorage - 完整教程与代码示例

一、教程简介

LocalStorage 是 Web Storage API 中的持久化存储方案,提供了一种在浏览器端长期存储键值对数据的机制。与 sessionStorage 的会话级存储不同,localStorage 中存储的数据没有过期时间,除非被显式删除,否则将永久保存在浏览器中。localStorage 的同源共享特性使其成为存储用户偏好设置、主题配置、离线数据缓存等持久化数据的首选方案。

二、核心概念

持久化特性

代码示例

写入 localStorage → 数据永久保存
    │
    ├── 页面刷新 → 数据保留
    ├── 标签页关闭 → 数据保留
    ├── 浏览器重启 → 数据保留
    ├── 同源页面 → 数据共享
    │
    └── 手动清除 → 数据删除
        ├── localStorage.removeItem(key)
        ├── localStorage.clear()
        ├── 用户清除浏览器数据
        └── 浏览器存储空间不足时自动清理

同源共享

localStorage 遵循同源策略,同一源下的所有页面共享同一份 localStorage 数据:

代码示例

https://example.com/page1  ──┐
https://example.com/page2  ──┤── 共享同一份 localStorage
https://example.com:443/a  ──┘   (可互相读写)

http://example.com         ────── 不同协议,独立存储
https://api.example.com    ────── 不同域名,独立存储
https://example.com:8080   ────── 不同端口,独立存储

存储容量

浏览器 localStorage 容量
Chrome 约 10MB
Firefox 约 10MB
Safari 约 5MB
Edge 约 10MB
IE 约 5-10MB

当存储空间满时,继续写入会抛出 QuotaExceededError 异常。

API 接口

代码示例

localStorage.setItem(key, value);       // 设置数据
localStorage.getItem(key);              // 读取数据
localStorage.removeItem(key);           // 删除指定键
localStorage.clear();                   // 清空所有数据
localStorage.key(index);                // 按索引获取键名
localStorage.length;                    // 获取存储条目数量

三、语法与用法

基本操作

代码示例

// 存储
localStorage.setItem('username', '张三');
localStorage.setItem('theme', 'dark');

// 读取
const username = localStorage.getItem('username'); // '张三'
const theme = localStorage.getItem('theme');       // 'dark'

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

// 清空
localStorage.clear();

// 遍历
for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    console.log(key, localStorage.getItem(key));
}

存储复杂数据类型

代码示例

// 对象
const settings = { theme: 'dark', fontSize: 16, language: 'zh-CN' };
localStorage.setItem('settings', JSON.stringify(settings));
const saved = JSON.parse(localStorage.getItem('settings'));

// 数组
const history = ['首页', '关于', '联系'];
localStorage.setItem('history', JSON.stringify(history));
const savedHistory = JSON.parse(localStorage.getItem('history'));

// 数字(注意类型转换)
localStorage.setItem('count', '42');
const count = parseInt(localStorage.getItem('count'), 10); // 42

// 布尔值
localStorage.setItem('agreed', 'true');
const agreed = localStorage.getItem('agreed') === 'true'; // true

// 日期
const now = new Date().toISOString();
localStorage.setItem('lastVisit', now);
const lastVisit = new Date(localStorage.getItem('lastVisit'));

Storage 事件

当同源的其他页面修改 localStorage 时触发:

代码示例

window.addEventListener('storage', function(e) {
    console.log('键:', e.key);           // 被修改的键
    console.log('旧值:', e.oldValue);    // 修改前的值
    console.log('新值:', e.newValue);    // 修改后的值
    console.log('URL:', e.url);          // 触发变化的页面URL
    console.log('存储区:', e.storageArea); // localStorage 引用
});

注意storage 事件只在其他同源页面中触发,当前页面自身的修改不会触发。

四、代码示例

示例一:主题切换与偏好设置持久化

代码示例

<!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>
        :root {
            --bg-primary: #ffffff;
            --bg-secondary: #f5f7fa;
            --bg-card: #ffffff;
            --text-primary: #2d3436;
            --text-secondary: #636e72;
            --border-color: #e0e0e0;
            --accent: #0984e3;
            --shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
        }

        [data-theme="dark"] {
            --bg-primary: #1a1a2e;
            --bg-secondary: #16213e;
            --bg-card: #0f3460;
            --text-primary: #e0e0e0;
            --text-secondary: #7a8ba0;
            --border-color: #1a3a5c;
            --accent: #00cec9;
            --shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
        }

        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: var(--bg-secondary);
            color: var(--text-primary);
            min-height: 100vh;
            padding: 30px 20px;
            transition: background 0.3s, color 0.3s;
        }
        .container { max-width: 640px; margin: 0 auto; }
        h1 { font-size: 24px; margin-bottom: 8px; }
        .desc { color: var(--text-secondary); font-size: 14px; margin-bottom: 24px; }
        .card {
            background: var(--bg-card);
            border-radius: 12px;
            padding: 24px;
            margin-bottom: 16px;
            box-shadow: var(--shadow);
            border: 1px solid var(--border-color);
            transition: background 0.3s, border-color 0.3s;
        }
        .card h2 { font-size: 16px; margin-bottom: 16px; }
        .setting-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 14px 0;
            border-bottom: 1px solid var(--border-color);
        }
        .setting-row:last-child { border-bottom: none; }
        .setting-label { font-size: 14px; }
        .setting-desc { font-size: 12px; color: var(--text-secondary); margin-top: 2px; }

        /* 开关 */
        .toggle {
            position: relative;
            width: 48px;
            height: 26px;
            background: #ccc;
            border-radius: 13px;
            cursor: pointer;
            transition: background 0.3s;
        }
        .toggle.active { background: var(--accent); }
        .toggle::after {
            content: '';
            position: absolute;
            top: 3px;
            left: 3px;
            width: 20px;
            height: 20px;
            background: #fff;
            border-radius: 50%;
            transition: transform 0.3s;
        }
        .toggle.active::after { transform: translateX(22px); }

        /* 滑块 */
        .slider-group { display: flex; align-items: center; gap: 12px; }
        input[type="range"] {
            width: 120px;
            accent-color: var(--accent);
        }
        .slider-value {
            font-size: 14px;
            font-weight: 600;
            min-width: 36px;
            text-align: center;
        }

        /* 选择器 */
        .select-group { display: flex; gap: 8px; }
        .select-btn {
            padding: 6px 16px;
            border: 1px solid var(--border-color);
            border-radius: 6px;
            background: var(--bg-secondary);
            color: var(--text-primary);
            font-size: 13px;
            cursor: pointer;
            transition: all 0.2s;
        }
        .select-btn.active {
            background: var(--accent);
            color: #fff;
            border-color: var(--accent);
        }

        .preview-text {
            margin-top: 16px;
            padding: 16px;
            background: var(--bg-secondary);
            border-radius: 8px;
            line-height: 1.8;
            transition: font-size 0.3s;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>偏好设置</h1>
        <p class="desc">所有设置自动保存到 localStorage,刷新页面后保持不变。</p>

        <div class="card">
            <h2>外观</h2>
            <div class="setting-row">
                <div>
                    <div class="setting-label">深色模式</div>
                    <div class="setting-desc">切换页面主题</div>
                </div>
                <div class="toggle" id="themeToggle" onclick="toggleTheme()"></div>
            </div>
            <div class="setting-row">
                <div>
                    <div class="setting-label">字体大小</div>
                    <div class="setting-desc">调整正文字体大小</div>
                </div>
                <div class="slider-group">
                    <input type="range" id="fontSizeSlider" min="12" max="24" value="16" oninput="changeFontSize(this.value)">
                    <span class="slider-value" id="fontSizeValue">16px</span>
                </div>
            </div>
            <div class="setting-row">
                <div>
                    <div class="setting-label">语言</div>
                    <div class="setting-desc">选择界面语言</div>
                </div>
                <div class="select-group">
                    <button class="select-btn active" data-lang="zh-CN" onclick="changeLang('zh-CN')">中文</button>
                    <button class="select-btn" data-lang="en-US" onclick="changeLang('en-US')">English</button>
                </div>
            </div>
        </div>

        <div class="card">
            <h2>通知</h2>
            <div class="setting-row">
                <div>
                    <div class="setting-label">推送通知</div>
                    <div class="setting-desc">接收新消息推送</div>
                </div>
                <div class="toggle" id="notifToggle" onclick="toggleNotif()"></div>
            </div>
            <div class="setting-row">
                <div>
                    <div class="setting-label">邮件通知</div>
                    <div class="setting-desc">通过邮件接收更新</div>
                </div>
                <div class="toggle" id="emailToggle" onclick="toggleEmail()"></div>
            </div>
        </div>

        <div class="card">
            <h2>预览效果</h2>
            <div class="preview-text" id="previewText" style="font-size:16px;">
                这是一段预览文字,字体大小会随设置变化。所有偏好设置会自动保存到 localStorage 中,即使关闭浏览器后再次打开也会保持之前的设置。
            </div>
        </div>
    </div>

    <script>
        const PREFS_KEY = 'userPreferences';

        // 默认偏好
        const defaultPrefs = {
            theme: 'light',
            fontSize: 16,
            language: 'zh-CN',
            notifications: false,
            emailNotif: false
        };

        // 加载偏好
        function loadPreferences() {
            try {
                const saved = localStorage.getItem(PREFS_KEY);
                return saved ? { ...defaultPrefs, ...JSON.parse(saved) } : { ...defaultPrefs };
            } catch (e) {
                return { ...defaultPrefs };
            }
        }

        // 保存偏好
        function savePreferences(prefs) {
            try {
                localStorage.setItem(PREFS_KEY, JSON.stringify(prefs));
            } catch (e) {
                console.error('保存偏好失败:', e);
            }
        }

        // 应用偏好
        function applyPreferences(prefs) {
            // 主题
            document.documentElement.setAttribute('data-theme', prefs.theme);
            const themeToggle = document.getElementById('themeToggle');
            themeToggle.classList.toggle('active', prefs.theme === 'dark');

            // 字体大小
            document.getElementById('fontSizeSlider').value = prefs.fontSize;
            document.getElementById('fontSizeValue').textContent = prefs.fontSize + 'px';
            document.getElementById('previewText').style.fontSize = prefs.fontSize + 'px';

            // 语言
            document.querySelectorAll('.select-btn[data-lang]').forEach(btn => {
                btn.classList.toggle('active', btn.dataset.lang === prefs.language);
            });

            // 通知
            document.getElementById('notifToggle').classList.toggle('active', prefs.notifications);
            document.getElementById('emailToggle').classList.toggle('active', prefs.emailNotif);
        }

        function toggleTheme() {
            const prefs = loadPreferences();
            prefs.theme = prefs.theme === 'dark' ? 'light' : 'dark';
            savePreferences(prefs);
            applyPreferences(prefs);
        }

        function changeFontSize(size) {
            const prefs = loadPreferences();
            prefs.fontSize = parseInt(size);
            savePreferences(prefs);
            applyPreferences(prefs);
        }

        function changeLang(lang) {
            const prefs = loadPreferences();
            prefs.language = lang;
            savePreferences(prefs);
            applyPreferences(prefs);
        }

        function toggleNotif() {
            const prefs = loadPreferences();
            prefs.notifications = !prefs.notifications;
            savePreferences(prefs);
            applyPreferences(prefs);
        }

        function toggleEmail() {
            const prefs = loadPreferences();
            prefs.emailNotif = !prefs.emailNotif;
            savePreferences(prefs);
            applyPreferences(prefs);
        }

        // 初始化
        const currentPrefs = loadPreferences();
        applyPreferences(currentPrefs);
    </script>
</body>
</html>

示例二:带过期时间和 LRU 淘汰的缓存管理

代码示例

<!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>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #1e272e;
            min-height: 100vh;
            padding: 30px 20px;
            color: #d2dae2;
        }
        .container { max-width: 700px; margin: 0 auto; }
        h1 { font-size: 24px; color: #fff; margin-bottom: 8px; }
        .desc { color: #808e9b; font-size: 14px; margin-bottom: 24px; }
        .card {
            background: #2d3436;
            border-radius: 12px;
            padding: 24px;
            margin-bottom: 16px;
            border: 1px solid #485460;
        }
        .card h2 { font-size: 16px; color: #fff; margin-bottom: 16px; }
        .input-row { display: flex; gap: 10px; margin-bottom: 12px; }
        input, select {
            padding: 8px 12px;
            border: 1px solid #485460;
            border-radius: 6px;
            background: #1e272e;
            color: #d2dae2;
            font-size: 14px;
            outline: none;
        }
        input:focus, select:focus { border-color: #0fbcf9; }
        input { flex: 1; }
        button {
            padding: 8px 18px;
            border: none;
            border-radius: 6px;
            font-size: 13px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s;
        }
        .btn-blue { background: #0fbcf9; color: #1e272e; }
        .btn-blue:hover { background: #0da8d6; }
        .btn-green { background: #05c46b; color: #fff; }
        .btn-green:hover { background: #04a85d; }
        .btn-red { background: #ff3f34; color: #fff; }
        .btn-red:hover { background: #e0352b; }
        .btn-yellow { background: #ffc048; color: #1e272e; }
        .btn-yellow:hover { background: #e5ab3e; }
        .cache-table { width: 100%; border-collapse: collapse; margin-top: 12px; }
        .cache-table th, .cache-table td {
            padding: 10px 8px;
            text-align: left;
            border-bottom: 1px solid #485460;
            font-size: 13px;
        }
        .cache-table th { color: #808e9b; font-weight: 500; }
        .cache-table td { color: #d2dae2; }
        .expired { color: #ff3f34 !important; }
        .stats {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 12px;
            margin-bottom: 16px;
        }
        .stat {
            background: #1e272e;
            border-radius: 8px;
            padding: 14px;
            text-align: center;
        }
        .stat-label { font-size: 11px; color: #808e9b; margin-bottom: 4px; }
        .stat-value { font-size: 20px; font-weight: 700; color: #0fbcf9; }
        .btn-group { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
        .empty-msg { text-align: center; color: #808e9b; padding: 20px; font-size: 13px; }
    </style>
</head>
<body>
    <div class="container">
        <h1>LocalStorage 缓存管理</h1>
        <p class="desc">支持过期时间和 LRU 淘汰策略的 localStorage 缓存系统。</p>

        <div class="stats">
            <div class="stat">
                <div class="stat-label">缓存条目</div>
                <div class="stat-value" id="statCount">0</div>
            </div>
            <div class="stat">
                <div class="stat-label">已用空间</div>
                <div class="stat-value" id="statSize">0 KB</div>
            </div>
            <div class="stat">
                <div class="stat-label">过期条目</div>
                <div class="stat-value" id="statExpired">0</div>
            </div>
        </div>

        <div class="card">
            <h2>添加缓存</h2>
            <div class="input-row">
                <input type="text" id="cacheKey" placeholder="键名">
                <input type="text" id="cacheValue" placeholder="值">
                <select id="cacheTTL">
                    <option value="60000">1 分钟</option>
                    <option value="300000">5 分钟</option>
                    <option value="3600000" selected>1 小时</option>
                    <option value="86400000">1 天</option>
                    <option value="0">永不过期</option>
                </select>
            </div>
            <div class="btn-group">
                <button class="btn-blue" onclick="addCache()">添加</button>
                <button class="btn-yellow" onclick="addDemoData()">添加示例数据</button>
                <button class="btn-red" onclick="clearExpired()">清除过期</button>
                <button class="btn-red" onclick="clearAll()">清空所有</button>
            </div>
        </div>

        <div class="card">
            <h2>缓存列表</h2>
            <div id="cacheList"></div>
        </div>
    </div>

    <script>
        const CACHE_PREFIX = 'lru_cache_';
        const MAX_ENTRIES = 20;

        function getCache(key) {
            const raw = localStorage.getItem(CACHE_PREFIX + key);
            if (!raw) return null;
            try {
                const entry = JSON.parse(raw);
                if (entry.expire && Date.now() > entry.expire) {
                    localStorage.removeItem(CACHE_PREFIX + key);
                    return null;
                }
                // 更新访问时间(LRU)
                entry.lastAccess = Date.now();
                localStorage.setItem(CACHE_PREFIX + key, JSON.stringify(entry));
                return entry.value;
            } catch (e) {
                return null;
            }
        }

        function setCache(key, value, ttlMs) {
            // 检查是否需要淘汰
            evictIfNeeded();

            const entry = {
                value: value,
                created: Date.now(),
                lastAccess: Date.now(),
                expire: ttlMs > 0 ? Date.now() + ttlMs : null
            };
            try {
                localStorage.setItem(CACHE_PREFIX + key, JSON.stringify(entry));
                return true;
            } catch (e) {
                // 空间不足,执行 LRU 淘汰后重试
                lruEvict();
                try {
                    localStorage.setItem(CACHE_PREFIX + key, JSON.stringify(entry));
                    return true;
                } catch (e2) {
                    return false;
                }
            }
        }

        function removeCache(key) {
            localStorage.removeItem(CACHE_PREFIX + key);
        }

        function getAllCacheEntries() {
            const entries = [];
            for (let i = 0; i < localStorage.length; i++) {
                const k = localStorage.key(i);
                if (k.startsWith(CACHE_PREFIX)) {
                    try {
                        const entry = JSON.parse(localStorage.getItem(k));
                        const key = k.slice(CACHE_PREFIX.length);
                        entries.push({ key: key, ...entry });
                    } catch (e) { /* 忽略 */ }
                }
            }
            return entries.sort((a, b) => b.lastAccess - a.lastAccess);
        }

        function evictIfNeeded() {
            const entries = getAllCacheEntries();
            if (entries.length >= MAX_ENTRIES) {
                lruEvict();
            }
        }

        function lruEvict() {
            const entries = getAllCacheEntries();
            if (entries.length === 0) return;
            // 淘汰最久未访问的条目
            const lru = entries[entries.length - 1];
            removeCache(lru.key);
        }

        function clearExpired() {
            const entries = getAllCacheEntries();
            let count = 0;
            entries.forEach(entry => {
                if (entry.expire && Date.now() > entry.expire) {
                    removeCache(entry.key);
                    count++;
                }
            });
            refreshUI();
            alert('已清除 ' + count + ' 条过期数据');
        }

        function clearAll() {
            const entries = getAllCacheEntries();
            entries.forEach(entry => removeCache(entry.key));
            refreshUI();
        }

        function addCache() {
            const key = document.getElementById('cacheKey').value.trim();
            const value = document.getElementById('cacheValue').value.trim();
            const ttl = parseInt(document.getElementById('cacheTTL').value);
            if (!key) { alert('请输入键名'); return; }
            setCache(key, value, ttl);
            document.getElementById('cacheKey').value = '';
            document.getElementById('cacheValue').value = '';
            refreshUI();
        }

        function addDemoData() {
            const demos = [
                { key: 'api_users', value: JSON.stringify([{ id: 1, name: '张三' }, { id: 2, name: '李四' }]), ttl: 3600000 },
                { key: 'api_posts', value: JSON.stringify([{ id: 1, title: '文章一' }, { id: 2, title: '文章二' }]), ttl: 300000 },
                { key: 'config', value: JSON.stringify({ theme: 'dark', lang: 'zh' }), ttl: 0 },
                { key: 'temp_token', value: 'eyJhbGciOiJIUzI1NiJ9.demo', ttl: 60000 },
                { key: 'search_history', value: JSON.stringify(['HTML5', 'CSS3', 'JavaScript']), ttl: 86400000 },
            ];
            demos.forEach(d => setCache(d.key, d.value, d.ttl));
            refreshUI();
        }

        function refreshUI() {
            const entries = getAllCacheEntries();
            const now = Date.now();

            // 统计
            document.getElementById('statCount').textContent = entries.length;
            let totalSize = 0;
            let expiredCount = 0;
            entries.forEach(entry => {
                const raw = localStorage.getItem(CACHE_PREFIX + entry.key);
                totalSize += raw ? raw.length : 0;
                if (entry.expire && now > entry.expire) expiredCount++;
            });
            document.getElementById('statSize').textContent = (totalSize * 2 / 1024).toFixed(2) + ' KB';
            document.getElementById('statExpired').textContent = expiredCount;

            // 列表
            const listEl = document.getElementById('cacheList');
            if (entries.length === 0) {
                listEl.innerHTML = '<div class="empty-msg">暂无缓存数据</div>';
                return;
            }

            let html = '<table class="cache-table"><thead><tr><th>键名</th><th>值</th><th>过期时间</th><th>最后访问</th><th>操作</th></tr></thead><tbody>';
            entries.forEach(entry => {
                const isExpired = entry.expire && now > entry.expire;
                const expireText = entry.expire
                    ? new Date(entry.expire).toLocaleTimeString('zh-CN')
                    : '永不过期';
                const accessText = new Date(entry.lastAccess).toLocaleTimeString('zh-CN');
                const valueDisplay = entry.value.length > 30 ? entry.value.substring(0, 30) + '...' : entry.value;

                html += '<tr class="' + (isExpired ? 'expired' : '') + '">' +
                    '<td>' + entry.key + '</td>' +
                    '<td style="font-family:monospace;font-size:12px;">' + valueDisplay + '</td>' +
                    '<td>' + expireText + (isExpired ? ' (已过期)' : '') + '</td>' +
                    '<td>' + accessText + '</td>' +
                    '<td><button class="btn-red" style="padding:4px 10px;font-size:11px;" onclick="removeCache(\'' + entry.key + '\');refreshUI();">删除</button></td>' +
                    '</tr>';
            });
            html += '</tbody></table>';
            listEl.innerHTML = html;
        }

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

五、浏览器兼容性

浏览器 支持版本 存储容量
Chrome 4+ 约 10MB
Firefox 3.5+ 约 10MB
Safari 4+ 约 5MB
Edge 12+ 约 10MB
Opera 10.5+ 约 10MB
IE 8+ 约 5-10MB
iOS Safari 3.2+ 约 5MB
Android 2.1+ 视实现而定

六、注意事项与最佳实践

1. 存储容量管理

代码示例

// 检测剩余可用空间(间接方法)
function getLocalStorageSize() {
    let total = 0;
    for (let i = 0; i < localStorage.length; i++) {
        const key = localStorage.key(i);
        const value = localStorage.getItem(key);
        total += key.length + value.length;
    }
    // UTF-16 编码,每字符 2 字节
    return (total * 2 / 1024 / 1024).toFixed(2) + ' MB';
}

// 安全写入,处理空间不足
function safeSetItem(key, value) {
    try {
        localStorage.setItem(key, value);
        return true;
    } catch (e) {
        if (e.name === 'QuotaExceededError' ||
            e.code === 22 ||
            e.code === 1014) {
            console.error('localStorage 空间不足');
            // 执行清理策略
        }
        return false;
    }
}

2. 数据安全

  • 不要存储敏感数据:密码、令牌等不应存储在 localStorage

  • 防范 XSS:存储的数据如果被恶意脚本读取,可能导致信息泄露

  • 数据验证:读取后应验证数据完整性

代码示例

// 读取时验证数据
function getValidatedData(key, schema) {
    const raw = localStorage.getItem(key);
    if (!raw) return null;
    try {
        const data = JSON.parse(raw);
        // 简单验证
        if (schema.required) {
            for (const field of schema.required) {
                if (data[field] === undefined) {
                    localStorage.removeItem(key);
                    return null;
                }
            }
        }
        return data;
    } catch (e) {
        localStorage.removeItem(key);
        return null;
    }
}

3. 命名空间与版本管理

代码示例

// 使用前缀避免键名冲突
const APP_PREFIX = 'myapp_v1_';

function prefixedKey(key) {
    return APP_PREFIX + key;
}

// 版本迁移
function migrateStorage(oldVersion, newVersion) {
    if (oldVersion !== newVersion) {
        const oldPrefix = 'myapp_v' + oldVersion + '_';
        const newPrefix = 'myapp_v' + newVersion + '_';
        for (let i = 0; i < localStorage.length; i++) {
            const key = localStorage.key(i);
            if (key.startsWith(oldPrefix)) {
                const newKey = key.replace(oldPrefix, newPrefix);
                localStorage.setItem(newKey, localStorage.getItem(key));
                localStorage.removeItem(key);
            }
        }
    }
}

4. 性能优化

  • 避免频繁读写:使用内存变量缓存

  • 大数据使用 IndexedDB:替代 localStorage 存储大量数据

  • JSON 序列化开销:注意数据大小对性能的影响

七、代码规范示例

代码示例

// 推荐:完整的 localStorage 管理类
class LocalStorageManager {
    constructor(options = {}) {
        this.prefix = options.prefix || '';
        this.version = options.version || '1';
        this.maxAge = options.maxAge || null;
        this._cache = new Map();
    }

    _getKey(key) {
        return this.prefix + '_v' + this.version + '_' + key;
    }

    set(key, value, maxAge = null) {
        const entry = {
            value: value,
            timestamp: Date.now(),
            maxAge: maxAge || this.maxAge
        };
        const serialized = JSON.stringify(entry);
        try {
            localStorage.setItem(this._getKey(key), serialized);
            this._cache.set(key, entry);
            return true;
        } catch (e) {
            console.error('localStorage 写入失败:', e);
            return false;
        }
    }

    get(key, defaultValue = null) {
        // 先查内存缓存
        if (this._cache.has(key)) {
            const cached = this._cache.get(key);
            if (!this._isExpired(cached)) return cached.value;
            this._cache.delete(key);
        }

        const raw = localStorage.getItem(this._getKey(key));
        if (!raw) return defaultValue;

        try {
            const entry = JSON.parse(raw);
            if (this._isExpired(entry)) {
                this.remove(key);
                return defaultValue;
            }
            this._cache.set(key, entry);
            return entry.value;
        } catch (e) {
            this.remove(key);
            return defaultValue;
        }
    }

    remove(key) {
        localStorage.removeItem(this._getKey(key));
        this._cache.delete(key);
    }

    has(key) {
        return this.get(key) !== null;
    }

    clear() {
        const keys = [];
        for (let i = 0; i < localStorage.length; i++) {
            const k = localStorage.key(i);
            if (k.startsWith(this.prefix + '_v' + this.version + '_')) {
                keys.push(k);
            }
        }
        keys.forEach(k => localStorage.removeItem(k));
        this._cache.clear();
    }

    _isExpired(entry) {
        if (!entry.maxAge) return false;
        return Date.now() - entry.timestamp > entry.maxAge;
    }

    getSize() {
        let total = 0;
        for (let i = 0; i < localStorage.length; i++) {
            const k = localStorage.key(i);
            if (k.startsWith(this.prefix + '_v' + this.version + '_')) {
                total += k.length + localStorage.getItem(k).length;
            }
        }
        return (total * 2 / 1024).toFixed(2) + ' KB';
    }
}

// 使用示例
const store = new LocalStorageManager({ prefix: 'myapp', version: '2' });

store.set('user', { name: '张三' });
store.set('token', 'abc123', 3600000); // 1小时过期
console.log(store.get('user'));
console.log(store.getSize());

八、常见问题与解决方案

Q1:localStorage 数据突然丢失?

可能原因

  • 用户手动清除了浏览器数据

  • 浏览器存储空间不足时自动清理

  • 隐私模式下数据在关闭后清除

  • 浏览器升级或重装

解决方案:重要数据应同时存储在服务端,localStorage 仅作为缓存使用。

Q2:如何实现 localStorage 的数据加密?

代码示例

// 简单加密(非安全级别,仅增加读取难度)
function encrypt(data, key) {
    return btoa(encodeURIComponent(data).split('').reverse().join(''));
}

function decrypt(data, key) {
    return decodeURIComponent(atob(data).split('').reverse().join(''));
}

// 使用
localStorage.setItem('secret', encrypt(JSON.stringify(data)));
const decrypted = JSON.parse(decrypt(localStorage.getItem('secret')));

Q3:多个模块使用 localStorage 时键名冲突?

解决方案:使用命名空间前缀,如 module_name_key,或使用封装类自动添加前缀。

Q4:如何监听同一页面内的 localStorage 变化?

原因storage 事件只在其他标签页触发,当前页面不触发。

解决方案:重写 localStorage 方法或使用自定义事件。

代码示例

// 重写 setItem 触发自定义事件
const originalSetItem = localStorage.setItem.bind(localStorage);
localStorage.setItem = function(key, value) {
    originalSetItem(key, value);
    window.dispatchEvent(new CustomEvent('localstorage-changed', {
        detail: { key: key, value: value }
    }));
};

// 监听
window.addEventListener('localstorage-changed', function(e) {
    console.log('变化:', e.detail.key, e.detail.value);
});

九、总结

LocalStorage 提供了浏览器端持久化的键值对存储能力,其同源共享、永久存储的特性使其非常适合存储用户偏好设置、主题配置、离线缓存等数据。使用时需注意存储容量限制(5-10MB)、数据安全(防范 XSS、不存敏感信息)、命名空间管理(避免键名冲突)和异常处理(空间不足、JSON 解析失败)。通过封装带过期时间、LRU 淘汰、内存缓存的工具类,可以让 localStorage 在实际项目中更加安全、高效地使用。


常见问题

localStorage 数据为什么会突然丢失?

可能原因包括:用户手动清除了浏览器数据、浏览器存储空间不足时自动清理、隐私模式下数据在关闭后清除、浏览器升级或重装。解决方案是重要数据应同时存储在服务端,localStorage 仅作为缓存使用。

如何实现 localStorage 的数据加密?

可以使用简单的编码方式增加读取难度,如将数据转换为 Base64 编码后存储。但需要注意这不是真正的加密,只是增加读取门槛。对于敏感数据,建议不要存储在 localStorage 中。

多个模块使用 localStorage 时如何避免键名冲突?

使用命名空间前缀是最佳实践,如 module_name_key 格式,或使用封装类自动添加前缀。这样可以确保不同模块的数据互不干扰。

如何监听同一页面内的 localStorage 变化?

storage 事件只在其他同源标签页中触发,当前页面自身的修改不会触发。解决方案是重写 localStorage 方法(如 setItem)或使用自定义事件来监听变化。

localStorage 的存储容量限制是多少?

不同浏览器的容量不同,Chrome、Firefox、Edge 约为 10MB,Safari 约为 5MB,IE 约为 5-10MB。当存储空间满时,继续写入会抛出 QuotaExceededError 异常,需要进行异常处理和数据清理。

标签: LocalStorage 本地存储 持久化 Web Storage 缓存管理 LRU淘汰 数据加密

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: HTML5 API:HTML5 SessionStorage - 完整教程与代码示例 下一篇: HTML5 API:HTML5 Web Workers - 完整教程与代码示例

poll相关推荐