likes
comments
collection
share

【VueUse】useStorage让浏览器的Storage具有响应式

作者站长头像
站长
· 阅读数 41

这是我开设第一个系列文章,旨在提升自己的源码阅读和调试能力、学习到更多的代码语法和逻辑。因为我是个TypeScript 新手,我会在末尾对遇到的 TS 语法进行总结,十分建议初学 TS 的同学阅读这篇文章,配合源码使用更佳。此系列文章对不懂 TS 的人员也能无障碍阅读,欢迎大家点赞收藏。

useStorage 源码地址

如何调试

  1. 克隆最新的 VueUse 仓库 git clone https://github.com/vueuse/vueuse.git
  2. 安装依赖,因为依赖管理器配置的是 pnpm,所以使用 pnpm 来安装依赖 pnpm install
  3. 启动项目 pnpm dev
  4. 如果没有意外的话,就能在浏览器上看到运行在本地的官方文档页面
  5. /packages/core 文件夹下找到想要调试的目标方法,在 VSCode 和浏览器源代码中设置断点

接下来就可以开始愉快的调试了!

系列文章

TS 语法介绍

在阅读时可能会涉及到一些 TS 中的基本语法,我先在这做个介绍,后面有不理解的可以跳转到这查看,TS 大神可直接跳过。

  • T:是泛型的一个约定俗成的名称,它代表 Type(类型) 的缩写。泛型是指在定义函数、类或接口时不预先指定具体的类型,而是在使用时再指定类型。通过泛型,可以更加灵活地编写可重用的代码。
  • keyof:用于获得某个类型的所有可索引属性名称的联合类型。例如,如果有一个对象类型Person,它有两个属性nameage,那么keyof Person的结果就是 name | age,即一个字符串字面量类型的联合类型
  • as:用于将一个表达式的类型指定为另外一种类型。在编写代码时,有时候需要把一个变量或表达式的类型转换为我们期望的类型,这时候就可以使用as关键字进行类型断言。
  • Omit:是 TS 中的一个工具类型,用于从已有类型中删除指定的属性,并返回剩余的那些属性所构成的新类型。
  • Partial<T>:是 TS 中的一个工具类型,用于将T类型中的所有属性都变为可选,把原来每个属性的类型都变成原类型和null | undefined的联合类型。
  • Record<K, T>:类型别名,用于定义一个由指定类型的键和值组成的对象类型,表示对象的键名为K类型,键值为T类型。
  • 函数重载:是指可以定义一组具有相同名称但参数数量或类型不同的函数实现。在调用这个函数时,TS 编译器会根据传递的参数自动选择并调用匹配的重载函数。
  • 泛型约束:用于限制泛型类型T参数的类型范围,从而提高代码的类型安全性。泛型约束可以通过关键字 extends 来实现

useStorage

用途:创建一个响应式变量,该变量会自动与 LocalStorageSeesionStorage 同步

使用方法

import { useStorage } from '@vueuse/core'

// object 类型
const state = useStorage('my-store', { hello: 'hi', greeting: 'Hello' })

// boolean 类型
const flag = useStorage('my-flag', true) // returns Ref<boolean>

// number 类型
const count = useStorage('my-count', 0) // returns Ref<number>

// SessionStorage 保存数据
const id = useStorage('my-id', 'some-string-id', sessionStorage) // returns Ref<string>

// 从浏览器存储中删除数据
state.value = null

参数解析

源码位于/packages/core/useStorage/index.ts,在这一段源码中就用到了 TS 中的泛型、函数重载和泛型约束,不懂可看上面章节👆。话不多说,先看参数。 【VueUse】useStorage让浏览器的Storage具有响应式 对于不懂 TS 的来说,有些地方需要解释下:

  • 给函数 useStorage 添加类型参数时,使用了泛型约束:<T extends(string | number | boolean | object | null)>,表示泛型参数T必须是这些类型的其中之一,同时函数里面的泛型参数T也会受到这样的约束,例如MaybeRefOrGetter<T>UseStorageOptions<T>
  • 为什么文件导出多个同名的useStorage?这里用到了 TS 里的函数重载,直接看最后一个即可。

useStorage接受四个参数,key为必传参数,其他的为可选参数

  • key:保存在本地存储中的键名,用于读取和写入数据。
  • defaults:数据默认值,如果本地存储中没有数据时,则使用默认值。类型为MaybeRefOrGetter,类型定义代码如下,总结就是可能是 Ref 类型的值,可能是具体的 T 类型的值,或者是一个返回 T 类型的函数。
// 从 vue-demi 模块中导入 Ref 类型的定义
// vue-demi 能够允许编写 Vue2 和 Vue3 的通用库
import type { Ref } from 'vue-demi'

// 定义类型别名 MaybeRef<T>,表示可以是 T 类型或是 Ref<T> 类型
// 其中 T 是泛型类型参数,如果值为 Ref 类型则直接使用,否则默认当成常规类型来使用
export type MaybeRef<T> = T | Ref<T>

// 定义类型别名 MaybeRefOrGetter<T>,表示可以是 MaybeRef<T> 类型或者一个返回 T 类型的函数
// 如果传递的参数是函数,则返回值必须符合类型 T
export type MaybeRefOrGetter<T> = MaybeRef<T> | (() => T)
  • storage:指定使用的存储类型,localStoragesessionStorage,默认为localStorage,参数类型为 StorageLikeundefined,定义StorageLike代码如下,表示传递storage对象上必须要有getItem()setItem()removeItem()这三个方法。
export interface StorageLike {
  getItem(key: string): string | null
  setItem(key: string, value: string): void
  removeItem(key: string): void
}
  • options:可选的配置对象,UseStorageOptions 类型,下面在关键逻辑解析部分会涉及到这里的配置。
export interface UseStorageOptions<T> extends ConfigurableEventFilter, ConfigurableWindow, ConfigurableFlush {
  /**
   * Watch for deep changes
   * 是否深度监听数据变化
   *
   * @default true
   */
  deep?: boolean

  /**
   * Listen to storage changes, useful for multiple tabs application
   * 监听存储变化
   *
   * @default true
   */
  listenToStorageChanges?: boolean

  /**
   * Write the default value to the storage when it does not exist
   * 当默认值不存在时是否将其写入存储
   *
   * @default true
   */
  writeDefaults?: boolean

  /**
   * Merge the default value with the value read from the storage.
   * 当设置了默认值并 storage 中的 key 已存在的情况下,是否将数据进行合并
   * 为false 时,storage 中的数据会覆盖设置的默认值
   * 为true 时,会合并数据,若 storage 中的数据和默认值存在相同的属性,则采用 storage 中的数据
   *
   * When setting it to true, it will perform a **shallow merge** for objects.
   * 为 true 时,对于对象来说只是进行浅合并
   *
   * You can pass a function to perform custom merge (e.g. deep merge), for example:
	 * 可以传一个函数可以进行自定义合并
   *
   * @default false
   */
  mergeDefaults?: boolean | ((storageValue: T, defaults: T) => T)

  /**
   * Custom data serialization
   * 自定义数据序列化,查看定义 Serializer 接口代码可知
   * serializer 对象需要有 read 方法和 write 方法
   */
  serializer?: Serializer<T>

  /**
   * On error callback
   * 发生错误时的回调函数
   *
   * Default log error to `console.error`
   */
  onError?: (error: unknown) => void

  /**
   * Use shallow ref as reference
   * 是否使用 shallowRef 进行引用
   *
   * @default false
   */
  shallow?: boolean
}

主要逻辑

代码概览

下面我把里面一部分函数内代码先隐藏,先看看大概的逻辑,后面再对具体的功能点进行讲解

export function useStorage<T extends(string | number | boolean | object | null)>(
  key: string,
  defaults: MaybeRefOrGetter<T>,
  storage: StorageLike | undefined,
  options: UseStorageOptions<T> = {},
): RemovableRef<T> {
  const {
    flush = 'pre',
    deep = true,
    listenToStorageChanges = true,
    writeDefaults = true,
    mergeDefaults = false,
    shallow,
    window = defaultWindow,
    eventFilter,
    onError = (e) => {
      console.error(e)
    },
  } = options

  const data = (shallow ? shallowRef : ref)(defaults) as RemovableRef<T>

  if (!storage) {
    try {
      storage = getSSRHandler('getDefaultStorage', () => defaultWindow?.localStorage)()
    }
    catch (e) {
      onError(e)
    }
  }

  if (!storage)
    return data

  const rawInit: T = toValue(defaults)
  const type = guessSerializerType<T>(rawInit)
  const serializer = options.serializer ?? StorageSerializers[type]

  const { pause: pauseWatch, resume: resumeWatch } = pausableWatch(
    data,
    () => write(data.value),
    { flush, deep, eventFilter },
  )

  if (window && listenToStorageChanges) {
    useEventListener(window, 'storage', update)
    useEventListener(window, customStorageEventName, updateFromCustomEvent)
  }

  update()

  return data

  function write(v: unknown) {
    ...
  }

  function read(event?: StorageEventLike) {
    ...
  }

  function updateFromCustomEvent(event: CustomEvent<StorageEventLike>) {
    ...
  }

  function update(event?: StorageEventLike) {
    ...
  }
}

shallowRef 和 Ref

通过options中的shallow参数来控制数据为 ref 类型还是 shallowRef 类型,并且最后使用as将其转换为RemovableRef<T>

const data = (shallow ? shallowRef : ref)(defaults) as RemovableRef<T>

RemovableRef<T>Ref<T>接口类型进行了扩展,代码如下:

export type RemovableRef<T> = Omit<Ref<T>, 'value'> & {
  get value(): T
  set value(value: T | null | undefined)
}
  • 使用OmitRef<T>中剔除value属性;
  • 增加了get value(),返回T类型;
  • 增加了set value(value: T | null | undefined)方法,用于设置value值,可以接受T类型、null类型或undefined类型的参数;

获取缓存对象

if (!storage) {
  try {
    // 获取 storage 对象
    storage = getSSRHandler('getDefaultStorage', () => defaultWindow?.localStorage)()
  }
  catch (e) {
    onError(e)
  }
}

// 若当前环境没有 storage 对象,则直接返回 ref 或 shallowref 格式的初始值
if (!storage)
  return data

在这里如果没有指定缓存类型,会使用默认的 localStorage,源码这里对服务端渲染的项目做了处理,我简单的说下getSSRHandler()函数的作用:

  1. 首先会检查全局对象是有没有__vueuse_ssr_handlers__属性,此属性应该是服务端渲染时会添加到全局对象上的。
  2. 如果没有此对象的话就返回getSSRHandler()函数的第二个参数,也就是window.localStorage,获取storage对象。
  3. 发生错误时执行onError()回调函数。

数据序列化

// 获取默认数据
const rawInit: T = toValue(defaults)
// 获取数据类型
const type = guessSerializerType<T>(rawInit)
// 序列化器,若没有自己定义 options.serializer,则直接根据数据类型去选择默认的数据序列化器
const serializer = options.serializer ?? StorageSerializers[type]
  • toValue()- 函数意图很清晰,如果参数是函数的话就返回函数的返回值,不是函数的话就调用 Vue 中的 unref()函数并返回。
  • guessSerializerType()- 获取默认数据类型
export function guessSerializerType<T extends(string | number | boolean | object | null)>(rawInit: T) {
  return rawInit == null
    ? 'any'
    : rawInit instanceof Set
      ? 'set'
      : rawInit instanceof Map
        ? 'map'
        : rawInit instanceof Date
          ? 'date'
          : typeof rawInit === 'boolean'
            ? 'boolean'
            : typeof rawInit === 'string'
              ? 'string'
              : typeof rawInit === 'object'
                ? 'object'
                : !Number.isNaN(rawInit)
                    ? 'number'
                    : 'any'
}
  • StorageSerializers- 默认数据序列化器,里面定义不同类型的数据如何进行序列化和反序列化,Record 关键词上面有介绍👆
export interface Serializer<T> {
  read(raw: string): T
  write(value: T): string
}

export const StorageSerializers: Record<'boolean' | 'object' | 'number' | 'any' | 'string' | 'map' | 'set' | 'date', Serializer<any>> = {
  boolean: {
    read: (v: any) => v === 'true',
    write: (v: any) => String(v),
  },
  object: {
    read: (v: any) => JSON.parse(v),
    write: (v: any) => JSON.stringify(v),
  },
  number: {
    read: (v: any) => Number.parseFloat(v),
    write: (v: any) => String(v),
  },
  any: {
    read: (v: any) => v,
    write: (v: any) => String(v),
  },
  string: {
    read: (v: any) => v,
    write: (v: any) => String(v),
  },
  map: {
    read: (v: any) => new Map(JSON.parse(v)),
    write: (v: any) => JSON.stringify(Array.from((v as Map<any, any>).entries())),
  },
  set: {
    read: (v: any) => new Set(JSON.parse(v)),
    write: (v: any) => JSON.stringify(Array.from(v as Set<any>)),
  },
  date: {
    read: (v: any) => new Date(v),
    write: (v: any) => v.toISOString(),
  },
}

事件监听

// 当前环境为浏览器并且 options.listenToStorageChanges 为 true
if (window && listenToStorageChanges) {
  // 监听 localStorage 数据变化事件
  useEventListener(window, 'storage', update)
  // 监听自定义事件 vueuse-storage
  useEventListener(window, customStorageEventName, updateFromCustomEvent)
}

在这里面不对useEventListener进行解析,下一篇文章再来详细介绍它。

监听数据变化

参数options中有个隐藏的参数eventFilter,在useStorage文档中并没有标注。但是在下面使用到了这个参数。VueUse官方文档中配置项章节 Event Filters 有介绍,并且有示例参考。

const { pause: pauseWatch, resume: resumeWatch } = pausableWatch(
  data,
  () => write(data.value),
  { flush, deep, eventFilter },
)

pausableWatch的源码有点复杂,官方文档戳我,在这里就不详细介绍(其实我也没看懂,只能知道到该是干嘛的😅),大家感兴趣的话可以自行查看,在这里我介绍下它的作用:

  • 内部通过 Vue3 中的watch API 对 data 进行监听,数据发生变化时执行回调函数。
  • 通过传递eventFilter参数在可以执行回调函数之前可以对事件进行处理,/packages/shared/utils/filters中已内置防抖和节流。
  • 可以通过返回对象中的pauseresume方法来控制数据发生变化时回调函数暂停、恢复执行。

write 方法

用于将数据写入Storage

function write(v: unknown) {
  try {
    // 若新数据为 null,则从 Storage 中删除
    if (v == null) {
      storage!.removeItem(key)
    }
    else {
      // 数据序列化
      const serialized = serializer.write(v)
      // 获取 Storage 中的旧数据
      const oldValue = storage!.getItem(key)
      // 若新数据与旧数据不相等,才更新 Storage 中的数据
      if (oldValue !== serialized) {
        storage!.setItem(key, serialized)

        // 判断是否为浏览器环境
        if (window) {
          // 触发自定义事件 vueuse-storage
          window.dispatchEvent(new CustomEvent<StorageEventLike>(customStorageEventName, {
            // 传递数据
            detail: {
              key,
              oldValue,
              newValue: serialized,
              storageArea: storage!,
            },
          }))
        }
      }
    }
  }
  catch (e) {
    onError(e)
  }
}

read 方法

读取缓存中的最新数据,其中使用到了options中几个配置项:writeDefaultsmergeDefaults

// 当初始化时执行update()方法,参数 event 才为 undefined;其他情况时,event 的类型为 StorageEventLike
function read(event?: StorageEventLike) {
  // 初始化时从 Storage 中取数据,否则从事件信息 event 中获取最新数据
  const rawValue = event
    ? event.newValue
    : storage!.getItem(key)

  // 当 Storage 中不存在数据时
  if (rawValue == null) {
    // 默认值 rawInit 不为 null 并且设置了 options.writeDefaults 为 true
    if (writeDefaults && rawInit !== null)
      // 将默认值写入 Storage
      storage!.setItem(key, serializer.write(rawInit))
    // 返回默认值
    return rawInit
  }
    
  // 当初始化、 options.writeDefaults 为 true并且 Storage 已保存有数据时
  // 将默认值 rawInit 与 Storage 中的数据进行合并
  else if (!event && mergeDefaults) {
    // 将数据进行反序列化
    const value = serializer.read(rawValue)
    // 调用自定义合并函数
    if (typeof mergeDefaults === 'function')
      return mergeDefaults(value, rawInit)
    // 当默认数据值类型为 object,并且 Storage 中的数据反序列化之后不是数组
    else if (type === 'object' && !Array.isArray(value))
      // 对象合并
      return { ...rawInit as any, ...value }
    // 其他类型时直接返回 Storage 中的数据
    return value
  }

  // 非初始化且 event 中的数据不是 string 类型
  else if (typeof rawValue !== 'string') {
    return rawValue
  }
  // 非初始化且 event 中的数据为 string 类型
  else {
    // 返回返序列化后的数据
    return serializer.read(rawValue)
  }
}

update 方法

Storage中的数据发生变化时会执行该方法,用于更新useStorage返回的响应式数据。初始化时也会执行该方法,但是不会传递event参数。

function update(event?: StorageEventLike) {
  // 如果发生变化的 Storage 是否和设置的 Storage 一致
  if (event && event.storageArea !== storage)
    return

  // Storage 数据全部清空时
  if (event && event.key == null) {
    data.value = rawInit
    return
  }

  // Storage 中数据发生变化的键名是否和用来保存数据的键名一致
  if (event && event.key !== key)
    return

  // 只有初始化时才能执行到这里
  // 暂时 data 发生变化时执行的回调函数
  pauseWatch()
  try {
    // 此时 event 为 undefined
    data.value = read(event)
  }
  catch (e) {
    onError(e)
  }
  finally {
    // 恢复数据变化时执行回调函数
    // 使用 nextTick 防止无线循环
    if (event)
      nextTick(resumeWatch)
    else
      resumeWatch()
  }
}

updateFromCustomEvent 方法

触发自定义事件vueuse-storage时执行的回调。


function updateFromCustomEvent(event: CustomEvent<StorageEventLike>) {
  // 调用 update 方法,并将保存在自定义事件中的数据传入
    update(event.detail)
  }

useLocalStorage 和 useSessionStorage

这两个工具函数内部使用的就是useStorage,不用传递storage参数,只有keyinitialValueoptions三个参数。useLocalStorage内部使用的是window.localStorageuseSessionStorage内部使用的是window.sessionStorage

总结

实现响应式 LocalStorage 和 SesionStorage 原理就是通过 Vue 中的 watch 去监听数据变化,并在数据变化时将其写入Storage中;同时也通过监听storage事件和自定义事件来实现Storage中数据变化时更新响应式数据。其中还内置了数据的序列化以及返回序列化功能,用于 Storage 中的数据存储与获取。

其实实现这个功能不算复杂,大部分人对于实现都会有思路,但是 VueUse 中的 useStorage 在兼容性和扩展性上值得大家学习。

共同成长,无限进步!!! 💪

转载自:https://juejin.cn/post/7238769796351328316
评论
请登录