You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解决JS中浏览器关闭/系统崩溃时localStorage未更新问题

问题解决方案

现有代码核心问题

  1. onbeforeunload在系统崩溃、强制关机时不会触发,导致无法清理storage和更新dataList
  2. storage事件监听设置了once: true,只能同步一次,无法处理多次跨标签页操作
  3. 没有标签页存活检测机制,无法识别崩溃标签页遗留的无效流程项

修改后的完整代码

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);
};

关键改动说明

  1. 标签页唯一标识:每个标签页生成唯一ID绑定到sessionStorage,用于区分不同会话
  2. 心跳机制:定期更新localStorage中的活跃标签页时间戳,超过阈值未更新则判定为崩溃
  3. 崩溃自动清理:页面加载时检测过期心跳,移除崩溃标签页对应的dataList项
  4. 浏览器重启清空:通过存储上次启动时间,判断是否为崩溃重启,自动清空localStorage
  5. 跨标签页同步优化:移除once: true,确保每次storage变化都能同步
  6. 主动关闭清理:页面卸载时清空所有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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 11:10:53