解决JS中浏览器关闭/系统崩溃时localStorage未更新问题
问题解决方案
现有代码核心问题
onbeforeunload在系统崩溃、强制关机时不会触发,导致无法清理storage和更新dataListstorage事件监听设置了once: true,只能同步一次,无法处理多次跨标签页操作- 没有标签页存活检测机制,无法识别崩溃标签页遗留的无效流程项
修改后的完整代码
import { useRef, useEffect } from 'react'; // 非React环境可移除useRef/useEffect import { v4 as uuidv4 } from 'uuid'; // 可自行实现唯一ID生成逻辑替代 const data = useRef(''); const tabId = useRef(''); // 心跳间隔(毫秒) const HEARTBEAT_INTERVAL = 10000; // 超时阈值(毫秒),超过则判定标签页已崩溃 const TIMEOUT_THRESHOLD = 30000; useEffect(() => { // 生成当前标签页唯一ID tabId.current = uuidv4(); sessionStorage.setItem('tabId', tabId.current); // 页面加载时:清理崩溃标签页遗留数据 + 清空上次崩溃的localStorage cleanupCrashedTabs(); clearStorageOnBrowserRestart(); // 启动心跳 const heartbeatInterval = setInterval(updateHeartbeat, HEARTBEAT_INTERVAL); // 页面卸载时:清理当前标签页的心跳、data项,并清空storage return () => { clearInterval(heartbeatInterval); removeCurrentTabHeartbeat(); cleanupCurrentTabData(); sessionStorage.clear(); localStorage.clear(); }; }, []); // 跨标签页storage同步监听 useEffect(() => { const handleStorageChange = (event) => { if (event.key === 'dataList') { const newDataList = JSON.parse(event.newValue || '[]'); localStorage.setItem('dataList', JSON.stringify(newDataList)); } else if (event.key === 'activeTabs') { cleanupCrashedTabs(); } }; window.addEventListener('storage', handleStorageChange); return () => window.removeEventListener('storage', handleStorageChange); }, []); // 更新当前标签页心跳 const updateHeartbeat = () => { const activeTabs = JSON.parse(localStorage.getItem('activeTabs') || '{}'); activeTabs[tabId.current] = Date.now(); localStorage.setItem('activeTabs', JSON.stringify(activeTabs)); }; // 移除当前标签页心跳 const removeCurrentTabHeartbeat = () => { const activeTabs = JSON.parse(localStorage.getItem('activeTabs') || '{}'); delete activeTabs[tabId.current]; localStorage.setItem('activeTabs', JSON.stringify(activeTabs)); }; // 清理崩溃标签页的dataList项 const cleanupCrashedTabs = () => { const activeTabs = JSON.parse(localStorage.getItem('activeTabs') || '{}'); const currentTime = Date.now(); const crashedTabIds = Object.keys(activeTabs).filter( id => currentTime - activeTabs[id] > TIMEOUT_THRESHOLD ); if (crashedTabIds.length === 0) return; const tabDataMap = JSON.parse(localStorage.getItem('tabDataMap') || '{}'); let dataList = JSON.parse(localStorage.getItem('dataList') || '[]'); crashedTabIds.forEach(tabId => { const crashedData = tabDataMap[tabId] || []; dataList = dataList.filter(item => !crashedData.includes(item)); delete tabDataMap[tabId]; delete activeTabs[tabId]; }); localStorage.setItem('dataList', JSON.stringify(dataList)); localStorage.setItem('tabDataMap', JSON.stringify(tabDataMap)); localStorage.setItem('activeTabs', JSON.stringify(activeTabs)); }; // 浏览器重启时清空localStorage(sessionStorage会自动清空) const clearStorageOnBrowserRestart = () => { const lastBrowserCloseTime = localStorage.getItem('lastBrowserCloseTime'); if (!lastBrowserCloseTime) localStorage.clear(); localStorage.setItem('lastBrowserCloseTime', Date.now().toString()); }; // 清理当前标签页的dataList项 const cleanupCurrentTabData = () => { const tabDataMap = JSON.parse(localStorage.getItem('tabDataMap') || '{}'); const currentTabData = tabDataMap[tabId.current] || []; let dataList = JSON.parse(localStorage.getItem('dataList') || '[]'); dataList = dataList.filter(item => !currentTabData.includes(item)); delete tabDataMap[tabId.current]; localStorage.setItem('dataList', JSON.stringify(dataList)); localStorage.setItem('tabDataMap', JSON.stringify(tabDataMap)); }; const updateDataList = (newData) => { // 更新window对象 window.dataList = window.dataList ? [...window.dataList, newData] : [newData]; sessionStorage.setItem('dataList', JSON.stringify(window.dataList)); // 更新localStorage let dataList = JSON.parse(localStorage.getItem('dataList') || '[]'); if (!dataList.includes(newData)) dataList.push(newData); localStorage.setItem('dataList', JSON.stringify(dataList)); const tabDataMap = JSON.parse(localStorage.getItem('tabDataMap') || '{}'); tabDataMap[tabId.current] = tabDataMap[tabId.current] ? [...tabDataMap[tabId.current], newData] : [newData]; localStorage.setItem('tabDataMap', JSON.stringify(tabDataMap)); updateHeartbeat(); }; const removeDataList = () => { if (!window.dataList || window.dataList.length === 0) return; const removedItem = window.dataList.shift(); sessionStorage.setItem('dataList', JSON.stringify(window.dataList)); let dataList = JSON.parse(localStorage.getItem('dataList') || '[]'); dataList = dataList.filter(item => item !== removedItem); localStorage.setItem('dataList', JSON.stringify(dataList)); const tabDataMap = JSON.parse(localStorage.getItem('tabDataMap') || '{}'); if (tabDataMap[tabId.current]) { tabDataMap[tabId.current] = tabDataMap[tabId.current].filter(item => item !== removedItem); if (tabDataMap[tabId.current].length === 0) delete tabDataMap[tabId.current]; } localStorage.setItem('tabDataMap', JSON.stringify(tabDataMap)); }; const callData = () => { if (!data.current) data.current = uuidv4(); updateDataList(data.current); const intervalID = setInterval(() => { const dataList = JSON.parse(localStorage.getItem('dataList') || '[]'); if (dataList.indexOf(data.current) === 0) { clearInterval(intervalID); // 执行业务逻辑 removeDataList(); } }, HEARTBEAT_INTERVAL); };
关键改动说明
- 标签页唯一标识:每个标签页生成唯一ID绑定到sessionStorage,用于区分不同会话
- 心跳机制:定期更新localStorage中的活跃标签页时间戳,超过阈值未更新则判定为崩溃
- 崩溃自动清理:页面加载时检测过期心跳,移除崩溃标签页对应的dataList项
- 浏览器重启清空:通过存储上次启动时间,判断是否为崩溃重启,自动清空localStorage
- 跨标签页同步优化:移除
once: true,确保每次storage变化都能同步 - 主动关闭清理:页面卸载时清空所有storage,同时清理当前标签页的心跳和data项
替代实现方案
方案1:Broadcast Channel API
替代storage事件,跨标签页通信更可靠,无需依赖localStorage监听:
// 创建广播通道 const channel = new BroadcastChannel('dataListChannel'); // 发送更新消息 const sendDataUpdate = (newData) => { channel.postMessage({ type: 'UPDATE_DATA', data: newData }); }; // 接收消息并更新本地数据 channel.onmessage = (event) => { if (event.data.type === 'UPDATE_DATA') { updateDataList(event.data.data); } };
方案2:IndexedDB存储状态
对于复杂会话管理,IndexedDB支持事务和更丰富的数据结构,比localStorage更稳定,适合存储标签页状态、心跳信息等。
内容的提问来源于stack exchange,提问作者Lax_Sam
相关产品推荐
相关产品推荐

