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-...

相关推荐
杨进军4 分钟前
React 创建根节点 createRoot
前端·react.js·前端框架
ModyQyW19 分钟前
用 AI 驱动 wot-design-uni 开发小程序
前端·uni-app
说码解字25 分钟前
Kotlin lazy 委托的底层实现原理
前端
Q_970956391 小时前
java+vue+SpringBoo校园失物招领网站(程序+数据库+报告+部署教程+答辩指导)
java·数据库·vue.js
爱分享的程序员1 小时前
前端面试专栏-算法篇:18. 查找算法(二分查找、哈希查找)
前端·javascript·node.js
翻滚吧键盘1 小时前
vue 条件渲染(v-if v-else-if v-else v-show)
前端·javascript·vue.js
vim怎么退出1 小时前
万字长文带你了解微前端架构
前端·微服务·前端框架
你这个年龄怎么睡得着的1 小时前
为什么 JavaScript 中 'str' 不是对象,却能调用方法?
前端·javascript·面试
Java水解1 小时前
前端常用单位em/px/rem/vh/vm到底有什么区别?
前端
CAD老兵1 小时前
Vite 如何借助 esbuild 实现极速 Dev Server 体验,并支持无 source map 的源码调试
前端