前言
最近在项目需要做数据存储,调研到了localforage这个库,在项目中也使用了,接下里我来介绍下它的实现方式以及在
React项目如何落地
(直接copy下面的hooks解决方案就可以在项目中使用了)
使用
localforage是一个开源的JavaScript库,用于简化浏览器中的本地存储。它提供了一种易于使用的API,使开发者能够轻松地在浏览器中存储数据,而无需关心底层的存储细节。本地存储是Web应用程序中常用的功能之一,它可以让应用程序在用户的浏览器中存储数据,如配置设置、用户偏好、缓存数据等。
// 存储数据 localforage.setItem('username', 'John Doe').then(function () { console.log('数据存储成功!'); }).catch(function (err) { console.error('数据存储失败:', err); }); // 获取数据 localforage.getItem('username').then(function (value) { console.log('获取的数据:', value); }).catch(function (err) { console.error('获取数据失败:', err); }); // 移除数据 localforage.removeItem('username').then(function () { console.log('数据已成功移除!'); }).catch(function (err) { console.error('数据移除失败:', err); });
localforage主要提供了setItem、getItem和removeItem等方法,分别用于存储、获取和移除数据。此外,它还支持许多其他方法,如clear用于清空所有数据、key用于根据索引获取键名等等。
原理
存储后端的自动选择
localforage在底层使用了异步存储API来存储数据。它会自动检测浏览器支持的存储后端,并选择最适合的后端。它首先尝试使用IndexedDB(现代浏览器通常都支持),如果不可用,则回退到WebSQL数据库(一些旧版的WebKit浏览器支持),最后再回退到localStorage(所有支持HTML5的浏览器都支持)。
这种自动选择存储后端的方式保证了在各种浏览器环境下都能正常工作,并且利用了现代浏览器提供的更强大的存储机制,从而在性能和存储容量方面获得了最佳的表现。
异步存储与回调
localforage在执行存储操作时是异步的,它使用Promise来处理回调。这样做的好处是避免了在进行大量数据存储时阻塞JavaScript主线程,保持了良好的用户体验。
localforage.setItem('username', 'John Doe').then(function () { console.log('数据存储成功!'); }).catch(function (err) { console.error('数据存储失败:', err); });
在上面的例子中,setItem方法返回一个Promise对象,我们可以使用then和catch方法来处理存储成功和失败的情况。这种方式使得代码逻辑更加清晰和简洁。
数据的序列化与反序列化
localforage允许我们存储JavaScript原生的数据类型,如字符串、数字、数组、对象等等。但是,在底层存储时,数据需要先进行序列化,以便于存储在后端数据库中。而在获取数据时,localforage会自动将存储的序列化数据反序列化为JavaScript原生数据类型。
存储容量限制
需要注意的是,虽然localforage可以提供比Cookie更大的存储容量,但不同的浏览器和存储后端对于本地存储的容量限制是有差异的。对于大规模数据存储,仍然需要慎重考虑存储容量的问题,避免超出浏览器的限制。
在项目中落地
我们要在项目实现这样的效果
在下面的例子中,useLocalStorage
Hook被用来在组件中维护一个myData
的状态,并且这个状态会与localforage同步。每当输入框的值发生变化时,setData
会更新组件状态并且自动将数据存储到localforage中。而在组件初始化时,会尝试从localforage中获取之前存储的数据,并且作为初始状态。
import React from 'react'; import { useLocalStorage } from './useLocalStorage'; function MyComponent() { const [data, setData] = useLocalStorage('myData', 'default-value',false,'my-domain'); const handleChange = (event) => { setData(event.target.value); }; return ( <div> <input type="text" value={data} onChange={handleChange} /> <p>Current value: {data}</p> </div> ); } export default MyComponent;
首先,我们来看一下这个自定义HookuseLocalStorage
的参数和返回值:
export function useLocalStorage<T>( key: string, defaultValue: T, isDefaultOnFirst: boolean = true, pathname?: string ) { // ... return [state, updateState] as const; }
key
: 存储数据时使用的键名,它会被用来在LocalStorage中唯一标识数据。defaultValue
: 作为默认值使用的数据,当LocalStorage中没有对应的数据时,会返回该默认值。pathname
(可选): 用于生成实际的存储键名。如果没有提供该参数,将使用默认的location.pathname
(当前页面的URL路径)来生成存储键名。isDefaultOnFirst
(可选): 是否在第一次渲染时使用默认值。如果设置为true
,组件第一次渲染时会使用defaultValue
作为初始状态。
import { useCallback, useEffect, useRef, useState } from 'react'; import localforage from 'localforage'; export function useLocalStorage<T>( key: string, defaultValue: T, isDefaultOnFirst: boolean = true pathname?: string, ) { const refKey = useRef(key); const refInit = useRef(false); const EventMapRef = useRef(new Map<string, Function[]>()); const EventEmitterRef = useRef( class EventEmitter { static on<T>(key: string, callback: (value: T) => void) { if (EventMapRef.current.has(key)) { EventMapRef.current.get(key)?.push(callback); } else { EventMapRef.current.set(key, [callback]); } return () => { const funcList = EventMapRef.current.get(key); EventMapRef.current.set( key, funcList!.filter((func) => func !== callback), ); }; } static emit<T>(key: string, value: T) { if (EventMapRef.current.has(key)) { EventMapRef.current.get(key)?.forEach((func) => { func(value); }); } } }, ); function getStoredValue() { return new Promise<T>((resolve) => { localforage .getItem(refKey.current) .then((raw) => { if (typeof raw !== 'undefined' && raw !== null) { resolve(raw as T); } else { resolve(defaultValue); } }) .catch((e) => { console.error(e); resolve(defaultValue); }); }); } const [state, setState] = useState( isDefaultOnFirst ? defaultValue : undefined, ); const [initSetList, setInitSetList] = useState<Function[]>([]); useEffect(() => { const path = pathname || location.pathname.replace(/\//g, '_'); refKey.current = `${path}-${key}`; getStoredValue().then((value) => { setState(value); if (initSetList.length) { initSetList.forEach((func) => func()); } }); refInit.current = true; }, [key, pathname]); useEffect(() => { const handleEventEmitter = (eventValue: T) => { if (JSON.stringify(eventValue) !== JSON.stringify(state)) { updateState(eventValue, true); } }; const removeHandler = EventEmitterRef.current.on<T>( key, handleEventEmitter, ); return () => { removeHandler(); }; }, [state]); const updateState = useCallback( (value: T, isEmit?: boolean) => { function updateForageState(currentState: T) { setState(currentState); if (typeof currentState === 'undefined') { localforage.removeItem(refKey.current); } else { localforage .setItem(refKey.current, currentState) .then(() => { if (!isEmit) { console.log('emit'); EventEmitterRef.current.emit(key, currentState); } }) .catch((e) => { console.error(e); }); } } if (!refInit.current) { setInitSetList((list) => [ ...list, updateForageState.bind(useLocalStorage, value), ]); } else { updateForageState(value); } }, [refKey, refInit, key], ); return [state, updateState] as const; }
我们分析这个Hook的实现细节:
refKey.current
和refInit.current
:这两个ref用于在Hook内部存储key
和初始化状态标记,以便在多次渲染之间保持稳定。EventMapRef
和EventEmitterRef
:用于在本地管理事件订阅和发布机制。通过EventEmitterRef.current.on
方法,组件可以订阅特定key
的变化事件,并通过EventEmitterRef.current.emit
方法触发事件回调。getStoredValue
:这个函数用于从LocalStorage中获取数据。如果有数据,则解析并返回;如果没有数据或者出现异常,返回defaultValue
作为初始状态。state
和setState
:这两个用于管理组件内部状态的变量,state
用于存储当前的值,setState
用于更新state
。initSetList
和setInitSetList
:用于存储在组件第一次渲染之前调用的更新函数,以便在获取到本地存储的数据后再调用这些函数来更新组件状态。useEffect
:有两个useEffect
钩子函数。第一个用于初始化数据,通过useLocalStorage
Hook的参数来生成对应的refKey.current
,然后调用getStoredValue
获取本地存储的数据,并更新组件状态。第二个useEffect
用于监听组件内部状态变化,如果组件内部状态发生变化且不是由事件触发的,则会更新本地存储的数据,并触发对应key
的事件回调。