> 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/02-jin-jie-05web-li-xian-ying-yong.md).

# 进阶 05 Web离线应用

丢失网络连接是一个困扰 Web 用户多年的难题，即使是世界上最好的 Web App，如果因为网络原因访问不了它，那体验也是非常糟糕的。所谓 Web 离线应用，就是指在设备没有网络的情况下，Web App 依然可以运行。

离线应用的核心是**离线缓存技术**。用户第一次访问 Web App 后，将所需资源缓存在本地，以后的访问可以直读取缓存资源，就算没有网络连接也不防碍用户使用。

使用离线缓存技术带来三个优势：

* 离线浏览 - 用户离线时可以访问 Web App；
* 速度 - 资源直接来自磁盘，无需网络访问；
* 减少服务器负载 - 浏览器将只从服务器下载更新过或更改过的资源。

前端有两种离线缓存技术：

* **AppCache：** 也称 Application Cache。目前已经从 Web 标准中删除，请尽量避免使用。
* **Service Workers：** 目前最新的离线缓存技术，是 Web Worker 的一部分。它比 AppCache 更加灵活，因为它可以通过 JavaScript 代码去控制缓存的逻辑。它也是构建 PWA 应用的关键技术之一。

## 如何判断是否离线？

要开发离线应用，首先应该知道**设备是否处于离线状态**下：

```js
if (navigator.onLine){
    alert('联网中')
} else {
    alert('离线中')
}
```

同时，我们还可以**监视网络状态改变**，当网络状态在 **联网/离线** 状态变化时，指定执行相应处理。

```js
window.addEventListener('online',function(){
    alert("网络重新连接了！！")
})
window.addEventListener('offline',function(){
    alert("网络断开了。。。")
})
```

## Application Cache

HTML5 提供了一套可以让 Web App 离线运行的机制，开发者可以使用 Application Cache（AppCache）接口定义浏览器需要缓存的供离线用户使用的资源。即便是用户在离线的情况下刷新页面，被缓存的应用都应该被正确的加载和运行。

AppCache 就是从浏览器的缓存中分出来的一块缓存区。要想在这个缓存区保存数据，需要使用一个缓存清单文件（manifest file），列出要下载和缓存的资源。

### 兼容性

目前（2021.12） Chrome、Firefox、Edge 都已不支持该功能，Safari、IE10\~11 还支持。

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

查看浏览器是否支持 Application Cache：

```javascript
if (window.applicationCache) {
    // 支持 Application Cache
}
```

### 引用清单文件

想要在应用中使用 AppCache，需要在应用页面的 html 节点上增加 `manifest` 属性：

```html
<html manifest="example.appcache"></html>
```

`manifest` 属性链接到一个缓存清单文件，文件中列出了应用中需要缓存的资源列表。 `manifest` 属性需要包含在每一个需要缓存的页面上，浏览器不会缓存没有 `manifest` 属性的页面。在 `manifest` 文件中不需要列出所有希望缓存的页面，对于**包含了 `manifest` 属性的页面，浏览器会隐式的将其缓存到 AppCache 中**。

### 缓存使用流程

使用 AppCache 会影响加载 Document 的流程：

* 当浏览器访问一个包含了 `manifest` 属性的页面时，如果 AppCache 不存在，浏览器会加载主文档，然后获取所有在 `manifest` 文件中列出的需要加载的资源，创建 AppCache 的第一个版本。
* 此后访问页面，浏览器会从 AppCache（浏览器缓存区） 加载主文档以及 `manifest` 文件中定义的的缓存资源。此外，浏览器会向 `window.applicationCache` 对象发送一个 `checking` 事件，通过 HTTP 请求获取 `manifest` 文件。
* 如果当前的 `manifest` 文件是最新的，浏览器发送一个 `noupdate` 事件给 `window.applicationCache` 对象，更新结束。需要强调的是，如果在服务器端更新了任何缓存中的资源文件，必须同时更新 `manifest` 文件，以保证浏览器知道需要重新获取所有资源。
* 如果 `manifest` 文件有更新，所有 `manifest` 文件中列出的需要缓存的文件（包括通过 `applicationCache.add()` 添加的资源）都会遵循基础的 HTTP 缓存规则，被加载到一个临时缓存中。对于每一个加载到临时缓存区的文件，浏览器会发送一个 `progress` 事件给 `window.applicationCache` 对象，如果过程中出现了任何错误，浏览器会发送一个 `error` 事件，停止更新的过程。
* 当浏览器收到所有的文件之后，他们自动会被移动到一个实际的离线缓存区，同时 `cahced` 事件被发送给 `window.applicationCache` 对象。由于主文档已经缓存到浏览器中，已经更新的文档除非重新加载（包括人工或者程序内部），否则都不会被重新渲染。

### 缓存清单文件

Web App 中的 `manifest` 属性，可以通过相对路径和绝对路径（需要和 Web 应用在同一个域下）两种方式进行。`manifest` 文件的建议的文件扩展名是：`.appcache`，也可以使用任意的后缀，但是 MIME type 必须是 `text/cache-manifest`。

比如，在 Apache 中提供此 MIME 类型，请将以下行添加到您的配置文件中：

```shell
AddType text/cache-manifest .appcache
```

或者，在 Google App Engine 的 `app.yaml` 文件中：

```yaml
- url: /mystaticdir/(.*\.appcache)
  static_files: mystaticdir/\1
  mime_type: text/cache-manifest
  upload: mystaticdir/(.*\.appcache)
```

`manifest` 文件是一个列出了浏览器需要缓存（以及不需要缓存）的文件的简单文本文件，资源通过 URI 进行识别。

一个 `manifest` 文件看起来像这样：

```shell
CACHE MANIFEST

# 指定缓存的资源
/favicon.ico
index.html

CACHE:
stylesheet.css
images/logo.png
scripts/main.js

# 需要用户联网请求的资源
NETWORK:
*

FALLBACK:
# 如果main.py不可访问，则会提供Static.html
# offline.jpg将代替images/large/目录下的所有图片
# offline.html将代替所有其他无法访问的.html文件 
/main.py /static.html
images/large/ images/offline.jpg
```

清单可以有三个不同的部分：

* `CACHE:` 在此标题下或紧接在 `CACHE MANIFEST` 之后列出的文件，在第一次下载后将被缓存。
* `NETWORK:` 在此标题下列出的文件永远不会被缓存，且离线时是不可用的。但是，如果 `CACHE` 也指定了同一文件，则该文件直接使用缓存。通常可以使用 `*` 来指示所有其他文件都需要因特网连接。
* `FALLBACK:` 在此标题下列出的文件，如果无法建立网络连接，则使用指定的文件替代。第一个 URI 是资源，第二个 URI 是网络请求失败或错误时使用的替代文件，这两个 URI 必须与清单文件来自同一来源。

**有几点要注意：**

* `CACHE MANIFEST` 字符串是第一行并且是必需的。
* CACHE，NETWORk 和 FALLBACK 三个段在 `manifest` 文件中出现的顺序是随意的，并且每个段在文件中出现的次数没有限制。
* 以 `#` 开头的是注释行，但也可满足其他用途。比如：更新注释行中的日期和版本号是一种使浏览器重新缓存文件的办法。
* 如果有 `manifest` 属性的 html 文件没有在 `manifest` 文件中列出来，会将其自身作为需要缓存的资源，加入到 AppCache 中。
* 如果清单或其中指定的资源无法下载，则整个缓存更新过程将失败。如果出现故障，浏览器将继续使用旧的应用程序缓存。
* 不要在 `manifest` 文件中缓存自身，否则想要通知浏览器更新 `manifest` 是几乎不可能的。
* `/cached-page`、`/cached-page?value=1`、`/cached-page?value=2` 被认为是单独的页面，它们的 AppCahe 是无法共享的。

  如果需要链接到有参数传递的资源，在 hash 上增加参数，例如：`/cached-page#whatever?`。

### applicationCache对象

JavaScript 提供了相应的 API 让我们去监控描述文件的状态，这个 API 的核心是 `applicationCache` 对象，这个对象有一个 `status` 属性用来标示缓存的当前状态，属性的值是常量，可能值如下：

* 0： UNCACHE，无缓存，即没有与页面相关的应用缓存；
* 1： IDLE，闲置，即应用缓存不在被更新的过程中；
* 2： CHECKING，检查中，即正在加载 `manifest` 文件或者检查是否已更新；
* 3： DOWNLOADING，下载中，即资源正在被下载到缓存中；
* 4： UPDATEREADY，更新完成，即应用缓存已经更新了资源，而且所有资源都已下载完毕。但是没有使用 `swapCache()` 来更新，该状态会触发 `updateready` 事件；
* 5： OBSOLETE，缓存废弃，即 AppCache 已经过时（`manifest` 文件已删除）。

```javascript
const appCache = window.applicationCache
switch (appCache.status) {
    case appCache.UNCACHED: // appCache.UNCACHED === 0
        return 'UNCACHED'
        break
    case appCache.IDLE: // appCache.IDLE === 1
        return 'IDLE'
        break
    case appCache.CHECKING: // appCache.CHECKING === 2
        return 'CHECKING'
        break
    case appCache.DOWNLOADING: // appCache.DOWNLOADING === 3
        return 'DOWNLOADING'
        break
    case appCache.UPDATEREADY:  // appCache.UPDATEREADY === 4
        return 'UPDATEREADY'
        break
    case appCache.OBSOLETE: // appCache.OBSOLETE === 5
        return 'OBSOLETE'
        break
    default:
        return 'UKNOWN CACHE STATUS'
        break
}
```

正如您所料， `applicationCache` 对象会提供事件来监视缓存的状态。浏览器会针对下载进度、更新应用程序缓存和错误条件等事件触发事件。缓存状态的改变会相应触发以下事件：

* `checking`：在浏览器为应用缓存查找更新时触发；
* `error`：在检测更新或下载资源期间发生错误时触发；
* `noupdate`：在检查 `manifest` 文件无变化时触发；
* `downloading`：在开始下载应用缓存资源时触发；
* `progress`：在文件下载应用缓存的过程中持续不断的触发；
* `updateready`：在页面新的应用缓存下载完毕且通过 `swapCache()` 方法替换旧缓存时触发；
* `cached`：在清单的第一个缓存之后触发。

```javascript
// 在清单的第一个缓存之后触发。
appCache.addEventListener('cached', () => {}, false)

// 在浏览器为应用缓存查找更新时触发。
appCache.addEventListener('checking', () => {}, false)

// 在开始下载应用缓存资源时触发。
appCache.addEventListener('downloading', () => {}, false)

// 在检测更新或下载资源期间发生错误时触发。
appCache.addEventListener('error', () => {}, false)

// 在检查 manifest 文件无变化时触发。
appCache.addEventListener('noupdate', () => {}, false)

// manifest 文件返回 404 或 410 时触发，这会导致 AppCache 被删除。
appCache.addEventListener('obsolete', () => {}, false)

// 在文件下载应用缓存的过程中持续不断的触发。
appCache.addEventListener('progress', () => {}, false)

// 当清单资源重新下载完成时触发。
appCache.addEventListener('updateready', () => {}, false)
```

需要手动检查清单更新，首先调用 `applicationCache.update()` 更新用户的缓存（这需要更改清单文件）；然后，当`applicationCache.status` 处于其 `UPDATEREADY` 状态时，调用 `applicationCache.swapCache()` 会将旧缓存交换为新缓存。

```javascript
const appCache = window.applicationCache
appCache.update()
if (appCache.status === window.applicationCache.UPDATEREADY) {
    appCache.swapCache()
}
```

使用 `update()` 和 `swapCache()` 方法更新了缓存，此时用户已经下载了页面和所有资源，但这些文件不会自动重新加载。所有，最后还需要再次刷新页面以获取最新版本的页面和资源，也可以 `updateready` 事件中定义一个刷新页面的操作：

```javascript
window.applicationCache.addEventListener('updateready', function(e) {
    if (window.applicationCache.status === window.applicationCache.UPDATEREADY) {
        window.location.reload()
    }
}, false)
```

### 更新缓存

一旦应用被缓存，它就会保持缓存直到发生下列情况：

* 用户清除浏览器的数据存储；
* 缓存清单文件已修改。注意：更新清单中列出的文件并不意味着浏览器将重新缓存该资源，清单文件本身必须更改。比如：编辑图片资源或更改 JavaScript 函数，**您必须修改 `manifest` 文件本身（如：修改注释中的日期或版本）以通知浏览器刷新缓存文件**。

### 缓存存储位置以及 AppCache 的清除

在 Chrome 中，可以通过点击 `preferences -> Clear browsing data…` 或者访问 `chrome://appcache-internals/` 来清除离线缓存。Safari 在 `preferences` 中有一个类似的 `Empty cache` 设置项，但是设置之后需要重启浏览器。

AppCache 也会过时，如果应用的 `manifest` 文件被从服务器端删除，浏览器会删除使用 `manifest` 的所有应用缓存，同时发送 `obsoleted` 事件给 `window.applicationCache` 对象。AppCache 的状态被设置为 `OBSOLETE`。

**注意： Application Cache 被标准废弃了**，这个接口设计很不合理，最明显的是它一直无法有效的解决离线资源精细化控制。因此，推荐使用更强大的 Service Worker。

## Service Worker

Service Worker 本质上充当 Web 应用程序、浏览器与网络（可用时）之间的代理服务器。这个 API 旨在创建有效的离线体验，它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。

Service Worker 是一个注册在指定源和路径下的事件驱动 worker。它采用 JavaScript 控制关联的页面或者网站，拦截并修改访问和资源请求，细粒度地缓存资源。你可以完全控制应用在特定情形（最常见的情形是网络不可用）下的表现。

Service Worker 有以下特性：

* Service Worker 线程有自己完全独立的执行上下文。一旦被安装成功就永远存在，除非线程被程序主动解除，而且 Service Worker 在访问页面的时候可以直接被激活，如果关闭浏览器或者浏览器标签的时候会自动睡眠，以减少资源损耗。
* Service Worker 是完全异步实现的，内部的接口的异步化都是通过 Promise 实现，并且在 Service Worker 中不能直接操作 DOM，出于安全和体验的考虑，UI 的渲染工作必须只能在主线程完成。
* 出于安全的考虑 Service Worker **必须运行在 HTTPS 协议下**，为了便于本地开发，`localhost`、`127.0.0.1` 这种非 HTTPS 协议也被浏览器认为是安全源。（IP 地址：`192.168.x.x`，不行）。
* Service Worker 可以拦截并代理请求，可以处理请求的返回内容，可以持久化缓存静态资源达到离线访问的效果，和 ApplicationCache 不同，Service Worker 的所有的离线内容开发者完全可控，甚至是可以控制动态请求，第三方静态资源等。
* 由于 Service Worker 可以离线并且在后台工作，所以可以进行 **推送消息**（第六章会详细说明）、**后台同步**资源等功能，在不久的将来，利用 Service Worker 的这一特性，甚至可以衍生出更多的 Web App 原生化的功能。

### 兼容性

目前（2021.12） Chrome、Firefox、Safari、Edge、Opera 都已经全面支持 Service Workers，但 IE 6\~11 都不支持。 由于 Service Workers 无法通过注入 polyfill 去实现兼容，所以在你打算使用它前请先调查清楚你的网页的运行场景。

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

查看浏览器是否支持 Service Workers：

```javascript
if (navigator.serviceWorker) {
    // 支持 serviceWorker
}
```

### Service Worker 生命周期

Service Worker 的工作原理主要体现在它的生命周期上，一个 Service Worker 从被注册开始，就会经历自身的一些生命周期的节点，而在这些节点都可以去做一些特定的事情，比如一些复杂的计算、缓存的写入、缓存的读取等操作。通过这些生命周期节点的联合调度，Service Worker 才能完成复杂的资源离线缓存的工作。

每个 Service Worker 都有一个独立于 Web 页面的生命周期：

* 在主线程成功注册 Service Worker 之后，开始下载并解析执行 Service Worker 文件，执行过程中开始安装 Service Worker，在此过程中会触发 worker 线程的 install 事件。
* 如果 install 事件回调成功执行（在 install 回调中通常会做一些缓存读写的工作，可能会存在失败的情况），则开始激活 Service Worker，在此过程中会触发 worker 线程的 activate 事件，如果 install 事件回调执行失败，则生命周期进入 Error 终结状态，终止生命周期。 如果发生这种情况，不必担心，它下次会再试一次。
* 完成激活之后，Service Worker 就能够控制作用域下的页面的资源请求，可以监听 fetch 事件。不过，激活之后，页面需要再次加载才会受其控制。因为注册是一个异步的过程，在激活完成后，当前页面要发的请求都已经发送完了。
* 如果在激活后 Service Worker 被 unregister 或者有新的 Service Worker 版本更新，则当前 Service Worker 生命周期完结，进入 Terminated 终结状态。

简单来说，Service Worker 生命周期分为三个阶段：注册、安装、激活，还有最重要的一个拦截事件 fetch。

需要注意以下几点：

* Service Worker 文件只在首次注册的时候执行了一次。
* 安装、激活流程也只是在首次执行 Service Worker 文件的时候进行了一次。
* 首次注册成功的 Service Worker 没能拦截当前页面的请求。
* 非首次注册的 Service Worker 可以控制当前的页面并能拦截请求。

### 注册 Service Workers

若要安装 Service Worker，您需要通过在页面中对其进行**注册**来启动安装，其中最重要的是告诉浏览器 Service Worker 脚本文件的位置。如果注册成功，Service Worker 就会被下载到客户端并尝试安装或激活。

```javascript
if ('serviceWorker' in navigator) {
    window.addEventListener('DOMContentLoaded',function() {
        // sw.js 为 Service Workers 脚本文件，在其中加入 Service Workers 安装、激活以及 fetch事件的逻辑。
        // scope 表示定义 Service Worker 注册范围的 URL
        navigator.serviceWorker.register('/sw.js').then((registration) => {
            console.log(registration.scope)
        }).catch((err) => {
            console.log('注册失败！', err)
        })
    })
}
```

Service Worker 注册的默认作用域是与 Service Worker 脚本文件路径相对的 `./`。如果 Service Worker 脚本文件位于根网域， 就意味着 Service Worker 的作用域将是整个域名。如果位于 `/example/` ，则它的默认作用域为 `/example/`，即只能管控 URI 以 `/example/` 开头（ `/example/page1/`、`/example/page2/`）的页面。

可以通过打开 `chrome://inspect/#service-workers` 检查 Service Worker 是否已启用，并查看所有注册了的 Service Workers。

另外，还可以通过 `chrome://serviceworker-internals` 来查看 Service Worker 详情。

**注意：** Service Worker 脚本文件只在首次注册的时候执行了一次；安装、激活流程也只是在**首次**执行该文件的时候进行了一次。

用户首次访问 Service Worker 控制的网站或页面时，Service Worker 注册后，会立刻被下载。

### 安装 Service Worker

Service Workers 在注册成功后会在其生命周期中派发出一些事件，通过监听对应的事件在特点的时间节点上做一些事情。在 Service Worker 脚本文件中，引入了新的关键字 self 代表当前的 Service Workers 实例。

在 Service Workers 安装成功后会派发出 `install` 事件，在这个事件中可以执行缓存资源的逻辑，实现代码如下：

```javascript
// sw.js
const CACHE_VERSION = 'v1.0.0'
const urlsToCache = [
    '/others_01.html?type=8',
    '/assets/third/utils.js',
    '/assets/js/others_01_03.js',
]

self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(CACHE_VERSION).then((cache) => {
            console.log('安装成功：', cacheAllowlist)
            return cache.addAll(urlsToCache)
        })
    )
})
```

我们以所需的缓存名称调用 `caches.open()`，之后再调用 `cache.addAll()` 并传入文件数组。 这是一个 promise 链（`caches.open()` 和 `cache.addAll()`）。 `event.waitUntil()` 方法带有 promise 参数并使用它来判断安装所花费的时间，以及安装是否成功。

如果所有文件都成功缓存，则将安装 Service Worker。 如有**任何**文件无法下载，则安装步骤将失败。 这可让您依赖于所定义的所有资源，但也意味着需要对您决定在安装步骤缓存的文件列表格外留意。 定义一个过长的文件列表将会增加文件缓存失败的几率，从而导致服务工作线程未能安装。

`install` 事件在安装成功后立即触发，并且它只能被每个 Service Worker 调用一次。如果 Service Worker 文件有更新，则浏览器将其视为一个不同的 Service Worker，并且它将获得自己的 install 事件。

### 使用 Service Worker（fetch）

在安装 Service Worker 且用户转至其他页面或刷新当前页面后，Service Worker 将开始接收 fetch 事件。

```javascript
// sw.js
self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request).then((response) => {
            if (response) {
                console.log('存在缓存：', event.request.url)
                return response
            }
            return fetch(event.request)
        })
    )
})
```

这里定义了 `fetch` 事件，并且在 `event.respondWith()` 中，传入来自 `caches.match()` 的一个 promise。 此方法拦截请求，并从 Service Worker 所创建的缓存中查找该请求缓存的结果。

如果发现匹配的响应，则返回缓存的值，否则，将调用 `fetch` 以发出网络请求，并将从网络检索到的任何数据作为结果返回。

你也可以通过 `fetch` 事件，向 Service Worker 添加新的缓存：

```javascript
// sw.js
self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request).then((response) => {
            if (response) {
                console.log('存在缓存：', event.request.url)
                return response
            }
            return fetch(event.request).then((response) => {
                // 注意：必需先复制 response。
                // 因为 response 是数据流，主体只能使用一次。 
                // 由于我们想要 response 能被浏览器使用，并将其传递到缓存以供使用，因此需要复制一份副本：一份发送给浏览器，另一份则保留在缓存。
                const responseToCache = response.clone()
                if(responseToCache && responseToCache.status === 200 && responseToCache.url.match('others_01_02.js')) {
                    caches.open(CACHE_VERSION).then((cache) => {
                        cache.put(event.request, responseToCache)
                        console.log('新增缓存：', responseToCache.url)
                    })
                }
                return response
            })
        })
    )
})
```

**注意：** 必需先复制 response。因为 response 是数据流，主体只能使用一次。 由于我们想要 response 能被浏览器使用，并将其传递到缓存以供使用，因此需要备份一份副本，一份发送给浏览器，另一份则保留在缓存。

**注意：** Chrome 开发者工具， `Application -> Cache -> Cache Storage` 可查看缓存的资源。

### 更新 Service Worker（activate）

浏览器会在以下情况，重新触发注册、安装、激活流程：

* 一个前往作用域内页面的导航；
* 更新 push 和 sync 等功能事件，没有 24 小时内进行更新检查；
* Service Worker 脚本文件的 URI 或者内容更新；

Service Worker 脚本文件更新， 会触发以下流程：

* Service Worker 脚本文件更新后， 用户打开页面时，浏览器会尝试在后台加载定义该文件。 如果该文件与其当前所用 Service Worker 脚本文件存在字节差异，则将其视为新 Service Worker。
* 新 Service Worker 将会启动，且将会触发 `install` 事件。
* 此时，旧 Service Worker 仍控制着当前页面，因此新 Service Worker 将进入 `waiting` 状态。
* 直到旧 Service Worker 对**所有**终端失去控制（Service Worker 管控的所有页面都关闭）时，旧 Service Worker 才会被终止，新 Service Worker 取得控制权。
* 新 Service Worker 取得控制权后，将会触发其 `activate` 事件。

`activate` 事件常用于管理缓存（比如：删除旧版本的缓存）。因为，如果您在安装事件中清除了旧缓存，则当前页面的旧 Service Worker 可能突然无法从缓存中提供文件。

`activate` 事件管理缓存的具体做法为：遍历 Service Worker 中的所有缓存版本，并删除未在白名单中定义的任何缓存版本。

```javascript
// sw.js
self.addEventListener('activate', (event) => {
    console.log('update06')
    event.waitUntil(
        caches.keys().then((cacheNames) => {
            const cacheAllowVersions = [CACHE_VERSION]
            return Promise.all(
                cacheNames.map((name) => {
                    if (cacheAllowVersions.indexOf(name) === -1) {
                        return caches.delete(name)
                    }
                })
            )
        })
    )
})
```

Service Worker 一旦更新，需要等所有的终端都关闭之后，再重新打开页面才能激活新的 Service Worker，这个过程太复杂了。如果不想等所有的终端都关闭再打开的话，可采用以下方法：

* **skipWaiting：** Service Worker 在全局提供了一个 skipWaiting() 方法来跳过 waiting 状态，直接激活新的 Service Worker。

  **注意：** 可能会出现其他终端还没有受当前终端激活的 Service Worker 控制的情况，切回其他终端之后，Service Worker 控制页面的效果可能不符合预期。

  ```javascript
  self.addEventListener('install', (event) => {
      self.skipWaiting()
      event.waitUntil(
          caches.open(CACHE_VERSION).then((cache) => {
              console.log('安装成功：', cacheAllowlist)
              return cache.addAll(urlsToCache)
          })
      )
  })
  ```
* **注销 Service Worker：**

  ```javascript
  navigator.serviceWorker.getRegistrations().then(function(registrations) {
      for(let registration of registrations) {
          registration.unregister()
      }
  })
  ```
* **通过浏览器管理：** 在 Chrome 浏览器的 `chrome://serviceworker-internals/` ，或者在 Chrome 开发者工具 `Application -> Service Workers` 注销 Service Worker。

## 参考资料

[使用 Application Cache](http://zoomzhao.github.io/2012/11/08/using-the-application-cache/)

[A Beginner's Guide to Using the Application Cache](https://www.html5rocks.com/en/tutorials/appcache/beginner/)

[Service Worker: 简介](https://developers.google.cn/web/fundamentals/primers/service-workers?hl=zh-cn)

[Service Worker 生命周期](https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle?hl=zh-cn)

[Service Worker](https://lavas-project.github.io/pwa-book/chapter04.html)

[MDN Service\_Worker\_API](https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API)
