手撸一个 useLocalStorage
前言
最近在用 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
的类型就是接下来要操作的类型,稍作调整如下,这样返回值 data
和 defaultValue
的类型就一致了。
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'
来试试删除 count
,IDE又报错了,提示不能将类型“null”分配给类型“number”,确实有道理。
那来点暴力的,在定义 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
时一些心得体验。
- 不要把所有功能写到一个
hook
中,这样没有任何意义,一定要一个功能一个hook
,功能越单一越好 - 有时候
hook
在初始化的时候需要传递一些参数,如果这些参数是给hook
中某个函数使用的,那么最好是在调用该函数的时候传参,这样可以多次调用传不同的作者:胡先生参数。
来源:juejin.cn/post/7256620538092290107