Service Worker初探(工作原理讲解,附发送message和cache缓存demo代码)

1,357 阅读15分钟

前言

什么是 Service Worker ? Service Worker是运行于浏览器后台的独立脚本,它让单页应用拥有了拦截网页请求、离线缓存、离线通知、并发处理复杂的业务逻辑等能力,使用它可以搭建渐进式Web应用(PWA)。本文将通过几个demo来熟悉Service Worker工作原理,了解Service Worker如何发送message和缓存网页中的路径。

Service Worker 工作原理

本文参考了这篇文章中的Service Worker章节 ,该文对service worker、注册安装流程和工作原理讲的很详细,本文中的demo也是看了这篇文章的介绍后搭建和扩展的,下面简单整理下service worker开发所需知识点。

作用域

Service Worker作用域是一个URL路径,即注册时传给navigator.serviceWorker.register方法的url,例如:navigator.serviceWorker.register('./a/b/sw.js')的作用域为 https://somehost/a/b/,那这个 Service Worker 能控制 https://somehost/a/b/ 目录下的所有页面,可以包含下面列出的页面:

  • https://somehost/a/b/index.html
  • https://somehost/a/b/c/index.html
  • https://somehost/a/b/anothor.html
  • ...

由于SPA工程上都只有一个 index.html 入口,所以一般使用根目录下的sw文件来控制整个应用的缓存,即使用 navigator.serviceWorker.register('./sw.js') 来注册service worker,其作用域为”/’。

生命周期

每次打开应用时主进程需要注册service worker(后面简称sw),注册成功后会去安装sw并触发安装事件(Install Event),一般会在该事件中处理应用缓存,安装成功后会激活sw并触发激活事件(Activate Event),激活成功后sw进入idle状态,在该状态页面的所有请求都会触发Fetch事件(Fetch Event),直到应用关闭。如下图:

image.png

需要注意的是,一个sw的作用域是一个url(spa应用作用域一般都会设置为根目录,即当前域名,后面讨论sw时,我们都默认作用域为根目录),所以同一个域名下的网页都公用一个sw的,并且只用所用该域名下的网页都关闭,sw才会关闭结束。而一个浏览器可以同时打开多个域名一样的网页,所以sw和网页的关系是一对多的,如下图:

image.png

如上所述会引发一个问题,因为所有同域名网页都公用一个sw,只有当所有sw作用域下网页都关闭了才会结束sw,那当sw有更新时该怎么办呢,我们通过例子来分析sw安装注册激活流程。

示例代码

通过以下几个场景示例代码来进一步了解server worker工作原理:

首先,初始化一个工程,并安装local-web-server依赖

npm init
npm install local-web-server

然后添加如下html文件

<!DOCTYPE html>
  <head>
    <title>Service Worker Demo</title>
  </head>
  <body>
    <button id="loadImage">load</button>
    <img id="img" alt="demo image" style="width: 100px;height: 100px;" />
    <img src="./imgs/test.jpg" alt="demo image" />
    <script>
      if ('serviceWorker' in navigator) {
        // 由于 127.0.0.1:8000 是所有测试 Demo 的 host
        // 为了防止作用域污染,将安装前注销所有已生效的 Service Worker
        navigator.serviceWorker.getRegistrations()
          .then(regs => {
            for (let reg of regs) {
              reg.unregister()
            }
            navigator.serviceWorker.register('./sw.js')
          })
      }
      /**
       * 用于测试img fetch
       */
      document.getElementById('loadImage').onclick = function () {
        if (!!document.getElementById('img').src.includes("test2.jpg")) {
          document.getElementById('img').src = ""
        } else {
          console.log('load ./imgs/test2.jpg')
          document.getElementById('img').src = "./imgs/test2.jpg"
        }
      }
    </script>
  </body>
</html>

添加如下sw文件

// sw.js
console.log('service worker 注册成功')

self.addEventListener('install', () => {
  // 安装回调的逻辑处理
  console.log('service worker 安装成功')
})

self.addEventListener('activate', () => {
  // 激活回调的逻辑处理
  console.log('service worker 激活成功')
})

self.addEventListener('fetch', event => {
  console.log(`service worker 抓取请求成功:${event.request.url} (clientId:${event.clientId})`)
})

最后运行ws --spa index.html启动sw

场景一:第一次注册sw

第一次执行上面的例子会发现console控制台打印结果为:

service worker 注册成功
service worker 安装成功
service worker 激活成功

可以看到第一次注册时会执行注册→安装→激活流程,但fetch事件并没有触发,说明首次注册成功的 Service Worker 没能拦截注册前的页面请求

需要注意的是这里说的第一次注册的意思是该域名(sw作用域下的域名)网页中没有已注册的sw,并且是第一次打开含有注册sw代码的网页。如果之前已经注册过了想要复现第一次注册的情况,可以打开控制台中的应用→Service Workers把该域名下的sw手动取消注册,然后在关闭掉所用该域名下的网页,然后第一次打开该域名网页就可以复现第一次注册sw的场景了

image.png

场景二:刷新网页

刷新已经注册过sw的网页会发现console控制台打印结果为:

service worker 抓取请求成功:<http://127.0.0.1:8000/imgs/test.jpg> (clientId:2d87d3b7-8606-4897-8a1d-8772929c0b36)

可以看到重新刷新页面不会执行注册→安装→激活流程,但可以拦截所有的当前页面请求

场景三:打开新的标签页

在浏览器新开一个http://127.0.0.1:8000标签页,console控制台打印结果为:

service worker 注册成功
service worker 安装成功
service worker 激活成功
service worker 抓取请求成功:<http://127.0.0.1:8000/> (clientId:)
service worker 抓取请求成功:<http://127.0.0.1:8000/imgs/test.jpg> (clientId:2d87d3b7-8606-4897-8a1d-8772929c0b36)
service worker 抓取请求成功:<http://127.0.0.1:8000/> (clientId:)
service worker 抓取请求成功:<http://127.0.0.1:8000/imgs/test.jpg> (clientId:2eff2973-793e-406b-b749-0ec789412ce6)

如果再开一个标签页,console控制台打印结果为:

service worker 注册成功
service worker 安装成功
service worker 激活成功
service worker 抓取请求成功:<http://127.0.0.1:8000/> (clientId:)
service worker 抓取请求成功:<http://127.0.0.1:8000/imgs/test.jpg> (clientId:2d87d3b7-8606-4897-8a1d-8772929c0b36)
service worker 抓取请求成功:<http://127.0.0.1:8000/> (clientId:)
service worker 抓取请求成功:<http://127.0.0.1:8000/imgs/test.jpg> (clientId:2eff2973-793e-406b-b749-0ec789412ce6)
service worker 抓取请求成功:<http://127.0.0.1:8000/> (clientId:)
service worker 抓取请求成功:<http://127.0.0.1:8000/imgs/test.jpg> (clientId:394bdb9d-ec3a-4c9e-8d3e-aa61bacfb7af)

这时已经开启了三个同域名的标签页,你会发现除了刷新过页面的那个标签页,其他两个标签页的打印结果都一样

说明新开的网页也会执行注册→安装→激活流程,并且可以拦截sw所有的页面请求(包括当前页面和其他同域名页面之前的请求),这也解释了sw和网页终端时一对多的关系,每次打开页面sw都同步之前拦截过的所有请求,当然这些请求是有clientId的,可以区分是否是当前页面触发的

场景四:结束sw

此时关闭所有该域名下的网页,然后重新打开(需要等20s左右(chrome测试得到的数据)再打开,因为sw的结束前会有一小段的等待时间)[<http://127.0.0.1:8000/>](<http://127.0.0.1:8000/>)会发现还是会打印为:

service worker 注册成功
service worker 安装成功
service worker 激活成功

说明sw作用域下的网页全部关闭后,sw会结束,等下次重新打开时会重新执行 注册→安装→激活流程

提示:关闭所有该域名下的网页后可以新开一个tab,然后在控制台中查看sw是否真的被关闭

image.png

点击查看所有注册,如果http://127.0.0.1:8000/ 的按钮变为 UnregisterStart则代表sw已经结束,如果按钮为 StopInspectUnregister 则代表sw正在运行

image.png sw已经结束🔼

image.png sw还在运行🔼

场景五:更新sw

尝试更新sw文件,修改一下安装成功回调打印和fetch事件回调打印,如下: image.png

service worker 抓取请求成功:<http://127.0.0.1:8000/imgs/test.jpg> (clientId:c9d4c78a-ca02-4fd9-b5b5-5dedbbbc04a7)
service worker 注册成功
安装成功1

可以发现更新sw后刷新,sw重新注册安装了,但是没有执行激活,然后点击页面中的load按钮会得到如下打印,打印“抓取请求成功”而不是“抓取请求成功1”说明当前执行的还是旧的sw代码

service worker 抓取请求成功:<http://127.0.0.1:8000/imgs/test2.jpg> (clientId:c9d4c78a-ca02-4fd9-b5b5-5dedbbbc04a7)

结束sw进程(方法看上面的“结束sw”小节),重新打开新的网页,并点击页面中的load按钮 或刷新页面会发现打印更新了,说明sw也更新了

service worker 抓取请求成功1:<http://127.0.0.1:8000/imgs/test2.jpg> (clientId:2dffb96a-ff2e-42ce-9da3-27dd7940c114)

由此可以得出结论,sw更新后在当前sw结束前,新的sw只会执行重新注册和安装,不会立即生效,只有当sw进程结束后,在下一次重新打开时才会执行最新的sw代码

场景六:立即执行最新的sw(skipWaiting)

如果每次更新都要等待sw结束会导致我们最新的sw代码不能立即运行,那有没有什么方法可以让sw立即生效呢,当然有,在 install 回调中执行 self.skipWaiting() 可以跳过这个等待过程,让新的sw立即生效

install 回调中执行 self.skipWaiting() 跳过等待,并更新下回调打印,如下: image.png

刷新网页,发现打印结果如下

service worker 抓取请求成功1:<http://127.0.0.1:8000/imgs/test.jpg> (clientId:00c3be85-4f75-442e-a5e7-73594ecca22a)
service worker 注册成功
sw-lifecycle-test.js:10 service worker 安装成功2
sw-lifecycle-test.js:16 service worker 激活成功

在点击下页面中的 load按钮 发现打印更新了,说明sw立即生效了

service worker 抓取请求成功2:<http://127.0.0.1:8000/imgs/test2.jpg> (clientId:00c3be85-4f75-442e-a5e7-73594ecca22a)

最终流程图

打开一个带有sw代码的网页流程图如下: image.png

Service Worker应用

发送message

由于sw和client是一对多的关系,所以sw可以接收到所有client发来的消息并通过各自的id来区分消息哪个client发过来的,然后做出回应,client则只能接收到sw发给自己的消息。

配置主进程(web client)

client给sw发送消息: 使用 navigator.serviceWorker.controller.postMessage api

let count1 = 100
document.getElementById('send').onclick = function () {
  navigator.serviceWorker.controller.postMessage(++count1)
}

client接收sw的消息

navigator.serviceWorker.addEventListener('message', function(e) {
  console.log('接收到Service Worker消息: ', e.data);
})

配置service worker

sw给client发送消息

方法一:在message回调中发送,使用 event.source.postMessage 方法会将消息发送回对应的client(id为event.source.id)

self.addEventListener('message', event => {
  console.log('Service Worker(接收消息):', event.data, event)
  if (event.source && event.source.postMessage) {
    // 发送消息给id为event.source.id的client
    console.log('Service Worker(发送消息):', event.data, event.source.id)
    event.source.postMessage('我收到消息啦')
  }
})

方法二:使用 self.clients.get 获取到指定的client,然后使用 postMessage 向client发送消息

/**
 * 发送消息给client
 * @param {Number} clientId 
 * @param {String} message 
 * @returns 
 */
function sendMessageToClient(clientId, message) {
    // Get the client.
    const client = await self.clients.get(clientId);
    if (!client) return;

    // Send a message to the client.
    client.postMessage(message);
}

方法三:使用 self.clients.matchAll 获取到所有的client,然后给所有的client发送消息(当然也可以通过这种方式找到对应的client然后给单个client发消息)

/**
 * 发送消息给所有的client
 * @param {String} message 
 * @returns 
 */
function sendMessageToAllClients(message) {
  self.clients.matchAll().then(function(clients) {
    clients.forEach(function(client) {
      client.postMessage(message);
    });
  })
}

发送message最终测试代码:

<!DOCTYPE html>
  <head>
    <title>Service Worker Message Demo</title>
  </head>
  <body>
    <button id="send">send</button>
    <button id="loadImage">load</button>
    <img id="img" alt="demo image" style="width: 100px;height: 100px;" />
    <img src="./imgs/test.jpg" alt="demo image" style="width: 100px;height: 100px;" />
    <script>
      if ('serviceWorker' in navigator) {
        // 由于 127.0.0.1:8000 是所有测试 Demo 的 host
        // 为了防止作用域污染,将安装前注销所有已生效的 Service Worker
        navigator.serviceWorker.getRegistrations()
          .then(regs => {
            for (let reg of regs) {
              reg.unregister()
            }
            navigator.serviceWorker.register('./sw.js')
          })
      }
      /**
       * 用于测试img fetch
       */
       document.getElementById('loadImage').onclick = function () {
        if (!!document.getElementById('img').src.includes("test2.jpg")) {
          document.getElementById('img').src = ""
        } else {
          console.log('load ./imgs/test2.jpg')
          document.getElementById('img').src = "./imgs/test2.jpg"
        }
      }
      /**
       * 监听message事件
       */
        navigator.serviceWorker.addEventListener('message', function(e) {
        console.log('主进程(接收消息): ', e.data);
      })
      /**
       * 发送message
       */
      let count1 = 100
      document.getElementById('send').onclick = function () {
        console.log('主进程(发送消息)', ++count1)
        navigator.serviceWorker.controller.postMessage(++count1)
      }
      /**
       * serviceWorker Ready事件
       */
      navigator.serviceWorker.ready.then( registration => {
        registration.active.postMessage("Hi service worker");
      });
    </script>
  </body>
</html>
// sw.js
console.log('service worker 注册成功')

self.addEventListener('install', () => {
  // 安装回调的逻辑处理
  console.log('service worker 安装成功')
  // 跳过等待
  self.skipWaiting()
})

self.addEventListener('activate', () => {
  // 激活回调的逻辑处理
  console.log('service worker 激活成功')
})

let count = 0;
self.addEventListener('fetch', event => {
  console.log(`service worker 抓取请求成功:${event.request.url} (clientId:${event.clientId})`)
  ++count;

  event.waitUntil(async function() {
    await sendMessageToClient(event.clientId, `抓取请求成功: ${event.request.url}`, `${count}`)
  }())

})

self.addEventListener('message', event => {
  console.log('Service Worker(接收消息):', event.data)
  if (event.source && event.source.postMessage) {
    // 发送消息给id为event.source.id的client
    console.log('Service Worker(发送消息):', event.data, event.source.id)
    event.source.postMessage(`我收到消息啦`)
    sendMessageToClient(event.source.id, '我是通过self.clients.get获取到你的id的')
    sendMessageToAllClients('这条消息是发给所有client的')
  }
})

/**
 * 发送消息给client
 * @param {Number} clientId
 * @param {String} message
 * @returns
 */
async function sendMessageToClient(clientId, message) {
    // Get the client.
    const client = await self.clients.get(clientId);
    if (!client) return;

    // Send a message to the client.
    client.postMessage(message);
}

/**
 * 发送消息给所有的client
 * @param {String} message
 * @returns
 */
function sendMessageToAllClients(message) {
  self.clients.matchAll().then(function(clients) {
    clients.forEach(function(client) {
      client.postMessage(message);
    });
  })
}

缓存cache

我们可以cache api来缓存路径(如图片、文件、请求url等),参考service worker说明 来进行相关的配置,具体步骤如下:

  1. 初始化cache:在sw的 install 回调中初始化缓存路径,即将应用默认要缓存的路径通过 cache api 添加到缓存中,需要注意的是不要缓存根目录,否则获取页面时拿到的一直都是缓存中的数据

    const CACHE_NAME = 'my-site-cache-v1';
    const ROOT_URL = '<http://127.0.0.1:8000/>';
    const urlsToCache = [
      // '/',            // 不要缓存根目录,否则页面一直都会加载缓存无法更新
      '/imgs/test.jpg',  // 默认缓存/imgs/test.jpg
    ];
    
    /**
     * sw安装回调,一般在install中初始化缓存
     * 使用skipWaiting可以跳过等待,当sw文件有更新时可以立即生效
     */
    self.addEventListener('install', (event) => {
      // 安装回调的逻辑处理
      console.log('<======service worker 安装成功======>')
      // 跳过等待
      self.skipWaiting()
    
      /**
       * 在install中初始化cache,将urlsToCache中的路径缓存
       */
      event.waitUntil(new Promise((resolve, reject) => {
            // 返回处理缓存更新的相关事情的 Promise
            caches.open(CACHE_NAME)
              .then(async function(cache) {
                let currentCaches = await cache.keys()
                const rootCache = currentCaches.find(c => c.url === ROOT_URL)
                if (rootCache) {
                  // 如果缓存的根目录,则删除该缓存
                  await cache.delete(rootCache)
                }
                // 缓存urlsToCache中的路径
                await cache.addAll(urlsToCache)
                currentCaches = await cache.keys()
                console.log('Cache 初始化成功', currentCaches)
                resolve('Cache 初始化成功')
              })
      }))
    })
    
  2. 当页面请求的是缓存中的数据时,返回缓存

    /**
     * 拦截请求,可以再这个回调中处理请求或添加新的缓存路径
     */
    self.addEventListener('fetch', event => {
      wsLog(`service worker 抓取请求成功: ${event.request.url}`)
    
    	event.respondWith(
        caches.match(event.request)
          .then(function(response) {
            if (response) {
              // 命中缓存,把被缓存的值返回
              console.log('命中缓存,把被缓存的值返回', response)
              return response;
            } else {
              console.log('未找到缓存,正常请求', event.request)
              return fetch(event.request);
            }
          }
        ));
    })
    

    如果为请求未缓存,希望立即添加到缓存时,也可以在”fetch“回调中动态的添加缓存,但是要注意不要缓存根目录,否则请求页面时拿到的一直都是缓存中的页面,应用就没办法更新了

    /**
     * 拦截请求,可以再这个回调中处理请求或添加新的缓存路径
     */
    self.addEventListener('fetch', event => {
      wsLog(`service worker 抓取请求成功: ${event.request.url}`)
    
    	/**
       * 对于install阶段已缓存的请求,返回缓存;对于未缓存的请求,加入缓存(根路径除外)
       */
      event.respondWith(
        caches.match(event.request)
          .then(function(response) {
            if (response) {
              // 命中缓存,把被缓存的值返回
              console.log('命中缓存,把被缓存的值返回', response)
              return response;
            } else {
              if (event.request.url === ROOT_URL) {
                // 不要缓存根目录,否则页面一直都会加载缓存无法更新
                console.log('请求为根目录,直接返回', event.request)
                return fetch(event.request);
              }
              // 没有命中缓存,将这个请求添加到缓存
              console.log('没有命中缓存,将这个请求缓存', event.request)
    
    					/**
               * 克隆请求
               * 请求是一个流,只能使用一次。
               * 因为我们通过缓存请求和浏览器请求分别使用了一次获取,所以我们需要克隆响应
               */
              const fetchRequest = event.request.clone();
    
              return fetch(fetchRequest).then(
                function(response) {
                  /**
                   * 检查response是否有效
                   * 确保 response 有效
                   * 检查 response 的状态是200
                   * 确保 response 的类型是 basic 类型的,这说明请求是同源的,这意味着第三方的请求不能被缓存。
                   */
                  if(!response || response.status !== 200 || response.type !== 'basic') {
                    return response;
                  }
    
                  /**
                   * 克隆response
                   * response 是一个 Stream,那么它的 body 只能被使用一次
                   * 所以为了让浏览器跟缓存都使用这个body,我们必须克隆这个body,一份到浏览器,一份到缓存中缓存
                   */
                  const responseToCache = response.clone();
    
                  console.log('添加缓存', event.request)
                  caches.open(CACHE_NAME)
                    .then(function(cache) {
                      cache.put(event.request, responseToCache);
                    });
    
                  return response;
                }
              );
            }
          }
        ));
    })
    

缓存cache最终测试代码:

<!DOCTYPE html>
  <head>
    <title>Service Worker Demo</title>
  </head>
  <body>
		<button id="loadImage">load</button>
    <img id="img" alt="demo image" style="width: 100px;height: 100px;" />
    <img src="./imgs/test.jpg" alt="demo image" />
    <script>
      if ('serviceWorker' in navigator) {
        // 由于 127.0.0.1:8000 是所有测试 Demo 的 host
        // 为了防止作用域污染,将安装前注销所有已生效的 Service Worker
        navigator.serviceWorker.getRegistrations()
          .then(regs => {
            for (let reg of regs) {
              reg.unregister()
            }
            navigator.serviceWorker.register('./sw.js')
          })
      }
			/**
       * 用于测试img缓存
       */
      document.getElementById('loadImage').onclick = function () {
        if (!!document.getElementById('img').src.includes("test2.jpg")) {
          document.getElementById('img').src = ""
        } else {
          console.log('load ./imgs/test2.jpg')
          document.getElementById('img').src = "./imgs/test2.jpg"
        }
      }
    </script>
  </body>
</html>
// sw.js
/**
 * cacheApi: <https://developer.mozilla.org/zh-CN/docs/Web/API/Cache>
 */

const CACHE_NAME = 'my-site-cache-v2';
const ROOT_URL = '<http://127.0.0.1:8000/>';
const urlsToCache = [
  // '/',            // 不要缓存根目录,否则页面一直都会加载缓存无法更新
  '/imgs/test.jpg',  // 默认缓存/imgs/test.jpg
];

/**
 * sw安装回调,一般在install中初始化缓存
 * 使用skipWaiting可以跳过等待,当sw文件有更新时可以立即生效
 */
self.addEventListener('install', (event) => {
  // 安装回调的逻辑处理
  console.log('<======service worker 安装成功======>')
  // 跳过等待
  self.skipWaiting()

  /**
   * 在install中初始化cache,将urlsToCache中的路径缓存
   */
  event.waitUntil(new Promise((resolve, reject) => {
        // 返回处理缓存更新的相关事情的 Promise
        caches.open(CACHE_NAME)
          .then(async function(cache) {
            let currentCaches = await cache.keys()
            const rootCache = currentCaches.find(c => c.url === ROOT_URL)
            if (rootCache) {
              // 如果缓存的根目录,则删除该缓存
              await cache.delete(rootCache)
            }
            // 缓存urlsToCache中的路径
            await cache.addAll(urlsToCache)
            currentCaches = await cache.keys()
            console.log('Cache 初始化成功', currentCaches)
            resolve('Cache 初始化成功')
          })
  }))
})

/**
 * sw激活回调
 */
self.addEventListener('activate', (event) => {
  // 激活回调的逻辑处理
  console.log('<======service worker 激活成功======>')
})

/**
 * 拦截请求,可以再这个回调中处理请求或添加新的缓存路径
 */
self.addEventListener('fetch', event => {
  console.log(`service worker 抓取请求成功: ${event.request.url}`)
  if (!event.request) return
  /**
   * 缓存策略
   * 如果为0,对于install阶段已缓存的请求,返回缓存;对于未缓存的请求,正常去线上请求
   * 如果为1,对于install阶段已缓存的请求,返回缓存;对于未缓存的请求,加入缓存(根路径除外)
   */
  const mode = 1

  if (mode === 0) {
		/**
     * 对于install阶段已缓存的请求,返回缓存;对于未缓存的请求,正常去线上请求
     */
    event.respondWith(
	    caches.match(event.request)
	      .then(function(response) {
	        if (response) {
	          // 命中缓存,把被缓存的值返回
	          wsLog('命中缓存,把被缓存的值返回', response)
	          return response;
	        } else {
	          wsLog('未找到缓存,正常请求', event.request)
	          return fetch(event.request);
	        }
	      }
	    ));
  } else {
		/**
	   * 对于install阶段已缓存的请求,返回缓存;对于未缓存的请求,加入缓存(根路径除外)
	   */
	  event.respondWith(
	    caches.match(event.request)
	      .then(function(response) {
	        if (response) {
	          // 命中缓存,把被缓存的值返回
	          console.log('命中缓存,把被缓存的值返回', response)
	          return response;
	        } else {
	          if (event.request.url === ROOT_URL) {
	            // 不要缓存根目录,否则页面一直都会加载缓存无法更新
	            console.log('请求为根目录,直接返回', event.request)
	            return fetch(event.request);
	          }
	          // 没有命中缓存,将这个请求添加到缓存
	          console.log('没有命中缓存,将这个请求缓存', event.request)
	
						/**
	           * 克隆请求
	           * 请求是一个流,只能使用一次。
	           * 因为我们通过缓存请求和浏览器请求分别使用了一次获取,所以我们需要克隆响应
	           */
	          const fetchRequest = event.request.clone();
	
	          return fetch(fetchRequest).then(
	            function(response) {
	              /**
	               * 检查response是否有效
	               * 确保 response 有效
	               * 检查 response 的状态是200
	               * 确保 response 的类型是 basic 类型的,这说明请求是同源的,这意味着第三方的请求不能被缓存。
	               */
	              if(!response || response.status !== 200 || response.type !== 'basic') {
	                return response;
	              }
	
	              /**
	               * 克隆response
	               * response 是一个 Stream,那么它的 body 只能被使用一次
	               * 所以为了让浏览器跟缓存都使用这个body,我们必须克隆这个body,一份到浏览器,一份到缓存中缓存
	               */
	              const responseToCache = response.clone();
	
	              console.log('添加缓存', event.request)
	              caches.open(CACHE_NAME)
	                .then(function(cache) {
	                  cache.put(event.request, responseToCache);
	                });
	
	              return response;
	            }
	          );
	        }
	      }
	    ));  
  }
})

执行以上代码,通过控制台 应用→缓存 可以看到 images/test.jpg 已经被缓存的

image.png

点击页面中的load按钮会去加载images/test2.jpg ,如果将代码中mode改为1,即缓存策略为”对于install阶段已缓存的请求,返回缓存;对于未缓存的请求,加入缓存(根路径除外)“,则images/test2.jpg 也会被加入到缓存中

image.png

调试

调试方法看这里

所有demo代码

懒得敲代码的可以直接到github上下载 service worker demo

参考

Service Worker MDN

Service Worker 实践