前言
在公司项目里,本地存储是我们绕不开的一个部分。像保存登录 token、用户偏好设置、缓存一些接口数据,基本都会用到 本地存储。
一开始用觉得挺顺手的,API比较简单好用,也不用管太多。但项目一大、业务一多、接手的人一换,就会发现这个 API 太“随便”了:什么类型都能存,key 想叫什么就叫什么,哪怕重复了都没人提醒。
我们当时就遇到几个问题:有的模块用了同一个 key 结果互相覆盖了;本地存储散落在项目各处,没有很规范的管理方式;还有一堆历史遗留的 key,不知道谁用过、敢不敢删。本地存取对象时需要手动进行序列化,在字符串和json直接进行转换。
后来我就想着,能不能把这块稍微规范点?最好是:
- 有明确和统一的 key 管理方式;
- 存的时候知道自己在存什么类型,取出来也别担心报错;
- 操作起来尽量简单,别每次都手动 JSON 处理。
于是就封了一层小工具,统一管理这些本地数据,顺带带上类型提示。现在用下来,不管是自己写,还是和其他人协作,心里都更有底了。
直接使用本地存储API会出现的问题
1. 键名一多就乱了
本地存储一般 本质上就是一个全局的 key-value 存储,随手放一个键进去,也没人拦你。但时间一长,项目变复杂,谁存的什么、用了没用、能不能删,根本没人能说得清。
更要命的是,如果有两个地方用了同样的 key,会互相影响,很容易出现问题,还不好排查。
2. 没有类型提示,全靠记忆力
大部分本地存储只能存字符串,存个对象就得手动 JSON.stringify(),用的时候再 JSON.parse() 一下。
这个过程非常繁琐,还容易出错。比如有时候忘了 parse、写错了字段名,甚至数据格式变了,在开发中很容易出现问题。
我的解法:封装一个类型安全的 Storage
为了解决这些问题,我自己封装了一个 Storage 类,这篇文章我就以 LocalStorage 为例,当然,这套思路其实也可以照搬到 cookie、SessionStorage,甚至小程序、App 的本地存储上。
核心思路:
- 统一管理 key 和默认值
- 通过泛型实现类型提示
- 自动处理序列化类型
先上代码:
/**
* 泛型接口,用于定义每个存储项的 key 和默认值类型
*/
interface IStorage<T> {
key: string
defaultValue: T
}
/**
* 可统一设置 LocalStorage 的 key 前缀
*/
const prefix = 'MY_APP_'
/**
* Storage 封装类:用于类型安全地操作 localStorage
* 提供 get / set / remove 接口,自动处理 JSON 序列化、反序列化及异常情况
*
* @template T 存储的数据类型
*/
export class Storage<T> implements IStorage<T> {
key: string
defaultValue: T
/**
* 创建一个新的 Storage 实例
* @param key - 存储在 localStorage 中的 key(内部会自动添加前缀)
* @param defaultValue - 如果未设置值或解析失败时使用的默认值
*/
constructor(key: string, defaultValue: T) {
// 加前缀后存入 key,确保 key 命名统一、避免冲突
this.key = prefix + key
this.defaultValue = defaultValue
}
/**
* 设置值到 localStorage,会自动进行 JSON 序列化
* @param value - 要存储的值,类型由泛型决定
*/
set(value: T) {
try {
const str = JSON.stringify(value)
localStorage.setItem(this.key, str)
} catch (e) {
console.error(`Storage 设置失败: ${this.key}`, e)
}
}
/**
* 从 localStorage 中读取值,自动反序列化为指定类型
* 如果 key 不存在或解析失败,则返回默认值
* @returns 类型安全的值
*/
get(): T {
const raw = localStorage.getItem(this.key)
// 若为空值或非法值,直接返回默认值
if (!raw || raw === 'null' || raw === 'undefined') {
return this.defaultValue
}
try {
return JSON.parse(raw) as T
} catch (e) {
console.warn(`Storage 解析失败,返回默认值: ${this.key}`, e)
return this.defaultValue
}
}
/**
* 删除当前 key 对应的存储项
*/
remove() {
localStorage.removeItem(this.key)
}
}
使用方式
示例 1:存储字符串
// 创建一个 Storage 实例,用于存储字符串类型的 token,默认值为空字符串
export const tokenStorage = new Storage<string>('token', '')
// 存储 token 值
tokenStorage.set('abc123')
// 读取 token 值
const token = tokenStorage.get()
console.log('用户 token:', token)
// 删除 token 值
tokenStorage.remove()
如图所示,获取值的时候会有对应的类型提示:
示例 2:存储 JSON 对象
// 定义用户信息接口,明确属性类型
interface UserInfo {
id: number
name: string
email: string
}
// 创建一个 Storage 实例,用于存储 UserInfo 类型的数据,提供一个默认值
export const userInfoStorage = new Storage<UserInfo>('userInfo', {
id: 0,
name: '',
email: ''
})
// 存储用户信息
userInfoStorage.set({
id: 1,
name: 'John Doe',
email: 'john@example.com'
})
// 读取用户信息
const user = userInfoStorage.get()
console.log('用户信息:', user)
// 删除用户信息
userInfoStorage.remove()
如图所示,获取值的时候会有对应的类型提示,并且自动转对象,存储的时候也会有类型校验。
优化后可以明显发现:
- 编译器能提示类型,误操作少很多
- 每个 key 都是单独管理的,不容易搞混
- 数据异常时会自动返回默认值,不至于直接炸掉页面
- 本地存储可以进行统一管理,不会散落在所有的页面里面
- 页面代码也会清爽整洁很多
总结
虽然本地存储本身功能不复杂,但如果能封装成一个简单、可控、类型友好的工具类,确实能省下不少心力,也可以更好的管理全局的本地存储。
我这套方案只是一个轻量的实现思路,如果你在项目里经常需要操作本地数据,不妨根据自己的需求调整下逻辑,做出一套属于你自己的“小工具”。
如果你也在项目中有大量本地存储的场景,不妨试试看这种封装方式,写起来顺手,用起来放心。