> For the complete documentation index, see [llms.txt](https://lizh.gitbook.io/knowledge/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lizh.gitbook.io/knowledge/frontend/01-ji-chu-01-liu-lan-qi-huan-cun.md).

# 基础 01 浏览器缓存

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。

Web 缓存分为很多种，比如：数据库缓存、服务器缓存、代理服务器缓存、CDN缓存，以及客户端（浏览器）缓存。

Web 缓存大致可归为两类：私有与共享缓存。

* 共享缓存：存储的响应能够被多个用户使用。如：CDN 缓存、数据库缓存等。
* 私有缓存：只能用于单独用户。浏览器缓存拥有用户通过 HTTP 下载的所有文档。这些缓存为浏览过的文档提供向后/向前导航，保存网页，查看源码等功能，可以避免再次向服务器发起多余的请求。它同样可以提供缓存内容的离线浏览。

浏览器缓存又可分为 HTTP 缓存和本地数据存储。

* HTTP 缓存：在 HTTP 请求传输时用到的缓存，主要在服务器代码上设置；
* 本地数据存储：由 Web 开发者用 `javascript` 在浏览器中读写的数据。

## HTTP缓存

HTTP 缓存是将 Web 资源（如：HTML、img、js、css、数据等）临时存储（缓存）客户端（浏览器）的一种信息技术。

### HTTP缓存的位置

从缓存位置上来说分为四种，并且各自有优先级，当依次查找缓存且都没有命中的时候，才会去请求网络。

* Service Worker
* Memory Cache
* Disk Cache
* Push Cache

#### Service Worker

`Service Worker` 是运行在浏览器背后的独立线程，一般可以用来实现缓存功能。

使用 `Service Worker` 的话，传输协议必须为 `HTTPS`。因为 `Service Worker` 中涉及到请求拦截，所以必须使用 `HTTPS` 协议来保障安全。`Service Worker` 的缓存与浏览器其他内建的缓存机制不同，它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存，并且缓存是持续性的。

`Service Worker` 实现缓存功能一般分为三个步骤：首先需要先注册 `Service Worker`，然后监听到 `install` 事件以后就可以缓存需要的文件，那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存，存在缓存的话就可以直接读取缓存文件，否则就去请求数据。

当 `Service Worker` 没有命中缓存的时候，我们需要去调用 `fetch` 函数获取数据。也就是说，如果我们没有在 `Service Worker` 命中缓存的话，会根据缓存查找优先级去查找数据。但是不管我们是从 `Memory Cache` 中还是从网络请求中获取的数据，浏览器都会显示我们是从 `Service Worker` 中获取的内容。

#### Memory Cache

`Memory Cache` 也就是内存中的缓存，主要包含的是当前中页面中已经抓取到的资源，例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快，内存缓存虽然读取高效，可是**缓存持续性很短，会随着进程的释放而释放**。 一旦我们关闭 Tab 页面，内存中的缓存也就被释放了。

那么既然内存缓存这么高效，我们是不是能让数据都存放在内存中呢？

这是不可能的。计算机中的内存一定比硬盘容量小得多，操作系统需要精打细算内存的使用，所以能让我们使用的内存必然不多。

内存缓存中有一块重要的缓存资源是 `preloader` 相关指令（如：`<link rel="prefetch">`）下载的资源。`preloader` 的相关指令已经是页面优化的常见手段之一，它可以一边解析 `js/css` 文件，一边网络请求下一个资源。

需要注意的事情是，**内存缓存在缓存资源时并不关心返回资源的 HTTP 缓存头 Cache-Control 是什么值，同时资源的匹配也并非仅仅是对 URL 做匹配，还可能会对 Content-Type，CORS 等其他特征做校验**。

#### Disk Cache

`Disk Cache` 也就是存储在硬盘中的缓存，读取速度慢点，但是什么都能存储到磁盘中，比之 `Memory Cache` 胜在容量和存储时效性上。

在所有浏览器缓存中，`Disk Cache` 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存，哪些资源可以不请求直接使用，哪些资源已经过期需要重新请求。并且即使在跨站点的情况下，相同地址的资源一旦被硬盘缓存下来，就不会再次去请求数据。绝大部分的缓存都来自 `Disk Cache`。

浏览器会把哪些文件丢进内存中？哪些丢进硬盘中？关于这点，网上说法不一，不过以下观点比较靠得住：

* 对于大文件来说，大概率是不存储在内存中的，反之优先；
* 当前系统内存使用率高的话，文件优先存储进硬盘。

#### Push Cache

HTTP/2 PUSH 是一项功能，它允许服务器抢先向客户端推送资源（无需相应请求）。比如：使用 HTTP/2 PUSH，服务器可以在收到对 HTML 文件的请求时立即推送字体文件，因为它知道客户端将来会请求它。

而 PUSH 的资源单独存储在 PUSH 缓存中。当以上三种缓存都没有命中时，它才会被使用。

这里推荐阅读 Jake Archibald 的 [HTTP/2 push is tougher than I thought](https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/) 这篇文章，文章中的几个结论：

* 所有的资源都能被推送，并且能够被缓存，但是 Edge 和 Safari 浏览器支持相对比较差；
* 可以推送 no-cache 和 no-store 的资源；
* 推送缓存位于 HTTP/2 连接中，因此如果连接关闭，它就会丢失。即使推送的资源是高度可缓存的，也会发生这种情况。
* 多个页面可以使用同一个 HTTP/2 的连接。也就可以使用同一个 Push Cache。这主要还是依赖浏览器的实现而定，出于对性能的考虑，有的浏览器会对相同域名但不同的 tab 标签使用同一个 HTTP 连接；
* Push Cache 中的缓存只能被使用一次；
* 浏览器可以中止推送的项目，如果它已经有的话；
* 你可以给其他域名推送资源。

### HTTP缓存的读写流程

1. 调用 Service Worker 的 fetch 事件获取资源；
2. 查看 memory cache；
3. 查看 disk cache：如果有强制缓存且未失效，则使用强制缓存，不请求服务器。这时的状态码全部是 200；如果有强制缓存但已失效，使用协商缓存，比较后确定 304 还是 200；
4. 发送网络请求，等待网络响应；
5. 把响应内容存入 disk cache (如果请求头信息配置可以存的话)；
6. 把响应内容的引用存入 memory cache (无视请求头信息的配置，除了 no-store 之外)；
7. 把响应内容存入 Service Worker 的 Cache Storage (如果 Service Worker 的脚本调用了 cache.put())；

### HTTP缓存的作用

HTTP 缓存是一种操作简单、效果显著的前端性能优化手段。当 HTTP 缓存发现请求的资源已经被存储，它会拦截请求，返回该资源的拷贝，而不会去源服务器重新下载。

HTTP 缓存带来的好处有：

* 减少页面加载时间，优化 Web 性能；
* 减少服务器负载；
* 减少网络带宽消耗，降低运营成本。

缓存需要合理配置，因为并不是所有资源都是永久不变的：重要的是对一个资源的缓存应截止到其下一次发生改变（即不能缓存过期的资源）。

### HTTP缓存相关的Header字段

* Cache-Control：通用头部字段。控制缓存的行为。
* Pragma：通用头部字段。http1.0，值为 `no-cache` 时禁用缓存。
* If-Match：请求头部字段。比较 ETag 是否一致
* If-None-Match：请求头部字段。比较 ETag 是否不一致
* If-Modified-Since：请求头部字段。比较资源最后更新的时间是否一致
* If-Unmodified-Since：请求头部字段。比较资源最后更新的时间是否不一致
* ETag：响应头部字段。资源的匹配信息
* Expires：实体头部字段。http1.0，实体主体是否过期的时间
* Last-Modified：资源的最后一次修改的时间。

### HTTP缓存的策略

通常浏览器缓存策略分为两种：**强缓存和协商缓存，并且缓存策略都是通过设置 HTTP Header 来实现的**。

#### HTTP缓存的读取流程

1. 浏览器在加载资源时，先根据这个资源的一些 http header 判断它是否命中强缓存，强缓存如果命中，浏览器直接从自己的缓存中读取资源，不会发请求到服务器。
2. 当强缓存没有命中的时候，浏览器一定会发送一个请求到服务器，通过服务器端依据资源的另外一些 http header 验证这个资源是否命中协商缓存，如果协商缓存命中，服务器会将这个请求返回，但是不会返回这个资源的数据，而是告诉客户端可以直接从缓存中加载这个资源，于是浏览器就又会从自己的缓存中去加载这个资源；
3. 强缓存与协商缓存的共同点是：如果命中，都是从客户端缓存中加载资源，而不是从服务器加载资源数据；区别是：强缓存不发请求到服务器，协商缓存会发请求到服务器。
4. 当协商缓存也没有命中的时候，浏览器直接从服务器加载资源数据。

#### 强缓存：Expires\&Cache-Control

强缓存是利用 Expires 或者 Cache-Control 这两个 http response header 实现的，它们都用来表示资源在客户端缓存的有效期。

当浏览器对某个资源的请求命中了强缓存时，返回的 HTTP 状态为 `200`，在 chrome 的开发者工具的 network 里面 size 会显示为 `(memory cache)` 或者 `(disk cache)`。

![image-20210805224601271](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20210805224601271.png)

**Expires**

Expires 是 HTTP 1.0 提出的一个表示资源过期时间的 header，它描述的是一个绝对时间，由服务器返回，用 GMT 格式的字符串表示。

```js
new Date().toGMTString()
// Thu, 05 Aug 2021 14:49:41 GMT
```

Expires 的缓存原理是：

1. 浏览器第一次跟服务器请求一个资源，服务器在返回这个资源的同时，在 response 的 header 加上 Expires 的值；
2. 浏览器在接收到这个资源后，会把这个资源连同所有 response header 一起缓存下来（缓存命中 header 并不是来自服务器，而是来自之前缓存的header）；
3. 浏览器再请求这个资源时，先从缓存中寻找，找到这个资源后，拿出它的 Expires 跟当前的请求时间比较，如果请求时间在 Expires 指定的时间之前，就能命中缓存，否则就不行；
4. 如果缓存没有命中，浏览器直接从服务器加载资源时，Expires Header 在重新加载的时候会被更新。

Expires 是较老的强缓存管理 header，由于它是服务器返回的一个绝对时间，在服务器时间与客户端时间相差较大时，缓存管理容易出现问题，比如：随意修改下客户端时间，就能影响缓存命中的结果。

**Cache-Control**

Cache-Control 是 HTTP 1.1 提出了一个新的 header，它是一个相对时间，在配置缓存的时候，以秒为单位，用数值表示。

```js
Cache-Control:max-age=31536000 // 一年
```

Cache-Control 还可以指定以下值：

* public：所有内容都将被缓存（客户端和代理服务器都可缓存）。具体来说响应可被任何中间节点缓存，如 Browser <-- proxy1 <-- proxy2 <-- Server，中间的 proxy 可以缓存资源，比如下次再请求同一资源 proxy1 直接把自己缓存的东西给 Browser 而不再向 proxy2 要。
* private：所有内容只有客户端可以缓存，Cache-Control 的默认取值。具体来说，表示中间节点不允许缓存，对于 Browser <-- proxy1 <-- proxy2 <-- Server，proxy 会老老实实把 Server 返回的数据发送给 proxy1,自己不缓存任何数据。当下次 Browser 再次请求时 proxy 会做好请求转发而不是自作主张给自己缓存的数据。
* no-cache：客户端缓存内容，是否使用缓存则需要经过协商缓存来验证决定。表示不使用 Cache-Control 的缓存控制方式做前置验证，而是使用 Etag 或者 Last-Modified 字段来控制缓存。需要注意的是，no-cache 这个名字有一点误导。设置了 no-cache 之后，并不是说浏览器就不再缓存数据，只是浏览器在使用缓存数据时，需要先确认一下数据是否还跟服务器保持一致。
* no-store：所有内容都不会被缓存，即不使用强制缓存，也不使用协商缓存
* max-age：max-age=xxx (xxx is numeric)表示缓存内容将在 xxx 秒后失效
* s-maxage（单位为 s)：同 max-age 作用一样，只在代理服务器中生效（比如 CDN 缓存）。比如当 s-maxage=60 时，在这 60 秒中，即使更新了 CDN 的内容，浏览器也不会进行请求。max-age 用于普通缓存，而 s-maxage 用于代理缓存。s-maxage 的优先级高于 max-age。如果存在 s-maxage，则会覆盖掉 max-age 和 Expires header。
* max-stale：能容忍的最大过期时间。max-stale 指令标示了客户端愿意接收一个已经过期了的响应。如果指定了 max-stale 的值，则最大容忍时间为对应的秒数。如果没有指定，那么说明浏览器愿意接收任何 age 的响应（age 表示响应由源站生成或确认的时间与当前时间的差值）。
* min-fresh：能够容忍的最小新鲜度。min-fresh 标示了客户端不愿意接受新鲜度不多于当前的 age 加上 min-fresh 设定的时间之和的响应。

Cache-Control 的缓存原理是：

1. 浏览器第一次跟服务器请求一个资源，服务器在返回这个资源的同时，在 response 的 header 加上 Cache-Control 的值；
2. 浏览器在接收到这个资源后，会把这个资源连同所有 response header 一起缓存下来；
3. 浏览器再请求这个资源时，先从缓存中寻找，找到这个资源后，**根据它第一次的请求时间和 Cache-Control 设定的有效期**，计算出一个资源过期时间，再拿这个过期时间跟当前的请求时间比较，如果请求时间在过期时间之前，就能命中缓存，否则就不行；
4. 浏览器再请求这个资源时，先从缓存中寻找，找到这个资源后，根据它第一次的请求时间和 Cache-Control 设定的有效期，计算出一个资源过期时间，再拿这个过期时间跟当前的请求时间比较，如果请求时间在过期时间之前，就能命中缓存，否则就不行。

Cache-Control 描述的是一个相对时间，在进行缓存命中的时候，都是利用客户端时间进行判断，所以相比较 Expires，Cache-Control 的缓存管理更有效，安全一些。

**Expires 和 Cache-Control 对比**

其实这两者差别不大，区别就在于 Expires 是 http1.0 的产物，Cache-Control 是 http1.1 的产物，两者同时存在的话，Cache-Control 优先级高于 Expires；在某些不支持 HTTP1.1 的环境下，Expires 就会发挥用处。所以 Expires 其实是过时的产物，现阶段它的存在只是一种兼容性的写法。

强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段，而不关心服务器端文件是否已经更新，这可能会导致加载文件不是服务器端最新的内容。此时我们需要用到**协商缓存策略**。

**Expires 和 Cache-Control 可以只启用一个，也可以同时启用。当 Expires 和 Cache-Control 同时启用时，Cache-Control 优先级高于Expires。**

#### 协商缓存

协商缓存就是强制缓存失效后，浏览器携带缓存标识向服务器发起请求，由服务器根据缓存标识决定是否使用缓存的过程。它是利用 `Last-Modified、If-Modified-Since` 和 `ETag、If-None-Match` 这两对 Header 来管理的。

当浏览器对某个资源的请求没有命中强缓存，就会发一个请求到服务器，验证协商缓存是否命中，如果协商缓存命中，请求响应返回的http 状态为 `304`。

![image-20210805231353515](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20210805231353515.png)

**Last-Modified**

Last-Modified 的控制缓存的原理是：

1. 浏览器第一次跟服务器请求一个资源，服务器在返回这个资源的同时，在 response 的 header 加上 Last-Modified，表示这个资源在服务器上的最后修改时间。
2. 浏览器再次跟服务器请求这个资源时，在 request 的 header 上加上 If-Modified-Since，即上一次请求时返回的 Last-Modified 的值。
3. 服务器再次收到资源请求时，根据浏览器传过来 If-Modified-Since 和资源在服务器上的最后修改时间判断资源是否有变化，如果没有变化则返回 304 ，但是不会返回资源内容；如果有变化，就正常返回资源内容。当服务器返回 304 的响应时，response header 中不会再添加 Last-Modified 的 header，因为既然资源没有变化。
4. 浏览器收到 304 的响应后，就会从缓存中加载资源。
5. 如果协商缓存没有命中，浏览器直接从服务器加载资源时，Last-Modified Header 在重新加载的时候会被更新。

Last-Modified、If-Modified-Since 都是根据服务器时间返回的 header，一般来说，在没有调整服务器时间和篡改客户端缓存的情况下，这两个 header 配合起来管理协商缓存是非常可靠的，但是有时候也会服务器上资源其实有变化，但是最后修改时间却没有变化的情况，而这种问题又很不容易被定位出来，而当这种情况出现的时候，就会影响协商缓存的可靠性。

* 如果本地打开缓存文件，即使没有对文件进行修改，但还是会造成 Last-Modified 被修改，服务端不能命中缓存导致发送相同的资源；
* 因为 Last-Modified 只能以秒计时，如果在不可感知的时间内修改完成文件，那么服务端会认为资源还是命中了，不会返回正确的资源。

**ETag**

ETag 的控制缓存的原理是：

1. 浏览器第一次跟服务器请求一个资源，服务器在返回这个资源的同时，在 response 的 header 加上 ETag，这是服务器根据当前请求的资源生成的一个唯一标识，这个唯一标识是一个字符串，只要资源有变化这个串就不同；
2. 浏览器再次跟服务器请求这个资源时，在 request 的 header上加上 If-None-Match，即上一次请求时返回的 ETag 的值；
3. 服务器再次收到资源请求时，根据浏览器传过来 If-None-Match，然后再根据资源生成一个新的 ETag，如果这两个值相同就说明资源没有变化，否则就是有变化；如果没有变化则返回 304 ，但是不会返回资源内容；如果有变化，就正常返回资源内容。与 Last-Modified 不一样的是，当服务器返回 304 响应时，由于 ETag 重新生成过，response header 中还会把这个 ETag 返回，即使这个 ETag 跟之前的没有变化；
4. 浏览器收到304的响应后，就会从缓存中加载资源；

**Last-Modified 和 Etag 对比**

Last-Modified 和 Etag 非常相似，都是用来判断一个参数，从而决定是否启用缓存。它们也有以下区别：

* 在精确度上：Etag 要优于 Last-Modifie，Last-Modified 的时间单位是秒，如果某个文件在 1 秒内改变了多次，那么他们的 Last-Modified 其实并没有体现出来修改，但是 Etag 每次都会改变确保了精度；如果是负载均衡的服务器，各个服务器生成的 Last-Modified 也有可能不一致；
* 在性能上：Etag 要逊于 Last-Modified，毕竟 Last-Modified 只需要记录时间，而 Etag 需要服务器通过算法来计算出一个 hash 值；
* 在优先级上：Last-Modified、ETag 是可以同时启用，且 ETag 的优先级比 Last-Modified 高，这是为了处理 Last-Modified 不可靠的情况。

使用协商缓存需要注意的一种场景：

1. 分布式系统里多台机器间文件的 Last-Modified 必须保持一致，以免负载均衡到不同机器导致比对失败；
2. 分布式系统尽量关闭掉 ETag （每台机器生成的ETag都会不一样）；

> 协商缓存需要配合强缓存使用，因为如果不启用强缓存的话，协商缓存根本没有意义。
>
> 服务端不开启强缓存的话，浏览器会默认静态资源使用强缓存。而不会使用协商缓存。所以不开启强缓存，协商缓存就没有意义。开启了强缓存，过期时间设置为0，协商缓存就起作用了。

### HTTP缓存的应用场景

* **频繁变动的资源：** 对于频繁变动的资源，首先需要使用 `Cache-Control: no-cache` 使浏览器每次都请求服务器，然后配合 `ETag` 或者 `Last-Modified` 来验证资源是否有效。这样的做法虽然不能节省请求数量，但是能显著减少响应数据大小。
* **不常变化的资源：** 通常在处理这类资源时，给它们的 `Cache-Control` 配置一个很大的 `max-age`，如：`Cache-Control: max-age=31536000`。这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题，就需要在文件名(或者路径)中添加 hash， 版本号等动态字符，之后更改动态字符，从而达到更改引用 URL 的目的，让之前的强制缓存失效 (其实并未立即失效，只是不再使用了而已)。

### 用户行为对HTTP缓存的影响

所谓用户行为对浏览器缓存的影响，指的就是用户在浏览器如何操作时，会触发怎样的缓存策略。主要有 3 种：

* **地址栏输入地址**： 查找 `disk cache` 中是否有匹配。如有则使用；如没有则发送网络请求；
* **普通刷新 (F5)**：因为 tab 并没有关闭，因此 `memory cache` 是可用的，会被优先使用(如果匹配的话)。其次才是 `disk cache`；
* **强制刷新 (Ctrl + F5)**：浏览器不使用缓存，因此发送的请求头部均带有 `Cache-control: no-cache` (为了兼容，还带了 `Pragma: no-cache`)，服务器直接返回 200 和最新内容。

## 本地数据存储

本地数据存储就是把一些信息存储到客户端，最常用的是 cookie、Web Storage（localStroage、sessionStroage）、本地数据库存储（webSql、indexDB）。

HTML4 在客户端存储数据通常使用 Cookie 存储机制将数据保存在用户的客户端，但使用 Cookie 方式存储客户端数据存在一系列的制约发展因素，如限制存储数据空间大小、数据安全性差等。

HTML5 中新增两种数据存储方式：Web Storage 和 Web SQL Database。前者可用于临时或永久保存客户端的少量数据，后者是客户端本地化的一套数据库系统，可以将大量的数据保存在客户端，而无需与服务器交互。

#### Cookie

Cookie 是服务器保存在浏览器的一小段文本信息，每个 Cookie 的大小一般不能超过 4KB。浏览器每次向服务器发出请求，就会自动附上这段信息，用来分辨两个请求是否来自同一个浏览器，以及用来保存一些状态信息。

Cookie 的本职工作并非本地存储，而是“维持状态”。它有以下局限：

* Cookie 是有体积上限的，它最大只能有 4KB。当 Cookie 超过 4KB 时，它将面临被裁切的命运。
* 过量的 Cookie 会带来巨大的性能浪费。因为同一个域名下的所有请求，都会携带 Cookie。

#### Web Storage

Web Storage 是 HTML5 专门为浏览器存储而提供的数据存储机制。它又分为 `Local Storage` 与 `Session Storage`。

localStorage 对象可以将数据长期保存在客户端，直至人工清除为止。

sessionStorage 对象保存数据实质的保存在 session 对象中，用户在打开浏览器时，可以查看操作过程中要求的临时数据，一旦关闭浏览器，所有使用 sessionStorage 对象保存的数据都将会丢失。

两者的区别在于**生命周期**与**作用域**的不同。

* **生命周期：** Local Storage 是持久化的本地存储，存储在其中的数据是永远不会过期的，使其消失的唯一办法是手动删除；而 Session Storage 是临时性的本地存储，它是会话级别的存储，当会话结束（页面被关闭）时，存储内容也随之被释放。
* **作用域：** Local Storage、Session Storage 和 Cookie 都遵循同源策略。但 Session Storage 特别的一点在于，即便是相同域名下的两个页面，只要它们**不在同一个浏览器窗口中**打开，那么它们的 Session Storage 内容便无法共享。

Web Storage 的特性：

* **存储容量大：** Web Storage 根据浏览器的不同，存储容量可以达到 5-10M 之间。目前，每个域名的存储上限视浏览器而定，Chrome 是 2.5MB，Firefox 和 Opera 是 5MB，IE 是 10MB。
* 仅位于浏览器端，不与服务端发生通信。
* Local Storage 与 Session Storage 具有相同的 API。

  ```js
  localStorage.setItem('key', 'value') // 存入数据
  localStorage.getItem('key') // 读取数据
  localStorage.removeItem('key') // 清除某个键名对应的键值
  localStorage.clear() // 清除所有保存的数据
  localStorage.key(Number) // 接受一个整数作为参数（从零开始），返回该位置对应的键值
  ```

#### Web SQL

Web SQL 是一种在浏览器里存储数据的技术，就像是存储在浏览器中的关系型数据库，使用SQL查询数据。

不过，也正因为 Web SQL database 本质上是一个关系型数据库。在2010年年底，W3C 已经不再支持 webSql 这种技术。因为，前端要使用 SQL 语句，需要一定的学习成本。

目前，浏览器厂商已经支持的就支持了（如：Chrome），没有支持的也不打算支持了（如 IE 和 Firefox）。

Web SQL 的三个核心方法：

* **openDatabase：** 使用现有的数据库或者新建的数据库创建一个数据库对象；
* **transaction：** 控制一个事务，以及基于这种情况执行提交或者回滚；
* **executeSql：** 执行实际的 SQL 查询；

Web SQL 的基本用法：

```js
class MyWebSql {
    constructor () {
        this.DB = openDatabase('myDB', '1.0', '测试数据库', 2 * 1024 * 1024, d => {
            console.log('创建成功！')
        })
    }
    exec (statement, argus = [], callback = function (SQLTransaction, results){}) {
        /* transaction执行数据库操作，操作内容就是正常的数据库的增删改查 */
        this.DB.transaction(tx => {
            // executeSql是执行具体的sql。sql语句, [变量1, 变量2], 执行后的回调)
            // 基本操作与实际数据库操作基本一致。
            tx.executeSql(statement, argus, callback )
        })    
    }
    tCreate () {
        this.exec('CREATE TABLE IF NOT EXISTS myTable1 (id unique, desc)')
    }
    tInsert () {
        this.exec('INSERT INTO myTable1 (id, desc) VALUES (1, "第1条记录")');
        this.exec('INSERT INTO myTable1 (id, desc) VALUES (?, ?)', [2, '第2条记录']);
        this.exec('INSERT INTO myTable1 (id, desc) VALUES (?, ?)', [3, '第3条记录']);
        this.exec('INSERT INTO myTable1 (id, desc) VALUES (?, ?)', [4, '第4条记录']);
    }
    tSelect () {
        this.exec('SELECT * FROM myTable1', [], (SQLTransaction, results) => {
            const len = results.rows.length
            for (let i = 0; i < len; i++) {
                console.log(results.rows.item(i).desc)
            }
            console.log(`查询记录条数: ${len}`)
        }, null)
    }
    tUpdate () {
        this.exec('UPDATE myTable1 SET desc="更新第3条记录" WHERE id=3')
        this.exec('UPDATE myTable1 SET desc="更新第4条记录" WHERE id=?', [4])
    }
    tDelete () {
        this.exec('DELETE FROM myTable1 WHERE id=1')
        this.exec('DELETE FROM myTable1 WHERE id=?', [2])
    }
    tDrop () {
        this.exec('DROP TABLE myTable1')
    }
}

var webSql = new MyWebSql()
webSql.tCreate()
webSql.tInsert()
webSql.tSelect()
// webSql.tUpdate()
// webSql.tDelete()
// webSql.tDrop()
```

[Web SQL DEMO](https://1927344728.github.io/demo-lizh/html/others_01.html?type=3)

> 注意：目前Chrome开发工具中无法操作 Web sql 创建的数据库，只能通过 executeSql 操作。

#### IndexedDB

在现代浏览器的本地存储方案中，indexedDB 是一项重要的能力组成， 它是可以在浏览器端使用的本地数据库，可以存储大量数据，提供接口来查询，还可以建立索引，这些都是其他存储方案 Cookie 或者 LocalStorage 无法提供的能力。单从数据库类型来看 IndexedDB 是一个非关系型数据库（不支持通过 SQL 语句操作）。

IndexedDB 是一个**事务型数据库系统**，类似于基于 SQL 的 RDBMS。 然而，不像 RDBMS 使用固定列表，IndexedDB 是一个基于 JavaScript 的面向对象数据库。

IndexedDB 具有以下特点：

* \*\*键值对储存：\*\*IndexedDB 内部采用对象仓库（object store）存放数据。所有类型的数据都可以直接存入，包括 JavaScript 对象。对象仓库中，数据以"键值对"的形式保存，每一个数据记录都有对应的主键，主键是独一无二的，不能有重复，否则会抛出一个错误。
* **异步：** IndexedDB 执行的操作是异步执行的，以免阻塞应用程序。IndexedDB 最初包括同步和异步 API。同步 API 仅用于 Web Workers，且已从规范中移除，因为尚不清晰是否需要。但如果 Web 开发人员有足够的需求，可以重新引入同步 API。
* **支持事务：** 这意味着一系列操作步骤之中，只要有一步失败，整个事务就都取消，数据库回滚到事务发生之前的状态，不存在只改写一部分数据的情况。
* **同源限制：** 每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库，而不能访问跨域的数据库。
* **储存空间大：** IndexedDB 的储存空间比 LocalStorage 大得多，一般来说不少于 250MB，甚至没有上限。
* **支持二进制储存：** IndexedDB 不仅可以储存字符串，还可以储存二进制数据（ArrayBuffer 对象和 Blob 对象）。

IndexedDB 的基本使用：

```js
class MyIndexedDB {
    constructor() {
        this.DB = null
        this.create()
        return new Promise(resolve => {
            this.create().then(event => {
                resolve(this)
            })
        })
    }
    create() {
        return new Promise((resolve, reject) => {
            const REQ = window.indexedDB.open('myIndexedDB', 1)
            REQ.onerror = event => {
                reject(event)
                throw event.target.error
            }
            // 下面事情执行于：数据库首次创建版本，或者window.indexedDB.open传递的新版本（版本数值要比现在的高）
            // onupgradeneeded 执行早于 onsuccess
            REQ.onupgradeneeded = event => {
                let tempDB = event.target.result
                if (!tempDB.objectStoreNames.contains('myIDB1')) {
                    const objectStore = tempDB.createObjectStore('myIDB1', {
                        keyPath: 'id',
                        autoIncrement: true
                    })
                    objectStore.createIndex('id', 'id', { unique: true })
                    objectStore.createIndex('name', 'name')
                    objectStore.createIndex('email', 'email')
                }
            }
            REQ.onsuccess = event => {
                this.DB = event.target.result
                resolve(event)
            }
        })
    }

    add() {
        let oStore = this.DB.transaction(['myIDB1'], 'readwrite').objectStore('myIDB1')
        oStore.add({ id: 1, name: '张三', email: 'zhangsan@example.com' })
        oStore.add({ id: 2, name: '李四', email: 'lisi@example.com' })

        oStore.onsuccess = event => console.log('数据写入成功！')
        oStore.onerror = event => console.log('数据写入失败！')
    }

    read(id) { 
        let oStore = this.DB.transaction(['myIDB1'], 'readwrite').objectStore('myIDB1').get(id)
        oStore.onerror = event => console.log('事务失败！')
        oStore.onsuccess = event => {
            let result = event.target.result
            if (result) {
                console.log(`Name: ${result.name}`, `Email: ${result.email}`)
                return
            }
            console.log('未获得数据记录！')
        }
    }

    remove(id) {
        let oStore = this.DB.transaction(['myIDB1'], 'readwrite').objectStore('myIDB1').delete(id)
        oStore.onerror = event => console.log('删除失败！')
        oStore.onsuccess = event => console.log('删除成功！')
    }

    put(record) {
        let oStore = this.DB.transaction(['myIDB1'], 'readwrite').objectStore('myIDB1')
        oStore.put(record)
        oStore.onsuccess = event => console.log('数据更新成功！')
        oStore.onerror = event => console.log('数据更新失败！')
    }

    readAll() {
        let oStore = this.DB.transaction(['myIDB1'], 'readwrite').objectStore('myIDB1')
        oStore.openCursor().onsuccess = event => {
            let result = event.target.result
            if (result) {
                console.log(`Name: ${result.value.name}`, `Email: ${result.value.email}`)
                result.continue()
                return
            }
            console.log('没有更多数据了！')
        }
    }
}

new MyIndexedDB().then(res => {
    myIndexedDB = res
    myIndexedDB.add()
    myIndexedDB.read(1)
    // myIndexedDB.remove(1)
    // myIndexedDB.put({ id: 1, name: '张三（更新）', email: 'zhangsan@example.com（更新）' })
    // myIndexedDB.readAll()
})
```

[IndexedDB DEMO](https://1927344728.github.io/demo-lizh/html/others_01.html?type=3)

在 IndexDB 中，可以创建多个数据库，一个数据库中创建多张表，一张表中存储多条数据——能满足复杂的结构性数据。

### 相关问题

#### Web Storage 存储容量限制

Web Storage 根据浏览器的不同，存储容量可以达到 5-10M 之间。目前，每个域名的存储上限视浏览器而定，Chrome 是 2.5MB，Firefox 和 Opera 是 5MB，IE 是 10MB。

## 参考资料

[HTTP 缓存 MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching)

[深入理解浏览器的缓存机制](https://www.infoq.cn/article/8vu-vcrhoxducafprnol)

[前端优化：浏览器缓存技术介绍](https://juejin.cn/post/6844903672556552205#heading-1)

[HTTP/2 push is tougher than I thought](https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/)
