搞定Web Storage

237 阅读4分钟

背景

Web应用的发展,使得客户端存储使用得也越来越多,而实现方式也是多种多样。最简单而且兼容性最佳的方案是Cookie,但是作为真正的客户端存储,Cookie 则存在很多致命伤。

  • Cookie 数量和长度的限制。每个 domain 最多只能有20条cookie,每个 Cookie 长度不能超过4KB, 否则会被截掉。
  • 安全性问题。如果 Cookie 被人拦截了,对方就可以取得所有的 session 信息。即使加密也于事无补。因为拦截者并不需要知道 Cookie 的意义,他只要原样转发 Cookie 就可以达到目的了。
  • 有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数 器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。
  • Cookie 不可以跨域调用。客户端每请求一个新的页面时 Cookie 都需要指定作用域被发送过去,无形中浪费了带宽。

注意:Cookie 虽然有以上缺点,但他依旧是不可或缺的。Cookie的作用是与服务器进行交互,作为HTTP规范的一部分而存在,而 Web Storage 仅仅是为了在本地“存储”数据而生。

浏览器本地存储

image.png

Web Storage 与 Cookie 对比而言是为了更大容量存储设计的,Web Storage 拥有 setItemgetItemremoveItemclear 等方法,不像cookie需要前端开发者自己封装 setCookie,getCookie,操作数据的增删改查更加便捷。

sessionStorage VS localStorage

  1. sessionStorage 为每一个给定的源维持一个独立的存储区域,该存储区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。
  2. localStorage 有同样的功能,但是在浏览器关闭,然后重新打开后数据仍然存在。
  3. sessionStoragelocalStorage 一般统称为 Web Storage (API)
  4. sessionStoragelocalStorage 都遵循同源策略,容量由客户端程序(浏览器)决定,一般而言,通常是1MB~5MB左右。

web Storage 注意事项

  1. web Storage 是同步API,是阻塞型的,如果存储的键值对太大,会影响用户的使用体验
  2. web Storage 存储的是字符串,要保存对象的时候,需要转为字符串时通常使用JSON.stringify进行序列化

JSON.stringify 缺点:

  1. 对象中有时间类型的时候,序列化之后会变成字符串类型。
  2. 对象中有undefined和Function类型数据的时候,序列化之后会直接丢失。
  3. 对象中有NaN、Infinity和-Infinity的时候,序列化之后会显示 null。
  4. 对象循环引用的时候,会直接报错。

sessionStroage是共享的吗?

sessionStroage 不是共享的,在新标签或窗口打开一个页面时会复制顶级浏览会话的上下文作为 新会话的上下文,打开多个相同的URL的 Tabs 页面,会创建各自的 sessionStorage 。

chrome浏览器89版本后,通过a标属性target="blank"跳到新页面时 sessionStorage就会丢失。a标签添加属性 rel="opener" 能够复制。仅仅能复制,之后的更改并不会同步!!

StorageEvent

当前页面使用的 storage 被其他页面修改时会触发 Storage Event 事件。

Storage Event 事件在同一个域下的不同页面间触发,即在A页面注册了 storge 的监听处理,只有在跟A同域名下的B页面操作 storage 对象,A页面才会被触发 storage 事件,B页面本身不会触发事件。

sessionStorage能触发 StorageEvent事件嘛?

能触发,但是有以下两种情况:

  • a标签打开:不触发(demo1)
  • iframe嵌套:触发(demo2)

举例说明demo1:

//index.html
<body>
  <button type="button" id="btnAdd">添加</button>
  <a href="./other.html" target="_blank" rel="opener">打开新页面</a>
  <script>
    let index = 1;
    btnAdd.onclick = function() {
      console.time(`key-1`)
      sessionStorage.setItem('key-1',"值-1")
      console.timeEnd(`key-1`)
      index++
    }
  </script>
</body>

//other.html
<body>
  <div>
    <div>消息:</div>
    <div id="message"></div>
  </div>
  <script>
    window.addEventListener("storage",function(ev){
      message.innerHTML = ev.newValue;
    })
  </script>
</body>
image.png image.png

举例说明dem02:

//index.html
<body>
  <button type="button" id="btnAdd">添加</button>
  <iframe src="./other.html"></iframe>
  <script>
    let index = 1;
    btnAdd.onclick = function() {
      console.time(`key-1`)
      sessionStorage.setItem('key-1',`值-${index}`)
      console.timeEnd(`key-1`)
      index++
    }
  </script>
</body>
image.png

如何区分 StorageEvent 事件是谁触发的

image.png

可以通过storageArea来判断到底是sessionStorage 触发的还是 localStorage 触发的。

localStorage支持过期

简单的实现

  1. 添加一个属性,记住过期的时间;
  2. 添加数据的时候,一起保存;
  3. 查询数据,比对事件,过期删除。
<body>
  <button type="button" id="btnSetItem">添加</button>
  <button type="button" id="btnGetItem">查询</button>
  <script>
    const myLocalStore = {
      setItem: (key, value, expire) => {
        const lsValue = JSON.parse(localStorage.getItem(key) || '{}');
        localStorage.setItem(
          key,
          JSON.stringify({
            ...lsValue,
            value,
            expire
          })
        )
      },
      getItem: (key) => {
        const lsValue = JSON.parse(
          localStorage.getItem(key) || '{}'
        )
        if(lsValue.expire && lsValue.expire >= Date.now()) {
          return lsValue.value
        } else {
          localStorage.removeItem(key)
          return null
        }
      }
    }

    btnSetItem. onclick = function () {
      myLocalStore.setItem('key-x','value-1', Date.now() + 10000);
    }
    btnGetItem. onclick = function () {
      console.log("getItem: ", myLocalStore.getItem('key-x'));
    }
  </script>
</body>

image.png

第三方库

  • web-storage-cache
var wsCache = new WebStorageCache();

// 缓存字符串‘wqteam’到‘username’中,超时时间100秒
wsCache.set('username','wqteam',{exp:100});

//超时截止日期,可使用Date类型
var nextYear = new Date();
nextYear.setFullYear(nextYear.getFullYear() + 1);
wsCache.set('username','wqteam',{exp:nextYear});

localStorage存储加密

简单加密

  1. URL方式 : encodeURIComponent 、 decodeURIComponent
// 编码(将 Unicode 字符串编码为 UTF-8)
var unicodeString = 'Hello, 你好';
var utf8String = encodeURIComponent(unicodeString);
console.log(utf8String); // 输出:Hello%2C%20%E4%BD%A0%E5%A5%BD

// 解码(将 UTF-8 编码还原为 Unicode 字符串)
var decodedString = decodeURIComponent(utf8String);
console.log(decodedString); // 输出:Hello, 你好

  1. base64:window.btoa + window.atob
var password = "***$$$ABCC"
var val = btoa(password)  //KioqJCQkQuJDQw==
var oriVal = atob(val)   //***$$$ABCC

复杂加密

  • Web Crypto API 的SubtleCrypto 接口提供了许多底层加密功能
image.png

使用案例

使用加密库

  • crypto-js
var CryptoJs = require("crypto-js");
var data = [{id:1},{id:2}]
// Encrypt
var ciphertext = CryptoJs.AES.encrypt(JSON.stringify(data),'secret key 123').toString();
// Decrypt
var bytes = CryptoJs.AES.decrypt(ciphertext,'secret key 123');
var decrypteData = JSON.parse(bytes.toString(CryptoJs.enc.Utf8));

console.log(decrypteData)

第三方库

  • secure-ls
  • localstorage-slim

web Storage的存储空间

localstorage 存储的键值采用什么字符编码?

UTF-16,每个字符使用两个字节,是有前提条件的,就是码点小于OxFFFF(65535),大于这个码点的是四个字节。

5M 的单位是什么

image.png
  • 字符的长度值,utf-16的编码单元
  • 字符的个数,并不等于字符的长度
  • 2个字节作为一个utf-16的字符编码单元,也可以说是5M 的utf-16的编码单元

localStorage 键占不占存储空间

答案:占,在存储key-value时应该使用简洁明了的设置key名称,为value节省空间。

<body>
  <button type="button" id="btnSave">保存</button>
  <script>
    btnSave.onclick = function () {
      const charTxt = "a";
      let count = 2.5 * 1024 * 1024;  //给键值对都设定2.5M长度
      let content = Array.from({ length: count }, (_) => charTxt).join("");
      const key = Array.from({ length: count }, (_) => charTxt).join("");
      localStorage.clear();
      try {
        console.time("setItem");
        // 此时如果给content再多设置一位字符就会报错
        localStorage.setItem(key, content);
        console.timeEnd("setItem");
      } catch (err) {
        console.log("err code:", err.code);
        console.log("err message:", err.message);
      }
    };
  </script>
</body>