likes
comments
collection
share

手撸一个 useLocalStorage

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

前言

最近在用 vue3 + typeScript + vite 重构之前的代码,想着既然都重写了那何不大刀阔斧的改革,把复杂的逻辑全部抽象成独立的 hook,不过官方称之为“组合式函数”(Composables),好家伙写着写着就陷入 “hook 陷阱” 了,啥都想用 hook 实现(自己强迫自己的那种🙃),下笔之前会先去vueuse上看看有没有现成可用的,没有就自己撸一个。

但回过头来发现有些地方确实刻意为之了,导致用起来不是那么爽,比如写了一个 usePxToRem hook,作用是把 px 转换为 rem,用法如下

import { usePxToRem } from './usePxToRem'

const { rem } = usePxToRem('120px')

初看确实没问题,但如果此时有两个px需要转换怎么办,下面这样写肯定不行的,会提示变量rem已经被定义了,不能重复定义。

import { usePxToRem } from './usePxToRem'

const { rem } = usePxToRem('120px')
const { rem } = usePxToRem('140px')

像这样变通下也是勉强能解决的。

import { usePxToRem } from './usePxToRem'

const { rem: rem1 } = usePxToRem('120px')
const { rem: rem2 } = usePxToRem('140px')
console.log(rem1, rem2)

但是总感觉有点麻烦不够优雅,重新思考下这个需求,好像不需要响应式,是不是更适合用函数 convertPxToRem 解决,所以说写着写着就掉进了 hook 陷阱了😂。

正文

扯远了回到正题,开发中经常需要操作 localStorage,直接用原生也没啥问题,如果再简单封装一下就更好了,用起来方便多了。

export function getLocalStorage(key: string, defaultValue?: any) {
  const value = window.localStorage.getItem(key)

  if (!value) {
    if (defaultValue) {
      window.localStorage.setItem(key, JSON.stringify(defaultValue))
      return defaultValue
    } else {
      return ''
    }
  }

  try {
    const jsonValue = JSON.parse(value)
    return jsonValue
  } catch (error) {
    return value
  }
}

export function setLocalStorage(key: string, value: any) {
  window.localStorage.setItem(key, JSON.stringify(value))
}

export function removeLocalStorage(key: string) {
  window.localStorage.removeItem(key)
}

假设有个需求在页面上实时显示 localStorage 里的值,那么必须单独设置一个变量接收 localStorage 的值,然后一边修改变量一边设置 localStorage,这样写起来就有点繁琐了。

<template>
  <div>
    {{ user }}
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { getLocalStorage, setLocalStorage } from './localStorage';

const user = ref('')
user.value = getLocalStorage('user', '张三')
user.value = '李四'
setLocalStorage('user', user.value)
</script>

我想要的效果是一步搞定,像下面这样,是不是很优雅。

import { useLocalStorage } from './useLocalStorage'

const user = useLocalStorage('user', '张三')
user.value = '李四'

第一想法是从 vueues 上找现成的,毕竟这个需求太通用了,useLocalStorage 确实很好用,然后就在想能不能学习 vueuse 自己实现一个简单的 useLocalStorage,正好锻炼下。

第一步搭框架实现基本功能。

import { ref, watch } from "vue";

export function useLocalStorage(key: string, defaultValue: any) {
  const data = ref<any>()

  // 读取 storage
  try {
    data.value = JSON.parse(window.localStorage.getItem(key) || '') 
  } catch (error) {
    data.value = window.localStorage.getItem(key)
  } finally {
    if (!data.value) {
      data.value = defaultValue
    }
  }

  // 上面只是读取 storage,并没有把更新后的值写入到 storage 中
  // 接下来监听 data,每次更新都更新 storage 中值
  watch(() => data.value, () => {
    if (data.value === null) {
      // 置为null表明要清空该值了
      window.localStorage.removeItem(key)
    } else {
      if (typeof data.value === 'object') {
        window.localStorage.setItem(key, JSON.stringify(data.value))
      } else {
        window.localStorage.setItem(key, data.value)
      }
    }
  }, {
    immediate: true
  })

  return data
}

虽然基本功能实现了,但有个问题,比如定义了一个 number 类型的 count 变量,正常情况下只能赋值数字,但这里赋值为字符串也是允许的,因为 data 设置 any 类型了,接下来想办法把类型固定住,比如一开始赋值为 number,后续更新只能是 number 类型,避免误操作。此时就不能使用 any 类型了,需要用范型来约束返回值了,至于范型是啥,请移步这里

我们约定好默认值 defaultValue 的类型就是接下来要操作的类型,稍作调整如下,这样返回值 datadefaultValue 的类型就一致了。

import { ref, watch } from "vue"
import type { Ref } from 'vue'

export function useLocalStorage<T>(key: string, defaultValue: T) {
  const data = ref() as Ref<T>

  // 读取 storage
  try {
    data.value = JSON.parse(window.localStorage.getItem(key) || '') 
  } catch (error) {
    data.value = window.localStorage.getItem(key) as T
  } finally {
    if (!data.value) {
      data.value = defaultValue
    }
  }

  // 上面只是读取 storage,并没有把更新后的值写入到 storage 中
  // 接下来监听 data,每次更新都更新 storage 中值
  watch(() => data.value, () => {
    if (data.value === null) {
      // 置为null表明要清空该值了
      window.localStorage.removeItem(key)
    } else {
      if (typeof data.value === 'object') {
        window.localStorage.setItem(key, JSON.stringify(data.value))
      } else {
        window.localStorage.setItem(key, data.value as string)
      }
    }
  }, {
    immediate: true
  })

  return data
}

继续举例子看看,会发现IDE报错了,提示不能将类型“string”分配给类型“number”,至此改造第一步算是完成了。

const count = useLocalStorage('count', 1);
count.value = 2
count.value = '3'

手撸一个 useLocalStorage

来试试删除 count,IDE又报错了,提示不能将类型“null”分配给类型“number”,确实有道理。

手撸一个 useLocalStorage

那来点暴力的,在定义 data 的时候给一个 null 类型,就像这样 const data = ref() as Ref<T | null>,那么 count.value = null 就不会报错了,也能清空了。不过当我们这样写的时候问题又来了,count.value += 1,IDE会提示 “count.value”可能为 “null” ,确实在定义的时候给了一个 null 类型,那该怎么办呢?

可以用 get set 实现,在 get 的时候返回当前类型,在 set 的时候可以设置 null,然后 count.value 在设置的时候可以为 null 或者 number,在读取的时候只是 number 了。

type RemovableRef<T> = {
  get value(): T
  set value(value: T | null)
}

const data = ref() as RemovableRef<T>

至此一个简单的 useLocalStorage 算是实现了,顺便聊聊自己在开发 hook 时一些心得体验。

  1. 不要把所有功能写到一个 hook 中,这样没有任何意义,一定要一个功能一个 hook,功能越单一越好
  2. 有时候 hook 在初始化的时候需要传递一些参数,如果这些参数是给 hook 中某个函数使用的,那么最好是在调用该函数的时候传参,这样可以多次调用传不同的参数。