大家好,我是 前端架构师 - 大卫。
更多优质内容请关注微信公众号 @程序员大卫。
初心为助前端人🚀,进阶路上共星辰✨,
您的点赞👍与关注❤️,是我笔耕不辍的灯💡。
背景
在某些业务场景下,我们需要监听 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 解析容错等细节。