localStorage 也能监听变化?带你实现组件和标签页的同步更新!【附完整 Vue/React Hook 源码】

大家好,我是 前端架构师 - 大卫

更多优质内容请关注微信公众号 @程序员大卫

初心为助前端人🚀,进阶路上共星辰✨,

您的点赞👍与关注❤️,是我笔耕不辍的灯💡。

背景

在某些业务场景下,我们需要监听 localStorage 的变更,并在当前页面的多个组件之间进行同步更新 ,同时还能支持多个浏览器标签页之间的数据同步

举个例子:在 Hybrid App 页面中切换字体大小(如从正常版切换到大字版)时,我们希望所有 WebView 页面都能及时感知并应用该变更。

那么,如何检测 localStorage 的值是否发生了变化呢?

一种简单的方法是使用轮询。但轮询的时间间隔难以权衡:设置太长会导致同步不及时,设置太短又会影响性能,并且不够优雅。

localStorage 可以跨标签页通信吗?

答案是:可以! 浏览器原生就支持跨标签页的通信。

不过要注意:

⚠️ 同一标签页中的 storage 事件不会被触发!

跨标签页面监听方式如下:

js 复制代码
window.addEventListener("storage", (event) => {
  console.log(event);
});

通过打印出的日志,我们可以看到 StorageEvent 中包含了哪些信息:

js 复制代码
StorageEvent {
  isTrusted: false,
  bubbles: false,
  cancelBubble: false,
  cancelable: false,
  composed: false,
  currentTarget: Window {window: Window, self: Window, document: document, name: '', location: Location, ...},
  defaultPrevented: false,
  eventPhase: 0,
  key: "a",
  newValue: "12",
  oldValue: "11",
  returnValue: true,
  srcElement: Window {window: Window, self: Window, document: document, name: '', location: Location, ...},
  storageArea: null,
  target: Window {window: Window, self: Window, document: document, name: '', location: Location, ...},
  timeStamp: 729.2999999523163,
  type: "storage",
  url: "http://127.0.0.1:5501/index.html",
};

本地 HTML 测试示例

以下是一个简单的 HTML 页面,我们使用 VSCode 的 Live Server 插件,同时打开两个浏览器标签页。此时当一个页面中的值发生变化时,另一个页面也会同步更新。

html 复制代码
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>LocalStorage 跨标签页面通信</title>
  </head>
  <body>
    <div>
      <div id="box"></div>
      <button id="btn">累加</button>
    </div>

    <script>
      const defaultCount = 0;

      const getStoredCount = () =>
        JSON.parse(localStorage.getItem("a") ?? defaultCount);

      // 初始化设置 count 值
      let count = getStoredCount();

      const setCount = (value) => {
        count = value;
        box.innerText = value;
      };

      // 初始化设置值
      setCount(count);

      // 按钮点击
      btn.addEventListener("click", () => {
        console.log("btn click");

        const newValue = count + 1;

        localStorage.setItem("a", newValue);

        setCount(newValue);
      });

      // 监听跨页面的 localStorage
      window.addEventListener("storage", (event) => {
        if (event.key === "a") {
          const newValue = JSON.parse(event.newValue ?? "");
          setCount(newValue);
        }
      });
    </script>
  </body>
</html>

封装 Vue Hook:useSyncedLocalStorage

我们封装了一个名为 useSyncedLocalStorage.ts 的 Vue Hook,核心实现思路如下:

  • 接收两个参数:localStorage 的 key 和默认值,默认值能让 TS 类型更友好。
  • 同一标签页中的多组件更新 通过 dispatchEvent + CustomEvent 实现。
  • 跨标签页的更新由浏览器原生的 storage 事件完成。
ts 复制代码
import { onMounted, onUnmounted, ref } from "vue";

const safeParseJSON = <T>(value: string | null, fallback: T): T => {
  try {
    return value ? JSON.parse(value) : fallback;
  } catch (e) {
    return fallback;
  }
};

const getLocalValue = (key: string) => window.localStorage.getItem(key);

const getCustomEventName = (key: string) => `app:event:localstorage:${key}`;

export const useSyncedLocalStorage = <T>(key: string, defaultValue: T) => {
  const getInitialValue = () => safeParseJSON(getLocalValue(key), defaultValue);

  const value = ref(getInitialValue());

  const updateValue = (newValue: T) => {
    value.value = newValue;
  };

  const setValue = (newValue: T) => {
    try {
      if (newValue !== value.value) {
        const newJSON = JSON.stringify(newValue);

        // 当前组件更新
        updateValue(newValue);

        // 多组件更新
        const customEvent = new CustomEvent(getCustomEventName(key), {
          detail: newJSON,
        });
        window.dispatchEvent(customEvent);

        // 跨标签页广播
        window.localStorage.setItem(key, newJSON);
      }
    } catch (e) {
      console.error("LocalStorage set error:", e);
    }
  };

  const handleStorage = (event: StorageEvent) => {
    if (event.key === key) {
      try {
        updateValue(JSON.parse(event.newValue ?? ""));
      } catch (e) {}
    }
  };

  const handleCustomEvent = (event: Event) => {
    const customEvent = event as CustomEvent<string>;
    try {
      updateValue(JSON.parse(customEvent.detail));
    } catch (e) {}
  };

  onMounted(() => {
    window.addEventListener("storage", handleStorage);
    window.addEventListener(getCustomEventName(key), handleCustomEvent);
  });
  
  onUnmounted(() => {
    window.removeEventListener("storage", handleStorage);
    window.removeEventListener(getCustomEventName(key), handleCustomEvent);
  });

  return { value, setValue };
};

使用示例 ------ Count.vue

html 复制代码
<script setup lang="ts">
import { useSyncedLocalStorage } from "../hooks/useSyncedLocalStorage";

const { value, setValue } = useSyncedLocalStorage("a", 0);

const increace = () => {
  setValue(value.value + 1);
};
</script>

<template>
  <button @click="increace">{{ value }}</button>
</template>

主应用组件 App.vue

html 复制代码
<script setup lang="ts">
import Count from "./components/Count.vue";
</script>

<template>
  <div style="display: flex; gap: 10px">
    <Count />
    <Count />
  </div>
</template>

封装 React Hook:useSyncedLocalStorage

React 实现原理相同,我们也提供一个对应的 Hook:

ts 复制代码
import { useCallback, useEffect, useState } from "react";

const safeParseJSON = <T>(value: string | null, fallback: T): T => {
  try {
    return value ? JSON.parse(value) : fallback;
  } catch (e) {
    return fallback;
  }
};

const getLocalValue = (key: string) => window.localStorage.getItem(key);

const getCustomEventName = (key: string) => `app:event:localstorage:${key}`;

export const useSyncedLocalStorage = <T>(key: string, defaultValue: T) => {
  const [value, setVal] = useState(() =>
    safeParseJSON(getLocalValue(key), defaultValue)
  );

  const updateValue = useCallback((newValue: T) => {
    setVal(newValue);
  }, []);

  const setValue = (newValue: T) => {
    try {
      if (newValue !== value) {
        const newJSON = JSON.stringify(newValue);

        // 当前组件更新
        updateValue(newValue);

        // 多组件更新
        const customEvent = new CustomEvent(getCustomEventName(key), {
          detail: newJSON,
        });
        window.dispatchEvent(customEvent);

        // 跨标签页广播
        window.localStorage.setItem(key, newJSON);
      }
    } catch (e) {
      console.error("LocalStorage set error:", e);
    }
  };

  const handleStorage = useCallback(
    (event: StorageEvent) => {
      if (event.key === key) {
        try {
          updateValue(JSON.parse(event.newValue ?? ""));
        } catch (e) {}
      }
    },
    [key, updateValue]
  );

  useEffect(() => {
    window.addEventListener("storage", handleStorage);
    return () => {
      window.removeEventListener("storage", handleStorage);
    };
  }, [handleStorage]);

  const handleCustomEvent = useCallback(
    (event: Event) => {
      const customEvent = event as CustomEvent<string>;
      try {
        updateValue(JSON.parse(customEvent.detail));
      } catch (e) {}
    },
    [updateValue]
  );

  useEffect(() => {
    const eventName = getCustomEventName(key);
    window.addEventListener(eventName, handleCustomEvent);
    return () => {
      window.removeEventListener(eventName, handleCustomEvent);
    };
  }, [handleCustomEvent, key]);

  return { value, setValue };
};

总结

在前端开发中,localStorage 通常被用来做简单的数据持久化,但如果想让它具备「跨组件」、「跨标签页」、「响应式」的能力,就需要我们做一些封装和处理。

本文通过一个具体的数字变化大小的例子,展示了:

  • 如何利用浏览器原生的 storage 事件完成跨标签页通信。
  • 如何通过自定义事件 dispatchEvent + addEventListener,实现同一标签页中多个组件的响应式更新。
  • 如何在 Vue 和 React 中分别封装 useSyncedLocalStorage Hook,以便在项目中复用。
  • 兼顾了类型安全、性能和代码可读性,支持默认值、JSON 解析容错等细节。

源码地址

github.com/zm8/wechat-...

相关推荐
蓝胖子的多啦A梦25 分钟前
搭建前端项目 Vue+element UI引入 步骤 (超详细)
前端·vue.js·ui
TE-茶叶蛋27 分钟前
WebSocket 前端断连原因与检测方法
前端·websocket·网络协议
骆驼Lara37 分钟前
前端跨域解决方案(1):什么是跨域?
前端·javascript
离岸听风39 分钟前
学生端前端用户操作手册
前端
onebyte8bits42 分钟前
CSS Houdini 解锁前端动画的下一个时代!
前端·javascript·css·html·houdini
yxc_inspire1 小时前
基于Qt的app开发第十四天
前端·c++·qt·app·面向对象·qss
一_个前端1 小时前
Konva 获取鼠标在画布中的位置通用方法
前端
[email protected]2 小时前
Asp.Net Core SignalR导入数据
前端·后端·asp.net·.netcore
小满zs7 小时前
Zustand 第五章(订阅)
前端·react.js
涵信8 小时前
第一节 基础核心概念-TypeScript与JavaScript的核心区别
前端·javascript·typescript