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

简介: 前言最近在项目需要做数据存储,调研到了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的事件回调。

目录
相关文章
|
7月前
|
存储 监控 安全
360 企业安全浏览器基于阿里云数据库 SelectDB 版内核 Apache Doris 的数据架构升级实践
为了提供更好的日志数据服务,360 企业安全浏览器设计了统一运维管理平台,并引入 Apache Doris 替代了 Elasticsearch,实现日志检索与报表分析架构的统一,同时依赖 Doris 优异性能,聚合分析效率呈数量级提升、存储成本下降 60%....为日志数据的可视化和价值发挥提供了坚实的基础。
360 企业安全浏览器基于阿里云数据库 SelectDB 版内核 Apache Doris 的数据架构升级实践
|
7月前
|
移动开发 小程序 API
微信外部浏览器或短信链接唤起微信小程序的解决方案
微信外部浏览器或短信链接唤起微信小程序的解决方案
1644 1
|
27天前
|
存储 缓存 前端开发
Web端IM聊天消息该不该用浏览器本地存储?一文即懂!
鉴于目前浏览器技术的进步(主要是HTML5的普及),在Web网页端IM聊天应用的技术选型阶段,很多开发者都会纠结到底该不该像原生移动端IM那样将聊天记录缓存在浏览器的本地,还是像传统Web端即时通讯那样继续存储在服务端?本文将为你简洁明了地讲清楚浏览器本地存储技术(Web Storage),然后你就知道到底该怎么选择了。
29 1
|
4月前
|
数据采集 Web App开发 测试技术
使用Selenium调试Edge浏览器的常见问题与解决方案
在互联网数据采集领域,Selenium常用于自动化网页爬取。针对使用Edge浏览器时遇到的启动远程调试失败、访问受限及代理IP设置等问题,本文提供了解决方案。通过特定命令启动Edge的远程调试模式,并利用Python脚本配合Selenium库,可实现代理IP、User-Agent的设定及Cookie管理等高级功能,有效提升爬虫稳定性和隐蔽性。遵循步骤配置后,即可顺畅执行自动化测试任务。
1001 1
使用Selenium调试Edge浏览器的常见问题与解决方案
|
4月前
|
存储 JavaScript 程序员
Vue学习之---浏览器本地存储(8/17)
这篇文章介绍了Vue中浏览器本地存储的使用方法,包括基础知识、localStorage和sessionStorage的代码实例及其测试效果,并提供了相关的API和操作示例。
Vue学习之---浏览器本地存储(8/17)
|
5月前
|
Web App开发 前端开发
canvas保存图片时,谷歌浏览器Chrome报错【解决方案】Not allowed to navigate top frame to data URL
canvas保存图片时,谷歌浏览器Chrome报错【解决方案】Not allowed to navigate top frame to data URL
159 0
|
7月前
|
Web App开发 前端开发 JavaScript
构建跨浏览器兼容的前端应用:技术实践与挑战
【5月更文挑战第16天】构建跨浏览器兼容的前端应用是应对浏览器差异和多样性的挑战。使用现代框架(如React、Vue)能自动转换代码,编写可移植的Web标准代码,结合浏览器兼容性测试工具和Polyfill解决旧浏览器支持问题。关注浏览器更新,应对性能、API差异和样式问题,采用渐进增强、条件判断和CSS Reset策略确保应用在各种浏览器上运行良好。
|
7月前
浏览器兼容性解决方案
【5月更文挑战第4天】浏览器兼容性解决方案。
64 5
|
7月前
|
存储 JSON 安全
[浏览器系列] : 客户端本地存储
[浏览器系列] : 客户端本地存储
|
Web App开发 移动开发 编解码
浏览器播放RTSP视频流几种解决方案
Streamedian 提供了一种“html5_rtsp_player + websock_rtsp_proxy”的技术方案,可以通过html5的video标签直接播放RTSP的视频流。
574 0