谈谈我在前端开发中是怎么使用本地存储的

482 阅读5分钟

前言

在公司项目里,本地存储是我们绕不开的一个部分。像保存登录 token、用户偏好设置、缓存一些接口数据,基本都会用到 本地存储

一开始用觉得挺顺手的,API比较简单好用,也不用管太多。但项目一大、业务一多、接手的人一换,就会发现这个 API 太“随便”了:什么类型都能存,key 想叫什么就叫什么,哪怕重复了都没人提醒。

我们当时就遇到几个问题:有的模块用了同一个 key 结果互相覆盖了;本地存储散落在项目各处,没有很规范的管理方式;还有一堆历史遗留的 key,不知道谁用过、敢不敢删。本地存取对象时需要手动进行序列化,在字符串和json直接进行转换。

后来我就想着,能不能把这块稍微规范点?最好是:

  • 有明确和统一的 key 管理方式;
  • 存的时候知道自己在存什么类型,取出来也别担心报错;
  • 操作起来尽量简单,别每次都手动 JSON 处理。

于是就封了一层小工具,统一管理这些本地数据,顺带带上类型提示。现在用下来,不管是自己写,还是和其他人协作,心里都更有底了。

直接使用本地存储API会出现的问题

1. 键名一多就乱了

本地存储一般 本质上就是一个全局的 key-value 存储,随手放一个键进去,也没人拦你。但时间一长,项目变复杂,谁存的什么、用了没用、能不能删,根本没人能说得清。

更要命的是,如果有两个地方用了同样的 key,会互相影响,很容易出现问题,还不好排查。

2. 没有类型提示,全靠记忆力

大部分本地存储只能存字符串,存个对象就得手动 JSON.stringify(),用的时候再 JSON.parse() 一下。

这个过程非常繁琐,还容易出错。比如有时候忘了 parse、写错了字段名,甚至数据格式变了,在开发中很容易出现问题。

我的解法:封装一个类型安全的 Storage

为了解决这些问题,我自己封装了一个 Storage 类,这篇文章我就以 LocalStorage 为例,当然,这套思路其实也可以照搬到 cookieSessionStorage,甚至小程序、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()            

如图所示,获取值的时候会有对应的类型提示:

1.png

示例 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()

如图所示,获取值的时候会有对应的类型提示,并且自动转对象,存储的时候也会有类型校验。

2.jpg

优化后可以明显发现:

  • 编译器能提示类型,误操作少很多
  • 每个 key 都是单独管理的,不容易搞混
  • 数据异常时会自动返回默认值,不至于直接炸掉页面
  • 本地存储可以进行统一管理,不会散落在所有的页面里面
  • 页面代码也会清爽整洁很多

总结

虽然本地存储本身功能不复杂,但如果能封装成一个简单、可控、类型友好的工具类,确实能省下不少心力,也可以更好的管理全局的本地存储。

我这套方案只是一个轻量的实现思路,如果你在项目里经常需要操作本地数据,不妨根据自己的需求调整下逻辑,做出一套属于你自己的“小工具”。

如果你也在项目中有大量本地存储的场景,不妨试试看这种封装方式,写起来顺手,用起来放心。