注册
web

手撸一个 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 的类型就是接下来要操作的类型,稍作调整如下,这样返回值 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'

image.png


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


image.png


那来点暴力的,在定义 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 中某个函数使用的,那么最好是在调用该函数的时候传参,这样可以多次调用传不同的
    作者:胡先生
    来源:juejin.cn/post/7256620538092290107
    参数。

0 个评论

要回复文章请先登录注册