createStorage
用于创建和管理本地存储(localStorage 或 sessionStorage)的工具
createStorage
createStorage 是一个用于创建和管理本地存储(localStorage 或 sessionStorage)的工具。它提供了类型安全的存储访问、跨标签页数据同步、以及 React Hooks 支持等特性。
特性
- 🔒 类型安全: 完整的 TypeScript 类型支持
- 🔄 跨标签页同步: 自动同步不同标签页之间的数据变化
- ⚡ 性能优化: 内置缓存机制,避免不必要的重新渲染
- 🎣 React Hooks 支持: 提供
useValueHook 订阅数据变化 - 📦 命名空间隔离: 通过命名空间避免不同模块间的存储冲突
- 🛡️ 数据一致性: 通过深度比较确保数据变化时才触发更新
基本用法
导入
import { createStorage } from '@dune2/tools/storage';
创建存储实例
// 定义数据结构
class DataMap {
token = '';
userInfo = { name: '', age: 0 };
theme = 'light';
preferences = {
language: 'zh-CN',
fontSize: 14,
notifications: true,
};
}
// 创建存储实例
const storage = createStorage({
DataMap,
namespace: 'myApp',
storageType: 'local', // 可选,默认为 "local"
});
基础操作
// 获取值
const token = storage.token.get(); // 获取 token,若不存在返回 ""
// 设置值
storage.token.set('new_token'); // 设置新的 token
// 删除值
storage.token.remove(); // 删除 token
storage.token.set(undefined); // 等同于 remove()
// 对象类型的存储
const userInfo = storage.userInfo.get();
storage.userInfo.set({ name: '张三', age: 25 });
// 获取存储键名
console.log(storage.token.key); // "myApp.token"
在 React 组件中使用
function ThemeSwitch() {
const theme = storage.theme.useValue(); // 自动订阅值的变化
const toggleTheme = () => {
storage.theme.set(theme === 'light' ? 'dark' : 'light');
};
return <button onClick={toggleTheme}>当前主题:{theme}</button>;
}
function UserProfile() {
const userInfo = storage.userInfo.useValue();
const token = storage.token.useValue();
if (!token) {
return <div>请先登录</div>;
}
return (
<div>
<h1>欢迎,{userInfo.name}</h1>
<p>年龄:{userInfo.age}</p>
</div>
);
}
配置选项
CreateStorageConfig
interface CreateStorageConfig<T> {
DataMap: new () => T;
namespace: string;
storageType?: 'local' | 'session';
}
参数说明
DataMap: 用于生成存储映射的类,其属性值将作为对应存储项的默认值namespace: 命名空间,用于隔离不同模块的存储,会作为存储键名的前缀storageType: 存储类型"local": 使用 localStorage(默认值)"session": 使用 sessionStorage
StorageHelper API
每个存储项都是一个 StorageHelper 实例,提供以下方法和属性:
方法
get(): T | undefined
获取存储值,若值不存在则返回默认值。
const theme = storage.theme.get(); // 返回 "light" 或存储的值
set(value: T | undefined): void
设置存储值,传入 undefined 时会删除该存储项。
storage.theme.set('dark'); // 设置主题为深色
storage.theme.set(undefined); // 删除主题设置
remove(): void
删除存储项(等同于 set(undefined))。
storage.token.remove(); // 删除 token
useValue(): T
React Hook,用于在组件中订阅存储值的变化。
function MyComponent() {
const theme = storage.theme.useValue();
// 当其他地方修改 theme 时,组件会自动重新渲染
return <div className={theme}>内容</div>;
}
subscribe(listener: () => void): () => void
订阅存储值的变化。
const unsubscribe = storage.theme.subscribe(() => {
console.log('主题已更改为:', storage.theme.get());
});
// 取消订阅
unsubscribe();
属性
key: string
完整的存储键名(包含命名空间)。
console.log(storage.token.key); // "myApp.token"
defaultValue: T
存储项的默认值。
console.log(storage.theme.defaultValue); // "light"
高级特性
缓存机制
为了优化性能和避免不必要的更新,内部实现了值缓存机制:
class DataMap {
config = { theme: 'light', fontSize: 14 };
}
const storage = createStorage({ DataMap, namespace: 'app' });
// 1. 防止重复渲染
function ConfigPanel() {
const config = storage.config.useValue();
// config 的引用在值未变化时保持不变,不会导致不必要的重复渲染
return <div>主题:{config.theme}</div>;
}
// 2. 智能更新
const currentConfig = storage.config.get();
storage.config.set({ ...currentConfig }); // 值未实际变化,不会触发更新事件
跨标签页同步
当在一个标签页中修改存储值时,其他标签页会自动同步更新:
// 标签页 A
function PageA() {
const theme = storage.theme.useValue();
return (
<div>
<div>当前主题:{theme}</div>
<button onClick={() => storage.theme.set('dark')}>切换到深色</button>
</div>
);
}
// 标签页 B - 会自动同步更新
function PageB() {
const theme = storage.theme.useValue();
// 当标签页 A 中点击按钮时,这里会自动更新
return <div>主题已更新为:{theme}</div>;
}
类型安全
通过 TypeScript 的类型推导,可以获得完整的类型提示和检查:
class DataMap {
count: number = 0;
user: { name: string; age: number } | null = null;
settings: {
theme: 'light' | 'dark';
language: string;
} = {
theme: 'light',
language: 'zh-CN',
};
}
const storage = createStorage({ DataMap, namespace: 'app' });
// ✅ 正确的使用
storage.count.set(42);
storage.user.set({ name: '张三', age: 25 });
storage.settings.set({ theme: 'dark', language: 'en-US' });
// ❌ 类型错误
storage.count.set('123'); // 错误:类型不匹配
storage.user.set({ name: '张三' }); // 错误:缺少必需属性 'age'
storage.settings.set({ theme: 'blue' }); // 错误:theme 值不在联合类型中
实际应用示例
1. 用户认证状态管理
class AuthDataMap {
token = '';
user = null as { id: string; name: string; email: string } | null;
isLoggedIn = false;
lastLoginTime = 0;
}
const authStorage = createStorage({
DataMap: AuthDataMap,
namespace: 'auth',
storageType: 'local',
});
// 认证服务
class AuthService {
static login(token: string, user: AuthDataMap['user']) {
authStorage.token.set(token);
authStorage.user.set(user);
authStorage.isLoggedIn.set(true);
authStorage.lastLoginTime.set(Date.now());
}
static logout() {
authStorage.token.remove();
authStorage.user.remove();
authStorage.isLoggedIn.set(false);
authStorage.lastLoginTime.remove();
}
static isAuthenticated(): boolean {
return authStorage.isLoggedIn.get();
}
}
// 使用在组件中
function LoginStatus() {
const isLoggedIn = authStorage.isLoggedIn.useValue();
const user = authStorage.user.useValue();
if (isLoggedIn && user) {
return (
<div>
<span>欢迎,{user.name}</span>
<button onClick={() => AuthService.logout()}>退出</button>
</div>
);
}
return (
<button
onClick={() => {
/* 打开登录弹窗 */
}}
>
登录
</button>
);
}
2. 应用设置管理
class SettingsDataMap {
theme = 'light' as 'light' | 'dark';
language = 'zh-CN';
fontSize = 14;
notifications = {
email: true,
push: true,
sms: false,
};
sidebar = {
collapsed: false,
width: 240,
};
}
const settingsStorage = createStorage({
DataMap: SettingsDataMap,
namespace: 'settings',
});
// 设置管理器
class SettingsManager {
static updateTheme(theme: 'light' | 'dark') {
settingsStorage.theme.set(theme);
document.documentElement.setAttribute('data-theme', theme);
}
static updateNotifications(
type: keyof SettingsDataMap['notifications'],
enabled: boolean,
) {
const current = settingsStorage.notifications.get();
settingsStorage.notifications.set({
...current,
[type]: enabled,
});
}
static resetToDefault() {
const defaultSettings = new SettingsDataMap();
Object.keys(defaultSettings).forEach((key) => {
settingsStorage[key].set(defaultSettings[key]);
});
}
}
// 设置面板组件
function SettingsPanel() {
const theme = settingsStorage.theme.useValue();
const language = settingsStorage.language.useValue();
const fontSize = settingsStorage.fontSize.useValue();
const notifications = settingsStorage.notifications.useValue();
return (
<div>
<h2>应用设置</h2>
<div>
<label>主题:</label>
<select
value={theme}
onChange={(e) =>
SettingsManager.updateTheme(e.target.value as 'light' | 'dark')
}
>
<option value='light'>浅色</option>
<option value='dark'>深色</option>
</select>
</div>
<div>
<label>语言:</label>
<select
value={language}
onChange={(e) => settingsStorage.language.set(e.target.value)}
>
<option value='zh-CN'>中文</option>
<option value='en-US'>English</option>
</select>
</div>
<div>
<label>字体大小:</label>
<input
type='number'
value={fontSize}
onChange={(e) => settingsStorage.fontSize.set(Number(e.target.value))}
/>
</div>
<div>
<h3>通知设置</h3>
<label>
<input
type='checkbox'
checked={notifications.email}
onChange={(e) =>
SettingsManager.updateNotifications('email', e.target.checked)
}
/>
邮件通知
</label>
<label>
<input
type='checkbox'
checked={notifications.push}
onChange={(e) =>
SettingsManager.updateNotifications('push', e.target.checked)
}
/>
推送通知
</label>
</div>
<button onClick={() => SettingsManager.resetToDefault()}>
重置为默认设置
</button>
</div>
);
}
3. 购物车管理
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
image: string;
}
class CartDataMap {
items: CartItem[] = [];
total = 0;
itemCount = 0;
lastUpdated = 0;
}
const cartStorage = createStorage({
DataMap: CartDataMap,
namespace: 'cart',
});
class CartManager {
static addItem(item: Omit<CartItem, 'quantity'>, quantity = 1) {
const items = cartStorage.items.get();
const existingItem = items.find((i) => i.id === item.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
items.push({ ...item, quantity });
}
this.updateCart(items);
}
static removeItem(itemId: string) {
const items = cartStorage.items.get().filter((item) => item.id !== itemId);
this.updateCart(items);
}
static updateQuantity(itemId: string, quantity: number) {
const items = cartStorage.items.get();
const item = items.find((i) => i.id === itemId);
if (item) {
if (quantity <= 0) {
this.removeItem(itemId);
} else {
item.quantity = quantity;
this.updateCart(items);
}
}
}
static clear() {
cartStorage.items.set([]);
cartStorage.total.set(0);
cartStorage.itemCount.set(0);
cartStorage.lastUpdated.set(Date.now());
}
private static updateCart(items: CartItem[]) {
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
cartStorage.items.set(items);
cartStorage.total.set(total);
cartStorage.itemCount.set(itemCount);
cartStorage.lastUpdated.set(Date.now());
}
}
// 购物车组件
function CartButton() {
const itemCount = cartStorage.itemCount.useValue();
return <button>购物车 ({itemCount})</button>;
}
function CartSummary() {
const items = cartStorage.items.useValue();
const total = cartStorage.total.useValue();
return (
<div>
<h3>购物车</h3>
{items.map((item) => (
<div key={item.id}>
<span>{item.name}</span>
<span>
¥{item.price} × {item.quantity}
</span>
<button
onClick={() =>
CartManager.updateQuantity(item.id, item.quantity - 1)
}
>
-
</button>
<button
onClick={() =>
CartManager.updateQuantity(item.id, item.quantity + 1)
}
>
+
</button>
<button onClick={() => CartManager.removeItem(item.id)}>删除</button>
</div>
))}
<div>总计:¥{total}</div>
<button onClick={() => CartManager.clear()}>清空购物车</button>
</div>
);
}
最佳实践
1. 合理设计数据结构
// ✅ 好的做法 - 扁平化结构
class AppDataMap {
theme = 'light';
userId = '';
userName = '';
userEmail = '';
notificationEnabled = true;
sidebarCollapsed = false;
}
// ❌ 避免的做法 - 过度嵌套
class AppDataMap {
user = {
profile: {
basic: {
name: '',
email: '',
},
settings: {
notifications: {
email: true,
push: true,
},
},
},
};
}
2. 使用命名空间隔离
// 用户相关存储
const userStorage = createStorage({
DataMap: UserDataMap,
namespace: 'user',
});
// 应用设置存储
const settingsStorage = createStorage({
DataMap: SettingsDataMap,
namespace: 'settings',
});
// 缓存数据存储(使用 sessionStorage)
const cacheStorage = createStorage({
DataMap: CacheDataMap,
namespace: 'cache',
storageType: 'session',
});
注意事项
-
存储限制: localStorage 通常有 5-10MB 的存储限制,请合理使用存储空间。
-
数据序列化: 复杂对象会被序列化为 JSON,确保数据可以被正确序列化和反序列化。
-
浏览器支持: 确保目标浏览器支持 localStorage 和 sessionStorage。
-
数据迁移: 当数据结构发生变化时,需要考虑向后兼容性。
-
隐私模式: 在浏览器隐私模式下,存储功能可能受限。
-
性能考虑: 避免频繁的大量数据存储操作,合理使用缓存机制。
createStorage 提供了一个强大而灵活的本地存储解决方案,特别适合需要跨组件共享状态且希望数据持久化的应用场景。