同事给我埋下一个LocalStorage的坑,快查查你的项目里有没有~

6,474 阅读9分钟

一、 前言

Hello~ 大家好。我是秋天的一阵风~

事情是这样的,最近在新客户项目的落地部署过程中,我发现了一个隐藏在前端项目代码深处的隐患。

在为用户进行定制化功能改造时,后端需要在已有的字典接口中增加部分字典值。接口数据更新后,页面展示的却仍然是旧的字典数据。于是,我深入前端项目中具体的字典数据文件进行排查,终于发现了问题所在:

image.png
  1. 前端代码在全局前置守卫里时会检查 localStorage 中是否存在字典数据。
  2. 如果不存在,则会请求一次接口,并将获取到的字典数据存入 localStorage
  3. 如果存在,后续所有页面的字典数据值都直接从localStorage里面获取。

我猜测前同事这样做的原因是:

字典数据变动频率极低,通过这种方式可以减少对字典接口的请求次数,避免资源浪费。从代码逻辑上看,这确实是一个合理的设计。

然而,当业务需求发生变化时,问题就出现了。如果前端不手动删除 localStorage 中的字典数据,哪怕关闭了浏览器,页面上展示的依然是旧数据,无法更新。

为了解决这一隐患,我总结了几种可行的解决方案。在详细介绍这些解决方案之前,我们不妨先回顾一下 localStorage 的一些基础知识。

二、LocalStorage简介

1. 什么是LocalStorage?

LocalStorage 是一种用于在用户浏览器中存储数据的Web存储技术,属于HTML5规范的一部分。它允许网站在用户的设备上存储键值对数据,这些数据在浏览器关闭后仍然可以被保留,直到被显式删除。LocalStorage 提供了一种比传统的Cookie更强大、更灵活的存储方式,适用于存储大量数据(最大容量通常为5MB)。

2. LocalStorage的特点

  1. 持久性存储

    • LocalStorage中的数据 不会在浏览器关闭后丢失 ,除非用户或代码显式删除。
    • 与SessionStorage不同,SessionStorage的数据在浏览器标签页关闭后会被清除。
  2. 存储容量大

    • LocalStorage的存储容量通常为5MB,远大于Cookie(通常为4KB)。
  3. 键值对存储

    • 数据以键值对的形式存储,每个键对应一个值,键和值都是字符串。
  4. 安全性

    • LocalStorage的数据只能被同一协议、同一域名下的页面访问,不能跨域共享。
  5. 同步存储

    • LocalStorage的操作是同步的,不会像异步存储(如IndexedDB)那样需要等待回调。

3. LocalStorage的基本用法

LocalStorage 提供了以下基本方法来操作数据:

  1. 存储数据

    localStorage.setItem('key', 'value');
    
  2. 读取数据

    let value = localStorage.getItem('key');
    
  3. 删除数据

    localStorage.removeItem('key');
    
  4. 清空所有数据

    localStorage.clear();
    

4. 注意事项

  1. 存储容量限制

    • 虽然LocalStorage的容量较大,但仍然有限制(通常为5MB)。如果存储的数据超过限制,浏览器会抛出错误。
  2. 安全性

    • LocalStorage的数据存储在客户端,容易被用户篡改。因此,不要存储敏感信息,如密码、用户身份验证令牌等。
  3. 跨域限制

    • LocalStorage的数据只能被同一协议、同一域名下的页面访问,不能跨域共享。
  4. 同步操作

    • LocalStorage的操作是同步的,如果存储大量数据,可能会导致页面卡顿。对于更复杂的数据存储需求,可以考虑使用IndexedDB等异步存储方式。

三、解决方案

1.SessionStorage

其实最简单快捷方便的方法就是将数据改成存放在SessionStorage里面,这样只要用户关闭页面后重新打开,就会重新请求接口获取最新的字典数据。

sessionStorage 是 Web Storage API 的一部分,用于在浏览器端存储数据,与 localStorage 类似,但它们在数据存储的生命周期和作用域上有所不同。以下是关于 sessionStorage 的简单介绍:

基本概念

  • 存储位置sessionStorage 将数据存储在浏览器的会话(Session)中,数据以键值对的形式存储,键和值都是字符串类型。
  • 存储生命周期sessionStorage 的数据仅在当前浏览器标签页的会话期间有效。当用户关闭浏览器标签页时,存储在 sessionStorage 中的数据会被自动清除。
  • 存储容量:与 localStorage 类似,sessionStorage 的存储容量通常也是 5MB 左右,但具体容量可能因浏览器而异。

使用方法

  • 存储数据:使用 sessionStorage.setItem(key, value) 方法,其中 key 是数据的键,value 是数据的值。例如:

    sessionStorage.setItem("username", "kimi");
    
  • 读取数据:使用 sessionStorage.getItem(key) 方法,根据键获取对应的值。例如:

    let username = sessionStorage.getItem("username");
    console.log(username); // 输出:kimi
    
  • 删除数据

    • 删除指定键值对:sessionStorage.removeItem(key)。例如:

      sessionStorage.removeItem("username");
      
    • 清空当前会话的所有数据:sessionStorage.clear()

特点与注意事项

  • 作用域限制sessionStorage 的数据仅在当前浏览器标签页中有效,不同标签页之间无法共享 sessionStorage 数据。如果需要跨标签页共享数据,可以使用 localStorage
  • 存储容量限制:虽然 5MB 的容量对于大多数简单应用来说足够,但如果存储过多数据,可能会导致存储失败或浏览器性能下降。
  • 数据安全性:与 localStorage 一样,sessionStorage 的数据存储在客户端,存在被篡改或窃取的风险。因此,不适合存储敏感信息(如密码等)。
  • 自动清理:由于 sessionStorage 的数据在关闭标签页时自动清除,因此不需要手动管理数据的生命周期,这使得它非常适合存储临时会话数据。

2. 定时刷新缓存

  • 可以在项目中设置一个定时器,定期从后端请求最新的字典数据,并更新localStorage中的内容。
  • 例如,每隔一段时间(如30分钟)检查一次字典数据是否有更新。
setInterval(async () => {
  const newData = await fetchDictionaryData(); // 假设这是请求字典数据的函数
  localStorage.setItem('dictionaryData', JSON.stringify(newData));
}, 30 * 60 * 1000); // 每30分钟刷新一次

3. 版本控制

  • 后端可以在字典数据发生变化时,生成一个版本号或时间戳,并将其返回给前端。
  • 前端在初始化时,先请求版本号,如果发现版本号与localStorage中存储的版本号不一致,则重新请求字典数据并更新localStorage
async function getDictionaryData() {
  const version = await fetchDictionaryVersion(); // 获取字典版本号
  const storedVersion = localStorage.getItem('dictionaryVersion');
  if (version !== storedVersion) {
    const newData = await fetchDictionaryData();
    localStorage.setItem('dictionaryData', JSON.stringify(newData));
    localStorage.setItem('dictionaryVersion', version);
    return newData;
  } else {
    return JSON.parse(localStorage.getItem('dictionaryData'));
  }
}

4. WebSocket 实时更新

  • 如果字典数据的变化频率较高,可以考虑使用WebSocket与后端建立长连接,当字典数据发生变化时,后端主动推送更新通知给前端,前端收到通知后立即更新localStorage
const socket = new WebSocket('ws://your-backend-url');

socket.onmessage = function(event) {
  const message = JSON.parse(event.data);
  if (message.type === 'dictionaryUpdate') {
    localStorage.setItem('dictionaryData', JSON.stringify(message.data));
  }
};

5. 请求时检查更新时间

  • 每次请求字典数据时,向后端发送localStorage中存储的字典数据的最后更新时间,后端根据这个时间判断是否需要返回新的数据。
async function getDictionaryData() {
  const lastUpdateTime = localStorage.getItem('dictionaryLastUpdateTime');
  const newData = await fetchDictionaryData(lastUpdateTime);
  if (newData) {
    localStorage.setItem('dictionaryData', JSON.stringify(newData));
    localStorage.setItem('dictionaryLastUpdateTime', new Date().toISOString());
  }
  return newData || JSON.parse(localStorage.getItem('dictionaryData'));
}

四、扩展知识:多窗口之间SessionStorage能否共享状态?

在第一个解决方案使用SessionStorage代替时,我们提到过其使用上有作用域的限制:

sessionStorage 的数据仅在当前浏览器标签页中有效,不同标签页之间无法共享 sessionStorage 数据。

但是在MDN中里面有一段话确实令人觉得蹊跷: 点击这里前往MDN文档

  • 页面会话在浏览器打开期间一直保持,并且重新加载或恢复页面仍会保持原来的页面会话。
  • 在新标签或窗口打开一个页面时会复制顶级浏览会话的上下文作为新会话的上下文,这点和 session cookie 的运行方式不同。
  • 打开多个相同的 URL 的 Tabs 页面,会创建各自的 sessionStorage
  • 关闭对应浏览器标签或窗口,会清除对应的 sessionStorage

测试:

我们创建 a.html,b.html这么两个页面。

  1. a.html页面初始化时往sessionStorage存入一个字典数组数据:dictArray
  2. 设置三种跳转方式,分别是window.open 和 a标签不带target=_blank 以及 a标签不带target=_blank
  3. b.html页面通过获取sessionStorage数据并且渲染,如果没有数据,则显示提示信息.

完整代码如下:

// a.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SessionStorage Example</title>
</head>
<body>
    <h1>SessionStorage Example</h1>
    <button id="openButton">通过window.open打开新页面</button> <br/>
    <a href="b.html">通过a标签打开新页面(加上 target=_blank)</a><br/>
    <a href="b.html" target="_blank">通过a标签打开新页面(不加 target=_blank)</a>
    <script>
        // 初始化时存入一个字典数组
        const dictArray = [
            { key: 'name', value: 'Alice' },
            { key: 'age', value: 25 },
            { key: 'city', value: 'New York' }
        ];

        // 将字典数组存储到 sessionStorage
        sessionStorage.setItem('dictArray', JSON.stringify(dictArray));

        // 按钮点击事件
        document.getElementById('openButton').addEventListener('click', () => {
            window.open('b.html', '_blank');
        });
    </script>
</body>
</html>
image.png
// b.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SessionStorage Data</title>
</head>
<body>
    <h1>Data from sessionStorage</h1>
    <div id="dataContainer"></div>

    <script>
        // 从 sessionStorage 中获取数据
        const dictArray = JSON.parse(sessionStorage.getItem('dictArray'));

        // 检查数据是否存在
        if (dictArray) {
            // 创建一个列表来显示数据
            const dataContainer = document.getElementById('dataContainer');
            const ul = document.createElement('ul');

            dictArray.forEach(item => {
                const li = document.createElement('li');
                li.textContent = `${item.key}: ${item.value}`;
                ul.appendChild(li);
            });

            dataContainer.appendChild(ul);
        } else {
            // 如果没有数据,显示一条消息
            document.getElementById('dataContainer').textContent = 'No data found in sessionStorage.';
        }
    </script>
</body>
</html>

1. window.open

点击第一个按钮:通过window.open打开新页面 ,跳转到b.html页面后发现数据能获取:

image.png

2. a标签不带target=_blank

点击第二个a标签:<a href="b.html">通过a标签打开新页面(不加 target=_blank)</a>,依然可以获得字典数据 image.png

3. a标签带target=_blank

点击第三个a标签:<a href="b.html" target="_blank">通过a标签打开新页面(加 target=_blank)</a>,无法获得字典数据

image.png

4. 结论:

多窗口之间sessionStorage不可以共享状态!!!但是在某些特定场景下比如说window.open或者a标签跳转的页面(不能加target=_blank,这样是新开一个窗口)会复制之前页面的sessionStorage!!

注意: 即使在特殊场景下获取了上一个页面的sessionStorage数据,也只是复制。并不是共享!!!

总结

以上方案可以根据项目的具体需求选择使用。如果字典数据变化不频繁,定时刷新或版本控制可能是最简单的解决方案。如果数据变化频繁,考虑使用WebSocket或Service Worker来实现实时更新。