本地存储 #

什么是本地存储? #

本地存储(localStorage)是 Web Storage API 的一部分,允许在浏览器中存储键值对数据。

text
┌─────────────────────────────────────────────────────────────┐
│                   浏览器存储方式对比                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐        │
│   │  Cookie     │  │ localStorage│  │sessionStorage│        │
│   ├─────────────┤  ├─────────────┤  ├─────────────┤        │
│   │ 大小: 4KB   │  │ 大小: 5MB   │  │ 大小: 5MB   │        │
│   │ 过期: 可设置 │  │ 过期: 永久  │  │ 过期: 会话   │        │
│   │ 请求: 自动  │  │ 请求: 不携带 │  │ 请求: 不携带 │        │
│   │ 跨域: 受限  │  │ 跨域: 同源  │  │ 跨域: 同源  │        │
│   └─────────────┘  └─────────────┘  └─────────────┘        │
│                                                              │
│   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐        │
│   │ IndexedDB   │  │  Web SQL    │  │ Cache API   │        │
│   ├─────────────┤  ├─────────────┤  ├─────────────┤        │
│   │ 大小: 无限  │  │ 大小: 无限  │  │ 大小: 无限  │        │
│   │ 类型: NoSQL │  │ 类型: SQL   │  │ 类型: 缓存  │        │
│   │ 异步: 是    │  │ 异步: 是    │  │ 异步: 是    │        │
│   │ 状态: 推荐  │  │ 状态: 废弃  │  │ 状态: 推荐  │        │
│   └─────────────┘  └─────────────┘  └─────────────┘        │
│                                                              │
└─────────────────────────────────────────────────────────────┘

localStorage 特点 #

text
┌─────────────────────────────────────────────────────────────┐
│                  localStorage 特点                           │
├─────────────────────────────────────────────────────────────┤
│  ✅ 存储容量大:约 5MB                                        │
│  ✅ 持久化存储:除非手动清除,否则永久保存                      │
│  ✅ 同源策略:相同协议、域名、端口才能访问                      │
│  ✅ 同步操作:API 简单,使用方便                               │
│  ✅ 仅客户端:不会随请求发送到服务器                           │
└─────────────────────────────────────────────────────────────┘

基础函数 #

store 模块 #

封装了 localStorage 的增删改查操作,支持过期时间。

javascript
const _toString = val => Object.prototype.toString.call(val).slice(8, -1);
const isString = val => _toString(val) === 'String';
const isNumber = val => _toString(val) === 'Number';
const isDate = val => _toString(val) === 'Date';

const getRealTime = (time) => {
    let dateTime = null;
    switch (true) {
        case isString(time):
            dateTime = new Date(parseInt(time));
            break;
        case isNumber(time):
            dateTime = new Date(time);
            break;
        case isDate(time):
            dateTime = time;
            break;
    }
    return dateTime;
};

const store = {
    get(key) {
        if (key === undefined) return null;
        let value = localStorage.getItem(key);
        try {
            value = JSON.parse(value) || {};
        } catch {
            value = {};
        }
        const { expires = '', data = null } = value;
        const _expires = getRealTime(expires);
        if (_expires && _expires < new Date()) {
            store.remove(key);
            return null;
        }
        return data;
    },
    set(key, value, expires) {
        if (key === undefined) throw new Error('key can not be undefined');
        const _expires = getRealTime(expires);
        if (!_expires) {
            localStorage.setItem(key, JSON.stringify({
                data: value
            }));
            return;
        }
        if (value === undefined || value === '' || value === null || 
            value !== value || _expires.getTime() < Date.now()) {
            store.remove(key);
            return;
        }
        localStorage.setItem(key, JSON.stringify({
            expires: _expires.getTime(),
            data: value
        }));
    },
    remove(key) {
        localStorage.removeItem(key);
    },
    removeAll() {
        localStorage.clear();
    }
};

export default store;

基本使用 #

javascript
import store from './utils/store';

store.set('user', { name: 'John', age: 25 });

const user = store.get('user');
console.log(user);

store.remove('user');

store.removeAll();

设置过期时间 #

javascript
import store from './utils/store';

const oneHour = Date.now() + 60 * 60 * 1000;
store.set('session', { token: 'abc123' }, oneHour);

const oneDay = Date.now() + 24 * 60 * 60 * 1000;
store.set('cache', { data: [1, 2, 3] }, oneDay);

const oneWeek = Date.now() + 7 * 24 * 60 * 60 * 1000;
store.set('preferences', { theme: 'dark' }, oneWeek);

进阶用法 #

存储容量检测 #

javascript
const getStorageSize = () => {
    let total = 0;
    for (let key in localStorage) {
        if (localStorage.hasOwnProperty(key)) {
            total += localStorage.getItem(key).length * 2;
        }
    }
    return {
        bytes: total,
        kb: (total / 1024).toFixed(2),
        mb: (total / 1024 / 1024).toFixed(4)
    };
};

const isStorageAvailable = (size = 1024 * 1024) => {
    try {
        const test = 'x'.repeat(size);
        localStorage.setItem('__test__', test);
        localStorage.removeItem('__test__');
        return true;
    } catch (e) {
        return false;
    }
};

console.log(getStorageSize());
console.log(isStorageAvailable());

存储事件监听 #

javascript
const onStorageChange = (callback) => {
    window.addEventListener('storage', (e) => {
        callback({
            key: e.key,
            oldValue: e.oldValue,
            newValue: e.newValue,
            url: e.url
        });
    });
};

onStorageChange((change) => {
    console.log('存储变化:', change);
});

批量操作 #

javascript
const storeMultiple = (items) => {
    Object.entries(items).forEach(([key, value]) => {
        store.set(key, value);
    });
};

const getMultiple = (keys) => {
    return keys.reduce((result, key) => {
        result[key] = store.get(key);
        return result;
    }, {});
};

const removeMultiple = (keys) => {
    keys.forEach(key => store.remove(key));
};

storeMultiple({
    user: { name: 'John' },
    settings: { theme: 'dark' },
    token: 'abc123'
});

const data = getMultiple(['user', 'settings', 'token']);
console.log(data);

removeMultiple(['user', 'token']);

命名空间存储 #

javascript
class NamespacedStore {
    constructor(namespace) {
        this.namespace = namespace;
    }
    
    getKey(key) {
        return `${this.namespace}:${key}`;
    }
    
    get(key) {
        return store.get(this.getKey(key));
    }
    
    set(key, value, expires) {
        return store.set(this.getKey(key), value, expires);
    }
    
    remove(key) {
        return store.remove(this.getKey(key));
    }
    
    clear() {
        const keys = Object.keys(localStorage)
            .filter(k => k.startsWith(`${this.namespace}:`));
        keys.forEach(k => localStorage.removeItem(k));
    }
}

const userStore = new NamespacedStore('user');
const appStore = new NamespacedStore('app');

userStore.set('profile', { name: 'John' });
appStore.set('config', { theme: 'dark' });

console.log(userStore.get('profile'));
console.log(appStore.get('config'));

实际应用场景 #

1. 用户登录状态 #

javascript
import store from './utils/store';

class AuthStorage {
    constructor() {
        this.tokenKey = 'auth_token';
        this.userKey = 'auth_user';
    }
    
    setToken(token, expiresDays = 7) {
        const expires = Date.now() + expiresDays * 24 * 60 * 60 * 1000;
        store.set(this.tokenKey, token, expires);
    }
    
    getToken() {
        return store.get(this.tokenKey);
    }
    
    setUser(user, expiresDays = 7) {
        const expires = Date.now() + expiresDays * 24 * 60 * 60 * 1000;
        store.set(this.userKey, user, expires);
    }
    
    getUser() {
        return store.get(this.userKey);
    }
    
    clear() {
        store.remove(this.tokenKey);
        store.remove(this.userKey);
    }
    
    isLoggedIn() {
        return !!this.getToken();
    }
}

const authStorage = new AuthStorage();
authStorage.setToken('abc123xyz', 7);
authStorage.setUser({ id: 1, name: 'John' }, 7);

2. 数据缓存 #

javascript
import store from './utils/store';

class CacheManager {
    constructor(prefix = 'cache') {
        this.prefix = prefix;
    }
    
    getKey(key) {
        return `${this.prefix}:${key}`;
    }
    
    async getOrSet(key, fetcher, ttl = 3600000) {
        const cacheKey = this.getKey(key);
        const cached = store.get(cacheKey);
        
        if (cached !== null) {
            console.log('缓存命中:', key);
            return cached;
        }
        
        console.log('缓存未命中,获取数据:', key);
        const data = await fetcher();
        const expires = Date.now() + ttl;
        store.set(cacheKey, data, expires);
        
        return data;
    }
    
    invalidate(key) {
        store.remove(this.getKey(key));
    }
    
    invalidateAll() {
        Object.keys(localStorage)
            .filter(k => k.startsWith(`${this.prefix}:`))
            .forEach(k => localStorage.removeItem(k));
    }
}

const cache = new CacheManager('api');

const userData = await cache.getOrSet(
    'user:123',
    () => fetch('/api/user/123').then(r => r.json()),
    5 * 60 * 1000
);

3. 表单数据持久化 #

javascript
import store from './utils/store';

class FormPersistence {
    constructor(formId, options = {}) {
        this.formId = formId;
        this.form = document.getElementById(formId);
        this.key = `form:${formId}`;
        this.autoSave = options.autoSave ?? true;
        this.ttl = options.ttl ?? 24 * 60 * 60 * 1000;
        
        if (this.autoSave) {
            this.enableAutoSave();
        }
    }
    
    save() {
        const formData = new FormData(this.form);
        const data = {};
        formData.forEach((value, key) => {
            data[key] = value;
        });
        store.set(this.key, data, Date.now() + this.ttl);
    }
    
    restore() {
        const data = store.get(this.key);
        if (!data) return false;
        
        Object.entries(data).forEach(([key, value]) => {
            const input = this.form.querySelector(`[name="${key}"]`);
            if (input) {
                if (input.type === 'checkbox') {
                    input.checked = value === 'on';
                } else if (input.type === 'radio') {
                    const radio = this.form.querySelector(`[name="${key}"][value="${value}"]`);
                    if (radio) radio.checked = true;
                } else {
                    input.value = value;
                }
            }
        });
        
        return true;
    }
    
    clear() {
        store.remove(this.key);
    }
    
    enableAutoSave() {
        this.form.addEventListener('input', () => this.save());
        this.form.addEventListener('change', () => this.save());
    }
    
    disableAutoSave() {
        this.form.removeEventListener('input', () => this.save());
        this.form.removeEventListener('change', () => this.save());
    }
}

const formPersistence = new FormPersistence('myForm', {
    autoSave: true,
    ttl: 7 * 24 * 60 * 60 * 1000
});

formPersistence.restore();

4. 购物车 #

javascript
import store from './utils/store';

class ShoppingCart {
    constructor() {
        this.key = 'shopping_cart';
    }
    
    getCart() {
        return store.get(this.key) || [];
    }
    
    addItem(item) {
        const cart = this.getCart();
        const existingIndex = cart.findIndex(i => i.id === item.id);
        
        if (existingIndex > -1) {
            cart[existingIndex].quantity += item.quantity || 1;
        } else {
            cart.push({ ...item, quantity: item.quantity || 1 });
        }
        
        store.set(this.key, cart);
        return cart;
    }
    
    removeItem(itemId) {
        const cart = this.getCart().filter(item => item.id !== itemId);
        store.set(this.key, cart);
        return cart;
    }
    
    updateQuantity(itemId, quantity) {
        const cart = this.getCart();
        const item = cart.find(i => i.id === itemId);
        if (item) {
            item.quantity = quantity;
            if (item.quantity <= 0) {
                return this.removeItem(itemId);
            }
        }
        store.set(this.key, cart);
        return cart;
    }
    
    getTotal() {
        const cart = this.getCart();
        return cart.reduce((total, item) => total + item.price * item.quantity, 0);
    }
    
    clear() {
        store.remove(this.key);
    }
}

const cart = new ShoppingCart();
cart.addItem({ id: 1, name: '商品A', price: 99.9, quantity: 2 });
cart.addItem({ id: 2, name: '商品B', price: 199.9 });
console.log(cart.getCart());
console.log('总价:', cart.getTotal());

错误处理 #

存储异常处理 #

javascript
const safeStore = {
    get(key) {
        try {
            return store.get(key);
        } catch (e) {
            console.error('存储读取失败:', e);
            return null;
        }
    },
    
    set(key, value, expires) {
        try {
            store.set(key, value, expires);
            return true;
        } catch (e) {
            console.error('存储写入失败:', e);
            if (e.name === 'QuotaExceededError') {
                console.warn('存储空间不足,尝试清理过期数据');
                clearExpiredData();
                try {
                    store.set(key, value, expires);
                    return true;
                } catch (e2) {
                    console.error('清理后仍然存储失败:', e2);
                    return false;
                }
            }
            return false;
        }
    }
};

const clearExpiredData = () => {
    Object.keys(localStorage).forEach(key => {
        try {
            const value = JSON.parse(localStorage.getItem(key));
            if (value.expires && new Date(value.expires) < new Date()) {
                localStorage.removeItem(key);
            }
        } catch (e) {}
    });
};

最佳实践 #

1. 数据序列化 #

javascript
const storeObject = (key, obj, expires) => {
    store.set(key, JSON.stringify(obj), expires);
};

const getObject = (key) => {
    const str = store.get(key);
    return str ? JSON.parse(str) : null;
};

2. 敏感数据加密 #

javascript
import md5 from './md5';

const encryptData = (data, secret) => {
    const str = JSON.stringify(data);
    const encrypted = btoa(str);
    const signature = md5(encrypted + secret);
    return { encrypted, signature };
};

const decryptData = (encrypted, signature, secret) => {
    const expectedSignature = md5(encrypted + secret);
    if (signature !== expectedSignature) {
        throw new Error('数据签名验证失败');
    }
    return JSON.parse(atob(encrypted));
};

3. 存储版本管理 #

javascript
const STORAGE_VERSION = '1.0';

const initWithVersion = () => {
    const version = localStorage.getItem('__version__');
    if (version !== STORAGE_VERSION) {
        localStorage.clear();
        localStorage.setItem('__version__', STORAGE_VERSION);
    }
};

initWithVersion();

下一步 #

现在你已经掌握了本地存储,接下来学习 文件处理,了解文件操作和 Base64 转换!

最后更新:2026-04-04