-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
314 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,314 @@ | ||
--- | ||
title: Cookie 和 Storage API 区别与详解 | ||
number: '#275' | ||
link: 'https://github.com/toFrankie/blog/issues/275' | ||
created_at: '2023-02-26 20:38:59' | ||
updated_at: '2024-03-17 17:53:12' | ||
labels: | ||
- 前端 | ||
- JS/ES | ||
- Ajax | ||
- '2021' | ||
--- | ||
 | ||
|
||
|
||
它们都是浏览器数据存储的方案,是用于解决**数据持久化**的问题。除此之外,数据也可以存储在内存中(比如挂载到 `window` 等全局对象下),但这种方式每当页面刷新就会丢失。 | ||
|
||
下面分别从几个方面,详细地介绍 `Cookie`、`sessionStorage`、`localStorage` 的区别。 | ||
|
||
#### 1. 空间限制 | ||
|
||
* `Cookie` - 大小约为 `4K`。 | ||
* `sessionStorage` 和 `localStorage` - 大小约为 `5M`。 | ||
|
||
请注意,其中 Cookie 的空间大小指的是 `name`、`value` 以及 `=` 号。 | ||
|
||
另外,这三个不同浏览器下可能会有细微的差异,可忽略。 | ||
|
||
|
||
> 在所有浏览器中,若 Cookie 大小已超出空间限制,后续设置的新 Cookie 就会被忽略。 | ||
#### 2. 数量限制 | ||
|
||
* `sessionStorage` 和 `localStorage` 无数量限制一说。 | ||
* `Cookie` 是有数量限制的。 | ||
|
||
下表为网上收集(非当前实测结论),看一眼知道有限制这回事就好。 | ||
|
||
| 浏览器 | 大小 | 数量 | | ||
| --- | --- | --- | | ||
| IE6 | 4095 个字节 | 每个域 20 个 | | ||
| IE7/8 | 4095 个字节 | 每个域 50 个 | | ||
| Opera | 4096 个字节 | 每个域 30 个 | | ||
| FireFox | 4097 个字节 | 每个域 50 个 | | ||
| Safari | 4097 个字节 | 没有数量限制 | | ||
| Chrome | 4097 个字节 | 每个域 53 个 | | ||
|
||
> 若有兴趣,实测可以看这个网站:[Browser Cookie Limits](http://browsercookielimits.iain.guru/)。作者简单跑了下,Chrome 每个域是 180 个;Firefox 很快卡住了,风扇嗡嗡响...... 测试结果 Firefox 和 Safari 都 N/A(没数据),应该是没数量限制吧。 | ||
鉴于各浏览器对 `Cookie` 的空间、数量限制不完全相同,为了较好地兼容,建议如下: | ||
|
||
> * 总共 300 个 Cookie | ||
> * 每个 Cookie 大小为 4096 个字节 | ||
> * 每个域 20 个 Cookie | ||
> * 每个域 81920 个字节(20 × 4096) 。 | ||
没错,面试官问到这么说吧,应该就 OK 了。 | ||
|
||
另外,与超出空间限制不同的是,超出数量限制之后,是可以继续添加 `Cookie` 的,但不同浏览器有不同的策略:一些是替换掉最先(老)的 `Cookie`,有些则是随机替换。作简单了解就好,一般项目不会设那么多的,而且 `Cookie` 过期浏览器是会自动清除的。 | ||
|
||
#### 3. 有效期 | ||
|
||
* `Cookie` - 有效期是由 Max-Age 或 Expires 决定的。当 `Cookie` 过期或失效,由浏览器自动删除。 | ||
* 若在设置 `Cookie` 时不写入这两个属性,那么它的就是会话级别的,即退出浏览器会被销毁。 | ||
* 若同时存在时,`Max-Age` 优先级更高。 | ||
* `SessionStorage` - 在浏览器标签(或窗口)关闭之前均有效。刷新页面不影响。 | ||
* `localStorage` - 若不主动清除,永久有效(“主动”是指由浏览器或脚本清除)。 | ||
|
||
> 请注意: | ||
> | ||
> 讲道理的话,会话级的 `Cookie` 在浏览器退出时就会被删除。但是有些浏览器不讲武德,比如 Chrome。在某些情况下,关闭浏览器重新进入,会话级别的 Cookie 并不会删除,在 Application 选项的 Cookie 栏还能看到。 | ||
> | ||
> 原因可能是:**Chrome - 设置 - 启动时** 选择了 **打开新标签** 之外的选项。切换过来就好了... | ||
#### 4. 作用域 | ||
|
||
这块内容比较多... | ||
|
||
**铺垫 - 了解同源和同站的区别** | ||
|
||
区别如下: | ||
|
||
|
||
> * **同源**(Same-Origin):协议(protocol)+ 主机名(hostname)+ 端口(port)完全一致。 | ||
> * **同站**(Same-Site):`eTLD + 1` 完全一致。 | ||
**铺垫 - 顶级域名和二级域名的认知** | ||
|
||
关于顶级域名、二级域名,也有很多认知问题。 | ||
|
||
> * **顶级域名**:也称为“一级域名”,常见的有 `.com`、`.cn`、`.org`、`.net` 等等,需要注意的是像 `.com.cn`、`.com.hk` 也属于顶级域名。 | ||
> * **二级域名**:就是顶级域名的下一级域名。 | ||
> | ||
在国内,很多资料认为,顶级域和一级域是分开的。比如 `.baidu.com`,如果按照这种方式划分,那么`.com` 是顶级域名,`.baidu.com` 就是一级域名。好像阿里云就是这样定义的。 | ||
|
||
而我更偏向于认为,`.baidu.com` 属于二级域名。不用过分纠结,在团队内统一即可。 | ||
|
||
`eTLD`(Top-Level Domains)表示有效顶级域名,那么 `eTLD + 1` 就表示二级域名。例如: | ||
|
||
```text | ||
https://www.example.com.cn | ||
``` | ||
|
||
其中 `eTLD` 是 `.com.cn`,那么 `eTLD + 1` 就是 `.example.com.cn`。关于 [eTLD](https://publicsuffix.org/list/public_suffix_list.dat) 更多请看[这里](https://juejin.cn/post/6877496781505200142)。 | ||
|
||
**铺垫 - 区分同源和同站** | ||
|
||
先看下,完整的 URL(网址)构成如下: | ||
|
||
 | ||
|
||
简单来说,只要二级域名相同,就属于同站。同源则要求更严格。因此同源一定同站,反之则不一定。 | ||
|
||
以下例子,同站,但不同源。 | ||
|
||
```text | ||
http://a.example.com:80 | ||
http://b.example.com:80 | ||
``` | ||
|
||
**正题 - 三者的作用域** | ||
|
||
> * `localStorage` - 这个最简单,必须**同源**才能访问,在不同标签(或窗口)之间可共享。 | ||
> * `sessionStorage` - **必须同源,且不同标签(或窗口)之间是不能共享的**(这点上盲猜挺多人会理解错的,我当初也理解错了)。 | ||
> * `Cookie` - **同站**是前提,它还受限于具体的 `Domain` 和 `Path`。 | ||
|
||
理解这些很重要,为什么这么说呢? | ||
|
||
**拓展 - 作用域引发的问题** | ||
|
||
很多公司,不是每个 Web 项目对应一个三级域名。 | ||
|
||
```text | ||
// 主营业务、较为重要的业务,可能会这样去区分: | ||
https://project-a.company.com | ||
https://project-b.company.com | ||
// 但一些非业务性、或者一些小项目,很可能是这样的: | ||
https://sub.company.com/project-a/index.html | ||
https://sub.company.com/project-b/index.html | ||
``` | ||
|
||
这些都很常见,那么问题来了。它们很多都是同源、同站的。假设按小项目划分场景,我在 `a` 项目中设置了一个 `sessionStorage` 会话级缓存,那么当前从 `a` 项目跳转至 `b` 项目时,`b` 是可以获取到 `a` 项目的所有 `sessionStorage` 的,反之也成立。如果 `a` 和 `b` 项目中某个 `sessionStorage` 的 `key` 不小心设置成相同的话,那很可能就会影响到对方。`localStorage` 同理。至于 `Cookie` 的话,由于它的空间限制最大只允许 4K,因此不适宜存过多数据,一般会存一些像鉴权信息等比较多。同个公司,业务的用户鉴权等是相似的,所以 `Cookie` 的访问机制也不会有太大的影响。 | ||
|
||
针对这些问题,建议是非必要的话,将数据存在内存中,比如用 Vuex、Redux、MobX 等状态管理工具来维护应用的状态。一是信息更不容易暴露,而是可以减少 IO 的读写。但是,这样的话,就要解决数据持久化的问题,因为在内存中的话,只要刷新页面就会丢失。怎么解决? | ||
|
||
以 Redux 为例,在创建 Store 时,是可以传入一个初始状态的,它的值取下面这个会话缓存即可。只要监听到状态发生变化变化,并设置或更新 `sessionStorage` 级别的缓存,将状态缓存起来即可。比如: | ||
|
||
```js | ||
import { createStore } from 'redux' | ||
|
||
// 定义一个可按项目划分的 key | ||
const { host, pathname } = window.location | ||
const stateCacheKey = `cache_state_${host}_${pathname}` | ||
|
||
// 设置 store 的初始值,取 stateCacheKey 缓存的值,首次为空对象 | ||
const initialState = JSON.parse(sessionStorage.getItem(stateCacheKey)) || {} | ||
|
||
// 创建 Store(reducers、middlewares 非本文讨论重点省略...) | ||
const reducers = (state = {}, action) => { /* some reducers... */ } | ||
const store = createStore(reducers, initialState) // 这里省略了中间件 | ||
|
||
// 监听状态,每次状态变化 stateCacheKey 都得以更新 | ||
const unsubscribe = store.subscribe(() => { | ||
const currentStateStr = JSON.stringify(store.getState()) | ||
window.sessionStorage.setItem(stateCacheKey, currentStateStr) | ||
}) | ||
|
||
// 解除监听,这样 store.unsubscribe() 调用即可 | ||
store.unsubscribe = unsubscribe | ||
|
||
// 个人习惯,也将 store 挂载全局,以备特殊情况调用 | ||
window.store = store | ||
|
||
// 作为模块导出,并传入 react-redux 的 Provider 组件 | ||
export default store | ||
``` | ||
|
||
**注意 - sessionStorage 鲜为人知的点(冷门)** | ||
|
||
下面例子,均在同源情况下。假设有两个同源页面: A 页面、B 页面对应 URL 为 `page_a_url`、`page_b_url`。 | ||
|
||
示例一: | ||
|
||
```js | ||
// 1. 在 A 页面,设置一个会话缓存:keyA | ||
sessionStorage.setItem('keyA', '123') | ||
|
||
// 2. 若 A 有一个链接,可跳转至 B 页面(将会以新窗口的形式打开 B 页面) | ||
<a target="_blank" href="page_b_url">To Page B</a> | ||
|
||
// 3. 点击链接,跳转到 B 页面 | ||
sessionStorage.getItem('keyA') // ❓ 打印结果是什么? | ||
|
||
// 4. 接着,在 B 页面中,设置另一个会话缓存:keyB | ||
sessionStorage.setItem('keyB', '456') | ||
|
||
// 5. 切换至 A 页面,打印一下: | ||
sessionStorage.getItem('keyB') // ❓ 打印结果又是什么? | ||
|
||
// 6. 在 A 页面中再次设置一个缓存:keyB | ||
sessionStorage.setItem('keyB', '789') | ||
|
||
// 7. 再次切换至 B 页面的窗口, | ||
sessionStorage.getItem('keyB') // ❓ 打印结果是 "456" 还是 "789" 呢? | ||
``` | ||
|
||
示例二:将上述第二步改成下面这样: | ||
|
||
```html | ||
<a onclick="window.open('page_b_url', 'DescriptiveWindowName')">To Page B</a> | ||
``` | ||
|
||
结果又是什么呢?直接看下结果: | ||
|
||
```text | ||
示例一,依次打印出:null、null、"456" | ||
示例二,依次打印出:"123"、null、"456" | ||
``` | ||
|
||
结论: | ||
|
||
> * 同一标签(或窗口)下,所有同源页面将共享 `sessionStorage`,同样地,在某个页面修改,将影响其他页面。 | ||
> | ||
> * 通过 `<a target="_blank" href="page_b_url"></a>` 或 `window.open('page_b_url', 'windowName')` 方式打开其他同源页面,有以下特点: | ||
> * 两个 Tab 之间的 `sessionStorage` 是独立的,互不影响。 | ||
> * 区别点在于,打开新窗口时,初始缓存不一样:前者的初始 `sessionStorage` 缓存为空。后者基于原 Tab 的 `sessionStorage` 拷贝一份,作为新窗口的初始 `sessionStorage` 缓存。 | ||
> * 前者表现与手动创建新窗口是一致的。 | ||
> * 需要另外一种情况,当在某页面内嵌套了一个同源的 `iframe`,它们之间 `sessionStorage` 是共享的。若非同源页面则不共享。(这一点不完全严谨,具体原因请往下看) | ||
|
||
**总结 - 作用域** | ||
|
||
> * `Cookie` 作用域前提是同站,同时还受限于 `Domain` 和 `Path`。若两者一致,即可理解为同站共享。 | ||
> * `sessionStorage` 和 `localStorage` 前提必须是同源,其次前者在不同标签(或窗口)相互独立;后者在所有标签之间共享。 | ||
> * 除此之外,以不同方式创建新标签(或窗口),它的 `sessionStorage` 初始值会有所差异。通过 `window.open()` 方式,会将原来先的 `sessionStorage` 值拷贝过来,作为其初始值;其他方式初始缓存为空。但注意,后续的 `sessionStorage` 读写操作都是独立对。 | ||
#### 5. 与服务器通信的差异 | ||
|
||
`sessionStorage` 和 `localStorage` 不会主动参与与服务器的通信。 | ||
|
||
Cookie 是保存在浏览器上的一小型文本文件。在每次与服务器的通信中都会携带在 HTTP 请求头之中。 | ||
|
||
> 但是会有一些限制,另起一文,请稍等... | ||
|
||
#### 6. 拓展(进阶) | ||
|
||
**Q1:storage 事件的迷惑行为** | ||
|
||
首先,注册 `storage` 事件监听器,它**只能**监听**其他同源页面**的缓存**发生改变**时,它才会被触发。 | ||
|
||
得出几个结论: | ||
> * 用于监听其他页面的缓存变化,而同一页面内的缓存变化,都不起作用。(奇葩吧) | ||
> * 由于不同 Tab 之间 `sessionStorage` 是独立的,因此无法监听 `sessionStorage` 的变化,即只能监听 `localStorage` 的变化。 | ||
> * “发生改变”包括:创建、更新、删除。但重复设置相同的 key-value 不会触发该事件,它至多在首次创建时触发。 | ||
```js | ||
const listener = function (e) { | ||
// key: 对应缓存 key | ||
// newValue: 该 key 新设置的缓存值 | ||
// oldValue: 该 key 对应的旧缓存值 | ||
// storageArea: 对应为 window.localStorage 对象 | ||
// storageArea: 触发该事件所对应的 URL | ||
} | ||
window.addEventListener('storage', listener) | ||
``` | ||
|
||
**Q2:当 localStorage 超过 5M 的空间限制之后,若再次 setItem 会怎样?** | ||
|
||
答案显而易见,这次 `setItem()` 将会失败,且会抛出错误。针对这种情况,可以做一些处理,比如清空再重新记录等... | ||
|
||
```js | ||
for (let i = 0; i < 2; i++) { | ||
try { | ||
localStorage.setItem('key', 'value') | ||
break | ||
} catch (e) { | ||
// 清空,并重试 | ||
// QuotaExceededError: The quota has been exceeded. localStorage缓存超出限制 | ||
localStorage.clear() | ||
} | ||
} | ||
``` | ||
|
||
**Q3:在 Safari 无痕模式下,对 sessionStorage 操作可能会抛出异常** | ||
|
||
请看这个帖子:[html5 localStorage error with Safari: "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to add something to storage that exceeded the quota."](https://stackoverflow.com/questions/14555347/html5-localstorage-error-with-safari-quota-exceeded-err-dom-exception-22-an) | ||
|
||
**Q4:sessionStorage 在 iframe 的问题** | ||
|
||
> 建议少用 `iframe`,尽管目前很多大网站仍然使用它。 | ||
前面提到顶级窗口和 iframe 窗口的页面都是同源的情况下,sessionStorage 是可共享的。但不完全是,比如受 `iframe` 的 [sandbox](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/iframe) 属性影响,分为两种情况: | ||
|
||
```html | ||
// 1️⃣ | ||
<iframe sandbox> | ||
|
||
// 2️⃣ | ||
<iframe sandbox="allow-same-origin"> | ||
``` | ||
|
||
假设两个页面同源;情况一 `sessionStorage` 不共享,在 `iframe` 中是一个全新的 `sessionStorage` 对象。情况二则共享 `sessionStorage`。 | ||
|
||
|
||
|
||
#### 7. References | ||
|
||
* [Web Storage API](https://developer.mozilla.org/zh-CN/docs/Web/API/Storage) | ||
* [Web Storage 的使用和进阶知识](http://caibaojian.com/web-storage.html) |