📚现代化浏览器本地存储解决方案以及落地实践

简介: 前言最近在项目需要做数据存储,调研到了localforage这个库,在项目中也使用了,接下里我来介绍下它的实现方式以及在React项目如何落地(直接copy下面的hooks解决方案就可以在项目中使用了)使用

前言

最近在项目需要做数据存储,调研到了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.currentrefInit.current:这两个ref用于在Hook内部存储key和初始化状态标记,以便在多次渲染之间保持稳定。
  • EventMapRefEventEmitterRef:用于在本地管理事件订阅和发布机制。通过EventEmitterRef.current.on方法,组件可以订阅特定key的变化事件,并通过EventEmitterRef.current.emit方法触发事件回调。
  • getStoredValue:这个函数用于从LocalStorage中获取数据。如果有数据,则解析并返回;如果没有数据或者出现异常,返回defaultValue作为初始状态。
  • statesetState:这两个用于管理组件内部状态的变量,state用于存储当前的值,setState用于更新state
  • initSetListsetInitSetList:用于存储在组件第一次渲染之前调用的更新函数,以便在获取到本地存储的数据后再调用这些函数来更新组件状态。
  • useEffect:有两个useEffect钩子函数。第一个用于初始化数据,通过useLocalStorage Hook的参数来生成对应的refKey.current,然后调用getStoredValue获取本地存储的数据,并更新组件状态。第二个useEffect用于监听组件内部状态变化,如果组件内部状态发生变化且不是由事件触发的,则会更新本地存储的数据,并触发对应key的事件回调。

目录
相关文章
|
2月前
|
移动开发 小程序 API
微信外部浏览器或短信链接唤起微信小程序的解决方案
微信外部浏览器或短信链接唤起微信小程序的解决方案
150 1
|
3月前
|
存储 监控 安全
360 企业安全浏览器基于阿里云数据库 SelectDB 版内核 Apache Doris 的数据架构升级实践
为了提供更好的日志数据服务,360 企业安全浏览器设计了统一运维管理平台,并引入 Apache Doris 替代了 Elasticsearch,实现日志检索与报表分析架构的统一,同时依赖 Doris 优异性能,聚合分析效率呈数量级提升、存储成本下降 60%....为日志数据的可视化和价值发挥提供了坚实的基础。
360 企业安全浏览器基于阿里云数据库 SelectDB 版内核 Apache Doris 的数据架构升级实践
|
2月前
|
计算机视觉
关于人脸识别最近浏览器打不开摄像头的解决方案
关于人脸识别最近浏览器打不开摄像头的解决方案
39 0
|
3月前
|
存储 安全 前端开发
浏览器跨窗口通信:原理与实践
浏览器跨窗口通信:原理与实践
46 0
|
8月前
|
Web App开发 移动开发 编解码
浏览器播放RTSP视频流几种解决方案
Streamedian 提供了一种“html5_rtsp_player + websock_rtsp_proxy”的技术方案,可以通过html5的video标签直接播放RTSP的视频流。
263 0
|
9月前
|
数据采集 JavaScript 前端开发
利用无头浏览器进行APP提取数据的技术与实践
利用无头浏览器进行APP提取数据的技术与实践
|
5月前
|
存储
【Vue2.0学习】—浏览器本地存储(五十七)
【Vue2.0学习】—浏览器本地存储(五十七)
|
6月前
|
缓存 前端开发 JavaScript
前端跨浏览器标签页数据共享解决方案
vue 项目中有一个工单消息通知列表页,每条消息有已读和未读状态,点击消息会用 window.open 打开一个新的浏览器标签页跳转到工单列表页,工单列表页里有项操作是查看消息,会弹窗显示出具体的详细内容,进入这个弹窗就代表用户已经看到消息了,此时会去调后端接口修改消息状态为已读
82 0
|
6月前
Edge浏览器崩溃解决方案
Edge浏览器崩溃解决方案
83 0
|
7月前
|
Web App开发 安全 Shell
【异常解决】浏览器无法访问此网站ERR_UNSAFE_PORT/网页可能无法连接,或者它已永久性地移动到了新网址问题解决方案
【异常解决】浏览器无法访问此网站ERR_UNSAFE_PORT/网页可能无法连接,或者它已永久性地移动到了新网址问题解决方案
281 0

热门文章

最新文章