优雅使用本地存储
🤔 为什么需要本地存储?
在前端开发中,我们经常需要在浏览器中存储一些数据,比如:
- 用户的偏好设置(如主题、语言)
- 表单的部分填写内容(防止页面刷新丢失)
- 应用的状态数据
- 缓存的API数据
传统的Cookie存储有很多限制(大小、安全性等),而本地存储(LocalStorage和SessionStorage)提供了更方便、更安全的数据存储方式。但是,你真的会优雅地使用本地存储吗?
今天,我们就来学习如何优雅地使用本地存储,包括封装、类型安全、错误处理等方面的技巧!
💡 本地存储的基础使用
1. LocalStorage vs SessionStorage
| 特性 | LocalStorage | SessionStorage |
|---|---|---|
| 存储时长 | 永久存储,除非手动删除 | 会话期间有效,关闭标签页/浏览器后删除 |
| 存储大小 | 约5MB | 约5MB |
| 作用域 | 同源的所有标签页/窗口共享 | 仅当前标签页/窗口有效 |
| 数据类型 | 仅支持字符串 | 仅支持字符串 |
2. 基础操作
// 存储数据
localStorage.setItem('username', '张三');
// 获取数据
const username = localStorage.getItem('username');
console.log(username); // '张三'
// 删除数据
localStorage.removeItem('username');
// 清空所有数据
localStorage.clear();
// 获取所有键
const keys = Object.keys(localStorage);
console.log(keys);
🚀 优雅的本地存储封装
直接使用localStorage有很多缺点:
- 只能存储字符串,需要手动序列化/反序列化
- 没有类型安全
- 没有错误处理
- 键名容易冲突
- 没有过期时间
让我们创建一个优雅的本地存储封装,解决这些问题!
1. 基础封装
/**
* 本地存储工具类
*/
class StorageUtil {
constructor(prefix = 'app_') {
this.prefix = prefix;
this.storage = localStorage;
}
/**
* 生成带前缀的键名
* @param {string} key - 原始键名
* @returns {string} 带前缀的键名
*/
getKey(key) {
return `${this.prefix}${key}`;
}
/**
* 存储数据
* @param {string} key - 键名
* @param {any} value - 要存储的值
*/
set(key, value) {
try {
const serializedValue = JSON.stringify(value);
this.storage.setItem(this.getKey(key), serializedValue);
} catch (error) {
console.error('存储数据失败:', error);
}
}
/**
* 获取数据
* @param {string} key - 键名
* @param {any} defaultValue - 默认值
* @returns {any} 存储的值或默认值
*/
get(key, defaultValue = null) {
try {
const serializedValue = this.storage.getItem(this.getKey(key));
if (serializedValue === null) {
return defaultValue;
}
return JSON.parse(serializedValue);
} catch (error) {
console.error('获取数据失败:', error);
return defaultValue;
}
}
/**
* 删除数据
* @param {string} key - 键名
*/
remove(key) {
try {
this.storage.removeItem(this.getKey(key));
} catch (error) {
console.error('删除数据失败:', error);
}
}
/**
* 清空所有数据
*/
clear() {
try {
// 只清空带前缀的数据
const keys = Object.keys(this.storage);
keys.forEach(key => {
if (key.startsWith(this.prefix)) {
this.storage.removeItem(key);
}
});
} catch (error) {
console.error('清空数据失败:', error);
}
}
/**
* 获取所有键名
* @returns {string[]} 所有键名
*/
keys() {
try {
const keys = Object.keys(this.storage);
return keys
.filter(key => key.startsWith(this.prefix))
.map(key => key.replace(this.prefix, ''));
} catch (error) {
console.error('获取键名失败:', error);
return [];
}
}
}
// 使用示例
const storage = new StorageUtil('my_app_');
storage.set('user', { id: 1, name: '张三' });
const user = storage.get('user');
console.log(user); // { id: 1, name: '张三' }
2. 支持过期时间的封装
/**
* 带过期时间的本地存储工具类
*/
class ExpiringStorageUtil extends StorageUtil {
constructor(prefix = 'app_', storage = localStorage) {
super(prefix, storage);
}
/**
* 存储带过期时间的数据
* @param {string} key - 键名
* @param {any} value - 要存储的值
* @param {number} ttl - 过期时间(毫秒)
*/
set(key, value, ttl = null) {
const item = {
value,
expiry: ttl ? Date.now() + ttl : null
};
super.set(key, item);
}
/**
* 获取数据(自动检查过期)
* @param {string} key - 键名
* @param {any} defaultValue - 默认值
* @returns {any} 存储的值或默认值
*/
get(key, defaultValue = null) {
const item = super.get(key);
if (item === null) {
return defaultValue;
}
// 检查是否过期
if (item.expiry && Date.now() > item.expiry) {
super.remove(key);
return defaultValue;
}
return item.value;
}
}
// 使用示例
const expiringStorage = new ExpiringStorageUtil('my_app_');
// 存储2小时后过期的数据
expiringStorage.set('temp_data', '这是临时数据', 2 * 60 * 60 * 1000);
const tempData = expiringStorage.get('temp_data');
console.log(tempData); // '这是临时数据'
3. TypeScript版本(类型安全)
/**
* 带类型安全的本地存储工具类
*/
class TypedStorageUtil<T extends Record<string, any>> {
private prefix: string;
private storage: Storage;
constructor(prefix = 'app_', storage: Storage = localStorage) {
this.prefix = prefix;
this.storage = storage;
}
/**
* 生成带前缀的键名
*/
private getKey<K extends keyof T>(key: K): string {
return `${this.prefix}${String(key)}`;
}
/**
* 存储数据
*/
set<K extends keyof T>(key: K, value: T[K], ttl?: number): void {
try {
const item = {
value,
expiry: ttl ? Date.now() + ttl : null
};
const serializedValue = JSON.stringify(item);
this.storage.setItem(this.getKey(key), serializedValue);
} catch (error) {
console.error('存储数据失败:', error);
throw error;
}
}
/**
* 获取数据
*/
get<K extends keyof T>(key: K, defaultValue: T[K]): T[K] {
try {
const serializedValue = this.storage.getItem(this.getKey(key));
if (serializedValue === null) {
return defaultValue;
}
const item = JSON.parse(serializedValue);
// 检查是否过期
if (item.expiry && Date.now() > item.expiry) {
this.storage.removeItem(this.getKey(key));
return defaultValue;
}
return item.value;
} catch (error) {
console.error('获取数据失败:', error);
return defaultValue;
}
}
/**
* 删除数据
*/
remove<K extends keyof T>(key: K): void {
try {
this.storage.removeItem(this.getKey(key));
} catch (error) {
console.error('删除数据失败:', error);
throw error;
}
}
/**
* 清空所有数据
*/
clear(): void {
try {
const keys = Object.keys(this.storage);
keys.forEach(key => {
if (key.startsWith(this.prefix)) {
this.storage.removeItem(key);
}
});
} catch (error) {
console.error('清空数据失败:', error);
throw error;
}
}
}
// 使用示例
interface AppStorage {
user: { id: number; name: string };
theme: 'light' | 'dark';
language: string;
preferences: { notifications: boolean; autoSave: boolean };
}
const storage = new TypedStorageUtil<AppStorage>('my_app_');
// 类型安全的存储
storage.set('user', { id: 1, name: '张三' });
storage.set('theme', 'dark');
storage.set('language', 'zh-CN');
// 类型安全的获取
const user = storage.get('user', { id: 0, name: '' });
const theme = storage.get('theme', 'light');
const language = storage.get('language', 'en-US');
// 错误的类型会被TypeScript检查到
// storage.set('theme', 'red'); // 类型错误
🎯 React中的本地存储使用
在React中,我们可以创建自定义Hook来优雅地使用本地存储。
1. 基础自定义Hook
import { useState, useEffect } from 'react';
/**
* 本地存储Hook
* @param {string} key - 存储键名
* @param {any} initialValue - 初始值
* @param {Object} options - 选项
* @returns {[any, function]} [存储的值, 设置值的函数]
*/
function useLocalStorage(key, initialValue, options = {}) {
const { prefix = 'react_', storage = localStorage } = options;
const prefixedKey = `${prefix}${key}`;
// 从本地存储获取初始值
const getStoredValue = () => {
try {
const item = storage.getItem(prefixedKey);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('获取本地存储失败:', error);
return initialValue;
}
};
const [storedValue, setStoredValue] = useState(getStoredValue);
// 设置值
const setValue = (value) => {
try {
// 允许值是函数,类似于useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
storage.setItem(prefixedKey, JSON.stringify(valueToStore));
} catch (error) {
console.error('设置本地存储失败:', error);
}
};
return [storedValue, setValue];
}
// 使用示例
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<div className={`app ${theme}`}>
<button onClick={toggleTheme}>
切换到{theme === 'light' ? '深色' : '浅色'}主题
</button>
</div>
);
}
2. 支持过期时间的Hook
import { useState, useEffect } from 'react';
/**
* 带过期时间的本地存储Hook
* @param {string} key - 存储键名
* @param {any} initialValue - 初始值
* @param {number} ttl - 过期时间(毫秒)
* @returns {[any, function]} [存储的值, 设置值的函数]
*/
function useLocalStorageWithExpiry(key, initialValue, ttl) {
const prefixedKey = `react_${key}`;
// 从本地存储获取初始值
const getStoredValue = () => {
try {
const item = localStorage.getItem(prefixedKey);
if (!item) {
return initialValue;
}
const parsedItem = JSON.parse(item);
// 检查是否过期
if (parsedItem.expiry && Date.now() > parsedItem.expiry) {
localStorage.removeItem(prefixedKey);
return initialValue;
}
return parsedItem.value;
} catch (error) {
console.error('获取本地存储失败:', error);
return initialValue;
}
};
const [storedValue, setStoredValue] = useState(getStoredValue);
// 设置值
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
const item = {
value: valueToStore,
expiry: ttl ? Date.now() + ttl : null
};
setStoredValue(valueToStore);
localStorage.setItem(prefixedKey, JSON.stringify(item));
} catch (error) {
console.error('设置本地存储失败:', error);
}
};
return [storedValue, setValue];
}
// 使用示例
function CacheExample() {
const [cachedData, setCachedData] = useLocalStorageWithExpiry(
'api_cache',
null,
5 * 60 * 1000 // 5分钟过期
);
// 模拟API请求
const fetchData = async () => {
if (cachedData) {
console.log('使用缓存数据');
return;
}
console.log('发起API请求');
// 模拟API请求
const data = await new Promise(resolve => {
setTimeout(() => {
resolve({ message: '这是API数据' });
}, 1000);
});
setCachedData(data);
};
return (
<div>
<button onClick={fetchData}>
获取数据
</button>
{cachedData && (
<div>
<h3>数据:</h3>
<p>{JSON.stringify(cachedData)}</p>
</div>
)}
</div>
);
}
⚠️ 注意事项与最佳实践
1. 安全性考虑
- 敏感数据不要存储在本地存储中:本地存储是明文存储的,不安全
- 使用HTTPS:确保数据在传输过程中是加密的
- 定期清理:不要永久存储不需要的数据
2. 性能优化
- 不要存储大量数据:本地存储的大小有限(约5MB)
- 避免频繁读写:频繁的读写会影响性能
- 缓存数据:使用本地存储缓存不经常变化的数据
3. 兼容性
- 本地存储在现代浏览器中都支持
- 对于不支持的浏览器,可以提供polyfill
- 检查浏览器是否支持本地存储
function isLocalStorageSupported() {
try {
const test = 'test';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
4. 错误处理
- 始终使用try/catch包裹本地存储操作
- 考虑存储配额满的情况
- 提供降级方案
5. 命名规范
- 使用前缀避免键名冲突
- 使用有意义的键名
- 保持键名的一致性
📝 总结
本地存储是前端开发中非常有用的功能,但直接使用原生API有很多缺点。通过封装,我们可以让本地存储变得更加优雅、安全、易用。
通过本文的介绍,我们学习了:
- 基础使用:LocalStorage和SessionStorage的区别和基础操作
- 优雅封装:解决了类型安全、错误处理、键名冲突等问题
- 高级功能:支持过期时间、类型安全等高级特性
- React集成:创建了自定义Hook来在React中优雅使用本地存储
- 最佳实践:安全性、性能、兼容性等方面的注意事项
希望这些小技巧对你有所帮助!下次使用本地存储时,不妨试试这些优雅的方法吧~✨
相关资源:
标签: #前端开发 #本地存储 #React #TypeScript #性能优化
840

被折叠的 条评论
为什么被折叠?



