目录

📦 JavaScript 浏览器存储

Cookie 是一串 直接存储在浏览器的数据;是 HTTP 协议的一部分;

  • 通常由 Web 服务器使用响应 Set-Cookie HTTP header 设置。
  • 浏览器使用 Cookie HTTP header 自动添加到(几乎) 每个对相同域 的请求中。

🌰 例子 / 登录身份验证:

  • 登录成功后,服务器在响应中使用 Set-Cookie HTTP-header 来设置具有唯一的会话标识符的 cookie。
  • 下次当请求被发送到同一个域时,浏览器会使用 Cookie HTTP-header 通过网络发送 cookie。
  • 服务器通过这个 cookie 识别身份的请求。

使用 document.cookie 读取网站存储的 cookie。

  • cument.cookie 的值由 name=value 对组成,以 ; 分隔。
  • 每一个都是独立的 cookie。

使用 JavaScript 写入 cookie 使用 document.cookie不是数据属性),这个访问器 (包含 getter / setter ),对其赋值操作会被特殊处理。

document.cookie 的写入操作只会更新其中提到的 cookie而不会涉及其他 cookie

🌰 例子:

document.cookie = "user=john"; // 只更新名称为 user 的 cookie
console.log(document.cookie); // 展示所有的 cookie
1
2

cookie 的名称和值 可以是任何字符。为了保持有效的格式,它们应该使用内建的 encodeURIComponent 函数对其进行转义(与 URL 对象添加参数同理):

// 特殊字符(空格),需要编码
let name = "my name";
let value = "John Smith"

// 将 cookie 编码为 my%20name=John%20Smith
document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);

console.log(document.cookie);
1
2
3
4
5
6
7
8

cookie 存储限制:

  • encodeURIComponent 编码后的 name=value 对,大小不能超过 4KB。
  • 每个域的 cookie 总数不得超过 20+ 左右,具体限制取决于浏览器

Cookie 中的要设置的选项参数:

document.cookie = "user=John; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT"
1
  • pathurl 路径前缀。必须是绝对路径,使得该路径下的页面运行访问该 cookie。默认为当前路径。通常设置为 path=/ ,即使 cookie 对该网站的所有页面可见。

    例子,一个 cookie 带有 path=/admin 设置,那么该 cookie 在 /admin/admin/something 下都是可见的,但是在 /home/adminpage 下不可见。

  • domain :控制可以访问 cookie 的域。在实际中,有一些安全限制无法设置的域,例如,无法从另一个二级域访问 cookie( other.com 永远不会收到在 site.com 设置的 cookie),这使得 cookie 的敏感数据只能存储在一个站点上可用的 cookie 中。

    ** 默认情况下,cookie 只有在设置的域下才能被访问到。 ** 注意,默认情况下,cookie 不会共享给 子域(例如, forum.site.comsite.com 的子域)。

    可以通过设置 domain 允许子域设置 cookie:当在 site.com 设置 cookie 时,应该明确地将 domain 选项设置为根域: domain=site.com 。那么,所有子域都可以访问到这样的 cookie。

    🌰 例子:

    document.cookie = "user=John; domain=site.com"
    
    1

    在子域 forum.site.com 中,可以访问该 cookie:

    alert(document.cookie); // 有 cookie user=John
    
    1
  • expires / max-age :控制 cookie 的过期事件。默认情况下, 不设置这两个参数的 cookie 在关闭浏览器后消失,即为 session cookie。可以通过设置这两个参数保持 cookie 在浏览器关闭后仍然存在。

    expire :设置浏览器会清除掉该 cookie 的时间日期。

    🌰 例子:

    expires=Tue, 19 Jan 2038 03:14:07 GMT
    
    1

    日期格式必须为 GMT 时区格式。可以通过 date.toUTCString() 方法转换。

    🌰 例子 / 将 cookie 设置为 一天后到期

    let date = new Date(Date.now() + 86400e3);
    date = date.toUTCString();
    document.cookie = "user=john; expires=" + date;
    
    1
    2
    3

    如果将 expires 设置为过去的日期,那么 cookie 会被直接删除。

    max-age :是 expires 的替代选项,指明 cookie 的过期时间距离当前时间的秒数。(设置在设定的秒数之后过期)。

    🌰 例子 / 将 cookie 设置为 1 个小时后失效:

    document.cookie = "user=John; max-age=3600";
    
    1

    🌰 例子 / 设置为 0 或者负数,cookie 立即过期:

    document.cookie = "user=John; max-age=0";
    
    1
  • secure :设置 cookie 的安全协议。

    默认情况下,如果在 http://site.com 上设置了 cookie,那么该 cookie 也会出现在 https://site.com 上,反之亦然。即此时的 cookie 不区分 HTTP 协议

    如果一个 cookie 是通过 https://site.com 设置的,那么它不会在相同域的 HTTP 环境下出现,例如 http://site.com 。所以,如果一个 cookie 包含绝不应该通过未加密的 HTTP 协议发送的敏感内容,那么就应该设置 secure 标识。

    🌰 例子 / 设置为只能在当前协议下可访问的 cookie(如当前在 HTTPS 环境下):

    document.cookie = "user=John; secure";
    
    1
  • samesite :cookie 的另一个安全特性,可以防止 XSRF (跨网站请求伪造) 攻击。

    XSRF 攻击的例子:当用户登录了 bank.com ,有了当前网站的 cookie。浏览器会在每次请求时将其发送到 bank.com 的服务器,以便识别身份执行敏感的操作。

    此时,在浏览器的另一个窗口访问另一个网站,该网站具有发送 bank.com/pay 请求的表单。当每次访问 bank.com 网站时,浏览器都会发送 cookie ,即使请求是从不同的网站发送的。

    实际的网站不会允许这样的情况发生,所有由 bank.com 生成的表单都具有一个特殊的字段,(XSRF 保护 token)。恶意页面既不能生成,也不能从远程页面提取它。它可以在那里提交表单,但是无法获取数据。并且,网站 bank.com 会对收到的每个表单都进行这种 token 的检查。(一种防止 XSRF 的方式)

    上面的方法,需要检查每个表单请求的 token 字段。使用 cookie 的 samesite 选项:

    samesite=strict :默认情况(没有值时):除了来自同一个网站的请求,设置了 samesite=strict 的 cookie 在其他网站域永远不会被发送。

    设置为 strict 后, XSRF 攻击是没有机会成功的,因为来自 evil.com 的提交没有 cookie。因此, bank.com 将无法识别用户,也就不会继续进行付款。

    但是 strict 有不方便的地方:当从其他地方访问 bank.com 连接时,同样也不能发送 samesite=strict 的 cookie。可以通过设置两种不同的 cookie 解决 :一种用于「一般识别」;另一种用于进行敏感数据信息操作时的识别(设为 strict 的 cookie)。这样用户从别处访问网站时同样可以识别身份;并且进行敏感操作时必须从同一个网站启动才能进行。

    samesite=lax :比 strict 轻松的 cookie 限制。 同样可以防止 XSRF 攻击,但是不会破坏用户的体验。

    strict 相似,从外部来到网站的,禁止浏览器发送 cookie,但是有例外的情况(符合以下两个条件):

    • HTTP 方法为安全方法;(安全的 HTTP 方法一般用于读取数据,不会更改数据,例如 GET)

    • 该操作执行顶级导航(会更改浏览器地址栏中的 URL 的操作)

      通常成立。如果在 <iframe> 中执行就不是顶级;用于网络请求的 JavaScript 方法不会执行任何导航。

    samesite=lax 所做的是基本上允许最常见的前往 URL 操作携带 cookie。(例如,从笔记中打开网站),但是来自另一个网站的网络请求 / 表单提交都不会携带 cookie。

  • httpOnly:Web 服务器使用 Set-Cookie header 来设置 cookie。并且,它可以设置 httpOnly 选项。这个选项禁止任何 JavaScript 访问 cookie,即使用 document.cookie 看不到此类 cookie,也无法对此类 cookie 进行操作。

编写常见的 cookie 操作函数,相对于使用 document.cookie 更加方便(可以使用相似第三方库引入使用):

  • getCookie(name) :返回具有给定的 name 的 cookie。如果没有找到则返回 undefined

    function getCookie(name) {
      let matches = document.cookie.match(new RegExp(
        "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
      ));
      return matches ? decodeURIComponent(matches[1]) : undefined;
    }
    
    1
    2
    3
    4
    5
    6
  • setCookie(name, value, options) :将 cookie 的 name 设置为具有默认值 path=/ (可以修改以添加其他默认值)和给定值 value

    function setCookie(name, value, options = {}) {
    
      options = {
        path: '/',
        // 如果需要,可以在这里添加其他默认值
        ...options
      };
    
      if (options.expires instanceof Date) {
        options.expires = options.expires.toUTCString();
      }
    
      let updatedCookie = encodeURIComponent(name) + "=" + encodeURIComponent(value);
    
      for (let optionKey in options) {
        updatedCookie += "; " + optionKey;
        let optionValue = options[optionKey];
        if (optionValue !== true) {
          updatedCookie += "=" + optionValue;
        }
      }
    
      document.cookie = updatedCookie;
    }
    
    // 使用范例:
    setCookie('user', 'John', {secure: true, 'max-age': 3600});
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
  • deleteCookie(name) :删除 cookie。

    通过设置过期时间 max-age 为 0 或者负数实现:

    function deleteCookie(name) {
      setCookie(name, "", {
        'max-age': -1
      })
    }
    
    1
    2
    3
    4
    5

注意

当进行更新或删除一个 cookie 操作时,应该使用和设置 cookie 时相同的路径和域选项。

常见的第三方 cookie 用与跟踪和广告服务。它们被绑定在原始域上,因此 ads.com 可以在不同网站之间跟踪同一用户,如果这些网站都可以访问 ads.com 的话。

例如:

  • site.com 网站的一个页面加载了另外一个网站的 banner: <img src="https://ads.com/banner.png">
  • 与 banner 一起, ads.com 的远程服务器可能会设置带有 id=1234 这样的 cookie 的 Set-Cookie header。此类 cookie 源自 ads.com 域,并且仅在 ads.com 中可见。
  • 下次访问 ads.com 网站时,远程服务器获取 cookie id 并识别用户。
  • 用户从 site.com 网站跳转至另一个也带有 banner 的网站 other.com 时, ads.com 会获得该 cookie,因为它属于 ads.com ,从而识别用户并在他在网站之间切换时对其进行跟踪。

现代浏览器可能会禁用此类的 cookie:

  • Safari 浏览器完全不允许第三方 cookie。
  • Firefox 浏览器附带了一个第三方域的黑名单,它阻止了来自名单内的域的第三方 cookie。

对于加载来自第三方域的脚本中使用 document.cookie 设置 cookie,无论脚本来自何处,设置的 cookie 都属于当前网页的域而不是第三方 cookie。

# LocalStorage / SessionStorage

通过 Web 存储对象 localStoragesessionStorage 在浏览器中保存 键 / 值对数据。在页面刷新( sessionStorage ) 或者浏览器重启之后( localStorage )数据仍然可以保留在浏览器中。

相对于 cookie,Web 存储对象:

  • Web 存储对象不会随每个请求被发送到服务器。所以可以存储更多的数据,并且浏览器允许保存至少 2 MB 的数据(或者更多),并且具有配置数据的设置;
  • 服务器无法通过 HTTP header 操作存储对象,一切通过 JavaScript 完成进行。
  • 存储绑定到源(域 / 协议 / 端口三者)。也就是说,不同协议或子域对应不同的存储对象,它们之间无法访问彼此数据。
  • 键值 key / value 必须为字符串;
  • 存储大小限制为 5 MB+。取决于浏览器;
  • 没有过期时间(LocalStorage)

两种存储对象提供相同的方法和属性

  • setItem(key, value) :存储键 / 值对。
  • getItem(key) :按照键获取值。
  • removeItem(key) :删除键及其对应的值。
  • clear() :删除所有数据。
  • key(index) : 获取该索引下的 键名
  • length :存储的内容的长度。

# localStorage 的使用

一般在所有同源的窗口之间localStorage 数据可以共享。

🌰 例子 / 使用 localStorage

  • 存储数据:

    localStorage.setItem('test', 1)
    
    1
  • 读取数据:

    console.log(localStorage.getItem('test')) // 1
    
    1

在重新打开浏览器的同一个页面(不同窗口)也可以获取该数据;在同一个源(域 / 端口 / 协议),URL 路径可以不同,都可以共享该数据。

🌰 例子 / 类对象形式的访问:

与使用一个普通对象一样使用 LocalStorage:

// 设置 key
localStorage.test = 2;

// 获取 key
alert( localStorage.test ); // 2

// 删除 key
delete localStorage.test;
1
2
3
4
5
6
7
8

但一般不建议这样使用:

  • 键值对由用户生成,可以为任何的内容。如果设置与内置对象属性相同的键(如 length / toString ),使用这种方式会出错,而使用 setItem / getItem 可以正常工作:

    let key = 'length';
    localStorage[key] = 5; // Error,无法对 length 进行赋值
    
    1
    2
  • 存在 storage 事件,使用 类对象的形式修改数据不会触发; setItem 正常修改数据会触发。

🌰 例子 / 遍历键:

由于存储对象是不可迭代的。要获取所有的键(或者值):

  • 使用遍历数组的方法:

    for (let i = 0; i < localStorage.length; i++) {
      let key = localStorage.key(i)
      console.log(`${key}: ${localStorage.getItem(key)}`)
    }
    
    1
    2
    3
    4
  • 使用 for... in 循环方法:

    注意这种方法会像处理常规对象一样;遍历所有的键,同时也会输出一些不需要的内建字段。

    for(let key in localStorage) {
      alert(key); // 显示 getItem,setItem 和其他内建的东西
    }
    
    1
    2
    3

    需要使用 hasOwnProperty 检查过滤掉原型中的字段:

    for(let key in localStorage) {
      if (!localStorage.hasOwnProperty(key)) {
        continue; // 跳过像 "setItem","getItem" 等这样的键
      }
      console.log(`${key}: ${localStorage.getItem(key)}`);
    }
    
    1
    2
    3
    4
    5
    6

    或者使用 Object.keys 获取只属于自己的键,需要的遍历出来:

    let keys = Object.keys(localStorage)
    for (let key of keys) {
      console.log(`${key}: ${localStorage.getItem(key)}`)
    }
    
    1
    2
    3
    4

注意,存储的对象的 键 / 值必须为 字符串。 如果为其他类型(数值、对象),会被自动转换为字符串。

可以使用 JSON.stringfy() 存储对象:

localStorage.user = JSON.stringify({name: "John"});
1

读取的数据使用 JSON.parse() 转换:

let user = JSON.parse(localStorage.user)
console.log(user)
1
2

或者对整个存储对象进行字符化处理:

console.log(JSON.stringfy(localStorage, null, 2))
1

# sessionStorage 的使用

localStorage 的使用基本类似,使用频率较低。使用 sessionStorage 有以下的限制:

  • 存储的数据只能保存在当前的会话(浏览器的标签页):
    • 意味着相同的页面不同的窗口会有不同的存储;
    • 但是在同一个页面下的 iframe 下的存储是相同的。
  • 存储的数据在刷新后能保留,但是关闭或者重启浏览器不会保留。

# storage 事件

localStoragesessionStorage 中的数据更新后, storage 事件就会被触发,它会有以下的属性:

  • key :发生数据更改的键 key ;(如果调用的是 clearkeynull
  • oldValue :旧值;(新增数值为 null
  • newValue :新值;(删除数值为 null
  • url发生数据更改的文档的 url
  • storageData :发生数据更改的( localStorage / sessionStorage )对象;
  • storageArea :包含存储对象( sessionStoragelocalStorage )具有相同的事件,所以 event.storageArea 引用了被修改的对象。 允许同源的不同窗口交换信息。

并且, storage 事件会在 所有可以访问到存储对象的 window 对象 上触发,导致当前数据发生改变的 window 对象除外。

🌰 例子 / 当有两个窗口具有相同的页面, localStorage 在它们之间共享数据。如果两个窗口都监听 window.onstorage 事件,那么每个窗口都会对另一个窗口中发生的更新作出反应。

  • 在其他文档对同一存储进行更新时会触发:

    window.onstorage = event => {
      if (event.key != 'now') return;
      console.log(event.key + ':' + event.newValue + " at " event.url)
    }
    
    // 或者使用 window.addEventListener('storage', event => {
    // ...
    // })
    
    1
    2
    3
    4
    5
    6
    7
    8
  • 会捕捉到关于 keynow 的数据的更新:

    localStorage.setItem('now', Date.now())
    
    1

# IndexedDB

是一个浏览器内建的数据库,比 localStorage 强大得多。

  • 支持多种类型的键,可以存储几乎任何类型的值;
  • 支持事务可靠性;
  • 支持键值的范围查询、索引;
  • 相比于 localStorage 可以存储更大的数据;

IndexedDB 适用于 离线应用,可与 ServiceWorkers 和其他技术相结合使用。

📢 上次更新: 2022/09/02, 10:18:16