服务工作器开发秘籍-全-

服务工作器开发秘籍(全)

原文:zh.annas-archive.org/md5/669bddba461da7975996adceb66d1506

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

浏览器的服务工作者功能将使你能够构建高度可用和性能良好的原生 Web 应用程序,这些应用程序可以无缝集成第三方 API。无论你想创建离线 Web 应用程序还是代理,这本书都会向你展示如何做到这一点。

本书涵盖的内容

第一章,学习服务工作者基础,涵盖了在你的环境中设置服务工作者,以及如何使用服务工作者开发来启动和运行。本章包括注册服务工作者和调试。

第二章,处理资源文件,提供了关于如何使用服务工作者处理资源文件的几个食谱,包括加载 CSS 和字体。

第三章,访问离线内容,探讨了如何缓存资源并离线提供服务。

第四章,使用高级技术访问离线内容,探讨了处理离线内容时的高级技术,包括模板和 Google Analytics。

第五章,超越离线缓存,提供了超越离线缓存的食谱,并探讨了从获取离线网络响应到如何将服务工作者用作负载均衡器的主题。

第六章,使用高级库,讨论了 Google Analytics、断路器和死信队列。

第七章,获取资源,涵盖了从不同来源获取资源的各种技术。

第八章,尝试 Web 推送,讨论了实现推送通知的不同方法。

第九章,查看一般用法,提供了关于服务工作者一般用法(从慢响应到实时流程图)的各种食谱。

第十章,提高性能,讨论了如何优化你的服务工作者应用程序以高效和性能良好的方式运行。

你需要这本书的内容

这本书是在使用 Mac 和 Google Chrome 浏览器,运行 Node.js 的环境下编写的。然而,Node.js 也可以在 Windows 或 Linux 机器上运行,同时配合 Google Chrome。

本书使用的所有软件都是免费和开源的。你肯定需要运行 Node.js 和 Google Chrome 来执行大多数食谱。

术语约定

在这本书中,你会发现许多不同风格的文本,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称按照以下方式显示:“skipWaiting()方法在活动事件处理程序中使用,而后者又使用Clients.claim()。”

代码块按照以下方式设置:

self.oninstall = function(event) {
  event.waitUntil(
    fetch(zipURL)
      .then(function(res) {
        return res.arrayBuffer();
      })
      .then(getZipFileReader)
      .then(cacheFileContents)
      .then(self.skipWaiting.bind(self))
  );
};

任何命令行输入或输出都按照以下方式编写:

$ git add –all
$ git commit -m "initial commit"
$ git push -u origin master

新术语重要词汇以粗体显示。屏幕上、菜单或对话框中看到的单词,例如,在文本中显示如下:“最后,在左侧边栏中,选择凭据。”

读者反馈

我们欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大价值的标题。

要向我们发送一般反馈,请简单地发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍标题。

如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南,网址为www.packtpub.com/authors

客户支持

现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您充分利用您的购买。

下载示例代码

您可以从www.packtpub.com上的账户下载此书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的支持选项卡上。

  3. 点击代码下载与勘误

  4. 搜索框中输入书籍名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书的来源。

  7. 点击代码下载

您也可以通过点击 Packt Publishing 网站上的书籍网页上的代码文件按钮下载代码文件。您可以通过在搜索框中输入书籍名称来访问此页面。请注意,您需要登录到您的 Packt 账户。

文件下载完成后,请确保您使用最新版本的以下软件解压缩或提取文件夹:

  • Windows 上的 WinRAR / 7-Zip

  • Mac 上的 Zipeg / iZip / UnRarX

  • Linux 上的 7-Zip / PeaZip

此书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Service-Worker-Development-Cookbook。我们还有其他来自我们丰富图书和视频目录的代码包可供使用,网址为github.com/PacktPublishing/。查看它们吧!

勘误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。

盗版

在互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供疑似盗版材料的链接。

我们感谢您在保护我们作者和我们提供有价值内容的能力方面的帮助。

问题和建议

如果您对本书的任何方面有问题,您可以联系我们的 <questions@packtpub.com>,我们将尽力解决问题。

第一章. 学习服务工作者基础

在本章中,我们将涵盖以下主题:

  • 设置服务工作者

  • 为 Windows 设置 SSL

  • 为 Mac 设置 SSL

  • 为 GitHub 页面设置 SSL

  • 注册服务工作者

  • 详细注册服务工作者

  • 调试

  • 在出错时提供陈旧版本

  • 创建模拟响应

  • 处理请求超时

简介

如果你稍微旅行一下,很可能会发现自己经常遇到零网络连接的情况。这很令人沮丧,尤其是当你只想继续阅读一些新闻文章,或者你想完成一些工作的时候。

不幸的是,使用你的浏览器,在离线状态下尝试从网络请求某些内容并不太顺利。

简介

尽管在飞机上、地铁里、酒店和会议上,Wi-Fi 可能会为你提供恢复连接的机会,但总的来说,你将不得不等待网络重新上线,才能请求你想要查看的页面。

解决这个问题的先前尝试包括AppCache。它在某种程度上似乎有效,但AppCache的问题在于它对用户交互做了很多假设。当这些假设没有得到满足时,应用程序将无法按预期工作。它也被设计成与单页应用程序很好地协同工作,而不是传统的多页网站。

此外,在提供无缝用户体验的 Web 应用程序中,最具挑战性的问题之一是在离线状态下使它们功能正常。鉴于大多数用户现在都在移动中访问 Web 应用程序,这是一个需要解决的问题。服务工作者应运而生,这是一种在浏览器后台运行的脚本。

能够使用不受连接性影响的网络应用程序意味着用户在乘坐飞机、地铁或在网络有限或不可用的地方时可以不间断地操作。这项技术将有助于提高客户的生产力,并增加应用程序的可用性。

使用服务工作者,我们能够预先缓存网站的一些资源。我们所说的资源包括 JavaScript 文件、CSS 文件、图片和一些字体。这将帮助我们加快加载时间,而不是每次访问同一网站时都要从网络服务器获取信息。当然,最重要的是,这些资源在我们离线时也会可用。

服务工作者

服务工作者是一个脚本,它位于你的浏览器和网络之间,为你提供拦截网络请求的能力,并以不同的方式响应它们。

为了使你的网站或应用能够工作,浏览器会获取其资产,例如 HTML 页面、JavaScript、CSS、图片和字体。在过去,管理这些资源主要是浏览器的责任。如果浏览器无法访问网络,你可能会看到其 无法连接到互联网 的消息。你可以使用一些技术来鼓励资产的本地缓存,但浏览器通常有最后的决定权。

服务工作者大量使用的一个特性是承诺。因此,对承诺有一个良好的理解非常重要。

承诺

承诺是运行相互依赖的异步操作的一个很好的机制。这是服务工作者工作的核心。

承诺可以做很多事情,但就目前而言,你需要知道的是,如果某个操作返回了一个承诺,你可以在其末尾附加 .then() 并在其中包含成功、失败等回调函数,或者如果你想包含一个失败回调,可以插入 .catch()

让我们比较传统的同步回调结构与其异步承诺等效结构:

sync

try {
  var value = Fn();
  console.log(value);
} catch(err) {
  console.log(err);
}

async

Fn().then(function(value) {
  console.log(value);
  }).catch(function(err) {
  console.log(err);
});

sync 示例中,我们必须等待 Fn() 运行并返回一个 value,然后才能执行更多代码。在 async 示例中,Fn() 返回一个包含值的承诺,然后其余代码可以继续运行。当承诺解决时,then 中的代码将异步运行。

Promise.resolve(value)

此方法返回一个 Promise.then 对象,该对象通过传递给 resolve 方法的值得到解决,就像 Promise.resolve(value) 一样。如果此值有一个 then 方法,则返回的方法将跟随它;否则,它将以该值得到满足。

Promise.reject(reason)

此方法将 reason 作为参数,并返回一个被拒绝的承诺对象。

设置服务工作者

为了使服务工作者运行,我们需要通过 超文本传输协议安全HTTPS)来提供我们的代码。出于安全原因,服务工作者仅设计在 HTTPS 上运行。源代码存储库,如 GitHub,支持 HTTPS,你可以在那里托管你的文件。

准备工作

如果你正在使用浏览器的新版本,那么服务工作者可能已经在其上启用。但是,如果情况不是这样,我们可能需要在浏览器设置中更改一些设置。在接下来的部分,我们将介绍如何在 Chrome 和 Firefox 中启用服务工作者功能。

如何操作...

按照以下步骤在 Chrome 和 Firefox 中启用服务工作者。

Chrome

为了在 Chrome 中进行实验,你需要下载 Chrome Canary:

  1. 前往 www.google.com/chrome/browser/canary.html 下载最新版本。

  2. 打开 Chrome Canary 并输入 chrome://flags

  3. 打开 experimental-web-platform-features

  4. 重新启动浏览器。

  5. 以下图像显示了 Chrome 浏览器中的实验性功能,您可以通过点击底下的启用链接来启用实验性 Web 平台功能:Chrome

Firefox

要在 Firefox 中进行实验,您需要下载 Firefox Nightly:

  1. 前往 nightly.mozilla.org/ 并下载最新版本。

  2. 打开 Firefox Nightly,并前往 about:configFirefox

  3. experimental-web-platform-features 设置为 true

  4. 重新启动浏览器。

注意

在撰写本文时,Opera 提供了对服务工作者部分的支持,包括基本支持以及安装和卸载事件。Internet Explorer 和 Safari 不支持服务工作者。

服务工作者目前是一个实验性技术,这意味着其语法和行为可能会随着规范的变更在未来版本中发生变化。

在 Windows 上设置 SSL

服务工作者仅设计在 HTTPS 上运行,因此为了测试我们的代码,我们需要确保我们的网页通过 HTTPS 传输。在这个菜谱中,我们将介绍如何在 Windows 上为您的网站设置 SSL 支持。

准备工作

此菜谱假设您正在运行 Windows 7 或更高版本,并且已启用互联网信息服务IIS)。

如何操作...

按照以下说明启用 SSL:

  1. 首先,打开 IIS;您可以在命令行中运行以下命令来完成此操作:

    Inetmgr
    
    
  2. 在树视图中选择服务器节点,然后在列表视图中双击服务器证书功能,如图所示:如何操作...

  3. 操作面板中点击创建自签名证书...链接。如何操作...

  4. 为新证书输入一个有意义的名称,然后点击确定如何操作...

    这将生成一个自签名证书,该证书标记为服务器身份验证使用,这意味着它使用服务器端证书来验证服务器的身份,同时也用于 HTTP SSL 加密。

    为了创建 SSL 绑定,我们必须在树视图中选择一个网站,然后在操作面板中点击绑定...。这将打开绑定编辑器,该编辑器管理您的网站的绑定,包括创建、编辑和删除。现在,要将您的新 SSL 绑定添加到网站,请点击添加...

    如何操作...

  5. 端口 80 是 HTTP 的新绑定的默认设置。我们可以在类型下拉列表中选择https。从SSL 证书下拉列表中选择我们在上一节中创建的自签名证书,然后点击确定如何操作...

  6. 现在我们已经在网站上设置了新的 SSL 绑定,剩下要做的就是确保它能够正常工作。如何操作...

  7. 点击继续访问此网站以继续。

在 Mac 上设置 SSL

如前所述,服务工作者旨在仅在 HTTPS 上运行。因此,为了测试我们的代码,我们需要我们的网页通过 HTTPS 交付。在本食谱中,我们将介绍如何为 Mac 网站设置 SSL 支持。

准备工作

本食谱假设您正在运行 OS X 10.11, El Capitan 或更高版本。我们将使用名为 Vim 的命令行工具来编辑文件,该工具已随 Mac 一起提供。请确保不要在 Vim 中使用数字键盘。请注意,此过程可能需要较长时间。

如何操作...

按照以下说明启用 SSL:

  1. 首先,我们需要确保 Apache 正在运行(您可能会收到密码提示):

    $ sudo apachectl start
    
    
  2. 下一步是修改您的 httpd.conf 文件。因为它是一个系统文件,您将需要再次使用 sudo

    $ sudo vim /etc/apache2/httpd.conf
    
    
  3. 在此文件中,您应取消注释 socache_shmcb_modulessl_module,并包含 httpd-ssl.conf 文件,通过删除这些行前面的 # 符号(您可以在 Vim 编辑器中使用 / 进行搜索):

    LoadModule socache_shmcb_module libexec/apache2/mod_socache_shmcb.so
    ...
    LoadModule ssl_module libexec/apache2/mod_ssl.so
    ...
    Include /private/etc/apache2/extra/httpd-ssl.conf
    
  4. 在保存上述文件(按 :wq)后,您应打开您的 /etc/apache2/extra/httpd-vhosts.conf 文件:

    $ sudo vim /etc/apache2/extra/httpd-vhosts.conf
    
    
  5. 在这里,您可以为您希望提供 SSL 支持的每个虚拟主机创建一个 VirtualHost 条目:

    <VirtualHost *:80>
           DocumentRoot "/Library/WebServer/Documents"
        ServerName localhost
        SSLEngine on
        SSLCertificateFile "/private/etc/apache2/localhost.crt"
        SSLCertificateKeyFile "/private/etc/apache2/localhost.key"
    </VirtualHost>
    

    确保您已将您的开发文件夹复制到之前所做的 DocumentRoot 目录:/Library/WebServer/Documents

    为了让所有这些与 Apache 一起工作,我们需要创建一个自签名证书,我们已经在 VirtualHost 定义中引用了它。

  6. 生成一个密钥:

    $ cd /etc/apache2
    
    
  7. 在以下命令后按 Enter 键,不要输入任何内容:

    $ sudo openssl genrsa -out localhost-key.pem 1024
    
    
  8. 接下来,我们必须生成一个证书签名请求:

    $ sudo openssl req -new -key localhost-key.pem -out localhost.csr
    
    
  9. 使用此 证书签名请求CSR)生成证书:

    $ sudo openssl x509 -req -days 365 -in localhost.csr -signkey 
    localhost-key.pem -out localhost.crt
    
    
  10. 然后,我们必须将密钥转换为无短语密钥:

    $ sudo openssl rsa -in localhost-key.pem -out localhost.key
    
    
  11. 现在将 server.crt 更改为 localhost.crt,以及将 server.key 更改为 localhost.key

    $ sudo vim /etc/apache2/extra/httpd-ssl.conf
    
    
  12. 现在您只需再次检查您的 Apache 配置语法:

    $ sudo apachectl configtest
    
    
  13. 如果一切顺利,请重新启动 Apache:

    $ sudo apachectl -k restart
    
    
  14. 现在,只需将您的浏览器指向 https://localhost。如果您在 Chrome 中被提示输入自签名证书,您可以在该页面上点击 高级 选项并继续,而在 Firefox 中,您需要展开 我了解风险 选项并添加一个例外。这是由于自签名证书未由任何权威机构签名,因此浏览器会添加有关它们的警告。尽管如此,由于您是创建证书的人,您知道接受它是安全的。

  15. 为了解决这个问题,您需要将证书添加为受信任的根权威机构。

  16. 在 OS X 中打开 密钥链访问 工具。在左侧选择 系统 选项。点击左上角的锁形图标以启用更改。如何操作...

  17. 点击底部的加号按钮,选择您复制到桌面的 /etc/apache2/localhost.cer 文件。在出现的对话框中,点击 始终信任。在 localhost 被添加到系统密钥链后,双击它再次打开。展开 信任 部分,对于第一个选项,选择 始终信任

  18. 到目前为止,一切配置已完成。退出 Chrome 和所有其他浏览器(这是必需的),启动网络服务器,并再次尝试导航到本地 HTTPS 网站。如何操作...

小贴士

有关下载代码包的详细步骤在本书的序言中提及。请查看。

该书的代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/Service-Worker-Development-Cookbook。我们还有其他来自我们丰富图书和视频目录的代码包,可在 github.com/PacktPublishing/ 找到。去看看吧!

设置 GitHub 页面的 SSL

服务工作者设计为仅在 HTTPS 上运行。因此,为了测试我们的代码,我们需要我们的网页通过 HTTPS 传输。GitHub 页面通过 HTTPS 提供,并且免费使用。所以让我们继续吧。

准备工作

在注册 GitHub 账户之前,请确保您有一个有效的电子邮件地址。

如何操作...

按照以下说明设置 GitHub 页面:

  1. 前往 GitHub (github.com) 并注册自己以获取账户,如果您还没有的话。

  2. 完成后,登录并创建一个新的仓库,格式如下:

    username.github.io,其中 username 是您的用户名。

    如何操作...

    如果仓库的第一部分与您的用户名不完全匹配,则不会工作。

  3. 打开您的命令行窗口并克隆您的仓库。

    $ git  clone https://github.com/username/username.github.io
    
    
  4. 切换到 username.github.io 目录:

    $ cd username.github.io
    
    
  5. 创建一个名为 service-workers 的目录:

    $ mkdir service-workers
    
    
  6. 创建一个包含一些文本的 index.html 文件:

    $ echo "Service Workers" > index.html
    
    
  7. 现在让我们提交并推送我们的更改到仓库:

    $ git add --all
    $ git commit -m "initial commit"
    $ git push -u origin master
    
    
  8. 打开浏览器并访问 http://username.github.io/service-workers/

注册服务工作者

注册服务工作者是启动服务工作者的第一步。通过注册服务工作者,我们告诉我们的网站使用服务工作者。并且这个过程是在服务工作者之外进行的,在我们的例子中是在 index.html 文件中。您可以在 JavaScript 文件中这样做,然后在 index.html 文件中引用它,但不能在服务工作者脚本文件中。

在这个基本的注册演示中,我们将测试我们的服务工作者是否成功注册。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一个菜谱:设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的开发生态,请参考以下菜谱:为 GitHub 页面设置 SSL为 Windows 设置 SSL,和为 Mac 设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 首先,我们需要创建如下所示的 index.html 文件:

    <!DOCTYPE html>
      <html lang="en">
       <head></head>
           <body>
           <p>Registration status: <strong id="status"></strong></p>
           <script>
              if ('serviceWorker' in navigator) {
               navigator.serviceWorker.register(
                  'service-worker.js',
                  { scope: './' }
                ).then( function(serviceWorker) {
                  document.getElementById('status').innerHTML =
                        'successful';
                  }).catch(function(error) {
                      document.getElementById('status').innerHTML = error;
                  });
              } else {
                    document.getElementById('status').innerHTML = 
               'unavailable';
                }
    </script>
    </body>
    </html>
    
  2. index.html 文件所在的同一文件夹中创建一个名为 service-worker.js 的空 JavaScript 文件。

  3. 在你的两个文件就绪后,你可以导航到 GitHub 页面,https://username.github.io/service-workers/01/01/index.html,你将在浏览器中看到成功消息。如何操作...

它是如何工作的...

我们首先确保服务工作者功能可用,通过这一行代码 if ('serviceWorker' in navigator). 如果不是这种情况,我们将消息设置为不可用。如果你的浏览器不支持服务工作者,你将看到这条消息。

现在我们使用空 JavaScript 文件和作用域来注册服务工作者。为了确保注册只适用于当前目录及其下级目录,我们在 { scope: './' } 这行代码中将默认作用域 '/' 覆盖为 './',因为作用域必须是同一来源。

如果你决定你的脚本文件应该放在其他地方,你需要一个特殊的头信息,例如 Service-Worker-allowed: true 或特定的内容类型,例如 text/javascript

如果注册成功,我们将打印消息 successful 到状态消息。

否则,我们将错误消息作为状态打印出来。错误的原因可能是注册过程中出现问题,例如,service-worker.js 文件可能不可用或其中可能包含语法错误。

还有更多...

我们可以通过调用以下 unregister() 函数来注销服务工作者:

serviceWorker.unregister().then(function() {
    document.getElementById('status').innerHTML = 'unregistered';
})

已知问题

当与服务工作者一起工作时,Chrome 浏览器存在一些问题,可能会让你感到困惑。

ERR_FILE_EXISTS 错误消息

使用服务工作者重新加载页面将始终显示 ERR_FILE_EXISTS 错误消息,即使你的代码没有问题。

这似乎发生在我们尝试访问已注册的服务工作者时。

 错误消息

过期控制台消息

从服务工作者脚本中记录的消息,如 console.log,可能不会从控制台中清除,这看起来像是后续页面加载时事件被触发得太多次。

详细注册服务工作者

理解服务工作者注册和状态转换中涉及的事件,将使你能够通过使用此功能来更好地控制你的应用程序。在这个详细的注册演示中,我们将查看服务工作者注册的状态转换。

准备中

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考之前的配方:设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的开发生态,请参考以下配方:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 首先,我们需要创建一个如下的 index.html 文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Detailed Registration</title>
    </head>
    <body>
      <p>Registration status: <strong id="status"></strong></p>
      <p>State: <strong id="state"></strong></p>
    
      <script>
        function printState(state) {
          document.getElementById('state').innerHTML = state;
        }
    
        if ('serviceWorker' in navigator) {
    
          navigator.serviceWorker.register(
            'service-worker.js',
            { scope: './' }
          ).then( function(registration) {
            var serviceWorker;
    
            document.getElementById('status').innerHTML = 'successful';
    
            if (registration.installing) {
              serviceWorker = registration.installing;
              printState('installing');
            } else if (registration.waiting) {
              serviceWorker = registration.waiting;
              printState('waiting');
            } else if (registration.active) {
              serviceWorker = registration.active;
              printState('active');
            }
    
            if (serviceWorker) {
              printState(serviceWorker.state);
    
              serviceWorker.addEventListener('statechange', function(e) {
                printState(e.target.state);
              });
            }
          }).catch(function(error) {
            document.getElementById('status').innerHTML = error;
          });
        } else {
            document.getElementById('status').innerHTML = 'unavailable';
          }
      </script>
    </body>
    </html>
    
  2. 在与 index.html 文件相同的文件夹中创建一个名为 service-worker.js 的 JavaScript 文件,并包含以下代码:

    self.addEventListener('install', function(e) {
      console.log('Install Event:', e);
    });
    
    self.addEventListener('activate', function(e) {
      console.log('Activate Event:', e);
    });
    
  3. 在你的两个文件就绪后,你可以导航到 GitHub 页面,https://username.github.io/service-workers/01/02/index.html,你将在浏览器中看到成功消息。如何操作...

它是如何工作的...

当注册成功时,我们检查注册状态并将其打印到浏览器。在这种情况下,它可能是安装中、等待中或激活中:

 if (registration.installing) {
          serviceWorker = registration.installing;
          printState('installing');
        } else if (registration.waiting) {
          serviceWorker = registration.waiting;
          printState('waiting');
        } else if (registration.active) {
          serviceWorker = registration.active;
          printState('active');
        }

printState(state) 辅助函数将打印状态到浏览器。

最后,我们向服务工作者添加一个名为 statechange 的事件监听器。此事件监听器的回调将打印状态变化到浏览器:

if (serviceWorker) {
          printState(serviceWorker.state);

          serviceWorker.addEventListener('statechange', function(e) {
            printState(e.target.state);
          });
        }

当注册的服务工作者处于激活状态时,我们可以刷新页面以看到服务工作者接管。

要演示页面加载,而不让服务工作者接管,请按 Shift 并刷新页面。你将在网页控制台中看到激活事件被记录。

我们在 service-worker.js 文件中订阅了两个事件监听器,installactivate

self.addEventListener('install', function(e) {
  console.log('Install Event:', e);
});

self.addEventListener('activate', function(e) {
  console.log('Activate Event:', e);
});

当这个版本的脚本首次在给定范围内注册时,会触发这些事件。

安装事件是预取数据和初始化缓存的绝佳位置,而 activate 事件非常适合清理旧版本脚本中的数据。

更多...

当服务工作者成功注册后,它将经历以下阶段。

安装

在服务工作者的生命周期中,当服务工作者在无错误的情况下注册,但尚未激活时,之前激活的任何服务工作者仍然处于控制状态。如果服务工作者文件在页面重新加载之间有任何变化,则服务工作者被视为新的。因此,它将经历安装步骤。在这个阶段,服务工作者不会拦截任何请求。

激活

当服务工人首次变得活跃时,我们说它处于激活阶段。服务工人现在能够拦截请求。这将在我们关闭标签页并重新打开它,或者通过使用Shift + 刷新来硬刷新页面时发生。它不会在安装事件之后立即发生。

获取

获取操作发生在当前服务工人作用域内有一个请求正在发起时。

终止

这可能随时发生,甚至可能发生在请求之外。但是,终止通常发生在浏览器需要回收内存时。当发起新的请求时,服务工人将根据需要重新启动,或者接收一条消息,但它不会回到激活步骤。

服务工人将拦截它注册以捕获的所有请求,即使它需要重新启动才能这样做。但是,话虽如此,我们无法保证它将存在任何长度的时间。正因为如此,全局状态将不会被保留,因此我们必须确保在服务工人文件中不使用任何全局变量。相反,我们可以使用索引或localStorage进行持久化。

相关链接

  • 之前的配方,注册服务工人

调试

服务工人运行在浏览器中与它们控制的页面分开的线程中。有方法在工人和页面之间进行通信,但它们在单独的作用域中执行。这意味着你将无法从服务工人脚本中访问那些网页的 DOM,例如。正因为如此,我们不能在同一网页上使用 DevTools 来调试服务工人脚本。我们需要打开一个单独的检查器来调试服务工人线程。

服务工人主要通过监听相关事件并以有效的方式响应它们来完成大部分工作。在服务工人的生命周期中,不同的事件在不同的生命周期点被触发。因此,如果我们想缓存资源,那么在安装状态下监听install事件是一个很好的时机。同样,我们也可以通过向相关事件处理器添加断点来调试服务工人。

准备就绪

要开始使用服务工人,你需要在浏览器设置中开启服务工人实验功能。如果你还没有这样做,请参考之前的配方:设置服务工人。服务工人仅在 HTTPS 上运行。要了解如何设置一个支持此功能的开发生态,请参考以下配方:为 GitHub 页面设置 SSL为 Windows 设置 SSL,和为 Mac 设置 SSL

如何操作...

按照以下说明设置服务工人的调试:

  1. 要找出你当前正在运行的服务工人,请在浏览器中输入以下内容:chrome://inspect/#service-workers如何操作...

  2. 否则,在你的浏览器中输入以下内容:chrome://serviceworker-internals以查找已注册的 worker。如果没有列出任何内容,那么当前没有正在运行的服务 worker。如何操作...

  3. 要使用 Chrome DevTools 调试你的服务 worker,请导航到服务 worker 页面并打开 DevTools。(在 Mac 上为Cmd + Alt + I,在 Windows 上为F12

  4. 你可以为你的代码添加一个断点来检查。如何操作...

    服务 worker 将在线程列表中显示,而服务 worker选项卡列出了此页面所属的所有活动运行服务 worker。

    如何操作...

    我们也可以使用控制台进行调试。安装过程中的任何错误都会打印在控制台页面上。控制台对于检查服务 worker 上下文很有用。

    如何操作...

  5. 你还可以在 DevTools 的资源选项卡中找到调试面板。为了查看 worker 的网络活动,点击资源选项卡上的检查链接以启动为 worker 专用的 DevTools 窗口。如何操作...

结果页面chrome://serviceworker-internals显示了已注册的服务 worker。它还显示了基本操作按钮,以下将详细解释:

  • 终止:注销 worker。

  • 启动/停止:启动或停止 worker。当你导航到 worker 作用域内的页面时,这会自动发生。

  • 同步:向 worker 发送同步事件。如果你不处理此事件,则不会发生任何操作。

  • 推送:向 worker 发送推送事件。如果你不处理此事件,则不会发生任何操作。

  • 检查:在检查器中打开 worker。

更多...

当你使用 DevTools 打开时,你可能想要检查确保在网络选项卡中禁用缓存没有被勾选。如果该选项被勾选,请求将发送到网络而不是服务 worker。

提供错误时的陈旧版本

如果你经常出差,那么你很可能经常遇到很多零网络连接的情况。这很令人沮丧,尤其是当你想查看之前查看过的页面时。在这个配方中,我们将探讨如何通过向用户提供缓存中的陈旧版本来解决此问题。

准备中

要开始使用服务 worker,你需要在浏览器设置中开启服务 worker 实验功能。如果你还没有这样做,请参考之前的配方:设置服务 worker。服务 worker 仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下配方:为 GitHub 页面设置 SSL为 Windows 设置 SSL,和为 Mac 设置 SSL

如何操作...

按照以下说明设置你的文件结构(或者你可以在提供的目录01/05中找到文件):

  1. 首先,我们需要创建一个如下所示的 index.html 文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Stale on Error</title>
    </head>
    <body>
      <p>Registration status: <strong id="status"></strong></p>
      <script>
        if ('serviceWorker' in navigator) {
          navigator.serviceWorker.register(
            'service-worker.js',
            { scope: './' }
          ).then( function(serviceWorker) {
            document.getElementById('status').innerHTML = 
            'successful';
          }).catch(function(error) {
            document.getElementById('status').innerHTML = error;
          });
    
        } else {
            document.getElementById('status').innerHTML = 
            'unavailable';
          }
      </script>
    </body>
    </html>
    
  2. 在与 index.html 文件相同的文件夹中创建一个名为 service-worker.js 的 JavaScript 文件,并包含以下代码:

    var version = 1;
    var cacheName = 'stale- ' + version;
    
    self.addEventListener('install', function(event) {
        self.skipWaiting();
    });
    
    self.addEventListener('activate', function(event) {
        if (self.clients && clients.claim) {
            clients.claim();
        }
    });
    
    self.addEventListener('fetch', function(event) {
    
        event.respondWith(
            fetch(event.request).then(function(response) {
                caches.open(cacheName).then(function(cache) {
    
                    if(response.status >= 500) {
                        cache.match(event.request).
    					then(function(response) {
    
                            return response;
                        }).catch(function() {
    
                            return response;
                        });
                    } else {
                          cache.put(event.request, response.clone());
                        return response;
                    }
                });
            })
        );
    });
    
  3. 将你的两个文件放置好,导航到 index.html

它是如何工作的...

当注册成功时,我们检查注册状态并将其打印到浏览器:

service-worker.js 文件中,我们始终从网络获取响应:

event.respondWith(
        fetch(event.request).then(function(response) {

如果我们收到一个错误响应,我们将从缓存中返回过时的版本:

if(response.status >= 500) {
                    cache.match(event.request).
					then(function(response) {
                        // Return stale version from cache
                        return response;
})

如果我们找不到过时的版本,我们将返回网络响应,即错误:

}).catch(function() {

return response;
});

如果响应成功(响应代码 200),我们更新缓存的版本:

} else {
cache.put(event.request, response.clone());
   return response;
}

还有更多...

缓存接口的 put() 方法允许将键/值对添加到当前缓存对象中。put() 方法还会覆盖缓存中之前存储的任何与请求匹配的键/值对:

fetch(url).then(function (response) {
  return cache.put(url, response);
});

创建模拟响应

为了模拟你的服务器对你的应用的 API 响应,而不是实际的 API 响应,我们可以让服务工作者返回与 API 响应相同的模拟响应。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考之前的配方:设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下配方:设置 GitHub 页面以支持 SSL设置 Windows 上的 SSL,和 设置 Mac 上的 SSL

如何操作...

按照以下说明设置你的文件结构(这些也可以在提供的目录 01/03 中找到):

  1. 首先,我们需要创建一个如下所示的 index.html 文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Detailed Registration</title>
    </head>
    <body>
      <p>Network status: <strong id="status"></strong></p>
      <div id="request" style="display: none">
        <input id="long-url" value="https://www.packtpub.com/" size="50">
          <input type="button" id="url-shorten-btn" value="Shorten URL" />
        </div>
        <div>
          <input type="checkbox" id="mock-checkbox" checked>Mock Response</input>
        </div>
        <div>
          <br />
          <a href="" id="short-url"></a>
        </div>
      </div>
    
      <script>
        function printStatus(status) {
          document.getElementById('status').innerHTML = status;
        }
    
        function showRequest() {
          document.getElementById('url-shorten-btn')
          .addEventListener('click', sendRequest);
          document.getElementById('request').style.display = 'block';
        }
    
        function sendRequest() {
          var xhr = new XMLHttpRequest(),
            request;
    
                xhr.open('POST',
                'https://www.googleapis.com/urlshortener/v1/url?' +
                'key=[Your API Key]');
          xhr.setRequestHeader('Content-Type', 'application/json');
    
          if (document.getElementById('mock-checkbox').checked) {
                    xhr.setRequestHeader('X-Mock-Response', 'yes');
          }
    
          xhr.addEventListener('load', function() {
            var response = JSON.parse(xhr.response);
            var el = document.getElementById('short-url');
    
            el.href = response.id;
            el.innerHTML = response.id;
          });
    
          request = {
            longUrl: document.getElementById('long-url').value
          };
    
          xhr.send(JSON.stringify(request));
        }
    
        if ('serviceWorker' in navigator) {
    
          navigator.serviceWorker.register(
            'service-worker.js',
            { scope: './' }
          ).then( function(registration) {
            if (navigator.serviceWorker.controller) {
                printStatus('The service worker is currently handling ' + 
                'network operations.');
                showRequest();
            } else {
                printStatus('Please reload this page to allow the ' + 'service worker to handle network operations.');
                  }
          }).catch(function(error) {
            document.getElementById('status').innerHTML = error;
          });
        } else {
                   document.getElementById('status').innerHTML = 'unavailable';
              }
      </script>
    </body>
    </html>
    
  2. 在与 index.html 文件相同的文件夹中创建一个名为 service-worker.js 的 JavaScript 文件,并包含以下代码:

    self.addEventListener('fetch', function(event) {
      console.log('Handling fetch event for', event.request.url);
      var requestUrl = new URL(event.request.url);
    
      if (requestUrl.pathname === '/urlshortener/v1/url' &&
          event.request.headers.has('X-Mock-Response')) {
    
        var response = {
          body: {
            kind: 'urlshortener#url',
            id: 'http://goo.gl/IKyjuU',
            longUrl: 'https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html'
          },
          init: {
            status: 200,
            statusText: 'OK',
            headers: {
              'Content-Type': 'application/json',
              'X-Mock-Response': 'yes'
            }
          }
        };
    
        var mockResponse = new Response(JSON.stringify(response.body),  
            response.init);
    
        console.log('Responding with a mock response body:', 
            response.body);
        event.respondWith(mockResponse);
      }
    });
    
  3. 将你的两个文件放置好,你可以导航到 GitHub 页面,https://username.github.io/service-workers/01/03/index.html,你将在浏览器中看到成功消息。如何操作...

它是如何工作的...

在服务工作者成功注册后,我们检查以确保它目前正在处理网络操作:

if (navigator.serviceWorker.controller) {
printStatus('The service worker is currently handling 
network operations.');
…
}

在这种情况下,我们调用 showRequest() 函数为 URL 缩短按钮添加事件监听器,并显示 request 块。否则,整个 request 块将被隐藏:

function showRequest() {
   document.getElementById('url-shorten-btn')
.addEventListener('click', sendRequest);
   document.getElementById('request').style.display = 'block';
}

sendRequest() 函数构建 HTTP 请求。它创建一个带有 Google API 短网址 URL 的 POST 请求:

xhr.open('POST',
        'https://www.googleapis.com/urlshortener/v1/url?' +
  'key=[Your API Key]');

你将需要为这个服务获取一个 API 密钥。为此,请按照以下说明操作:

  1. 访问 Google 开发者控制台页面 console.developers.google.com

  2. 你可以选择一个现有项目或创建一个新的项目。

  3. 在左侧侧边栏中展开 APIs & auth

  4. 点击 APIs。现在,在提供的 API 列表中,确保 Google URL Shortener API 的状态是 开启

  5. 最后,在左侧边栏中,选择 Credentials

如果 Mock Response 被勾选,将请求头 X-Mock-Response 设置为 yes

if (document.getElementById('mock-checkbox').checked) {
      xhr.setRequestHeader('X-Mock-Response', 'yes');
    }

现在添加一个事件监听器到 load 事件,并传递一个回调来分配响应数据到显示结果的链接:

xhr.addEventListener('load', function() {
var response = JSON.parse(xhr.response);
var el = document.getElementById('short-url');

   el.href = response.id;
   el.innerHTML = response.id;
});

sendRequest 函数的末尾,我们发送原始 URL 以及我们构建的 request 对象作为请求:

request = {
        longUrl: document.getElementById('long-url').value
      };
xhr.send(JSON.stringify(request));

service-worker.js 文件中,我们正在添加一个用于 fetch 事件的监听器。我们检查请求 URL 路径中包含 urlshortner,并且请求头有 X-Mock-Response

if (requestUrl.pathname === '/urlshortener/v1/url' &&
    event.request.headers.has('X-Mock-Response')) {
…
   }

我们构建一个包含正文、状态和头的模拟响应对象:

   var response = {
      body: {
        kind: 'urlshortener#url',
        id: 'https://goo.gl/KqR3lJ',
        longUrl: 'https://www.packtpub.com/books/info/packt/about'
      },
      init: {
        status: 200,
        statusText: 'OK',
        headers: {
          'Content-Type': 'application/json',
          'X-Mock-Response': 'yes'
        }
      }
    };

最后,我们使用模拟响应创建一个响应:

var mockResponse = new Response(
JSON.stringify(response.body), response.init);

console.log('Mock Response: ', response.body);
event.respondWith(mockResponse);

处理请求超时

长时间运行的请求可能是连接问题造成的。服务工作者是解决这些问题的理想解决方案。让我们看看我们如何使用服务工作者实现一个处理请求超时的解决方案。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考之前的配方:设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下配方:为 GitHub 页面设置 SSL为 Windows 设置 SSL,和为 Mac 设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 首先,我们需要创建一个如下的 index.html 文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Request Timeouts</title>
    </head>
    <body>
      <p>Registration status: <strong id="status"></strong></p>
    
      <script>
        if ('serviceWorker' in navigator) {
          navigator.serviceWorker.register(
            'service-worker.js',
            { scope: './' }
          ).then(function(serviceWorker) {
            document.getElementById('status').innerHTML = 'successful';
    });
        } else {
          document.getElementById('status').innerHTML = 'unavailable';
        }
      </script>
      <script src="img/jquery-2.2.0.js"></script>
    </body>
    </html>
    
  2. 在与 index.html 文件相同的文件夹中创建一个名为 service-worker.js 的 JavaScript 文件,并包含以下代码:

    function timeout(delay) {
        return new Promise(function(resolve, reject) {
            setTimeout(function() {
                resolve(new Response('', {
                    status: 408,
                    statusText: 'Request timed out.'
                }));
            }, delay);
        });
    }
    
    self.addEventListener('install', function(event) {
        self.skipWaiting();
    });
    
    self.addEventListener('activate', function(event) {
        if (self.clients && clients.claim) {
            clients.claim();
        }
    });
    
    self.addEventListener('fetch', function(event) {
      if (/\.js$/.test(event.request.url)) {
        event.respondWith(Promise.race([timeout(400), fetch(event.request.url)]));
      } else {
        event.respondWith(fetch(event.request));
      }
    });
    
  3. 将你的两个文件放置好,导航到 index.html 并打开开发者工具。你会在控制台看到超时错误记录。如何操作...

它是如何工作的...

在我们的 index.html 文件中,我们正在获取一个大型未压缩的 jQuery 库:

<script src="img/jquery-2.2.0.js"></script>

在我们的 service-worker.js 文件中,安装事件的监听器调用 skipWaiting() 方法,这强制等待中的服务工作者成为活动服务工作者:

self.addEventListener('install', function(event) {
    self.skipWaiting();
});

skipWaiting() 方法在活动事件处理器中使用,它反过来使用 Clients.claim() 确保对底层服务工作者的更新立即生效,对当前客户端和所有其他活动客户端都有效:

self.addEventListener('activate', function(event) {
    if (self.clients && clients.claim) {
        clients.claim();
    }
});

在 fetch 事件的监听器中,我们传递一个 Promise.race() 函数,其中第一个可迭代项 timeout(400) 首先解决:

self.addEventListener('fetch', function(event) {
  if (/\.js$/.test(event.request.url)) {
    event.respondWith(Promise.race([timeout(400), fetch(event.request.url)]));
  } else {
    event.respondWith(fetch(event.request));
  }
});

我们将在不久的将来详细说明 Promise.race() 函数。timeout() 函数返回一个 408 的 promise,这是请求超时状态的代码。

更多内容...

Promise.race()方法返回一个承诺,一旦可迭代中的任何一个承诺解决或拒绝,它就会解析或拒绝,并带有那个承诺的值或原因:

var p1 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 400, "one"); 
});
var p2 = new Promise(function(resolve, reject) { 
    setTimeout(reject, 100, "two");
});

Promise.race([p1, p2]).then(function(value) {
  // Not called              
}, function(reason) {
  console.log(reason); // "two"
  // p2 is faster, so it rejects
});

正如你所见,two更快,所以结果是reject

第二章. 与资源文件一起工作

在本章中,我们将涵盖以下主题:

  • 显示自定义离线页面

  • 离线加载图片

  • 离线加载 CSS

  • 离线加载字体

  • 实现多个 fetch 处理器

  • 获取远程资源

简介

你可能时不时地遇到过某些网站上出现损坏的图片。这可能是因为多种原因:图片可能不存在,可能没有正确命名,或者代码中的文件路径可能不正确。无论原因如何,它都可能影响你的网站,并可能导致用户认为你的网站已损坏。

图片并不是你网站中唯一必要的资源。层叠样式表CSS)、JavaScript 文件和字体文件也是使你的网站看起来功能齐全所必需的。在本章中,我们将探讨如何离线加载这些资源。

在我们开始处理离线加载资源之前,让我们了解一下如何通知用户网络不可用。

显示自定义离线页面

让我们回顾一下第一章中的场景,当时你正在火车上,从工作中回家,你正在使用你的移动设备阅读一篇重要的新闻文章。就在你点击链接查看更多详情的同时,火车突然消失在隧道中。你刚刚失去了连接,并显示无法连接到互联网的消息。好吧,如果你仍然可以通过在桌面/笔记本电脑上按空格键或在手机上轻触来玩恐龙游戏,你将不会那么烦恼。但是,这可以通过使用服务工作者在用户体验方面进行显著改进。服务工作者的一项伟大功能是它们允许你拦截网络请求并决定你想要如何响应:

显示自定义离线页面

在这个食谱中,我们将使用服务工作者来检查用户是否有连接性,如果他们没有连接,则显示一个非常简单的离线页面。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的设置服务工作者食谱,学习服务工作者基础。服务工作者仅在 HTTPS 上运行。要了解如何设置开发环境以支持此功能,请参考以下第一章的食谱:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

如何做到这一点...

按照以下说明设置你的文件结构:

  1. 首先,我们必须创建一个index.html文件,如下所示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Custom Offline Page</title>
    </head>
    <body>
      <p>Registration status: <strong id="status"></strong></p>
    
      <script>
        var scope = {
          scope: './'
        };
    
        if ('serviceWorker' in navigator) {
          navigator.serviceWorker.register('service-worker.js', scope)
          .then(
            function(serviceWorker) {
            document.getElementById('status').innerHTML = 'successful';
          }).catch(function(error) {
            document.getElementById('status').innerHTML = error;
          });
        } else {
            document.getElementById('status').innerHTML = 'unavailable';
          }
      </script>
    </body>
    </html>
    
  2. 在与index.html文件相同的文件夹中创建一个名为service-worker.js的 JavaScript 文件,并包含以下代码:

    'use strict';
    
    var version = 1;
    var currentCache = {
      offline: 'offline-cache' + version
    };
    
    var offlineUrl = 'offline.html';
    
    self.addEventListener('install', function(event) {
      event.waitUntil(
        caches.open(currentCache.offline).then(function(cache) {
          return cache.addAll([
            offlineUrl
          ]);
        })
      );
    });
    
    self.addEventListener('fetch', function(event) {
      var request = event.request,
        isRequestMethodGET = request.method === 'GET';
    
      if (request.mode === 'navigate' || isRequestMethodGET) {
        event.respondWith(
          fetch(createRequestWithCacheBusting(request.url)).catch(function(error) {
            console.log('OFFLINE: Returning offline page.', error);
            return caches.match(offlineUrl);
          })
        );
      } else {
        event.respondWith(caches.match(request)
            .then(function (response) {
            return response || fetch(request);
          })
        );
      }
    });
    function createRequestWithCacheBusting(url) {
      var request,
        cacheBustingUrl;
    
      request = new Request(url,
        {cache: 'reload'}
      );
    
      if ('cache' in request) {
        return request;
      }
    
      cacheBustingUrl = new URL(url, self.location.href);
      cacheBustingUrl.search += (cacheBustingUrl.search ? '&' : '') + 'cachebust=' + Date.now();
    
      return new Request(cacheBustingUrl);
    }
    
  3. 创建第二个 HTML 文件,命名为offline.html,如下所示:

    <!DOCTYPE html>
    <html>
     <head>
      <meta charset="UTF-8">
      <title>Offline</title>
      <style>
        #container {
          text-align: center;
          margin-top: 40px;
        }
        #container img {
          width: 80px;
          height: 80px;
        }
      </style>
     </head>
     <body>
       <div id="container">
         <svg  width="25" height="25" viewBox="0 0 25 25">
           <path d="M16 0l-3 9h9l-1.866 2h-14.4L16 0zm2.267 13h-14.4L2 15h9l-3 9 10.267-11z" fill="#04b8b8"/>
         </svg>
         <p>Whoops, something went wrong...!</p>
         <p>Your internet connection is not working.</p>
         <p>Please check your internet connection and try again.</p>
       <div>
      </body>
    </html>
    
  4. 打开一个浏览器并转到index.html。您将看到注册状态:成功的消息:如何操作...

  5. 现在打开开发者工具(Cmd + Alt + IF12),转到网络标签,点击显示无限制的下拉菜单,并选择离线如何操作...

  6. 现在刷新您的浏览器,您将看到离线消息和以下图像:如何操作...

工作原理...

当注册成功时,我们指示服务工作者使用 fetch 事件拦截请求,并从缓存内容中提供资源,如下面的图示所示:

工作原理...

index.html文件内部,当注册成功时,我们检查注册状态并将其打印到浏览器中。否则,我们打印由服务工作者返回的错误信息:

navigator.serviceWorker.register(
      'service-worker.js',
      { scope: './' }
   ).then(function(serviceWorker) {
      document.getElementById('status').innerHTML = 
          'successful';
   }).catch(function(error) {
      document.getElementById('status').innerHTML = error;
   });

服务工作者脚本文件将拦截网络请求,检查连接性,并向用户提供内容。

我们首先在安装服务工作者时将离线页面添加到缓存中。在前几行中,我们指定了缓存版本和离线页面的 URL。如果我们有不同的缓存版本,您只需更新这个版本号,这样文件的新版本就会生效。我们称之为缓存破坏

var version = 1;
var currentCache = {
  offline: 'offline-cache' + version
};

我们向 install 事件添加事件监听器,并在回调函数中请求这个离线页面及其资源;当我们收到成功的响应时,它会被添加到缓存中:

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(currentCache.offline)
    .then(function(cache) {
         return cache.addAll([
            offlineUrl
         ]);
    })
  );
});

现在离线页面已存储在缓存中,我们可以在需要时检索它。在同一个服务工作者中,我们需要添加逻辑以在没有连接性时返回离线页面:

self.addEventListener('fetch', function(event) {
  var request = event.request,
    isRequestMethodGET = request.method === 'GET';

  if (request.mode === 'navigate' || isRequestMethodGET) {
    event.respondWith(
      fetch(createRequestWithCacheBusting(request.url)).catch(function(error) {
        console.log('OFFLINE: Returning offline page.', error);
        return caches.match(offlineUrl);
      })
    );
  } else {
    event.respondWith(caches.match(request)
        .then(function (response) {
        return response || fetch(request);
      })
    );
  }
}); 

在前面的列表中,我们正在监听 fetch 事件,如果我们检测到用户正在尝试导航到另一个页面,并且在这个过程中出现错误,我们只需从缓存中返回离线页面。就这样,我们的离线页面开始工作。

更多内容...

waitUntil事件延长了 install 事件的寿命,直到所有缓存都已填充。换句话说,它延迟将安装中的工作者视为已安装,直到我们指定的所有资源都已缓存并且传递的 promise 成功解析。

我们看到了一个 HTML 文件和一个图像文件被缓存,然后在我们网站离线时被检索。我们还可以缓存其他资源,包括 CSS 和 JavaScript 文件:

caches.open(currentCache.offline)
.then(function(cache) {
    return cache.addAll([
        'offline.html',
        '/assets/css/style.css',
        '/assets/js/index.js'
    ]);
  })
);

参见

  • 第一章的详细注册服务工作者配方,学习服务工作者基础

  • 第一章的创建模拟响应配方,学习 Service Worker 基础知识

离线加载图片

图片是当今世界上几乎所有网站都使用的资源。就像你的 HTML、CSS 和 JavaScript 一样,你可以使用服务工作者缓存图片以便离线查看。在本章中,我们将探讨如何离线加载图片以及处理响应式图片。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的设置服务工作者配方,学习 Service Worker 基础知识。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下第一章的配方:设置 GitHub 页面以支持 SSL为 Windows 设置 SSL为 Mac 设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 首先,我们必须创建一个index.html文件,如下所示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Offline Images</title>
    </head>
    <body>
      <main>
        <p>Registration status: <strong id="status"></strong></p>
        <img src="img/packt-logo.png" alt="logo">
      <main>
      <script src="img/index.js"></script>
    </body>
    </html>
    
  2. 现在我们必须创建一个名为service-worker.js的 JavaScript 文件,并将其保存在与index.html文件相同的文件夹中,代码如下:

    'use strict';
    
    var version = 1;
    var cacheName = 'static-' + version;
    
    self.addEventListener('install', installHandler);
    self.addEventListener('fetch', fetchHandler);
    
    function installHandler(event) {
        event.waitUntil(
            caches.open(cacheName).then(function(cache) {
                return cache.addAll([
                  'index.html',
                  'packt-logo.png'
                ]);
            })
        );
    }
    
    event.respondWith(
      fetch(event.request).catch(function() {
        return caches.match(event.request);
      })
    );
    
  3. 在与index.html文件相同的文件夹中创建一个名为index.js的 JavaScript 文件,代码如下:

    'use strict';
    
    var scope = {
      scope: './'
    };
    
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('service-worker.js', scope
      ).then( function(serviceWorker) {
        printStatus('successful');
      }).catch(function(error) {
        printStatus(error);
      });
    } else {
      printStatus('unavailable');
    }
    
    function printStatus(status) {
      document.getElementById('status').innerHTML = status;
    }
    
  4. 下载一个图片文件并将其保存在与index.html文件相同的文件夹中。在这个例子中,我将其命名为packt-logo.png

  5. 打开浏览器并转到index.html文件:如何操作...

  6. 打开 Chrome 开发者工具(Cmd + Alt + IF12),选择网络选项卡,并点击离线如何操作...

  7. 通过按Cmd + RF5刷新页面,你会看到图片看起来和在线时一样。

它是如何工作的...

index.html文件中,我们通过img标签链接我们下载的图片:

<body>
  <p>Registration status: <strong id="status"></strong></p>
  <img src="img/packt-logo.png" alt="logo">
  <script src="img/index.js"></script>
</body>

在服务工作者脚本文件中,我们在安装服务工作者时将离线页面添加到缓存中。在前几行中,我们指定缓存版本和离线页面的 URL:

var version = 1;
var cacheName = 'static-' + version;

安装事件的监听器调用waitUntil函数,在那里我们缓存index.html和字体文件,在我们的例子中,是webfont-serif.woffcache.addAll函数接受一个要缓存的文件数组:

self.addEventListener('install', function(event) {
    event.waitUntil(
        caches.open(cacheName).then(function(cache) {
            return cache.addAll([
                'index.html',
                'packt-logo.png'
            ]);
        })
    );
});

当我们重新加载页面,在设置为离线后,fetch 事件被触发,从缓存中检索这两个文件,并将它们随响应发送:

self.addEventListener('fetch', function(event) {
    event.respondWith(caches.match(event.request));
});    

现在,页面将以在线时的样子显示。

更多内容...

如果我们按照移动优先策略开发我们的网站,拥有响应式图片将极大地受益。让我们看看我们如何实现这一点。

处理响应式图片

有许多方法可以启用图片的响应式行为。其中一种较老的方法(不推荐)是通过简单的脚本实现,但这会导致一些问题。首先,如果脚本确定要下载的图片,但脚本本身在指定的 HTML 图片下载之后才加载,你可能会最终下载两张图片。其次,如果你在 HTML 中没有指定任何图片,只想通过脚本加载定义的图片,那么对于禁用脚本的浏览器,你将没有任何图片。

因此,我们需要一种更好的方式来处理响应式图片。幸运的是,确实存在一种!推荐的方式是使用:

  • srcset

  • sizes

  • picture

srcset 属性

在我们探讨 srcset 实际使用方法之前,让我们先了解一些术语。

设备像素比

设备像素比是每 CSS 像素中的设备像素数量。有两个关键条件会影响设备像素比:

  • 设备的像素密度(每英寸物理像素的数量):高分辨率设备将具有更高的像素密度,因此,对于相同的缩放级别,与低分辨率设备相比,它将具有更高的设备像素比。例如:高端的 Lumia 950 手机比预算型的 Lumia 630 手机具有更高的分辨率,因此,在相同的缩放级别下,它将具有更高的设备像素比。

  • 浏览器的缩放级别:对于同一设备,更高的缩放级别意味着每 CSS 像素拥有更多的设备像素,因此设备像素比更高。例如,考虑这个图:设备像素比

当你在浏览器中放大(Ctrl + +)时,你的 div 的 CSS 像素数量保持不变,但占用的设备像素数量增加。因此,你每 CSS 像素拥有更多的设备像素。

当你希望根据设备像素比显示不同的图片(或通常,同一图片的不同资产)时,你可以选择基本的 srcset 实现:

<img src="img/image-src.png" srcset="image-src.png 1x, image-2x.png 2x" />

srcset 属性中的 x 描述符用于定义设备像素比:

  • 对于设备像素比为 1 的情况,将使用 image-src.png 图片。

  • 对于设备像素比为 2 的情况,将使用 image-2x.png 图片。

src 属性用于作为不支持 srcset 实现的浏览器的后备选项。

这效果很好。使用 x 描述符,你将在具有相似设备像素比的设备上始终获得相同的图片——即使这意味着你会在 13.5 英寸的笔记本电脑和 5 英寸的手机上获得相同的图片,这两者的设备像素比相同。

sizes 属性

当你希望在不同的屏幕尺寸上使用不同大小的图片(不同的高度和宽度)时,可以通过使用 sizes 属性以及 srcset 属性的 w 描述符来实现。

假设你希望图片的显示宽度为视口宽度的一半。你将输入:

<img src="img/image-src.png" sizes="50vw"
srcset="image-src.png 1x, image-2x.png 2x 400w">

picture 元素

正如我们在上一节中看到的,当您想要根据图片的渲染大小显示不同的图片时,会使用picture元素。picture元素是一个容器,它包含其他控制要下载的图片的元素:

<picture>
  <img src="img/image-src.png" sizes="50vw"srcset="image-src.png 1x, image-2x.png 2x 400w">
</picture>

在运行时,srcset属性或<picture>元素会选择最合适的图片资源,并执行网络请求。

如果您想在安装服务工作者步骤中缓存图片,您有几个选择:

  • 安装单个低分辨率版本的图片

  • 安装单个高分辨率版本的图片

为了保留内存,最好将图片数量限制在两到三张。

为了提高加载时间,您可能决定在安装时选择低分辨率版本,并在页面加载时尝试从网络检索更高分辨率的图片;然而,在高清图片失败的情况下,您可能会认为可以轻松地回退到低分辨率版本,但有一个问题。

假设我们有两个图片:

显示密度 宽度 高度
1x 400 400
2x 800 800

这里是srcset图片的标记:

<img src="img/image-src.png" srcset="image-src.png 1x, image-2x.png 2x" />

在一个2x显示的屏幕上,浏览器可以选择下载image-2x.png,如果我们离线,那么我们可以捕获这个请求,如果图片已缓存,则返回image-src.png图片代替。如果图片已缓存,浏览器可能期望一个考虑2x屏幕额外像素的图片,因此图片将显示为 200 x 200 像素,而不是 400 x 400 像素。唯一的解决办法是在图片上设置固定的宽度和高度:

<img src="img/image-src.png" srcset="image-src.png 1x, image-2x.png 2x"
style="width:400px; height: 400px;" />

我们可以采取相同的方法来处理srcset

未设置宽度和高度:

图片元素

设置高度和宽度:

图片元素

如果您想注销服务工作者,您可以前往 Chrome 的开发者工具栏,并在服务工作者部分点击注销按钮,如下面的截图所示:

图片元素

如果您想找出存储在缓存中的资源,您可以通过打开开发者工具并查看资源选项卡来做到:

图片元素

如果您使用 Firefox Nightly,您可以通过打开开发者工具并查看存储检查器来查看缓存:

图片元素

参见

  • 在第一章的详细注册服务工作者食谱,学习服务工作者基础

  • 在第一章的创建模拟响应食谱中,学习服务工作者基础

离线加载 CSS

CSS 对于构建你的网站和使其看起来功能齐全至关重要。正因为如此,如果你的网站离线且 CSS 不在缓存中,你的网站将看起来损坏。为了实现这一点,我们使用服务工作者缓存 CSS,并将这些 CSS 文件作为外部资源提供。在本教程中,我们将探讨如何离线加载 CSS。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的设置服务工作者教程,学习服务工作者基础。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的开发生态,请参考第一章的以下教程:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 首先,我们必须创建一个index.html文件,如下所示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Offline CSS</title>
      <link rel="stylesheet" href="style-2.css">
      <link rel="stylesheet" href="style-1.css">
    </head> 
    <body>
      <p>Registration status: <strong id="status"></strong></p>
    
      <script>
          var scope = {
          scope: './'
        };
    
        if ('serviceWorker' in navigator) {
          navigator.serviceWorker.register('service-worker.js', scope)
          .then(
            function(serviceWorker) {
            printStatus('successful');
          }).catch(function(error) {
            printStatus(error);
          });
        } else {
          printStatus('unavailable');
        }
    
        function printStatus(status) {
          document.getElementById('status').innerHTML = status;
        }  </script>
    </body>
    </html>
    
  2. 在与index.html文件相同的文件夹中创建一个名为service-worker.js的 JavaScript 文件,内容如下:

    var version = 1;
    var cacheName = 'static-' + version;
    
    self.addEventListener('install', installHandler);
    self.addEventListener('fetch', fetchHandler);
    
    function installHandler(event) {
        event.waitUntil(
            caches.open(cacheName).then(function(cache) {
                return cache.addAll([
                  'index.html',
                  'style-2.css'
                ]);
            })
        );
    }
    
    function fetchHandler(event) {
      if (/index/.test(event.request.url) || /style-2/.test(event.request.url)) {
        event.respondWith(caches.match(event.request));
      }
    }
    
  3. 在与index.html文件相同的文件夹中创建一个名为style-1.css的 CSS 文件,内容如下:

    body {
        background-color: lightgreen;
    }
    
  4. 在与index.html文件相同的文件夹中创建另一个名为style-2.css的 CSS 文件,内容如下:

    body {
        background-color: red;
    }
    
  5. 打开浏览器并访问index.html文件。你会看到背景颜色是绿色:如何操作...

  6. 现在打开开发者工具(Cmd + Shift + IF12)并将网络标签页切换到离线,如图所示:如何操作...

  7. 背景颜色现在是红色。

工作原理...

index.html文件的头部部分,我们链接了两个 CSS 文件:

<head>
  <meta charset="UTF-8">
  <title>Offline CSS</title>
  <link rel="stylesheet" href="style-2.css">
  <link rel="stylesheet" href="style-1.css">
</head>

在我们的样式表中,我们引用了 body 的相同 CSS 属性。

由于我们调用 CSS 文件的顺序,最后一个选择器在在线页面上生效,在我们的例子中,这是style-1.cssbody选择器:

body {
    background-color: lightgreen;
}

service-worker.js文件中,我们在安装服务工作者时将index.htmlstyle-2.css文件添加到缓存中。在前几行中,我们指定缓存版本和离线页面的 URL:

var version = 1;
var cacheName = 'static-' + version;

安装事件的监听器调用waitUntil函数,其中我们缓存index.html和 CSS 文件。cache.addAll函数接受一个要缓存的文件数组:

function installHandler(event) {
    event.waitUntil(
        caches.open(cacheName).then(function(cache) {
            return cache.addAll([
              'index.html',
              'style-2.css'
            ]);
        })
    );
}

当我们重新加载页面,在设置为离线后,fetch 事件被触发,从缓存中检索这两个文件,并将它们随响应一起发送:

self.addEventListener('fetch', function(event) {
    event.respondWith(caches.match(event.request));
});

现在,当我们刷新页面时,背景将变为红色,因为我们保存在缓存中的 CSS 文件这次将被应用到页面上。

离线加载字体

如果您的网站使用外部字体,例如开源网络字体,您可以使用服务工作者将其缓存以便离线查看。在本章中,我们将探讨如何离线加载字体。

准备工作

要开始使用服务工作者,您需要在浏览器设置中开启服务工作者实验功能。如果您还没有这样做,请参考第一章中的设置服务工作者配方,学习服务工作者基础。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下第一章中的配方:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

如何操作...

按照以下说明设置您的文件结构:

  1. 首先,我们必须创建一个如下所示的 index.html 文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Offline Fonts</title>
      <style>
        @font-face{
          font-family: 'MyWebFont';
          src: url('webfont-serif.woff') format('woff');
        }
        p { font-family: 'MyWebFont', Arial, sans-serif; }
      </style>
    </head>
    <body>
      <p>Registration status: <strong id="status"></strong></p>
    
      <script>
          var scope = {
          scope: './'
        };
    
        if ('serviceWorker' in navigator) {
          navigator.serviceWorker.register('service-worker.js', scope)
          .then(
            function(serviceWorker) {
            printStatus('successful');
          }).catch(function(error) {
            printStatus(error);
          });
        } else {
          printStatus('unavailable');
        }
    
        function printStatus(status) {
          document.getElementById('status').innerHTML = status;
        }  </script>
    </body>
    </html>
    
  2. 在与 index.html 文件相同的文件夹中创建一个名为 service-worker.js 的 JavaScript 文件,并包含以下代码:

    'use strict';
    
    var version = 1;
    var cacheName = 'static-' + version;
    
    self.addEventListener('install', installHandler);
    self.addEventListener('fetch', fetchHandler);
    
    function installHandler(event) {
        event.waitUntil(
            caches.open(cacheName).then(function(cache) {
                return cache.addAll([
                    'index.html',
                    'webfont-serif.woff'
                ]);
            })
        );
    }
    
    function fetchHandler(event) {
        event.respondWith(caches.match(event.request));
    }
    
  3. www.google.com/fonts 下载网络字体并将其保存在与 index.html 文件相同的文件夹中。如果您不确定如何操作,请参阅以下 更多内容... 部分。

  4. 打开浏览器并转到 index.html 文件:如何操作...

  5. 打开 Chrome 开发者工具(Cmd + Alt + I 或 Fb12),选择网络选项卡,并点击离线如何操作...

  6. 通过按 Cmd + RF5 刷新页面,您将看到字体看起来与在线时相同。

它是如何工作的...

index.html 文件的头部部分,我们在 style 标签内链接我们下载的字体文件:

<style>
   @font-face{
     font-family: 'MyWebFont';
     src: url('webfont-serif.woff') format('woff');
   }

   p { font-family: 'MyWebFont', Arial, sans-serif; }
</style>

@font-face 声明将指定一个名为 myWebFont 的字体,并指定可以找到该字体的 URL。在我们的例子中,它位于 index.html 文件相同的目录中。然后我们在段落声明中引用该字体作为 font-family 属性:

p { font-family: 'MyWebFont', Arial, sans-serif; }

在服务工作者脚本文件中,我们在安装服务工作者时将我们的离线页面添加到缓存中。在前几行中,我们指定缓存版本和离线页面的 URL:

var version = 1;
var cacheName = 'static-' + version;

安装事件的监听器调用 waitUntil 函数,在那里我们缓存 index.html 文件和字体文件;在我们的例子中是 webfont-serif.woffcache.addAll 函数接受要缓存的文件数组:

self.addEventListener('install', function(event) {
    event.waitUntil(
        caches.open(cacheName).then(function(cache) {
            return cache.addAll([
                'index.html',
                'webfont-serif.woff'
            ]);
        })
    );
});

当我们重新加载页面,在设置为离线后,fetch 事件被触发,从缓存中检索这两个文件,并将它们随响应发送:

self.addEventListener('fetch', function(event) {
    event.respondWith(caches.match(event.request));
});    

现在,页面将以在线时的样子显示。

更多内容...

为了从 Google 找到免费字体并下载它,请执行以下步骤:

  1. 请导航到www.google.com/fonts,搜索或浏览所需的字体,然后通过点击字体右侧的蓝色添加到收藏按钮将你喜欢的字体添加到你的收藏中:更多内容...

  2. 你的收藏将显示在屏幕底部。一旦有了,选择屏幕右上角的使用此样式复选框:更多内容...

  3. 现在,当你点击页面右上角的箭头时,它会弹出一个对话框。你可以选择第一个选项,它将文件作为 ZIP 文件下载:更多内容...

  4. 一旦解压文件,你将在文件夹内找到所需的字体。如果你想注销一个 service worker,你可以转到chrome://service-worker-internals并点击注销按钮:如何操作...

  5. 在 Firefox Nightly 中,你可以转到about:serviceworkers并点击注销按钮:更多内容...

相关内容

  • 在第一章的详细注册 Service Worker配方中,学习 Service Worker 基础知识

  • 在第一章的创建模拟响应配方中,学习 Service Worker 基础知识

实现多个 fetch 处理器

Service workers 可以处理多个 fetch 处理器,每个处理器拦截不同类型的请求。这个配方详细解释了如何通过实现单独的 fetch 处理器来处理不同类型的请求。

准备工作

要开始使用 service workers,你需要在浏览器设置中开启 service worker 实验功能。如果你还没有这样做,请参考第一章的设置 Service Workers配方,学习 Service Worker 基础知识。Service workers 仅在 HTTPS 上运行。要了解如何设置一个支持此功能的发展环境,请参考第一章的以下配方:设置 GitHub pages for SSL设置 Windows 的 SSL,和设置 Mac 的 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 首先,我们需要创建一个index.html文件,如下所示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Multiple Fetch</title>
    </head>
    <body>
      <p>Registration status: <strong id="status"></strong></p>
    
      <script>
        if ('serviceWorker' in navigator) {
          navigator.serviceWorker.register(
            'service-worker.js',
            { scope: './' }
          ).then( function(serviceWorker) {
            document.getElementById('status').innerHTML = 'successful';
          }).catch(function(error) {
            document.getElementById('status').innerHTML = error;
          });
        } else {
          document.getElementById('status').innerHTML = 'unavailable';
        }
      </script>
    </body>
    </html>
    
  2. 在与index.html文件相同的文件夹中创建一个名为service-worker.js的 JavaScript 文件,并包含以下代码:

    var cookFetchHandler = function(event) {
      console.log('DEBUG: Inside the /cook handler.');
      if (event.request.url.indexOf('/cook/') > 0) {
        event.respondWith(new Response('Fetch handler for /cook'));
      }
    };
    
    var cookBookFetchHandler = function(event) {
      console.log('DEBUG: Inside the /cook/book handler.');
      if (event.request.url.endsWith('/cook/book')) {
        event.respondWith(new Response('Fetch handler for /cook/book'));
      }
    };
    
    var fetchHandlers = [cookBookFetchHandler, cookFetchHandler];
    
    fetchHandlers.forEach(function(fetchHandler) {
      self.addEventListener('fetch', fetchHandler);
    });
    
  3. 打开浏览器并转到index.html文件。你会看到注册状态:成功的消息:如何操作...

  4. 通过在前面添加/cook/来更改 URL,如下所示:如何操作...

  5. 通过在前面添加/book来更改 URL,如下所示:如何操作...

它是如何工作的...

当注册成功时,我们检查注册状态,并将其打印到浏览器中。现在,是时候通过服务工作者触发响应了。在 service-worker.js 文件中,有两个注册的获取处理程序,cookFetchHandlercookBookFetchHandler

var cookFetchHandler = function(event) {
  console.log('DEBUG: Inside the /cook handler.');
  if (event.request.url.indexOf('/cook/') > 0) {
    event.respondWith(new Response('Fetch handler for /cook'));
  }
};

var cookBookFetchHandler = function(event) {
  console.log('DEBUG: Inside the /cook/book handler.');
  if (event.request.url.endsWith('/cook/book')) {
    event.respondWith(new Response('Fetch handler for /cook/book'));
  }
};

第一个处理程序,cookFetchHandler,拦截 URL 中任何位置的以 /cook 结尾的请求,并返回一个包含文字 Fetch handler for /cook 的新响应。

第二个处理程序,cookBookFetchHandler,拦截 URL 中任何位置的以 /cook/book 结尾的请求,并返回一个包含文字 Fetch handler for /cook/book 的新响应。

由于 cookBookFetchHandler 是首先注册的,当它拦截 /cook/book 请求时,它将首先有机会通过 event.respondWith() 返回响应。只有当第一个处理程序没有调用它时,第二个处理程序才会得到处理 event.respondWith() 的机会。

当发生获取事件时,它们将按注册顺序逐个调用。每当处理程序调用 event.respondWith() 时,其他注册的处理程序将不会运行:

var fetchHandlers = [cookBookFetchHandler, cookFetchHandler];

fetchHandlers.forEach(function(fetchHandler) {
  self.addEventListener('fetch', fetchHandler);
});

如果注册的任何获取处理程序都没有调用 event.respondWith(),浏览器将接管并执行一个正常的 HTTP 请求。这是没有服务工作者参与时的正常流程。

还有更多...

在每个获取处理程序内部,我们必须确保确定是否调用 event.respondWith() 的逻辑是同步执行的。简单的 if() 语句检查 event.request.url 是可以的。任何异步操作,例如执行 caches.match() 然后根据响应决定是否调用 event.respondWith(),都会触发竞争条件,你很可能会在控制台看到 event already responded to 错误。

如果你想注销服务工作者,你可以转到 Chrome 的开发者工具栏,并在 服务工作者 部分点击 注销 按钮,如图所示:

还有更多...

参见

  • 在第一章 学习服务工作者基础详细注册服务工作者 菜谱中,学习服务工作者基础

  • 在第一章 学习服务工作者基础创建模拟响应 菜谱中,学习服务工作者基础

获取远程资源

获取远程资源可以通过几种不同的方式完成。在这个菜谱中,我们将探讨两种获取远程资源的标准方式,以及了解如何使用服务工作者充当代理中间件。

准备工作

要开始使用服务工作者,您需要在浏览器设置中开启服务工作者实验功能。如果您还没有这样做,请参阅第一章 设置服务工作者 的配方,学习服务工作者基础知识。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参阅第一章 为 SSL 设置 GitHub 页面 的配方,学习服务工作者基础知识

如何操作...

按照以下说明设置你的文件结构:

  1. 首先,我们需要创建一个 index.html 文件,如下所示:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>Fetching Offline Resources</title>
      <style>
          .error {
            color: #FF0000;
          }
          .success {
            color: #00FF00;
          }
        </style>
      </head>
      <body>
        <section>
          <h1>Images</h1>
          <div id="https-acao-image"></div>
          <div id="https-image"></div>
          <div id="http-image"></div>
        </section>
    
        <section>
          <h1>HTTPS Fetch</h1>
          <div id="https-cors">
            <strong>https-cors</strong>
          </div>
          <div id="https-no-cors">
            <strong>https-no-cors</strong>
          </div>
          <div id="https-acao-cors">
            <strong>https-acao-cors</strong>
          </div>
          <div id="https-acao-no-cors">
            <strong>https-acao-no-cors</strong>
          </div>
          <div id="service-https-cors">
            <strong>service-https-cors</strong>
          </div>
          <div id="service-http-cors">
            <strong>service-http-cors</strong>
          </div>
          <div id="service-http-no-cors">
            <strong>service-http-no-cors</strong>
          </div>
        </section>
    
        <section>
          <h1>HTTP Fetch</h1>
          <div id="http-cors">
            <strong>http-cors</strong>
          </div>
          <div id="http-no-cors">
            <strong>http-no-cors</strong>
          </div>
        </section>
    
        <script src="img/index.js"></script>
      </body>
    </html>
    
  2. 在与 index.html 文件相同的文件夹中创建一个名为 index.js 的 JavaScript 文件,其中包含以下代码:

    'use strict';
    
    var protocols = {
    'https': 
    'https://dz13w8afd47il.cloudfront.net/sites/all/themes/packt_v4/images/packtlib-logo-dark.png',
    
      'https-acao': 
    'https://i942.photobucket.com/albums/ad261/szaranger/Packt/packt-logo.png',
    
      'http': 
    'http://seanamarasinghe.com/wp-content/uploads/2015/06/icon-128x128.jpg'
    };
    
    navigator.serviceWorker.getRegistration()
    .then(function(registration) {
      var fetchModes = ['cors', 'no-cors'];
    
      if (!registration || !navigator.serviceWorker.controller) {
        navigator.serviceWorker.register(
        './service-worker.js').then(function() {
            console.log('Service worker registered, 
            reloading the page');
            window.location.reload();
        });
      } else {
        console.log('Client is under service worker\s control');
    
        for (var protocol in protocols) {
          if (protocols.hasOwnProperty(protocol)) {
            buildImage(protocol, protocols[protocol]);
    
            for (var index = 0; index < fetchModes.length; index++) {
              var fetchMode = fetchModes[index],
                init = { method: 'GET',
                         mode: fetchMode,
                         cache: 'default' };
    
              fireRequest(fetchMode, protocol, init)();
            }
          }
        }
      }
    });
    
    function buildImage(protocol, url) {
      var element = protocol + '-image',
        image = document.createElement('img');
    
      image.src = url;
      document.getElementById(element).appendChild(image);
    }
    
    function fireRequest(fetchMode, protocol, init) {
      return function() {
        var section = protocol + '-' + fetchMode,
          url = protocols[protocol];
    
        fetch(url, init).then(function(response) {
          printSuccess(response, url, section);
        }).catch(function(error) {
          printError(error, url, section);
        });
    
        fetch('./proxy/' + url, init).then(function(response) {
          url = './proxy/' + url;
          printSuccess(response, url, section);
        }).catch(function(error) {
          section = 'service-' + section;
    
          console.log(section, 'ERROR: ', url, error);
          log(section, 'ERROR: ' + error, 'error');
        });
      };
    }
    
    function printSuccess(response, url, section) {
      if (response.ok) {
        console.log(section, 'SUCCESS: ', url, response);
        log(section, 'SUCCESS');
      } else {
        console.log(section, 'FAIL:', url, response);
        log(section, 'FAIL: response type: ' + response.type +
                     ', response status: ' + 
                     response.status, 'error');
      }
    }
    
    function printError(error, url, section) {
      console.log(section, 'ERROR: ', url, error);
      log(section, 'ERROR: ' + error, 'error');
    }
    
    function log(id, message, type) {
      var sectionElement = document.getElementById(id),
        logElement = document.createElement('p');
    
      if (type) {
        logElement.classList.add(type);
      }
      logElement.textContent = message;
      sectionElement.appendChild(logElement);
    }
    
  3. 在与 index.html 文件相同的文件夹中创建一个名为 service-worker.js 的 JavaScript 文件,其中包含以下代码:

    self.onfetch = function(event) {
      if (event.request.url.indexOf('proxy') > -1) {
        var init = { method: 'GET',
                     mode: event.request.mode,
                     cache: 'default' };
        var url = event.request.url.split('proxy/')[1];
        console.log('DEBUG: proxying', url);
        event.respondWith(fetch(url, init));
      } else {
        event.respondWith(fetch(event.request));
      }
    };
    
  4. 打开浏览器并转到 index.html 文件:如何操作...

工作原理...

index.js 文件的开始处,我们正在测试三种不同的协议来加载资源:

  • https:带有 Secure Socket LayerSSL)协议的 HTTP

  • https-acao:带有 Access-Control-Origin=* 标头的 SSL 协议

  • http:没有 SSL 的 HTTP

我们将使用三个不同的 URL,这些 URL 将被多次加载:

var protocols = {
  'https-acao':  
    'https://i942.photobucket.com/albums/ad261/szaranger/Packt/packt-logo.png',
  'https': 
    'https://dz13w8afd47il.cloudfront.net/sites/all/themes/packt_v4/images/packtlib-logo-dark.png',
  'http': 
    'http://seanamarasinghe.com/wp-content/uploads/2015/06/icon-128x128.jpg'
};

我们还使用两种不同的方法来获取资源,带有或没有 cors

var fetchModes = ['cors', 'no-cors'];

接下来,我们检查服务工作者是否已注册:

  navigator.serviceWorker.register(
'./service-worker.js').then(function() {
      console.log('Service worker registered, reloading the page');
      window.location.reload();
    });

如果情况不是这样,那么我们将注册它并重新加载页面以确保客户端处于服务工作者的控制之下:

for (var protocol in protocols) {
      if (protocols.hasOwnProperty(protocol)) {
        buildImage(protocol, protocols[protocol]);

        for (var i = 0; i < fetchModes.length; i++) {
          var fetchMode = fetchModes[i],
            init = { 
                 method: 'GET',
                 mode: fetchMode,
                 cache: 'default' 
            };

          fireRequest(fetchMode, protocol, init)();
        }
      }
}

for 循环遍历提供的 protocols 数组,为每个协议发出请求,使用每个 URL 构建一个 DOM 图像元素,并遍历 fetchModes 数组中的每个模式。

init 对象包含您想要应用于请求的任何自定义设置:

  • method:请求方法,例如,GETPOST

  • mode:您想要用于请求的模式,例如,corsno-corssame-origin

  • cache:您想要用于请求的缓存模式:defaultno-storereloadno-cacheforce-cacheonly-if-cached

buildImage 函数接受两个参数:protocolurl。它会动态创建一个图像元素,并将 URL 作为该图像的源。然后它继续将该图像添加到 DOM 树中,其中 ID 为 https-acao-imagehttps-imagehttp–image 之一。在此阶段,JavaScript 对 URL 的处理没有控制权;浏览器处理这些 URL:

function buildImage(protocol, url) {
  var element = protocol + '-image',
    image = document.createElement('img');

  image.src = url;
  document.getElementById(element).appendChild(image);
}

只有 HTTPS 请求的图像将被渲染,因为服务工作者仅支持 SSL 连接:

工作原理...

破损的图像是通过标准 HTTP 请求的,未能响应请求的图像。

其他通过 HTTP 发出的请求也未能成功交付,导致错误:

工作原理...

通过 SSL 进行的请求,带有**Access-Control-Origin=***头(访问控制允许源),将成功返回结果:

如何工作...

默认情况下,如果第三方 URL 不支持 CORS,则从该 URL 获取资源会失败。你可以向请求添加非 CORS 选项来克服这个问题,尽管这会导致一个不透明的响应,这意味着你无法判断响应是否成功:

如何工作...

fireRequest函数接受三个参数,fetchModeprotocolinit。这个函数反过来返回另一个函数,我们可以将其称为组合。我们首先从远程资源直接获取给定的资源:

 fetch(url, init).then(function(response) {
    printSuccess(response, url, section);
 }).catch(function(error) {
    printError(error, url, section);
 });

如果获取成功,我们将它打印到控制台,并在网页上记录。如果请求失败,我们也会这样做,只是打印error

我们还尝试使用服务工人的代理来获取资源,客户端将其识别为本地资源:

fetch('./proxy/' + url, init).then(function(response) {
    url = './proxy/' + url;
    printSuccess(response, url, section);
  }).catch(function(error) {
    section = 'service-' + section;

     console.log(section, 'ERROR: ', url, error);
    log(section, 'ERROR: ' + error, 'error');
   });

printSuccessprintError函数将响应记录到控制台,以及网页的 DOM:

function printSuccess(response, url, section) {
  if (response.ok) {
    console.log(section, 'SUCCESS: ', url, response);
    log(section, 'SUCCESS');
  } else {
    console.log(section, 'FAIL:', url, response);
    log(section, 'FAIL: response type: ' + response.type +
                 ', response status: ' + response.status, 'error');
  }
}

function printError(error, url, section) {
  console.log(section, 'ERROR: ', url, error);
  log(section, 'ERROR: ' + error, 'error');
}

辅助函数 log 通过 ID 查找 DOM 元素,并添加一个段落元素以及一个类属性,以描述消息的类型:

function log(id, message, type) {
  var type = type || 'success',
    sectionElement = document.getElementById(id),
    logElement = document.createElement('p');

  if (type) {
    logElement.classList.add(type);
  }
  logElement.textContent = message;
  sectionElement.appendChild(logElement);
}

index.html文件中,我们在head部分有样式声明:

<style>
.error {
     color: #FF0000;
   }
   .success {
     color: #00FF00;
   }
</style>

在我们的log()函数中,我们将未定义的类型设置为成功,这样当我们将其添加到classList时,它将显示绿色。错误类型将显示为之前声明的红色。

让我们转到我们的service-worker.js文件。在那里我们有onfetch事件处理程序,它在每次发生 fetch 事件时被触发。在这里,我们检查请求中是否有proxy/参数。如果有,则响应剩余的 URL 部分:

var url = event.request.url.split('proxy/')[1];
console.log('DEBUG: proxying', url);
event.respondWith(fetch(url, init));

否则,响应将使用完整的 URL 执行请求:

} else {
    event.respondWith(fetch(event.request));
}

还有更多...

让我们更详细地检查fetch()函数的默认值。

默认不包含凭据

当你使用 fetch 时,默认情况下,请求不会包含凭据,例如 cookies。如果你想包含凭据,你可以调用这个代替:

fetch(url, {
  credentials: 'include'
});

这种行为是有意为之,并且可以说是比 XHR 更复杂的默认行为(如果 URL 具有相同的源,则发送凭据,否则不发送)更好的选择。

Fetch 的行为更类似于其他 CORS 请求,例如<img crossorigin>,除非你通过<img crossorigin="use-credentials">选择加入,否则它永远不会发送 cookies。

默认情况下,非 CORS 请求会失败

默认情况下,如果第三方 URL 不支持 CORS,则从该 URL 获取资源会失败。你可以向Request函数添加非 CORS 选项来克服这个问题,尽管这会导致一个不透明的响应,这意味着你无法判断响应是否成功:

cache.addAll(urlsToPrefetch.map(function(urlToPrefetch) {
  return new Request(urlToPrefetch, { mode: 'no-cors' });
})).then(function() {
  console.log('All resources have been fetched and cached.');
});

如果你想要注销服务工作者,你可以前往 Chrome 中的 chrome://service-worker-internals 页面,并点击相关服务工作者的注销按钮,如下面的截图所示:

默认情况下非 CORS 失败

参见

  • 第一章, 详细注册服务工作者配方,学习服务工作者基础

  • 第一章, 创建模拟响应配方,学习服务工作者基础

第三章。访问离线内容

在本章中,我们将涵盖以下主题:

  • 缓存关键资源以供离线使用

  • 首先显示缓存内容

  • 实现缓存和网络竞态

  • 使用 window.caches

  • 实现陈旧数据验证

简介

您不需要网络访问,您的笔记本电脑或智能手机也能发挥作用。特别是在移动数据昂贵的地区,通过一些适当的规划,您可以下载某些应用程序,通过免费 Wi-Fi 进行同步,然后在其他地方离线使用。

如 Google Maps、FeedMe 和 Wikipedia 等移动应用程序为我们提供了离线应用程序,无论是否有互联网,都可以在任何地方使用。使我们的应用程序离线兼容是赢得客户芳心的好方法。

让我们从如何缓存关键资源以供离线使用开始这一章。

缓存关键资源以供离线使用

在这个食谱中,我们看看我们如何缓存一组关键资源,使用户能够离线使用,并给用户提供相同的使用体验。同时,我们将通知用户他们可以离线使用并继续使用相同的功能。

准备工作

要开始使用服务工作者,您需要在浏览器设置中开启服务工作者实验功能。如果您还没有这样做,请参考第一章的第一个食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章的以下食谱,学习服务工作者基础设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

如何操作...

按照以下说明设置您的文件结构。或者,您可以从以下位置下载文件:

github.com/szaranger/szaranger.github.io/tree/master/service-workers/03/01/

  1. 首先,我们必须创建一个如下所示的index.html文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Caching Critical Resources</title>
      <link rel="stylesheet" href="style.css">
    </head>
    <body>
      <section id="registration-status">
        <p>Registration status: <strong id="status"></strong></p>
        <input type="button" id="resetButton" value="Reset" />
      </section>
    
      <main>
        <section>
        <h1>Brand Game</h1>
        <p>Attempts: <span id="attempts">0</span></p>
        <select id="choice">
          <option value="0">Apple</option>
          <option value="1">Google</option>
          <option value="2">Adobe</option>
          <option value="3">Facebook</option>
          <option value="4">Amazon</option>
        </select>
        <input type="button" id="tryButton" value="Try" />
        </section>
        <section>
          <img src="img/" id="logo" data-image="0" />
          <p id="result">
        </section>
        <div id="notification" class="hidden">
          <p>Ready to go offline!</p>
        </div>
      </main>
    
      <script src="img/index.js"></script>
      <script src="img/game.js"></script>
    </body>
    </html>
    
  2. 在与index.html文件相同的文件夹中创建一个名为index.js的 JavaScript 文件,并包含以下代码:

    'use strict';
    
    var scope = {
      scope: './'
    };
    
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register(
        'service-worker.js',
        scope
      ).then( function(serviceWorker) {
        printStatus('successful');
      }).catch(function(error) {
        printStatus(error);
      });
    } else {
      printStatus('unavailable');
    }
    
    navigator.serviceWorker.addEventListener('controllerchange',
      function(event) {
        console.log('EVENT: controllerchange', event);
    
        navigator.serviceWorker.controller
          .addEventListener('statechange',
            function() {
              console.log('EVENT: statechange', this.state);
              if (this.state === 'activated') {
                document.querySelector('#notification')
    			.classList.remove('hidden');
              }
            }
          );
      }
    );
    
    function printStatus(status) {
      document.querySelector('#status').innerHTML = status;
    }
    
    document.querySelector('#resetButton').addEventListener('click',
      function() {
        navigator.serviceWorker.getRegistration().then(function(registration) {
          registration.unregister();
          window.location.reload();
        });
      }
    );
    
  3. 在与index.html文件相同的文件夹中创建一个名为game.js的 JavaScript 文件,并包含以下代码:

    'use strict';
    
    var attempts = 0,
      images = [
          'adobe',
          'apple',
          'google',
          'facebook',
          'amazon'
        ];
    
    document.getElementById('tryButton').addEventListener('click', function() {
      var imageElement = document.getElementById('logo'),
        choice = document.querySelector('#choice').value,
        attemptsEl = document.querySelector('#attempts'),
        result = document.querySelector('#result'),
        currentIndex = imageElement.getAttribute('data-image'),
        newIndex = getRandomIndex();
    
      do {
        newIndex = getRandomIndex();
      } while(newIndex === currentIndex);
    
      imageElement.src = images[newIndex] + '-logo.png';
      imageElement.setAttribute('data-image', newIndex);
    
      result.className = '';
      attempts++;
    
      if(newIndex == choice) {
        result.innerText = "Yay! Well done! You did it in " + attempts + " attempt(s)";
        result.classList.add('success');
        attemptsEl.innerText = attempts;
        attempts = 0;
      } else {
        result.innerText = "Boo! Try again..";
        result.classList.add('fail');
        attemptsEl.innerText = attempts;
      }
    
    });
    
    function getRandomIndex() {
      return Math.floor(Math.random() * 5);
    }
    
  4. 在与index.html文件相同的文件夹中创建一个名为service-worker.js的 JavaScript 文件,并包含以下代码:

    'use strict';
    var cacheName= 'dependencies-cache';
    
    self.addEventListener('install', function(event) {
      event.waitUntil(
        caches.open(cacheName)
          .then(function(cache) {
            return cache.addAll([
              'apple',
        'google',
        'adobe',
        'facebook',
        'amazon'
            ]);
          })
          .then(function() {
             return self.skipWaiting();
          })
      );
    });
    
    self.addEventListener('fetch', function(event) {
      event.respondWith(
        caches.match(event.request)
          .then(function(response) {
            if (response) {
              console.log('Fetching from the cache: ', event.request.url);
              return response;
            } else {
              console.log('Fetching from server: ', event.request.url);
            }
           return fetch(event.request);
         }
       )
     );
    });
    
    self.addEventListener('activate', function(event) {
       console.log('Activating the service worker!');
       event.waitUntil(self.clients.claim());
    });
    
  5. 在与index.html文件相同的文件夹中创建一个名为style.css的 CSS 文件,并包含以下代码:

    * {
      -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
    }
    
    body {
      margin: 0 auto;
      text-align: center;
      font-family: sans-serif;
    }
    
    main {
      max-width: 350px;
      border: 1px solid #4CAF50;
      padding: 20px;
      border-radius: 5px;
      width: 350px;
      margin: 20px auto;
    }
    
    h1 {
      color: #4CAF50;
    }
    
    img {
      padding: 20px 0;
      max-width: 200px;
    }
    
    .success {
      color: #4CAF50;
      font-size: 2em;
    }
    
    .fail {
      color: #FF8401;
      font-size: 1.5em;
    }
    
    .hidden {
      display: none;
    }
    
    #registration-status {
      background-color: #FFE454;
      padding: 10px;
    }
    
    #notification {
      background-color: #4CAF50;
      padding: 3px;
      border-radius: 5px;
      max-width: 350px;
      color: #FFF;
    }
    
  6. 打开浏览器并访问index.html文件:如何操作...

  7. 您将看到 Ready to go offline! 消息。这意味着我们可以离线玩游戏。现在打开 DevTools (Cmd + Alt + IF12),转到 Network 选项卡,点击显示 No throttling 的下拉菜单,并选择 Offline如何操作...

  8. 现在刷新您的浏览器,您将能够继续玩游戏。

  9. 您可以从下拉菜单中选择一个公司名称,然后点击 Try 按钮:如何操作...

  10. 每次选择不匹配结果时,都会显示一条消息说 Boo! Try again..,并且您将看到尝试次数:如何操作...

  11. 一旦您的选择匹配,您将获得一个带有尝试次数的成功消息,并且您将看到尝试次数:如何操作...

它是如何工作的...

我们的 index.html 文件包含下拉菜单、按钮和图像的结构。下拉菜单包含品牌的选项:

<select id="choice">
    <option value="0">Apple</option>
    <option value="1">Google</option>
    <option value="2">Adobe</option>
    <option value="3">Facebook</option>
    <option value="4">Amazon</option>
</select>

选项的值由数字指定,这些数字稍后会与图像匹配,因此顺序很重要。如您所见,它们从 0 开始,以符合我们将要存储公司名称的数组的 0 基索引。

style.css 文件包含我们页面所需的全部样式。前两个声明是所有元素和 body 元素的通用样式:

* {
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}

body {
  margin: 0 auto;
  text-align: center;
  font-family: sans-serif;
}

失败和成功消息的样式包含橙色和绿色:

.success {
  color: #4CAF50;
  font-size: 2em;
}

.fail {
  color: #FF8401;
  font-size: 1.5em;
}

通知消息最初是隐藏的。这是通过分配一个带有 display:none 类来实现的:

.hidden {
  display: none;
}

第三个 JavaScript 文件 index.js 执行服务工作者注册,然后监听 controllerchange 事件。它还处理重置按钮的事件。

我们的小游戏的引擎在 game.js 文件中。所以让我们看看这个文件内部发生了什么。

首先,我们在顶部声明了两个变量,attemptsimages,并赋予初始值;attempts 包含尝试的初始值,0,而 images 是一个数组常量,包含品牌名称的顺序。品牌的顺序很重要,以便与 index.html 文件中的下拉菜单相匹配:

var attempts = 0,
images = [
  'apple',
  'google',
  'adobe',
  'facebook',
  'amazon'
];

当用户点击 try 按钮,回调函数中的游戏逻辑被处理:

document.getElementById('tryButton').addEventListener('click', function() {
  // callback  
…
});

在回调处理程序中的声明部分有很多事情发生。所以让我们看看每个初始化:

  1. 首先,我们从 Document Object Model (DOM) 中获取标志:

    var imageElement = document.getElementById('logo'),
    
  2. 我们还从 DOM 中捕获了用户的 choiceattempts 元素,以及结果元素:

    choice = document.querySelector('#choice').value,
    attemptsEl = document.querySelector('#attempts'),
    result = document.querySelector('#result'),
    
  3. 然后我们从标志元素捕获数据属性,并使用 getRandomIndex() 生成一个随机数:

    currentIndex = imageElement.getAttribute('data-image'),
    newIndex = getRandomIndex();
    
  4. 我们为下一个索引生成一个随机数,只要它不是我们已为该索引拥有的一个:

    do {
        newIndex = getRandomIndex();
      } while(newIndex === currentIndex);
    
  5. 接下来,我们将品牌图像的源设置为随机索引创建的图像。然后我们将 data-image 属性设置为相同的索引:

    imageElement.src = images[newIndex] + '-logo.png';
    imageElement.setAttribute('data-image', newIndex);
    

    例如,这可能在我们的网页上创建一个如下所示的 HTML 元素:

    <img src="img/google-logo" data-image="1" />
    
  6. 在添加新的之前,我们确保结果元素的类名被清除。然后我们增加尝试次数:

    result.className = '';
    attempts++;
    
  7. 接下来,我们找出新的索引是否等于用户所做的选择。请注意,我们故意使用双等号而不是三等号,因为选择是一个字符串,而newIndex是一个整数:

    if(newIndex == choice) {
    

让我们继续到service-worker.js文件。在那里我们处理三个事件:install、fetch 和 activate。在 install 事件处理器中,我们缓存所有依赖项——我们需要离线运行的文件:

return cache.addAll([
          'adobe-logo.png',
          'apple-logo.png',
          'google-logo.png',
          'style.css',
          'index.html',
          'index.js',
          'style.css'
        ]);

在 fetch 处理器内部,我们检查资源是否在缓存中。如果是,则由缓存提供响应:

caches.match(event.req uest)
      .then(function(response) {
        if (response) {
          console.log('Fetching from the cache: ', event.request.url);
          return response;
        } 

否则,直接从服务器返回结果:

else {
          console.log('Fetching from server: ', event.request.url);
     }
     return fetch(event.request);

最后,我们通过调用claim()navigator.serviceWorker上强制触发一个controllerchange事件:

self.addEventListener('activate', function(event) {
   console.log('Activating the service worker!');
   event.waitUntil(self.clients.claim());
});

参见

  • 第一章的详细注册 Service Worker配方,学习 Service Worker 基础知识

  • 第二章的离线加载 CSS配方,处理资源文件

首先显示缓存内容

如果你经常访问某个网站,那么你很可能正在从你的缓存而不是从服务器本身加载大部分资源,例如 CSS 和 JavaScript 文件。这为我们节省了服务器必要的带宽,以及网络请求。控制我们从缓存和服务器提供的内容是一个巨大的优势。服务器工作者通过程序性地控制内容为我们提供了这个强大的功能。在这个配方中,我们将查看使我们能够通过创建性能艺术事件查看器 Web 应用程序来实现这一点的各种方法。

准备工作

要开始使用 Service Worker,你需要在浏览器设置中开启 Service Worker 实验功能。如果你还没有这样做,请参考第一章的第一篇配方,设置 Service Worker设置 Service Worker。Service Worker 仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章的以下配方:设置 GitHub pages for SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构。或者,你也可以从以下位置下载文件:

github.com/szaranger/szaranger.github.io/tree/master/service-workers/03/02/

  1. 首先,我们必须创建一个如下所示的index.html文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Cache First, then Network</title>
      <link rel="stylesheet" href="style.css">
    </head>
    <body>
      <section id="events">
        <h1><span class="nyc">NYC</span> Events TONIGHT</h1>
        <aside>
          <img src="img/hypecal.png" />
          <h2>Source</h2>
          <section>
            <h3>Network</h3>
            <input type="checkbox" name="network" id="network-disabled-checkbox">
            <label for="network">Disabled</label><br />
            <h3>Cache</h3>
            <input type="checkbox" name="cache" id="cache-disabled-checkbox">
            <label for="cache">Disabled</label><br />
          </section>
          <h2>Delay</h2>
          <section>
            <h3>Network</h3>
            <input type="text" name="network-delay" id="network-delay" value="400" /> ms
            <h3>Cache</h3>
            <input type="text" name="cache-delay" id="cache-delay" value="1000" /> ms
          </section>
        <input type="button" id="fetch-btn" value="FETCH" />
      </aside>
      <section class="data connection">
        <table>
          <tr>
            <td><strong>Network</strong></td>
            <td><output id='network-status'></output></td>
          </tr>
          <tr>
            <td><strong>Cache</strong></td>
            <td><output id='cache-status'></output><td>
          </tr>
        </table>
      </section>
      <section class="data detail">
        <output id="data"></output>
      </section>
      <script src="img/index.js"></script>
    </body>
    </html>
    
  2. 在与index.html文件相同的文件夹中创建一个名为style.css的 CSS 文件。你可以在 GitHub 的以下位置找到源代码:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/03/02/style.css

  3. 在与index.html文件相同的文件夹中创建一个名为index.js的 JavaScript 文件。你可以在 GitHub 的以下位置找到源代码:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/03/02/index.js

  4. 打开浏览器并转到index.html如何操作...

  5. 首先,我们正在启用缓存从网络请求数据。点击获取按钮:如何操作...

  6. 如果你再次点击获取,数据首先从缓存中检索,然后从网络中检索,所以你会看到重复的数据(注意最后一行与第一行相同):如何操作...

  7. 现在,我们将选择网络标签下的禁用复选框,并再次点击获取按钮,以便只从缓存中获取数据:如何操作...

  8. 选择网络标签下的禁用复选框,以及缓存标签,然后再次点击获取按钮:如何操作...

如何工作...

index.js文件中,我们为缓存设置一个特定于页面的名称,因为缓存是基于源的,并且没有其他页面应该使用相同的缓存名称:

var CACHE_NAME = 'cache-and-then-network';

如果你检查开发工具的资源标签页,你将在缓存存储标签页内找到缓存:

如何工作...

如果我们已经获取了网络数据,我们不希望缓存获取完成并覆盖我们从网络刚刚获取的数据。我们使用networkDataReceived标志让缓存获取回调知道网络获取是否已经完成:

var networkDataReceived = false;

我们在两个变量中存储网络和缓存的经过时间:

var networkFetchStartTime;
var cacheFetchStartTime;

源 URL,例如,是通过 RawGit 指向 GitHub 上的文件位置:

var SOURCE_URL = 'https://cdn.rawgit.com/szaranger/szaranger.github.io/master/service-workers/03/02/events';

如果你想要设置自己的源 URL,你可以通过在 GitHub 上创建一个 gist 或仓库,并创建一个包含你的数据的 JSON 格式的文件(不需要.json扩展名)轻松做到这一点。一旦你完成了这个,复制文件的 URL,转到rawgit.com,并将链接粘贴在那里以获得另一个带有内容类型头的链接,如下面的截图所示:

如何工作...

在我们按下获取按钮和接收到数据之间,我们必须确保用户不会更改搜索条件,或者再次按下获取按钮。为了处理这种情况,我们禁用控件:

function clear() {
  outlet.textContent = '';
  cacheStatus.textContent = '';
  networkStatus.textContent = '';
  networkDataReceived = false;
}

function disableEdit(enable) {
  fetchButton.disabled = enable;
  cacheDelayText.disabled = enable;
  cacheDisabledCheckbox.disabled = enable;
  networkDelayText.disabled = enable;
  networkDisabledCheckbox.disabled = enable;

  if(!enable) {
    clear();
  }
}

返回的数据将以行形式渲染到屏幕上:

function displayEvents(events) {

  events.forEach(function(event) {
    var tickets = event.ticket ?
      '<a href="' + event.ticket + '" class="tickets">Tickets</a>' : '';

    outlet.innerHTML = outlet.innerHTML +
      '<article>' +
      '<span class="date">' + formatDate(event.date) + '</span>' +
      ' <span class="title">' + event.title + '</span>' +
      ' <span class="venue"> - ' + event.venue + '</span> ' +
      tickets +
      '</article>';
  });

}

events数组中的每个项目都将作为行打印到屏幕上:

如何工作...

函数handleFetchComplete是缓存和网络的双重回调。

如果禁用复选框被勾选,我们通过抛出错误来模拟网络错误:

var shouldNetworkError = networkDisabledCheckbox.checked,
    cloned;

  if (shouldNetworkError) {
    throw new Error('Network error');
  }

由于请求体只能读取一次,我们必须克隆响应:

cloned = response.clone();

我们使用cache.put将克隆的响应放入缓存,作为键/值对。这有助于后续的缓存获取找到这些更新数据:

caches.open(CACHE_NAME).then(function(cache) {
   cache.put(SOURCE_URL, cloned); // cache.put(URL, response)
});

现在我们以 JSON 格式读取响应。同时,我们确保任何正在进行的缓存请求不会被我们刚刚接收到的数据覆盖,使用networkDataReceived标志:

response.json().then(function(data) {
    displayEvents(data);
    networkDataReceived = true;
  });

为了防止覆盖我们从网络接收到的数据,我们确保只有在网络请求尚未返回的情况下才更新页面:

result.json().then(function(data) {
    if (!networkDataReceived) {
      displayEvents(data);
    }
  });

当用户按下获取按钮时,他们会几乎同时向网络和缓存请求数据。这在现实世界的应用程序中是在页面加载时发生的,而不是由用户操作引起的:

fetchButton.addEventListener('click', function handleClick() {
...
}

我们首先在启动网络获取请求时禁用任何用户输入:

disableEdit(true);

networkStatus.textContent = 'Fetching events...';
networkFetchStartTime = Date.now();

我们使用带有缓存破坏 URL 的fetch API 请求数据,以及 Firefox 的 no-cache 选项,以支持尚未实现缓存选项的 Firefox:

networkFetch = fetch(SOURCE_URL + '?cacheBuster=' + now, {
   mode: 'cors',
   cache: 'no-cache',
   headers: headers
})

为了模拟网络延迟,我们在调用网络获取回调之前等待。在回调出错的情况下,我们必须确保拒绝我们从原始获取中收到的承诺:

return new Promise(function(resolve, reject) {
      setTimeout(function() {
        try {
          handleFetchComplete(response);
          resolve();
        } catch (err) {
          reject(err);
        }
      }, networkDelay);
    });

为了模拟缓存延迟,我们在调用缓存获取回调之前等待。如果回调出错,我们确保拒绝我们从原始调用中获得的承诺以匹配:

return new Promise(function(resolve, reject) {
        setTimeout(function() {
          try {
            handleCacheFetchComplete(response);
            resolve();
          } catch (err) {
            reject(err);
          }
        }, cacheDelay);
      });

formatDate函数是我们将接收到的响应中的日期格式转换为屏幕上更易读格式的辅助函数:

function formatDate(date) {
  var d = new Date(date),
      month = (d.getMonth() + 1).toString(),
      day = d.getDate().toString(),
      year = d.getFullYear();

  if (month.length < 2) month = '0' + month;
  if (day.length < 2) day = '0' + day;

  return [month, day, year].join('-');
}

如果你使用不同的日期格式,你可以根据你喜欢的格式在返回语句中调整数组的顺序:

实现缓存和网络竞争

如果你的客户端正在使用较旧且较慢的硬件,包括较旧的硬盘驱动器,那么从硬盘驱动器访问资源可能比从较快的互联网连接访问相同的资源要慢。但仅仅因为一些用户正在使用较慢的硬件,这并不能证明总是通过网络访问已经存在于硬件中的资源是合理的,因为一些用户可能拥有更快的硬件,这可能会浪费数据。为了解决这个问题,我们可以实现一个执行竞争条件的解决方案,并根据哪个先解决来获取数据:

准备就绪

要开始使用服务工作者,你需要在浏览器设置中打开服务工作者实验功能。如果你还没有这样做,请参阅第一章的第一个配方,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的开发生态,请参阅第一章的以下配方,学习服务工作者基础设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构。或者,你也可以从以下位置下载文件:

github.com/szaranger/szaranger.github.io/tree/master/service-workers/03/03/

  1. 首先,我们必须创建一个如下所示的index.html文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Cache &amp; Network Race</title>
      <link rel="stylesheet" href="style.css">
    </head>
    <body>
      <section id="registration-status">
        <p>Registration status: <strong id="status"></strong></p>
        <input type="button" id="resetButton" value="Reset" />
      </section>
      <script src="img/index.js"></script>
    </body>
    </html>
    
  2. 在与index.html文件相同的文件夹中创建一个名为style.css的 JavaScript 文件,并包含以下代码:

    * {
      -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
    }
    
    body {
      margin: 0 auto;
      text-align: center;
      font-family: sans-serif;
    }
    
    main {
      max-width: 350px;
      border: 1px solid #4CAF50;
      padding: 20px;
      border-radius: 5px;
      width: 350px;
      margin: 20px auto;
    }
    
    h1 {
      color: #4CAF50;
    }
    
    img {
      padding: 20px 0;
      max-width: 200px;
    }
    
    .hidden {
      display: none;
    }
    
    #registration-status {
      background-color: #FFE454;
      padding: 10px;
    }
    
  3. 在与index.html文件相同的文件夹中创建一个名为index.js的 JavaScript 文件,并包含以下代码:

    'use strict';
    
    var scope = {
      scope: './'
    };
    
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register(
        'service-worker.js',
        scope
      ).then( function(serviceWorker) {
        printStatus('successful');
      }).catch(function(error) {
        printStatus(error);
      });
    } else {
      printStatus('unavailable');
    }
    
    function printStatus(status) {
      document.querySelector('#status').innerHTML = status;
    }
    
    document.querySelector('#resetButton').addEventListener('click',
      function() {
        navigator.serviceWorker.getRegistration().then(function(registration) {
          registration.unregister();
          window.location.reload();
        });
      }
    );
    
  4. 在与index.html文件相同的文件夹中创建一个名为service-worker.js的 JavaScript 文件,并包含以下代码:

    'use strict';
    
    var cacheName = 'cache-network-race';
    
    self.addEventListener('install', function(event) {
      event.waitUntil(
        caches.open(cacheName)
          .then(function(cache) {
            return cache.addAll([
              'index.html',
              'style.css',
              'index.js'
            ]);
          })
          .then(function() {
            return self.skipWaiting();
          })
      );
    });
    
    self.addEventListener('fetch', function(event) {
      event.respondWith(
        resolveAny([
          caches.match(event.request),
          fetch(event.request)
        ])
      );
    });
    
    function resolveAny(promises) {
      return new Promise(function(resolve, reject) {
        promises = promises.map(function(promise) {
          return Promise.resolve(promise);
        });
    
        promises.forEach(function(promise) {
          promise.then(resolve);
        });
    
        promises.reduce(function(a, b) {
          return a.catch(function() {
            return b;
          });
        }).catch(function() {
          return reject(Error("All have failed"));
        });
      });
    }
    
  5. 打开浏览器并访问index.html文件:如何操作...

  6. 现在打开开发者工具(Cmd + Alt + IF12),转到网络标签,点击下拉菜单,并选择GPRS(50 kb/s)以模拟较慢的网络速度:如何操作...

  7. 刷新页面,你会看到相同的页面。但如果查看网络请求,你会发现服务工作者已经启动:如何操作...

它是如何工作的...

service-worker.js文件中,我们正在缓存必要的资源,以便我们可以在离线状态下仍然使用应用程序:

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheName)
      .then(function(cache) {
        return cache.addAll([
          'index.html',
          'style.css',
          'index.js'
        ]);
      })
      .then(function() {
        return self.skipWaiting();
      })
  );
});

然后我们创建一个名为resolveAny的函数。这个函数的目的是以适当的方式处理竞争条件。Promise 有一个名为race()的函数。这个函数对我们没有帮助,因为它在 Promise 在完成之前被拒绝时也会拒绝。

resolveAny函数返回一个新的 Promise。在 Promise 内部,我们确保我们传入的数组是一个 Promise 数组:

promises = promises.map(function(promise) {
      return Promise.resolve(promise);
    });

接下来,我们确保在数组中的另一个 Promise 解决后立即解决当前的 Promise:

promises.forEach(function(promise) {
      promise.then(resolve);
    });

我们还确保如果所有 Promise 都被拒绝,则拒绝:

promises.reduce(function(a, b) {
      return a.catch(function() {
        return b;
      });
    }

fetch调用的事件监听器的回调函数调用resolveAny函数,并传入两个函数,caches.match(event.request)fetch(event.request)。这两个函数发送相同的请求,导致竞争条件。

使用window.caches

在这个菜谱中,我们探讨如何在服务工作线程安装期间预取特定资源,以及如何使用 window.cache 来对缓存存储 API 发起请求,不是在服务工作线程的作用域内,而是在 HTML 文档的上下文中。

准备工作

要开始使用服务工作线程,你需要在浏览器设置中开启服务工作线程实验功能。如果你还没有这样做,请参考第一章的第一道菜谱 第一章:学习服务工作线程基础,学习服务工作线程基础设置服务工作线程。服务工作线程仅在 HTTPS 上运行。要了解如何设置开发环境以支持此功能,请参考第一章 第一章:学习服务工作线程基础 的以下菜谱:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,以及 在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构。或者,你也可以从以下位置下载文件:

github.com/szaranger/szaranger.github.io/tree/master/service-workers/03/04/

  1. 首先,我们必须创建一个 index.html 文件,如下所示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Using window.caches</title>
      <link rel="stylesheet" href="style.css">
    </head>
    <body>
      <section id="registration-status">
        <p>Registration status: <strong id="status"></strong></p>
        <input type="button" id="resetButton" value="Reset" />
      </section>
      <section id="article-area">
        <h1>Bookmark App</h1>
        <form action="#" method="post">
          <div>
            <label for="new-bookmark">+Add Bookmark</label>
            <input type="text" name="new-bookmark" id="new-bookmark" placeholder="new bookmark">
            <input type="submit" value="Add">
          </div>
        </form>
        <ul id="articles"></ul>
        <div id="bookmark-status"></div>
      </section>
      <script src="img/index.js"></script>
      <script src="img/app.js"></script>
    </body>
    </html>
    
  2. 在与 index.html 文件相同的文件夹中创建一个名为 style.css 的 CSS 文件。你可以在 GitHub 上找到源代码,位置如下:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/03/04/style.css

  3. 在与 index.html 文件相同的文件夹中创建一个名为 index.js 的 JavaScript 文件,内容如下:

    'use strict';
    
    var scope = {
      scope: './'
    };
    
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register(
        'service-worker.js',
        scope
      ).then( function(serviceWorker) {
        printStatus('successful');
      }).catch(function(error) {
        printStatus(error);
      });
    } else {
      printStatus('unavailable');
    }
    
    function printStatus(status) {
      document.querySelector('#status').innerHTML = status;
    }
    
    document.querySelector('#resetButton').addEventListener('click',
      function() {
        navigator.serviceWorker.getRegistration().then(function(registration) {
          registration.unregister();
          window.location.reload();
        });
      }
    );
    
  4. 在与 index.html 文件相同的文件夹中创建一个名为 app.js 的 JavaScript 文件。该文件的源代码可以在 GitHub 上找到,位置如下:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/03/04/app.js

  5. 创建一个名为 prefetched.html 的 HTML 文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Prefetched</title>
    </head>
    <body>
      <p>Prefetched Page</p>
    </body>
    </html>
    
  6. 在与 index.html 文件相同的文件夹中创建一个名为 service-worker.js 的 JavaScript 文件,内容如下:

    'use strict';
    
    var cacheVersion = 1;
    var currentCaches = {
      prefetch: 'window-cache-v' + cacheVersion
    };
    
    self.addEventListener('install', function(event) {
      var prefetchUrls = [
        './prefetched.html',
      ];
    
      console.log('EVENT: install. Prefetching resource:',
        prefetchUrls);
    
      event.waitUntil(
        caches.open(currentCaches.prefetch).then(function(cache) {
          return cache.addAll(prefetchUrls.map(function(prefetchUrl) {
            return new Request(prefetchUrl, {mode: 'no-cors'});
          })).then(function() {
            console.log('SUCCESS: All resources fetched and cached.');
          });
        }).catch(function(error) {
          console.error('FAIL: Prefetch:', error);
        })
      );
    });
    
    self.addEventListener('activate', function(event) {
      var expectedCacheNames = Object.keys(currentCaches).map(function(key) {
        return currentCaches[key];
      });
    
      event.waitUntil(
        caches.keys().then(function(cacheNames) {
          return Promise.all(
            cacheNames.map(function(cacheName) {
              if (expectedCacheNames.indexOf(cacheName) === -1) {
                console.log('DELETE: Out of date cache:', cacheName);
                return caches.delete(cacheName);
              }
            })
          );
        })
      );
    });
    
  7. 打开浏览器并访问 index.html 文件。你会看到一个预取的书签:如何操作...

  8. 通过输入 URL 并点击右侧的 添加 按钮来添加书签:如何操作...

  9. 你可以通过点击书签右侧的勾选图标来删除书签。

  10. 添加另一个书签并刷新页面。你会看到书签仍然完好无损。

如何工作...

在我们的 service-worker.js 文件中,我们维护一个缓存版本,以便通过增加 cacheVersion 值来使用新的缓存。当更新的服务工作线程启动时,旧缓存将作为激活事件处理程序的一部分被删除:

var cacheVersion = 1;
var currentCaches = {
  prefetch: 'window-cache-v' + cacheVersion
};

当你首次加载页面时,以下资源将显示为书签 URL,并且该事件将在你的开发者控制台中记录:

var prefetchUrls = [
    './prefetched.html',
  ];
console.log('EVENT: install. Prefetching resource:',
    prefetchUrls);

如果有可能会从不支持 CORS 的服务器获取资源,那么使用{mode: 'no-cors'}是非常重要的:

return new Request(prefetchUrl, {mode: 'no-cors'});

catch()方法处理caches.open()cache.addAll()步骤中的任何异常:

}).catch(function(error) {
    console.error('FAIL: Prefetch:', error);
})

在激活事件的处理器中,我们删除了所有不在currentCaches中命名的缓存。虽然在这个例子中只有一个缓存,但同样的逻辑也处理了存在多个版本化缓存的情况:

var expectedCacheNames = Object.keys(currentCaches).map(function(key) {
    return currentCaches[key];
  });

如果这个缓存名称不在“预期”的缓存名称数组中,那么就删除它:

if (expectedCacheNames.indexOf(cacheName) === -1) {
console.log('DELETE: Out of date cache:', cacheName);
      return caches.delete(cacheName);
}

让我们继续到app.js文件,大部分工作都在这里进行。initializeBookmarks函数将事件监听器附加到表单的提交按钮上。在提交的回调中,提取文本字段的值,然后根据它生成一个列表。然后,这个列表被附加到由index.html文件中文章的 ID 表示的无序列表。然后我们调用showBookmarks()函数:

function initializeBookmarks() {
  form.addEventListener('submit', function( event ) {
    var text = newBookmark.value;
    if (text !== '') {
      articles.innerHTML += '<li>' + text + '</li>';
      addUrlToCache(text);
      newBookmark.value = '';
      newBookmark.focus();
    }
    event.preventDefault();
  }, false);
  showBookmarks();
}

我们还向无序列表的文章列表添加了一个点击事件监听器。在回调内部,如果它是列表(li)元素,我们就移除该项。这就是我们从列表中移除文章的方式:

articles.addEventListener( 'click', function( event ) {
  var target = event.target;
  if ( target.tagName === 'LI' ) {
    target.parentNode.removeChild( target );
  };

  event.preventDefault();
}, false);

showBookmarks()函数中,我们清除任何之前的 URL,以防这个函数在向缓存添加新 URL 之后被调用:

while (articles.firstChild) {
   articles.removeChild(articles.firstChild);
}

然后,我们遍历所有可用的缓存,并对每个缓存遍历所有 URL,将每个缓存添加到书签列表中:

window.caches.keys().then(function(cacheNames) {
    cacheNames.forEach(function(cacheName) {
      window.caches.open(cacheName).then(function(cache) {
        cache.keys().then(function(requests) {
          requests.forEach(function(request) {
            addRequestToBookmarks(cacheName, request);
          });
        });
      });
    });
  });

现在,让我们看看实际使用window.fetch()从网络获取响应并将其存储在命名缓存中的函数。这里重要的是,控制此页面的服务工作者没有 fetch 事件处理器,因此这个请求是在没有服务工作者参与的情况下发出的:

function addUrlToCache(url) {
  window.fetch(url, { mode: 'no-cors' }).then(function(response) {
    if (response.status < 400) {
      caches.open(cacheName).then(function(cache) {
        cache.put(url, response).then(showBookmarks);
      });
    }
  }).catch(function(error) {
    document.querySelector('#status').textContent = error;
  });
}

addRequestToBookmarks()函数是一个辅助函数,用于将缓存的请求添加到缓存的列表中。在这个函数中,我们创建一个 span、一个按钮和一个列表项,并将它们附加到无序列表的文章列表中:

function addRequestToBookmarks(cacheName, request) {
  var url = request.url,
    span = document.createElement('span'),
    button = document.createElement('button'),
    li = document.createElement('li');

  span.textContent = url;
  button.textContent = '✔';
  button.dataset.url = url;
  button.dataset.cacheName = cacheName;
  button.classList.add('done');
  button.addEventListener('click', function() {
    removeCachedBookmark(this.dataset.cacheName, this.dataset.url).then(function() {
      var parent = this.parentNode,
        grandParent = parent.parentNode;
        grandParent.removeChild(parent);
    }.bind(this));
  });
  li.appendChild(span);
  li.appendChild(button);
  articles.appendChild(li);
}

接下来,removeCachedBookmark()函数通过给定的缓存名称和 URL 删除缓存条目:

function removeCachedBookmark(cacheName, url) {
  return window.caches.open(cacheName).then(function(cache) {
    return cache.delete(url);
  });
}

waitUntilInstalled()辅助函数返回一个承诺,一旦服务工作者注册通过installing状态,该承诺就会被解决:

function waitUntilInstalled(registration) {
  return new Promise(function(resolve, reject) {
    if (registration.installing) {

如果当前注册的服务工作者是installing状态,那么我们确保等待安装步骤完成,其中资源被预获取,然后显示书签列表:

registration.installing.addEventListener('statechange', function(event) {
   if (event.target.state === 'installed') {
   	resolve();
   } else if(event.target.state === 'redundant') {
   	reject();
   }
});

如果不是这种情况,并且这不是installing服务工作者,那么我们可以安全地假设安装是在访问当前页面的前一次访问期间完成的,并且资源已经被预获取。因此,我们现在可以立即显示书签列表:

  } else {
     resolve();
  }

实现缓存失效并重新验证

有时,对于网页中某些图像等资源来说,拥有最新版本的缓存并不是绝对必要的。如果可用,我们可以使用缓存的版本,并在下次获取更新。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置开发环境以支持此功能,请参考第一章的以下食谱,学习服务工作者基础为 SSL 设置 GitHub 页面为 Windows 设置 SSL,以及为 Mac 设置 SSL

如何操作...

按照以下说明设置你的文件结构。或者,你也可以从以下位置下载文件:

github.com/szaranger/szaranger.github.io/tree/master/service-workers/03/05/

  1. 首先,我们必须创建一个index.html文件,如下所示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Cache First, then Network</title>
      <link rel="stylesheet" href="style.css">
    </head>
    <body>
      <section id="registration-status">
        <p>Registration status: <strong id="status"></strong></p>
        <input type="button" id="resetButton" value="Reset" />
      </section>
      <section>
        <img src="img/adobe-logo" alt="adobe logo">
      </section>
      <script src="img/index.js"></script>
    </body>
    </html>
    
  2. 在与index.html文件相同的文件夹中创建一个名为index.js的 JavaScript 文件,并包含以下代码:

    'use strict';
    
    var scope = {
      scope: './'
    };
    
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register(
        'service-worker.js',
        scope
      ).then( function(serviceWorker) {
        printStatus('successful');
      }).catch(function(error) {
        printStatus(error);
      });
    } else {
      printStatus('unavailable');
    }
    
    function printStatus(status) {
      document.querySelector('#status').innerHTML = status;
    }
    
    document.querySelector('#resetButton').addEventListener('click',
      function() {
        navigator.serviceWorker.getRegistration().then(function(registration) {
          registration.unregister();
          window.location.reload();
        });
      }
    );
    
  3. 在与index.html文件相同的文件夹中创建一个名为service-worker.js的 JavaScript 文件,并包含以下代码:

    'use strict';
    
    var cacheName= 'stale-while-revalidate';
    
    self.addEventListener('install', function(event) {
      event.waitUntil(
        caches.open(cacheName)
          .then(function(cache) {
            return cache.addAll([
              'adobe-logo.png',
              'style.css',
              'index.html',
              'index.js',
              'style.css'
            ]);
          })
          .then(function() {
            return self.skipWaiting();
          })
      );
    });
    
    self.addEventListener('fetch', function(event) {
      event.respondWith(
        caches.open('stale-while-revalidate')
          .then(function(cache) {
            return cache.match(event.request)
              .then(function(response) {
                var promise;
    
                if (response) {
                  console.log('Fetching from the cache: ', event.request.url);
                } else {
                  console.log('Fetching from server: ', event.request.url);
                }
    
                promise = fetch(event.request)
                  .then(function(networkResponse) {
                    var cloned = networkResponse.clone();
                    cache.put(event.request, cloned);
                    console.log('Fetching from the cache: ', event.request.url);
                    return networkResponse;
                  }
                )
                console.log('Fetching from server: ', event.request.url);
                return response || promise;
              }
            )
          }
        )
      );
    });
    
    self.addEventListener('activate', function(event) {
       console.log('Activating the service worker!');
       event.waitUntil(self.clients.claim());
    });
    
  4. 从源代码下载adobe-log.png图像,或者使用与index.html文件相同的文件夹中的自己的图像。

  5. 打开浏览器并访问index.html。你会看到注册状态:成功的消息和标志:如何操作...

  6. 现在,如果你刷新页面并检查开发者工具的控制台标签页,你将能够看到adobe-logo.png文件已经被从缓存中获取。

它是如何工作的...

在我们的service-worker.js文件中,我们确保如果可用,则使用缓存版本而不是网络请求,但下次获取更新:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open('stale-while-revalidate')
      .then(function(cache) {
        return cache.match(event.request)
          .then(function(response) {
            var promise;

            if (response) {
              console.log('Fetching from the cache: ', event.request.url);
            } else {
              console.log('Fetching from server: ', event.request.url);
            }
            promise = fetch(event.request)
              .then(function(networkResponse) {
                var cloned = networkResponse.clone();
                cache.put(event.request, cloned);
                console.log('Fetching from the cache: ', event.request.url);
                return networkResponse;
              }
            )
            console.log('Fetching from server: ', event.request.url);
            return response || promise;
          }
        )
      }
    )
  );
});

第四章. 使用高级技术访问离线内容

在本章中,我们将涵盖以下主题:

  • 模板化

  • 实现读取缓存

  • 允许离线 Google Analytics

  • 允许离线用户交互

  • 实现选择性缓存

简介

在本章中,我们将继续改进使用服务工作者处理离线内容的经验。

我们将探讨高级技术,包括如何使用模板引擎进行模板化,深入 Google Analytics,如何解决离线用户交互问题,以及实现选择性缓存。

让我们从本章开始,通过实验服务工作者的模板化来开始。

模板化

传统的服务器端页面渲染已成为现代单页应用SPA)的过去式。尽管服务器端渲染更快,但使用服务工作者实现状态数据将变得困难。相反,我们可以请求 JSON 数据和模板,允许服务工作者接收数据和模板,并渲染响应页面。JavaScript 模板化是一种客户端数据绑定方法,使用 JavaScript 语言实现。

为了了解更多关于模板化的信息,请参考以下链接:

en.wikipedia.org/wiki/JavaScript_templating

准备工作

要开始使用服务工作者,您需要在浏览器设置中开启服务工作者实验功能。如果您还没有这样做,请参考第一章的第一个食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下食谱:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

如何操作...

按照以下说明设置您的文件结构。或者,您可以从以下位置下载文件:

github.com/szaranger/szaranger.github.io/tree/master/service-workers/04/01/

  1. 首先,我们必须创建一个如下所示的index.html文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Templating</title>
      <style>
      * {
        -webkit-box-sizing: border-box;
        -moz-box-sizing: border-box;
        box-sizing: border-box;
      }
    
      body {
        margin: 0 auto;
        text-align: center;
        font-family: sans-serif;
      }
    
      main {
        max-width: 350px;
        border: 1px solid #4CAF50;
        padding: 20px;
        border-radius: 5px;
        width: 350px;
        margin: 20px auto;
      }
    
      h1 {
        color: #4CAF50;
      }
    
      img {
        padding: 20px 0;
        max-width: 200px;
      }
    
      .hidden {
        display: none;
      }
    
      .frameworks {
        margin: 20px auto;
      }
    
      table, th, td {
        border: 1px solid black;
        border-collapse: collapse;
      }
    
      .frameworks th {
        background-color: #000;
        color: #FFF;
        padding: 3px 10px;
      }
    
      .frameworks tr {
        text-align: left;
      }
    
      .frameworks td {
        background-color: #FFF;
        padding: 3px 10px;
      }
    
      #registration-status {
        background-color: #FFE454;
        padding: 10px;
      }
      </style>
    </head>
    <body>
      <section id="registration-status">
        <p>Registration status: <strong id="status"></strong></p>
        <input type="button" id="resetButton" value="Reset" />
      </section>
      <section>
          <h2>JS Frameworks & Creators</h2>
          <table class="frameworks">
            <tr>
              <th>Framework</th>
              <th>Name</th>
              <th>Twitter</th>
            </tr>
            {{#users}}
              <tr>
                  <td>{{framework}}</td>
                  <td>{{person.firstName}} 
    			  {{person.lastName}}</td>
                  <td><a href="https://twitter.com/{{twitter}}">@{{twitter}}</a></td>
              </tr>
            {{/users}}
          </table>
      </section>
    
      <script>
      'use strict';
    
      var scope = {
        scope: './'
      };
    
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register(
          'service-worker.js',
          scope
        ).then( function(serviceWorker) {
          printStatus('successful');
        }).catch(function(error) {
          printStatus(error);
        });
      } else {
        printStatus('unavailable');
      }
    
      function printStatus(status) {
        document.querySelector('#status').innerHTML = status;
      }
    
      document.querySelector('#resetButton').addEventListener('click',
        function() {
          navigator.serviceWorker.getRegistration().then(function(registration) {
            registration.unregister();
            window.location.reload();
          });
        }
      );
      </script>
    </body>
    </html>
    
  2. 在与index.html文件相同的文件夹中创建一个名为service-worker.js的 JavaScript 文件,包含以下代码:

    'use strict';
    
    importScripts('handlebars.js');
    
    var cacheName= 'template-cache';
    
    self.addEventListener('install', function(event) {
      event.waitUntil(
        caches.open(cacheName)
          .then(function(cache) {
            return cache.addAll([
              'index.html',
              'service-worker.js',
              'people.json'
            ]);
          })
          .then(function() {
            return self.skipWaiting();
          })
      );
    });
    
    self.addEventListener('fetch', function(event) {
      var requestURL = new URL(event.request.url);
    
      event.respondWith(
        Promise.all([
          caches.match('index.html').then(function(res) {
            if(res) {
              return res.text();
            }
          }),
          caches.match('people.json').then(function(res) {
            return res.json();
          })
        ]).then(function(resps) {
          var template = resps[0],
            data = resps[1],
            renderTemplate = Handlebars.compile(template);
    
          return new Response(renderTemplate(data), {
            headers: {
              'Content-Type': 'text/html'
            }
          });
        })
      );
    });
    
  3. 在与index.html文件相同的文件夹中创建一个名为people.js的 JSON 文件,包含以下代码:

    {
      "users":[
        {
          "framework": "Ember",
          "person":{
            "firstName": "Yehuda",
            "lastName": "Katz"
          },
          "twitter": "wycats"
        },
        {
          "framework": "React",
          "person":{
            "firstName": "Jordan",
            "lastName": "Walke"
          },
          "twitter": "jordwalke"
        },
        {
          "framework": "Angular",
          "person":{
            "firstName": "Miško",
            "lastName": "Hevery"
          },
          "twitter": "mhevery"
        }
      ]
    }
    
  4. handlebarsjs.com/installation.html下载 handlebars 库,并将其保存为与index.html文件相同的目录下的handlebars.js,如图所示:如何操作...

  5. 打开浏览器并转到index.html文件:如何操作...

工作原理...

在这个例子中,我们选择使用 handlebars 作为模板引擎。

服务工人的模板是 index.html 文件本身。我们在表格中使用双大括号,Handlebars 语法:

<table class="frameworks">
    <tr>
    <th>Framework</th>
       <th>Name</th>
       <th>Twitter</th>
    </tr>
    {{#users}}
       <tr>
          <td>{{framework}}</td>
          <td>{{person.firstName}} {{person.lastName}}</td>
          <td><a href="https://twitter.com/{{twitter}}">
		  @{{twitter}}</a></td>
          </tr>
        {{/users}}
</table>

值是从我们输入模板的 JSON 文件中读取的。{{#users}} 标签是一个数组,它就像一个循环,通过用相关值替换占位符,如 {{twitter}},将 users 属性中的内容打印到屏幕上。

people.json 文件包含我们模板所需的数据。users 属性包含用户数组:

{
  "users":[
    {
      "framework": "Ember",
      "person":{
        "firstName": "Yehuda",
        "lastName": "Katz"
      },
      "twitter": "wycats"
    },
    {
      "framework": "React",
      "person":{
        "firstName": "Jordan",
        "lastName": "Walke"
      },
      "twitter": "jordwalke"
    },
    {
      "framework": "Angular",
      "person":{
        "firstName": "Miško",
        "lastName": "Hevery"
      },
      "twitter": "mhevery"
    }
  ]
}

让我们继续到 service-worker.js 文件。在那里我们处理两个事件:安装和获取。在安装事件处理器中,我们正在缓存所有依赖项:

return cache.addAll([
          'index.html',
          'style.css',
          'service-worker.js',
          'people.json'
        ]);

在获取处理器内部,我们检查获取请求是否是模板,在我们的例子中是 index.html 文件,并以文本格式发送响应:

caches.match('index.html').then(function(response) {
        if(response) {
          return response.text();
        }
      }),

如果获取请求是 JSON 文件,我们以 JSON 格式返回结果:

   caches.match('people.json').then(function(response) {
        return response.json();
      })

最后,我们使用 JSON 数据渲染模板,然后发送带有标题的响应:

]).then(function(resps) {
      var template = resps[0],
        data = resps[1],
        renderTemplate = Handlebars.compile(template);

      return new Response(renderTemplate(data), {
        headers: {
          'Content-Type': 'text/html'
        }
      });
    })

Handlebars.compile() 函数接受一个模板并返回一个函数,该函数可以进一步接受数据并渲染输出。

参见

  • 在第一章 学习服务工人基础详细注册服务工人 菜谱中,学习服务工人基础

实现读取缓存

读取缓存是一种对所有经常访问的静态内容类型进行全面缓存的方法。这并不适合动态内容,如新闻或体育。在这种情况下,选择性的缓存方法会更好。读取缓存为我们节省了服务器必要的带宽,以及网络请求。读取缓存的工作方式是,在服务工人接管你的页面后,当第一次调用 fetch() 请求时,响应将被缓存,并且对同一 URL 的后续请求将来自缓存。

准备工作

要开始使用服务工人,你需要在浏览器设置中开启服务工人实验功能。如果你还没有这样做,请参考第一章 学习服务工人基础 的第一个菜谱:设置服务工人。服务工人仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下第一章 学习服务工人基础 的菜谱:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL在 Mac 上设置 SSL

如何做到这一点...

按照说明设置你的文件结构。或者,你也可以从以下位置下载文件:

github.com/szaranger/szaranger.github.io/tree/master/service-workers/04/02/

  1. 首先,我们必须创建一个如下的index.html文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Read-through Caching</title>
      <link rel="stylesheet" href="style.css">
    </head>
    <body>
      <section id="registration-status">
        <p>Registration status: <strong id="status"></strong></p>
        <input type="button" id="resetButton" value="Reset" />
      </section>
      <section</section>
      <script src="img/index.js"></script>
    </body>
    </html>
    
  2. 在与index.html文件相同的文件夹中创建一个名为style.css的 CSS 文件:

    * {
      -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
    }
    
    body {
      margin: 0 auto;
      text-align: center;
      font-family: sans-serif;
    }
    
    main {
      max-width: 350px;
      border: 1px solid #4CAF50;
      padding: 20px;
      border-radius: 5px;
      width: 350px;
      margin: 20px auto;
    }
    
    h1 {
      color: #4CAF50;
    }
    
    img {
      padding: 20px 0;
      max-width: 200px;
    }
    
    .hidden {
      display: none;
    }
    
    #registration-status {
      background-color: #FFE454;
      padding: 10px;
    }
    
  3. 在与index.html文件相同的文件夹中创建一个名为index.js的 JavaScript 文件:

    'use strict';
    
    var scope = {
      scope: './'
    };
    
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register(
        'service-worker.js',
        scope
      ).then( function(serviceWorker) {
        printStatus('successful');
      }).catch(function(error) {
        printStatus(error);
      });
    } else {
      printStatus('unavailable');
    }
    
    function printStatus(status) {
      document.querySelector('#status').innerHTML = status;
    }
    
    document.querySelector('#resetButton').addEventListener('click',
      function() {
        navigator.serviceWorker.getRegistration().then(function(registration) {
          registration.unregister();
          window.location.reload();
        });
      }
    );
    
  4. 在与index.html文件相同的文件夹中创建一个名为service-worker.js的 JavaScript 文件:

    'use strict';
    
    var version = 1,
      currentCaches= { readThrough : 'version-' + version },
      NOT_FOUND = -1,
      ERROR_RESPONSE = 400;
    
    self.addEventListener('activate', function(event) {
      var expectingCacheNames = Object.keys(currentCaches).map(function(key) {
        return currentCaches[key];
      });
    
      event.waitUntil(
        caches.keys().then(function(cacheNames) {
          return Promise.all(
            cacheNames.map(function(cacheName) {
              if (expectingCacheNames.indexOf(cacheName) === NOT_FOUND) {
                console.log(
                  '%c DELETE: Out of date cache: %s',
                  'color: #ff0000',
                  cacheName
                );
                return caches.delete(cacheName);
              }
            })
          );
        })
      );
    });
    
    self.addEventListener('fetch', function(event) {
      var request = event.request,
        requestUrl = request.url;
    
      console.log(
        '%c  EVENT: %c Handling fetch event for %s',
        'color: #F57F20',
        'color: #000',
        requestUrl
      );
    
      event.respondWith(
        caches.open(currentCaches['readThrough']).then(function(cache) {
          return cache.match(request).then(function(response) {
            if (response) {
              console.log(
                '%c P RESPONSE: %c Found in cache: %s',
                'color: #5EBD00',
                'color: #000000',
                response
              );
    
              return response;
            }
            console.log(
              '%c O RESPONSE: %c For %s not found in cache. ' +
              'fetching from network...',
              'color: #F05266',
              'color: #000',
              requestUrl
            );
    
            return fetch(request.clone()).then(function(response) {
              console.log(
                '%c RESPONSE: %c For %s from network is: %O',
                'color: #F05266',
                'color: #000',
                requestUrl,
                response
              );
    
              if (response.status < ERROR_RESPONSE) {
                cache.put(request, response.clone());
              }
    
              return response;
            });
          }).catch(function(err) {
            console.error('FAIL: Read-through cache:', err);
            throw error;
          });
        })
      );
    });
    
  5. 打开浏览器并转到index.html如何操作...

  6. 打开开发者工具(Cmd + Alt + IF12)。你将在控制台看到记录的消息,表明大多数资源都没有在缓存中找到,因此从网络上获取:如何操作...

  7. 如果你刷新页面,你将在控制台看到不同的消息。这次,资源是从缓存中获取的:如何操作...

它是如何工作的...

service-worker.js文件中,我们为缓存设置了一个特定于页面的名称,因为缓存是基于源的,其他页面不应使用相同的缓存名称。我们还对缓存进行了版本控制,以便解决你可能想要一个新缓存的情况;在这种情况下,我们可以更新版本:

var version = 1,
   currentCaches= { readThrough : 'version-' + version },

我们还确保在服务工作者激活时清除旧缓存。因此,我们删除了所有与我们之前指定的缓存名称不匹配的缓存:

self.addEventListener('activate', function(event) {
  var expectingCacheNames = Object.keys(currentCaches).map(function(key) {
    return currentCaches[key];
  });

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (expectingCacheNames.indexOf(cacheName) === NOT_FOUND) {
            console.log('%c DELETE: Out of date cache: %s',
              'color: #ff0000', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

fetch 事件监听器会检查缓存以确定我们请求的资源是否在缓存中;如果找到,它将响应条目:

event.respondWith(
    caches.open(currentCaches['readThrough']).then(function(cache) {
      return cache.match(request).then(function(response) {
        if (response) {
          console.log(
            '%c ✔ RESPONSE: %c Found in cache: %s',
            'color: #5EBD00','color: #000000',response
          );

          return response;
        }

否则,如果缓存中没有event.request的条目,响应将是未定义的,因此我们必须使用fetch()来获取资源:

return fetch(request.clone()).then(function(response) {
          console.log(
            '%c RESPONSE: %c For %s from network is: %O',
            'color: #F05266',
            'color: #000',
            requestUrl,
            response
          );

在这里,如果之后我们使用cache.put()clone()调用是有用的。

制作副本是必要的,因为cache.put()fetch()都会消耗请求。

我们还确保通过检查response.status参数不是 400 或更高来避免缓存错误响应:

if (response.status < ERROR_RESPONSE) {

最后,我们在响应上调用clone()方法,以便在缓存中保存一个副本,然后将响应返回给浏览器:

…
cache.put(request, response.clone());
}

return response;

允许离线 Google Analytics

在之前的配方中,我们讨论了读透缓存。让我们快速回顾一下什么是读透缓存。当一个服务工作者获得页面控制权后,第一次请求新的资源时,响应将被存储在服务工作者缓存中。

在这个配方中,我们利用这个功能将任何失败的 Google Analytics/collect pings 存储在 IndexedDb 数据库中。IndexedDb 是一个客户端、用户特定的存储规范,它允许我们以索引的方式存储数据,并由一个提供搜索功能的 API 支持。因此,每次服务工作者启动时,任何保存的 Google Analytics pings 都将重新播放。

要了解更多关于 IndexedDb 的信息,可以参考以下链接:

这将为您提供一个在离线状态下执行交易的平台,无论连接性和可用性如何。

准备工作

要开始使用服务工作者,您需要在浏览器设置中开启服务工作者实验功能。如果您还没有这样做,请参考第一章的第一个食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下食谱:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,以及在 Mac 上设置 SSL

如何操作...

按照说明设置您的文件结构。或者,您也可以从以下位置下载文件:

github.com/szaranger/szaranger.github.io/tree/master/service-workers/04/03/

  1. 首先,我们必须创建一个如下所示的index.html文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Offline Google Analytics</title>
      <link rel="stylesheet" href="style.css">
    </head>
    <body>
      <section id="registration-status">
        <p>Registration status: <strong id="status"></strong></p>
        <input type="button" id="resetButton" value="Reset" />
      </section>
      <section id="outlet">
        <p id="message"></p>
        <div id="images" style="display: none">
           <img src="img/serice-worker.jpg">
        </div>
      </section>
      <script src="img/index.js"></script>
      <script>
          /* jshint ignore:start */
          (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
            (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
            m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
          })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
          ga('create', 'UA-12345678-1', 'auto');
          ga('send', 'pageview');
          /* jshint ignore:end */
        </script>
    </body>
    </html>
    
  2. 在与index.html文件相同的文件夹中创建一个名为style.css的 CSS 文件,并包含以下代码:

    * {
      -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
    }
    
    body {
      margin: 0 auto;
      text-align: center;
      font-family: sans-serif;
    }
    
    main {
      max-width: 350px;
      border: 1px solid #4CAF50;
      padding: 20px;
      border-radius: 5px;
      width: 350px;
      margin: 20px auto;
    }
    
    h1 {
      color: #4CAF50;
    }
    
    img {
      padding: 20px 0;
      max-width: 400px;
    }
    
    .hidden {
      display: none;
    }
    
    #registration-status {
      background-color: #FFE454;
      padding: 10px;
    }
    
  3. 在与index.html文件相同的文件夹中创建一个名为index.js的 JavaScript 文件,并包含以下代码:

    'use strict';
    
    var scope = {
      scope: './'
    };
    
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register(
        'service-worker.js',
        scope
      ).then( function(serviceWorker) {
        printStatus('successful');
        if (navigator.serviceWorker.controller) {
          showImages();
        } else {
          document.querySelector('#message').textContent = 'Reload the page for images to be loaded from the service worker cache';
        }
      }).catch(function(error) {
        printStatus(error);
      });
    } else {
      printStatus('unavailable');
    }
    
    function printStatus(status) {
      document.querySelector('#status').innerHTML = status;
    }
    
    document.querySelector('#resetButton').addEventListener('click',
      function() {
        navigator.serviceWorker.getRegistration().then(function(registration) {
          registration.unregister();
          window.location.reload();
        });
      }
    );
    
    function showImages() {
      document.querySelector('#images').style.display = 'block';
    }
    
  4. 在与index.html文件相同的文件夹中创建一个名为analytics.js的 JavaScript 文件,并包含以下代码:

    'use strict';
    
    var RW = 'readwrite';
    
    function checkForAnalyticsRequest(requestUrl) {
      var url = new URL(requestUrl),
        regex = /^(w{3}|ssl)\.google-analytics.com$/;
    
      if (url.hostname.match(regex) && url.pathname === '/collect') {
        console.log('INDEXEDDB: Store request(Google analytics) for replaying later.');
        saveGoogleAnalyticsRequest(requestUrl);
      }
    }
    
    function saveGoogleAnalyticsRequest(requestUrl) {
      getIDBObjectStore(idbInstance, idbStore, RW).add({
        url: requestUrl,
        timestamp: Date.now()
      });
    }
    
    function replayGoogleAnalyticsRequests(idbInstance, idbStore, throttle) {
      var savedGoogleAnalyticsRequests = [];
    
      getIDBObjectStore(idbInstance, idbStore).openCursor().onsuccess = function(event) {
        var cursor = event.target.result;
    
        if (cursor) {
          savedGoogleAnalyticsRequests.push(cursor.value);
          cursor.continue();
        } else {
          console.log('REPLAY: Starting %d Google Analytics requests',
            savedGoogleAnalyticsRequests.length);
    
          savedGoogleAnalyticsRequests.forEach(function(savedRequest) {
            var queueTime = Date.now() - savedRequest.timestamp;
            if (queueTime > throttle) {
              getIDBObjectStore(idbInstance, idbStore, RW).delete(savedRequest.url);
              console.log('REQUEST: Queued for %dms ' +
                'STOPPED: Replay attempt', queueTime);
            } else {
              var requestUrl = savedRequest.url + '&qt=' + queueTime;
    
              console.log('%c ♫ REPLAY: %s %s', 'color: #1C99D8', 'in progress...', requestUrl);
    
              fetch(requestUrl).then(function(response) {
                if (response.status < 400) {
                  getIDBObjectStore(idbInstance, idbStore, RW).delete(savedRequest.url);
                  console.log('%c ♫ REPLAY: %s', 'color: #1C99D8', 'success');
                } else {
                  console.error('♫ REPLAY: fail -', response);
                }
              }).catch(function(err) {
                console.error('♫ REPLAY: fail - ', err);
              });
            }
          });
        }
      };
    }
    
  5. 打开浏览器并访问index.html文件:如何操作...

  6. 现在,打开开发者工具(Cmd + Alt + IF12)并转到网络选项卡。您将看到/collect请求的状态为200,这意味着成功:如何操作...

  7. 刷新页面。您将看到如下截图所示的图像:如何操作...

  8. 转到开发者工具的控制台选项卡:如何操作...

  9. 现在,转到开发者工具的网络选项卡并选择离线如何操作...

  10. 刷新页面,您仍然会看到相同的页面,但如果您查看网络请求,您将能够发现服务工作者已将分析请求保存到 IndexedDb 中:如何操作...

  11. 现在,在开发者工具中将离线选项更改为无节流如何操作...

  12. 点击此页面的重置按钮以刷新服务工作者,并在开发者工具的控制台中进行监控。您将看到从 IndexedDb 提取的重放消息已被发送:如何操作...

工作原理...

service-worker.js文件中,我们通过获取其引用来访问 IndexedDb。然后我们继续为数据库实例的errorupgradeneedonsuccess事件附加事件处理器:

var db = openIDBDatabase('offline-google-analytics', idbVersion);
db.onerror = function(err) {
  console.error('%c ✗ ERROR: IndexedDB - %s', 'color: #ff0000', err);
};

db.onupgradeneeded = function() {
  this.result.createObjectStore(idbStore, {keyPath: 'url'});
};

db.onsuccess = function() {
  idbInstance = this.result;
  replayGoogleAnalyticsRequests(idbInstance, idbStore, throttle);
};

由于读取穿透,缓存将缓存初始请求;对同一资源的后续请求将由服务工人的fetch()事件处理器处理。fetch 事件处理器查询currentCaches缓存中的请求,并将响应发送回浏览器:

event.respondWith(
    caches.open(currentCaches['offline-google-analytics']).then(function(cache) {
      return cache.match(event.request).then(function(res) {
        if (res) {
          console.log(
            '%c ✔ RESPONSE: %c Found in cache: %s',
            'color: #5EBD00', 'color: #000000', res
          );

          return res;
        }

如果响应未找到,它将向网络发送 fetch 请求:

 return fetch(event.request.clone()).then(function(res) {
          console.log('%c ✔ RESPONSE: %c For %s from network: %O',
            'color: #5EBD00', 'color: #000000',
            event.request.url, res);

          if (res.status < 400) {
            cache.put(event.request, res.clone());
          }

如果前一个请求的响应是成功的,响应将被克隆并添加到缓存中,请求作为键,响应作为值:

promises = promises.map(function(promise) {
      return Promise.resolve(promise);
    });

接下来,我们要确保在数组中的另一个 promise 被解析后,立即解析当前的 promise:

if (res.status < 400) {
            cache.put(event.request, res.clone());
          }

如果响应不是服务器错误,并且可能是超时,我们将请求 URL 传递给checkForAnalyticsRequest()

} else if (res.status >= 500) {
            checkForAnalyticsRequest(event.request.url);
          }

checkForAnalyticsRequest()函数位于analytics.js文件中。让我们检查这个方法。传入的 URL 首先被检查以确保它是对google-analytics.com的调用,无论子域名是www还是ssl,路径名中是否有/collect。这是为了确保这是一个分析 ping:

function checkForAnalyticsRequest(requestUrl) {
  var url = new URL(requestUrl),
    regex = /^(w{3}|ssl)\.google-analytics.com$/;

  if (url.hostname.match(regex) && url.pathname === '/collect') {
    console.log('INDEXEDDB: Store request(Google analytics) for replaying later.');
    saveGoogleAnalyticsRequest(requestUrl);
  }
}

saveGoogleAnalyticsRequest()方法将 URL 和时间戳添加到存储中,从而保存条目。

service-worker.js文件中,onsuccess()方法调用replayGoogleAnalyticsRequests()方法。在这个方法中,分析请求将被保存到一个名为savedGoogleAnalyticsRequests的队列中:

function replayGoogleAnalyticsRequests(idbInstance, idbStore, throttle) {
  var savedGoogleAnalyticsRequests = [];

  getIDBObjectStore(idbInstance, idbStore).openCursor().onsuccess = function(event) {
    var cursor = event.target.result;

openCursor()函数是指向数据库的指针,你可以遍历记录。

onsuccess事件处理器的回调将event.target.result的值传递到savedGoogleAnalyticsRequests数组中:

getIDBObjectStore(idbInstance, idbStore).openCursor().onsuccess = function(event) {
    var cursor = event.target.result;

    if (cursor) {
      savedGoogleAnalyticsRequests.push(cursor.value);
      cursor.continue();
    }

否则,每个保存的 Google Analytics 请求将被重新播放:

} else {
      console.log('REPLAY: Starting %d Google Analytics requests',
        savedGoogleAnalyticsRequests.length);

      savedGoogleAnalyticsRequests.forEach(function(savedRequest) {
        var queueTime = Date.now() - savedRequest.timestamp;
        if (queueTime > throttle) {
          getIDBObjectStore(idbInstance, idbStore, RW).delete(savedRequest.url);
          console.log('REQUEST: Queued for %dms ' +
            'STOPPED: Replay attempt', queueTime);
        } else {
          var requestUrl = savedRequest.url + '&qt=' + queueTime;

          console.log('%c ♫ REPLAY: %s %s', 'color: #1C99D8', 'in progress...', requestUrl);

          fetch(requestUrl).then(function(response) {
            if (response.status < 400) {
              getIDBObjectStore(idbInstance, idbStore, RW).delete(savedRequest.url);
              console.log('%c ♫ REPLAY: %s', 'color: #1C99D8', 'success');
            } else {
              console.error('♫ REPLAY: fail -', response);
            }
          }).catch(function(err) {
            console.error('♫ REPLAY: fail - ', err);
          });
        }
      });
    }

参见

  • 实现读取穿透缓存菜谱

允许离线用户交互

大多数网站,包括新闻文章、体育视频或音乐,由于内容量很大,并不能完全离线。如果你不访问它们,没有必要在缓存中保存所有内容。但是,给用户保存内容到缓存以便稍后阅读的选项是理想的解决方案。在这个菜谱中,我们将探讨如何实现这一点。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置开发环境以支持此功能,请参考以下食谱:设置 GitHub 页面以支持 SSL设置 Windows 的 SSL,和设置 Mac 的 SSL

如何操作...

按照说明设置你的文件结构。或者,你也可以从以下位置下载文件:

github.com/szaranger/szaranger.github.io/tree/master/service-workers/04/04/

  1. 首先,我们必须创建一个index.html文件,如下所示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Offline User Interaction</title>
      <link rel="stylesheet" href="style.css">
    </head>
    <body>
      <section id="registration-status">
        <p>Registration status: <strong id="status"></strong></p>
        <input type="button" id="resetButton" value="Reset" />
      </section>
      <section>
        <video width="320" height="240" id="video-01" controls>
          <source src="img/video.mp4" type="video/mp4">
          Your browser does not support the video tag.
        </video>
        <input type="button" id="watch-later" value="Watch Later" />
      </section>
      <script src="img/index.js"></script>
    </body>
    </html>
    
  2. 在与index.html文件相同的文件夹中创建一个名为index.js的 JavaScript 文件,包含以下代码:

    'use strict';
    
    var scope = {
      scope: './'
    };
    
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register(
        'service-worker.js',
        scope
      ).then( function(serviceWorker) {
        printStatus('successful');
      }).catch(function(error) {
        printStatus(error);
      });
    } else {
      printStatus('unavailable');
    }
    
    document.querySelector('#resetButton').addEventListener('click',
      function() {
        navigator.serviceWorker.getRegistration().then(function(registration) {
          registration.unregister();
          window.location.reload();
        });
      }
    );
    
    function printStatus(status) {
      document.querySelector('#status').innerHTML = status;
    }
    
    document.getElementById('watch-later').addEventListener('click', function(event) {
      event.preventDefault();
    
      caches.open('video').then(function(cache) {
        fetch('video.mp4').then(function(response) {
          return response.url;
        }).then(function(url) {
          cache.add(url);
        });
      });
    });
    
  3. 在与index.html文件相同的文件夹中创建一个名为service-worker.js的 JavaScript 文件,包含以下代码:

    'use strict';
    
    var cacheName= 'user-interaction-cache';
    
    self.addEventListener('install', function(event) {
      event.waitUntil(
        caches.open(cacheName)
          .then(function(cache) {
            return cache.addAll([
              'index.html'
            ]);
          })
          .then(function() {
            return self.skipWaiting();
          })
      );
    });
    
    self.addEventListener('fetch', function(event) {
      event.respondWith(
        caches.match(event.request)
          .then(function(response) {
            if (response) {
              console.log('Fetching from the cache: ', event.request.url);
              return response;
            } else {
              console.log('Fetching from server: ', event.request.url);
            }
           return fetch(event.request);
         }
       )
     );
    });
    
    self.addEventListener('activate', function(event) {
       console.log('Activating the service worker!');
       event.waitUntil(self.clients.claim());
    });
    
  4. 在与index.html文件相同的文件夹中创建一个名为style.css的 CSS 文件,包含以下代码:

    * {
      -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
    }
    
    body {
      margin: 0 auto;
      text-align: center;
      font-family: sans-serif;
    }
    
    main {
      max-width: 350px;
      border: 1px solid #4CAF50;
      padding: 20px;
      border-radius: 5px;
      width: 350px;
      margin: 20px auto;
    }
    
    h1 {
      color: #4CAF50;
    }
    
    img {
      padding: 20px 0;
      max-width: 200px;
    }
    
    .hidden {
      display: none;
    }
    
    #registration-status {
      background-color: #FFE454;
      padding: 10px;
    }
    
    input#watch-later {
        display: block;
        margin: 10px auto;
        padding: 50px;
    }
    
  5. 打开浏览器并访问index.html文件。你会看到一个预取的书签:如何操作...

  6. 通过点击稍后观看按钮将视频添加到缓存中:

  7. 现在,在 DevTools 中将离线选项更改为无节流如何操作...

  8. 现在刷新页面。你会看到样式没有加载,但视频仍然可以访问:如何操作...

它是如何工作的...

在我们的service-worker.js文件中,我们只缓存index.html文件以加载页面:

caches.open(cacheName)
      .then(function(cache) {
        return cache.addAll([
          'index.html'
        ]);
      })

index.html文件中,我们有一个视频标签,其源指向一个视频文件。如果浏览器不支持.mp4扩展名,它将显示您的浏览器不支持视频标签的消息:

<section>
    <video width="320" height="240" id="video-01" controls>
      <source src="img/video.mp4" type="video/mp4">
      Your browser does not support the video tag.
    </video>
    <input type="button" id="watch-later" value="Watch Later" />
  </section>

当你点击稍后观看按钮时,事件处理程序被触发,这反过来又在index.js文件内部处理:

document.getElementById('watch-later').addEventListener('click', function(event) {
  event.preventDefault();

  caches.open('video').then(function(cache) {
    fetch('video.mp4').then(function(response) {
      return response.url;
    }).then(function(url) {
      cache.add(url);
    });
  });
});

cache.add()函数将响应 URL 添加到缓存中。回到service-worker.js,fetch 事件监听器在离线模式下刷新页面时检索这个保存的响应:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        if (response) {
          console.log('Fetching from the cache: ', event.request.url);
          return response;
        } else {
          console.log('Fetching from server: ', event.request.url);
        }
       return fetch(event.request);
     }
   )
 );
});

实现选择性缓存

在本章的第二部分,实现读取缓存,我们讨论了在第一次请求时缓存所有资源,并讨论了它不适合某些场景,例如新闻或体育,其中大多数文章都会过时,你将永远不会再次访问它们。当时我们提出的解决方案是选择性缓存。那么,让我们看看一个工作示例。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个菜谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下菜谱:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 首先,我们必须创建一个如下所示的index.html文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Cache First, then Network</title>
      <link rel="stylesheet" href="style.css">
    </head>
    <body>
      <section id="registration-status">
        <p>Registration status: <strong id="status"></strong></p>
        <input type="button" id="resetButton" value="Reset" />
      </section>
      <section id="outlet">
        <p></p>
      </section>
      <script src="img/index.js"></script>
    </body>
    </html>
    
  2. 在与index.html文件相同的文件夹中创建一个名为index.js的 JavaScript 文件,并包含以下代码:

    'use strict';
    
    var scope = {
      scope: './'
    };
    
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register(
        'service-worker.js',
        scope
      ).then( function(serviceWorker) {
        printStatus('successful');
        document.querySelector('#outlet p').textContent =
        'The service worker controlling this page has cached this funky font.';
      }).catch(function(error) {
        printStatus(error);
      });
    } else {
      printStatus('unavailable');
    }
    
    function printStatus(status) {
      document.querySelector('#status').innerHTML = status;
    }
    
    document.querySelector('#resetButton').addEventListener('click',
      function() {
        navigator.serviceWorker.getRegistration().then(function(registration) {
          registration.unregister();
          window.location.reload();
        });
      }
    );
    
  3. 从源代码下载webfont-serif.woff文件,或者使用你自己的字体文件,与index.html文件在同一文件夹中。

  4. 在与index.html文件相同的文件夹中创建一个名为service-worker.js的 JavaScript 文件,并包含以下代码:

    'use strict';
    
    var cacheVersion = 1,
      currentCaches = {
      font: 'selective-caching-v' + cacheVersion
    };
    
    self.addEventListener('activate', function(event) {
      var cacheNamesExpected = Object.keys(currentCaches).map(function(key) {
        return currentCaches[key];
      });
    
      event.waitUntil(
        caches.keys().then(function(cacheNames) {
          return Promise.all(
            cacheNames.map(function(cacheName) {
              if (cacheNamesExpected.indexOf(cacheName) === -1) {
                console.log('DELETE: out of date cache:', cacheName);
                return caches.delete(cacheName);
              }
            })
          );
        })
      );
    });
    self.addEventListener('fetch', function(event) {
      console.log('%c  EVENT: %c Handling fetch event for %s','color: #F57F20',
      'color: #000',
      event.request.url);
    
      event.respondWith(
        caches.open(currentCaches.font).then(function(cache) {
          return cache.match(event.request).then(function(res) {
            if (res) {
              console.log(
                '%c ✔ RESPONSE: %c Found in cache: %s',
                'color: #5EBD00', 'color: #000000', res
              );
              return res;
            }
    
            console.log('%c  ✗ RESPONSE: %c Not found for %s in cache. ' +
              'Attempt to fetch from network', 'color: #EB4A4B', 'color: #000000',
              event.request.url);
    
            return fetch(event.request.clone()).then(function(res) {
              console.log('%c ✔ RESPONSE: %c For %s from network: %O',
                'color: #5EBD00', 'color: #000000',
                event.request.url, res);
    
              if (res.status < 400 &&
                  res.headers.has('content-type') &&
                  res.headers.get('content-type').match(/^font\//i)) {
                console.log('%c ✔ RESPONSE: %c Caching to %s ',
                  'color: #5EBD00', 'color: #000000',
                  event.request.url);
                cache.put(event.request, res.clone());
              } else {
                console.log('%c ✔ RESPONSE: %c Not caching to %s ',
                  'color: #5EBD00', 'color: #000000',
                  event.request.url);
              }
    
              return res;
            });
          }).catch(function(err) {
            throw error;
          });
        })
      );
    });
    
  5. 在与index.html文件相同的文件夹中创建一个名为style.css的 CSS 文件,并包含以下代码:

    * {
      -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
    }
    
    body {
      margin: 0 auto;
      text-align: center;
      font-family: sans-serif;
    }
    
    main {
      max-width: 350px;
      border: 1px solid #4CAF50;
      padding: 20px;
      border-radius: 5px;
      width: 350px;
      margin: 20px auto;
    }
    
    h1 {
      color: #4CAF50;
    }
    
    img {
      padding: 20px 0;
      max-width: 200px;
    }
    
    .hidden {
      display: none;
    }
    
    #registration-status {
      background-color: #FFE454;
      padding: 10px;
    }
    
    @font-face{
      font-family: 'MyWebFont';
      src: url('webfont-serif.woff') format('woff');
    }
    
    #outlet p {
      font-family: 'MyWebFont', Arial, sans-serif;
    }
    
  6. 打开浏览器并转到index.html。你会看到注册状态:成功的消息和标志:如何操作...

  7. 现在,如果你刷新页面并检查开发者工具的控制台标签页,你将能够看到webfont-serif.woff文件已从缓存中获取。

它是如何工作的...

在我们的index.html文件中,我们添加一个占位符部分以显示我们的消息:

  <section id="outlet">
    <p></p>
  </section>

style.css文件中,我们声明我们将要使用的字体家族,然后将其分配给我们要针对的段落:

  @font-face{
  font-family: 'MyWebFont';
  src: url('webfont-serif.woff') format('woff');
}

#outlet p {
  font-family: 'MyWebFont', Arial, sans-serif;
}

在我们的service-worker.js文件中,我们确保如果可用,则使用缓存的版本而不是网络请求,但下次会获取更新:

if (res.status < 400 &&
              res.headers.has('content-type') &&
              res.headers.get('content-type').match(/^font\//i)) {
            console.log('%c ✔ RESPONSE: %c Caching to %s ',
              'color: #5EBD00', 'color: #000000',
              event.request.url);
            cache.put(event.request, res.clone());
          } else {
            console.log('%c ✔ RESPONSE: %c Not caching to %s ',
              'color: #5EBD00', 'color: #000000',
              event.request.url);
          }

参见

  • 实现读取缓存菜谱

第五章。超越离线缓存

在本章中,我们将涵盖以下主题:

  • 离线获取网络响应

  • 从 ZIP 文件中缓存内容

  • 选择最佳内容提供者(负载均衡器)

  • 重定向请求

  • 设置请求头

  • 使服务工作者表现得像远程服务器(虚拟服务器)

  • 使服务工作者充当依赖注入器

  • 强制立即控制

  • 实现回退响应

  • 延迟离线请求

简介

在本章中,我们将超越离线缓存,探讨一些高级技术,例如离线网络响应;高级请求处理,包括重定向、设置请求头、延迟离线请求和实现回退请求;以及使用服务工作者作为负载均衡器或依赖注入器,强制立即控制,并从 ZIP 文件中缓存内容。

让我们从本章开始,看看如何离线获取网络响应。

离线获取网络响应

读取缓存是一种对所有静态内容进行全面缓存 assertive approach,这些静态内容是你经常访问的。这并不适合动态内容,如新闻和体育。在这种情况下,选择性的缓存方法会更好。读取缓存可以为我们节省服务器带宽以及网络请求。读取缓存的工作方式是,当服务工作者在第一次 fetch() 请求被调用时接管你的页面,响应将被缓存,后续对同一 URL 的请求将来自缓存。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考 第一章 的第一个配方,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考 第一章 的以下配方,学习服务工作者基础设置 GitHub pages for SSL设置 Windows 的 SSL设置 Mac 的 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置复制 index.htmlindex.jsservice-worker.jsstyle.css 文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/05/01/

  2. 打开浏览器并访问 index.html如何操作...

  3. 打开开发者工具栏(Cmd + Alt + IF12)。现在刷新页面并查看控制台中的消息。你会看到 style.css 文件是从网络中提供的,但 index.js 文件是从缓存中提供的。如何操作...

它是如何工作的...

当服务工作线程文件安装服务工作线程时,它会将index.html文件和index.js文件保存在缓存中。我们故意跳过了style.css文件,因此当您刷新页面时,服务工作线程首先查看缓存文件,找到那里的index.htmlindex.js文件,并从缓存中提供它们。然而,style.css文件不在缓存中,因此服务工作线程从网络中获取它。

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheName)
      .then(function(cache) {
        return cache.addAll([
          'index.html',
          'index.js'
        ]);
      })
      .then(function() {
        return self.skipWaiting();
      })
  );
});

从 ZIP 中缓存内容

如果您担心应用程序在互联网上的加载速度,您可能需要考虑的一个领域是减少应用程序为下载资源而发出的请求数量。减少 HTTP 请求的一种方法是将资源文件,例如图像,作为 ZIP 包发送给客户端。

在本食谱中,我们将探讨如何从 ZIP 文件中缓存资源。

准备工作

要开始使用服务工作线程,您需要在浏览器设置中开启服务工作线程实验功能。如果您还没有这样做,请参考第一章的第一个食谱,学习服务工作线程基础设置服务工作线程。服务工作线程仅在 HTTPS 上运行。要了解如何设置开发环境以支持此功能,请参考第一章的以下食谱,学习服务工作线程基础为 SSL 设置 GitHub 页面为 Windows 设置 SSL,以及为 Mac 设置 SSL

如何操作...

按照以下说明设置您的文件结构:

  1. 首先,我们必须创建一个index.html文件,并从以下位置复制代码:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/05/02/index.html

  2. 在与index.html相同的文件夹中创建一个名为service-worker.js的 JavaScript 文件,并从以下位置复制代码:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/05/02/index.html

  3. 将第三方代码从以下位置复制到一个名为vendor的新文件夹中:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/05/02/vendor

  4. archive.zipcacheProvider.jshelper.jsindex.jsimages文件夹和style.css添加到与index.html文件相同的目录中:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/05/02/

  5. 打开浏览器并转到index.html文件。如何操作...

  6. 从下拉菜单中选择一个品牌并点击加载如何操作...

  7. 现在点击卸载按钮。如何操作...

工作原理...

在我们的index.html文件中,我们检查服务工人是否控制着页面。如果是,我们显示图片。

navigator.serviceWorker.getRegistration().then(function(reg) {
     if (reg && reg.active) {
       displayImages();
     }
   });

当服务工人在安装时处于活动状态,我们显示下拉列表:

navigator.serviceWorker.oncontrollerchange = function() {
  if (navigator.serviceWorker.controller) {
    printInstall('INSTALL: Completed');
    displayImages();
  }
};

卸载包不会从缓存中删除资源,因为卸载服务工人不会擦除离线缓存:

document.querySelector('#uninstall').onclick = function() {      
  …
}

让我们继续到service-worker.js文件。在那里,我们从供应商文件夹导入一些第三方脚本,以及我们的一些脚本:

importScripts('./vendor/zip.js');
importScripts('./vendor/ArrayBufferReader.js');
importScripts('./vendor/deflate.js');
importScripts('./vendor/inflate.js');
importScripts('./helper.js');
importScripts('./cacheProvider.js');

在安装时,我们从 ZIP 文件responses-offline获取内容,并将其存储在缓存中。

self.oninstall = function(event) {
  event.waitUntil(
    fetch(zipURL)
      .then(function(res) {
        return res.arrayBuffer();
      })
      .then(getZipFileReader)
      .then(cacheFileContents)
      .then(self.skipWaiting.bind(self))
  );
};

在激活点控制客户端:

self.onactivate = function(evt) {
  evt.waitUntil(self.clients.claim());
};

查询缓存,如果请求不匹配,则将请求发送到网络:

self.onfetch = function(evt) {
  evt.respondWith(openCache().then(function(cache) {
    var request = evt.request;

    return cache.match(request).then(function(res) {
      return res || fetch(request);
    });
  }));
  };

现在我们来看看处理缓存的cacheProvider.js文件。

我们不缓存文件夹,只缓存其中的文件:

function cacheEntry(entry) {
  if (entry.directory) {
    return Promise.resolve();
  }

块写入器是响应对象构造函数的支持格式。数据将以写入器想要的方式读取:

return new Promise(function(fulfill, reject) {
    var blobWriter = new zip.BlobWriter();

    entry.getData(blobWriter, function(data) {
      return openCache().then(function(cache) {
        var fileLocation = getFileLocation(entry.filename),
          response = new Response(data, { headers: {

我们通过查看扩展名来识别文件的类型:

  response = new Response(data, { headers: {
            'Content-Type': getContentType(entry.filename)
          } });

我们必须克隆response对象,因为一旦它被使用,就不能再次使用:

if (entry.filename === ROOT) {
          cache.put(getFileLocation(), response.clone());
        }

        return cache.put(fileLocation, response);
      }).then(fulfill, reject);

让我们再看看helper.js文件。getZipFileReader(data)函数将zip.js API 包装在 Promise 中:

function getZipFileReader(data) {
  return new Promise(function(fulfill, reject) {
    var arrayBufferReader = new zip.ArrayBufferReader(data);
    zip.createReader(arrayBufferReader, fulfill, reject);
  });
}

getContentType(filename)方法通过扩展名返回文件的类型:

function getContentType(filename) {
  var tokens = filename.split('.');
  var extension = tokens[tokens.length - 1];
  return contentTypes[extension] || 'text/plain';
}

选择最佳内容提供者(负载均衡器)

在这个菜谱中,我们将探讨如何使用服务工人作为负载均衡器,以便我们可以根据内容提供者的负载决定哪个内容提供者最适合我们从其获取内容。

准备工作

要开始使用服务工人,您需要在浏览器设置中启用服务工人实验功能。如果您还没有这样做,请参阅第一章的第一个菜谱,学习服务工人基础知识设置服务工人。服务工人仅在 HTTPS 上运行。要了解如何设置支持此功能的开发生态,请参阅以下菜谱:设置 GitHub 页面以支持 SSL设置 Windows 的 SSL,和设置 Mac 的 SSL。您还需要确保 Node.js 可用。您可以在nodejs.org/en/上阅读如何安装 Node.js。

如何操作...

按照以下说明设置您的文件结构:

  1. 从以下位置下载所有文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/05/03/

  2. 在命令行中运行以下命令(确保已安装 Node.js,或者阅读如何在nodejs.org/en/上安装):

    npm install
    
  3. 打开浏览器并转到index.html如何做到这一点...

  4. 你可以从下拉列表中选择标志,它将从最佳内容提供商加载图像,减少负载。你也可以手动设置服务器负载并点击重置

它是如何工作的...

service-worker.js文件中,我们正在强制服务工作者立即控制客户端:

self.oninstall = function(evt) {
  evt.waitUntil(self.skipWaiting());
};

self.onactivate = function(evt) {
  evt.waitUntil(self.clients.claim());
};

我们使用正则表达式模式来检查请求是否包含图像:

function isResource(req) {
  return req.url.match(/\/images\/.*$/) && req.method === 'GET';
}

最佳服务器的方法返回负载最低的服务器,然后从该服务器返回图像:

function fetchContentFromBestServer(req) {
  var session = req.url.match(/\?session=([^&]*)/)[1];
  return getContentServerLoads(session)
    .then(selectContentServer)
    .then(function(serverUrl) {
      var resourcePath = req.url.match(/\/images\/[^?]*/)[0],
        serverReq = new Request(serverUrl + resourcePath);

      return fetch(serverReq);
    });
}

Express 服务器将被查询以找到服务器负载:

function getContentServerLoads(session) {
  return fetch(baseURL + '/server-loads?session=' + session).then(function(res) {
    return res.json();
  });
}

index.js文件中,我们正在设置图像选择的处理器:

navigator.serviceWorker.ready.then(displayUI);

function displayUI() {
  getServerLoads().then(function(loads) {
    serverLoads.forEach(function(input, index) {
      input.value = loads[index];
      input.disabled = false;
    });
    document.querySelector('#image-selection').disabled = false;
  });
}

此外,当用户点击重置按钮时,手动负载值将被发送到 Express 服务器:

document.querySelector('#load-configuration').onsubmit = function(event) {
  event.preventDefault();

  var loads = serverLoads.map(function(input) {
    return parseInt(input.value, 10);
  });

  fetch(setSession(baseURL + '/server-loads'), {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(loads)
  }).then(function(res) {
    return res.json();
  }).then(function(result) {
    document.querySelector('#server-label').textContent = result;
  });
};

在更改图像选择时,为了防止缓存,我们添加了一个缓存破坏参数:

document.querySelector('#image-selection').onchange = function() {
  var imgUrl = document.querySelector('select').value,
    img = document.querySelector('img');
  if (imgUrl) {
    img.src = setSession(imgUrl) + '&_b=' + Date.now();

我们使用随机字符串设置会话值并将它们存储在localStorage中:

function getSession() {
  var session = localStorage.getItem('session');
  if (!session) {
    session = '' + Date.now() + '-' + Math.random();
    localStorage.setItem('session', session);
  }
  return session;
}

server.js文件是一个在指定端口上运行的 Express 服务器。服务工作者需要服务器通过 SSL 连接运行。为了实现这一点,我们正在使用 HTTP 节点模块,并设置我们在以下第一章的以下食谱中创建的键/值对的位置,学习服务工作者基础设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

var https = require('https');
var fs = require('fs');

var privateKey = fs.readFileSync('/private/etc/apache2/localhost-key.pem', 'utf8');
var certificate = fs.readFileSync('/private/etc/apache2/localhost-cert.pem', 'utf8');
var credentials = { key: privateKey, cert: certificate };
var httpsServer = https.createServer(credentials, app);

我们还允许网页跨源资源共享以访问此服务器。

app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  next();
});

重定向请求

相对 URL,例如test/,如果test/目录中有一个,则应重定向到index.html。让我们通过服务工作者测试这个场景。

准备工作

要开始使用服务工作者,你需要在浏览器设置中打开服务工作者实验功能。如果你还没有这样做,请参阅第一章的第一道食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的开发生态,请参阅以下食谱:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

如何做到这一点...

按照以下说明设置你的文件结构:

  1. 首先,我们必须创建一个index.html文件,如下所示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Redirect request</title>
      <link rel="stylesheet" href="style.css">
    </head>
    <body>
      <section id="registration-status">
        <p>Registration status: <strong id="status"></strong></p>
        <input type="button" id="resetButton" value="Reset" />
      </section>
      <section>
        <h1>Redirect</h1>
        <p>Relative URLs should redirect to <strong>index.html</strong> if it exists.</p>
        <p><a href="test">Click</a></p>
      </section>
      <script src="img/index.js"></script>
    </body>
    </html>
    
  2. 在与index.html文件相同的文件夹中创建一个名为style.css的 CSS 文件,其中包含以下代码:

    * {
      -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
    }
    
    body {
      margin: 0 auto;
      text-align: center;
      font-family: sans-serif;
    }
    
    main {
      max-width: 350px;
      border: 1px solid #4CAF50;
      padding: 20px;
      border-radius: 5px;
      width: 350px;
      margin: 20px auto;
    }
    
    h1 {
      color: #4CAF50;
    }
    
    img {
      padding: 20px 0;
      max-width: 400px;
    }
    
    .hidden {
      display: none;
    }
    
    #registration-status {
      background-color: #FFE454;
      padding: 10px;
    }
    
  3. 在与index.html文件相同的文件夹中创建一个名为index.js的 JavaScript 文件,其中包含以下代码:

    'use strict';
    
    var scope = {
      scope: './'
    };
    
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register(
        'service-worker.js',
        scope
      ).then( function(serviceWorker) {
        printStatus('successful');
        if (navigator.serviceWorker.controller) {
          showImages();
        } else {
          document.querySelector('#message').textContent = 'Reload the page for images to be loaded from the service worker cache';
        }
      }).catch(function(error) {
        printStatus(error);
      });
    } else {
      printStatus('unavailable');
    }
    
    function printStatus(status) {
      document.querySelector('#status').innerHTML = status;
    }
    
    document.querySelector('#resetButton').addEventListener('click',
      function() {
        navigator.serviceWorker.getRegistration().then(function(registration) {
          registration.unregister();
          window.location.reload();
        });
      }
    );
    
  4. 在与index.html文件相同的文件夹中创建一个名为service-worker.js的 JavaScript 文件

  5. :

    'use strict';
    
    var cacheName= 'redirect-request';
    
    self.addEventListener('activate', function() {
      clients.claim();
    });
    
    self.addEventListener('fetch', function(evt) {
      console.log(evt.request);
      evt.respondWith(
        fetch(evt.request).catch(function() {
          return new Response("FETCH: failed");
        })
      );
    });
    
  6. 打开浏览器并转到 index.html如何操作...

  7. 现在打开开发者工具 (Cmd + Alt + IF12) 并确保点击了 Preserve log 复选框。现在点击 Click 链接。页面将被重定向到 test/ 目录下的 index.html 文件。如何操作...

它是如何工作的...

service-worker.js 文件中,我们让所有 fetch 请求通过到网络:

self.addEventListener('fetch', function(evt) {
  console.log(evt.request);
  evt.respondWith(
    fetch(evt.request).catch(function() {
      return new Response("FETCH: failed");
    })
  );
});

这样,如果 test/index.html 文件存在,相对 URL 将重定向到 test/ 的 HTTP 版本。

设置请求头

如果我们想找出发送到网络的请求头详细信息,我们可以将请求头详细信息记录到控制台。在这个菜谱中,我们将了解如何做到这一点。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一道菜谱 Chapter 1,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下菜谱:设置 GitHub pages for SSL设置 Windows 的 SSL设置 Mac 的 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 首先,我们必须创建一个 index.html 文件,如下所示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Request headers</title>
      <link rel="stylesheet" href="style.css">
    </head>
    <body>
      <section id="registration-status">
        <p>Registration status: <strong id="status"></strong></p>
        <input type="button" id="resetButton" value="Reset" />
      </section>
      <script src="img/index.js"></script>
      <section>
        <img src="img/adobe-logo.png" alt="logo">
      </section>
    </body>
    </html>
    
  2. 在与 index.html 文件相同的文件夹中创建一个名为 service-worker.js 的 JavaScript 文件,并包含以下代码:

    'use strict';
    
    self.addEventListener('fetch', function(evt) {
      var request = evt.request;
    
      console.log(
        "FETCH: ",
        evt.request.url,
        "HEADERS: ",
        new Set(request.headers)
      );
    
      evt.respondWith(fetch(request));
    });
    
  3. 在与 index.html 文件相同的文件夹中创建一个名为 style.css 的 CSS 文件,并包含以下代码:

    * {
      -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
    }
    
    body {
      margin: 0 auto;
      text-align: center;
      font-family: sans-serif;
    }
    
    main {
      max-width: 350px;
      border: 1px solid #4CAF50;
      padding: 20px;
      border-radius: 5px;
      width: 350px;
      margin: 20px auto;
    }
    
    h1 {
      color: #4CAF50;
    }
    
    img {
      padding: 20px 0;
      max-width: 200px;
    }
    
    .hidden {
      display: none;
    }
    
    #registration-status {
      background-color: #FFE454;
      padding: 10px;
    }
    
  4. 打开浏览器并转到 index.html 文件。你会看到一个预取的书签。如何操作...

  5. 现在打开开发者工具 (Cmd + Alt + IF12),并刷新页面。检查控制台上的日志详情。如何操作...

它是如何工作的...

在我们的 service-worker.js 文件中,fetch 事件处理程序记录了请求详情,以及任何请求的头部详情:

caches.open(cacheName)
      .then(function(cache) {
        return cache.addAll([
          'index.html'
        ]);
      })

index.html 文件中,我们正在加载一个将被控制服务工作者的 fetch 事件拦截的图像。

<section >
    <img src="img/adobe-logo.png" alt="logo">
</section>

使服务工作者表现得像远程服务器

服务工作者不仅像我们在本章的 选择最佳内容提供者(负载均衡器) 菜谱中讨论的那样充当负载均衡器;它们还可以充当虚拟服务器。这允许我们将 UI 与典型的服务器端业务逻辑解耦。

在这个菜谱中,我们将学习如何将业务逻辑部分移动到响应传统 RESTful fetch 请求的服务工作者。为了演示这个功能,我们将实现一个待办事项应用。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下食谱:设置 GitHub 页面以支持 SSL设置 Windows 的 SSL,和设置 Mac 的 SSL

如何做...

按照以下说明设置你的文件结构:

  1. 从以下位置下载所有文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/05/06/

  2. 打开浏览器并转到index.html文件。你会看到带有预取待办事项的待办事项应用。如何做...

  3. 现在打开开发者工具(Cmd + Alt + IF12)并刷新页面。检查控制台上的日志详情。你会看到已经访问了端点。如何做...

  4. 你可以添加待办事项及其优先级,也可以通过点击删除图标来删除它们。如何做...

它是如何工作的...

我们需要实现端点以开始。在worker.js文件中,我们创建了一个ServiceWorkerWare模块的实例。然后我们声明了我们的待办事项的路由。

我们使用self.location确定根:

root = (function() {
    var tkns = (self.location + '').split('/');
    tkns[tkns.length - 1] = '';
    return tkns.join('/');
  })();

获取所有待办事项:

worker.get(root + 'api/todos', function(request, response) {
  return new Response(JSON.stringify(todos.filter(function(item) {
    return item !== null;
  })));
});

获取要删除的特定待办事项的 0 基于位置(id):

worker.delete(root + 'api/todos/:id', function(request, response) {
  var id = parseInt(request.parameters.id, 10) - 1;
  if (!todos[id].isSticky) {
    todos[id] = null;
  }
  return new Response({ status: 204 });
});

要将新的待办事项添加到集合中,请使用以下代码:

worker.post(root + 'api/todos', function(request, response) {
  return request.json().then(function(quote) {
    quote.id = todos.length + 1;
    todos.push(quote);
    return new Response(JSON.stringify(quote), { status: 201 });
  });
});

index.html文件中,我们加载了一个将被控制服务工作者的 fetch 事件拦截的图片:

<section >
    <img src="img/adobe-logo.png" alt="logo">
</section>

在我们的service-worker.js文件中,我们导入了一个名为ServiceWorkerWare.js的第三方脚本以及我们的自定义脚本worker.js,并声明了一个预填充待办事项的待办事项列表:

importScripts('./vendor/ServiceWorkerWare.js');
importScripts('./worker.js');

var todos = [
  {
    text: 'Buy Milk',
    priority: 'high'
  },
  {
    text: 'Refill Car',
    priority: 'medium'
  },
  {
    text: 'Return loaned book',
    priority: 'low'
  }
].map(function(todo, index) {
  todo.id = index + 1;
  todo.isSticky = true;

  return todo;
});

index.js文件是我们大部分工作的地方。当服务工作者获得页面控制权时,它会显示待办事项列表:

navigator.serviceWorker.oncontrollerchange = function() {
    this.controller.onstatechange = function() {
      if (this.state === 'activated') {
        loadTodos();
      }
    };
};

点击+按钮将检索待办事项并将其发送到后端:

document.getElementById('add-form').onsubmit = function(event) {

没有提供优先级的任何待办事项都将留空:

todoPriority = document.getElementById('priority').value.trim() ||
                    'Not specified';

最后,通过 POST 请求将待办事项发送到后端:

fetch(endPoint, {
    method: 'POST',
    body: JSON.stringify(todo),
    headers: headers
  })

使服务工作者充当依赖注入器

依赖注入是一个避免为组件硬编码依赖项的绝佳模式。在这个食谱中,我们将探讨我们如何通过向组件传递两个注入器而不硬编码依赖项来使用服务工作者进行开发和生产环境。

准备工作

要开始使用服务工作者,您需要在浏览器设置中开启服务工作者实验功能。如果您还没有这样做,请参考第一章的第一个配方,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下配方:设置 GitHub 页面以支持 SSL设置 Windows 的 SSL,和设置 Mac 的 SSL

如何操作...

按照以下说明设置您的文件结构:

  1. 从以下位置下载所有文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/05/07/

  2. 打开浏览器并转到 index.html 文件:如何操作...

  3. 点击 生产 链接。您将看到 URL 前添加了一个哈希,#production如何操作...

  4. 现在点击按钮。您将得到 JavaScript 警告消息作为结果。如何操作...

  5. 打开开发者工具 (Cmd + Alt + IF12)。

  6. 现在点击 开发 按钮,然后点击按钮。检查控制台上的日志详情。您将得到控制台消息作为结果。如何操作...

它是如何工作的...

我们在 index.html 文件中添加了一个包含两个链接和三个按钮的部分;链接用于生产和发展,按钮用于提示:

<section id="actions">
      <h1>Environment Switch</h1>
      <p>
        <a href="#production">Production</a>&nbsp;|&nbsp;
        <a href="#development">Development</a>
      </p>
      <p>
        <button id="alert">Alert</button>
        <button id="confirm">Confirm</button>
        <button id="prompt">Prompt</button>
      </p>
    </section>

index.js 文件中,服务工作者注册处理程序被赋予即将实现的 development-sw.js 文件:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register(
    'development-sw.js',
    scope
  )

当我们点击开发或生产链接时,我们创建一个引导文件来识别位置 URL 哈希变化并执行依赖注入。

这样,通过检查 URL 的哈希,我们可以在开发和生产环境之间切换:

window.onhashchange = function() {
  var newInjector = window.location.hash.substr(1) || 
  'production',
    lastInjector = getLastInjector();

  if (newInjector !== lastInjector) {
    navigator.serviceWorker.oncontrollerchange = function() {
      this.controller.onstatechange = function() {
        if (this.state === 'activated') {
          window.location.reload();
        }
      };
    };
    registerNewInjector(newInjector);
  }
};

现在我们强制进行初始检查:

window.onhashchange();

根据环境类型,注册服务工作者:

function registerNewInjector(newInjector) {
  var newInjectorUrl = newInjector + '-sw.js';
  return navigator.serviceWorker.register(newInjectorUrl);
}

如果有任何已注册的服务工作者,获取当前正在检查的注入器:

    function getLastInjector() {
        var newInjector,
        ctr = navigator.serviceWorker.controller;

        if (ctr) {
            newInjector = ctr.scriptURL.endsWith('production-sw.js')
                 ? 'production' : 'development';
        }
        return newInjector;
    }

现在让我们看看 injector.js 文件。让我们让服务工作者立即控制客户端:

function onInstall(evt) {
  evt.waitUntil(self.skipWaiting());
}

function onActivate(evt) {
  evt.waitUntil(self.clients.claim());
}

其余的代码负责检索实际和抽象资源,并根据请求相应地做出响应:

function onFetch(evt) {
  var abstractRes = evt.request.url,
    actualRes = getActualRes(abstractRes);

  evt.respondWith(fetch(actualRes || abstractRes));
}

function getActualRes(abstractRes) {
  var actualRes,
    keys = Object.keys(mapping);

  for (var i = 0, len = keys.length; i < len; i++) {
    var key = keys[i];

    if (abstractRes.endsWith(key)) {
      actualRes = mapping[key];
      break;
    }
  }

  return actualRes;
}

fake-dialogs.js 文件是一个模拟实现,它将提示信息控制台输出;也就是说,它不会显示警告消息,而是记录到控制台:

(function(window) {
  window.dialogs = {
    alert: function(msg) {
      console.log('alert:', msg);
    },

    confirm: function(msg) {
      console.log('confirm:', msg);
      return true;
    },

    prompt: function(msg) {
      console.log('prompt:', msg);
      return 'development';
    }
  };
})(window);

real-dialogs.js 文件则生成一个警告消息:

(function(window) {
  window.dialogs = {
    alert: function(msg) {
      window.alert(msg);
    },

    confirm: function(msg) {
      return window.confirm(msg);
    },

    prompt: function(msg) {
      return window.prompt(msg);
    }
  };
})(window);

production-sw.js 文件导入默认映射以及注入器。我们还为以下事件连接了事件监听器:

importScripts('injector.js');
importScripts('default-mapping.js');

self.onfetch = onFetch;
self.oninstall = onInstall;
self.onactivate = onActivate;

development-sw.js 文件导入默认映射以及注入器。但不同之处在于它覆盖了 utils/dialogs 以提供模拟数据:

importScripts('injector.js');
importScripts('default-mapping.js');

mapping['utils/dialogs'] = 'fake-dialogs.js';

self.onfetch = onFetch;
self.oninstall = onInstall;
self.onactivate = onActivate;

强制立即控制

通常,当导航事件触发时,服务工作者会接管页面。在这个菜谱中,我们探讨的是如何在不需要等待任何类型的导航事件的情况下接管页面。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个菜谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下菜谱:设置 GitHub 页面以支持 SSL为 Windows 设置 SSL,和为 Mac 设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置下载所有文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/05/08/

  2. 打开浏览器并访问index.html文件。如何操作...

  3. 现在刷新页面。你会看到没有注册,也没有触发控制器更改事件。如何操作...

  4. 打开开发者工具 (Cmd + Alt + IF12) 并刷新页面。检查控制台上的日志详情。你会看到图片是从缓存中提供的。如何操作...

工作原理...

index.js文件中,fetchServiceWorkerUpdate方法更新图片和当前版本:

function fetchServiceWorkerUpdate() {
  var img = document.getElementById('picture');
  img.src = 'picture.jpg?' + Date.now();

  fetch('./version').then(function(res) {
    return res.text();
  }).then(function(text) {
    debug(text, 'version');
  });
}

服务工作者在页面加载时接管网站并处理离线回退:

if (navigator.serviceWorker.controller) {
  var url = navigator.serviceWorker.controller.scriptURL;
  console.log('serviceWorker.controller', url);
  debug(url, 'onload');
  fetchServiceWorkerUpdate();
} else {
  navigator.serviceWorker.register('service-worker.js', {
    scope: './'
  }).then(function(registration) {
    debug('REFRESH for the Service Worker to control this client', 'onload');
    debug(registration.scope, 'register');
  });
}

skipWaiting()方法将通过触发onactivate事件强制等待中的服务工作者变为活跃状态。结合Clients.claim(),这将允许服务工作者立即在客户端生效:

    }).then(function() {
      console.log('SERVICE_WORKER: Skip waiting on install');
      return self.skipWaiting();
    })

通常,onactivate方法在工作者安装并刷新页面后调用一次。然而,由于我们在oninstall点调用skipWaiting()onactivate方法立即被调用:

self.addEventListener('activate', function(evt) {
  self.clients.matchAll({
    includeUncontrolled: true
  }).then(function(clientList) {
    var urls = clientList.map(function(client) {
      return client.url;
    });
    console.log('SERVICE_WORKER:  Matching clients:', urls.join(', '));
  });

  evt.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheName !== VERSION) {
            console.log('SERVICE_WORKER: Deleting old cache:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    }).then(function() {
      console.log('SERVICE_WORKER: Claiming clients for version', VERSION);
      return self.clients.claim();
    })
  );
});

实现回退响应

通常,你可以信任你的应用程序连接到的 API 端点,但总有可能那些服务会中断。在这种情况下有一个备用计划是好的。在这个菜谱中,我们将使用服务工作者在这种情况下提供回退响应。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个菜谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下菜谱:设置 GitHub 页面以支持 SSL设置 Windows 的 SSL,和设置 Mac 的 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置下载所有文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/05/09/

  2. 打开浏览器并转到index.html文件。如何操作...

  3. 点击有效请求按钮。你将看到三个品牌的列表。如何操作...

  4. 现在点击无效请求按钮。你将看到三个品牌的列表。如何操作...

  5. 打开开发者工具(Cmd + Alt + IF12)并刷新页面。检查控制台上的日志详情。你将看到后备响应在起作用。如何操作...

它是如何工作的...

我们在index.html文件中添加了一个包含两个按钮的部分,一个用于有效请求,另一个用于无效请求:

<section id="actions">
    <button id="valid-call" disabled>Valid Request</button>
    <button id="invalid-call" disabled>Invalid Request</button>
    <div id="output"></div>
</section>

index.js文件中,enableRequestLinks方法将按钮的事件处理程序连接起来。这两个处理程序都会以链接作为参数触发fetchApiRequest方法:

function enableRequestLinks() {
  var validButton = document.querySelector('#valid-call');
  validButton.addEventListener('click', function() {
    fetchApiRequest('https://raw.githubusercontent.com/szaranger/szaranger.github.io/master/service-workers/05/09/brands.json');
  });
  validButton.disabled = false;

  var invalidButton = document.querySelector('#invalid-call');
  invalidButton.addEventListener('click', function() {
    fetchApiRequest('https://raw.githubusercontent.com/szaranger/szaranger.github.io/master/service-workers/05/09/blah.json');
  });
  invalidButton.disabled = false;
}

我们在这个菜谱中为此准备了一个模拟 API 响应,位置如下:

github.com/szaranger/szaranger.github.io/blob/master/service-workers/05/09/brands.json

这是一个简单的 JSON 对象,包含少量品牌名称:

{
    "brands": [
        {
          "name": "Apple"
        },
        {
          "name": "Google"
        },
        {
          "name": "Facebook"
        }
    ]
}

fetchApiRequest方法调用 fetch 以获取一个返回结果的承诺。我们将使用响应来构建我们需要的列表:

function fetchApiRequest(url) {
  fetch(url).then(function(res) {
    return res.json();
  }).then(function(res) {
    var brands = res.brands.map(function(brand) {
      return '<li>' + brand.name + '</li>';
    }).join('');

    brands = '<ol>' + brands+ '</ol>';

    document.querySelector('#output').innerHTML = '<h1>Brands</h1>' + brands;
  });
}

服务工作者是我们实现 API 不可用时的后备响应的地方。但首先,我们需要告诉服务工作者立即接管页面:

    self.addEventListener('install', function(evt) {
      evt.waitUntil(self.skipWaiting());
    });

   self.addEventListener('activate', function(evt) {
      evt.waitUntil(self.clients.claim());
   });

在 fetch 处理程序中,我们检查响应以确定它是否成功,通过检查res.ok。否则,我们将动态构建一个响应作为后备:

self.addEventListener('fetch', function(evt) {
    evt.respondWith(
      fetch(evt.request).then(function(res) {
        if (!res.ok) {
          throw Error('response status ' + res.status);
        }

        return res;
      }).catch(function(err) {
        console.warn('RESPONSE: Error in constructing a fallback response - ', err);

        var fallbackRes = {
          brands: [
            {
              name: 'Fallback Brand 1'
            },
            {
              name: 'Fallback Brand 1'
            },
            {
              name: 'Fallback Brand 1'
            }
          ]
        };

        return new Response(JSON.stringify(fallbackRes), {
          headers: {'Content-Type': 'application/json'}
        });
      })
    );
});

延迟离线请求

如 Gmail 之类的应用程序可以在网络不可用时在缓冲区中排队请求。当连接恢复时,它将按顺序执行请求以完成操作。

在这个菜谱中,我们正在构建一个可以在离线时延迟待办事项的应用程序。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置开发环境以支持此功能,请参考以下食谱:设置 GitHub 页面以支持 SSL设置 Windows 的 SSL,和设置 Mac 的 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置下载所有文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/05/10

  2. 打开浏览器并转到index.html文件。如何操作...

  3. 添加和删除待办事项,然后按照指示离线。一旦重新连接,待办事项将自动同步。如何操作...

它是如何工作的...

我们正在向index.html文件添加一个包含输入和按钮的部分:

<section id="todo-area">
    <ol>
      <li>Add/delete some todos.</li>
      <li>Go offline (disconnect Internet).</li>
      <li>Continue to add some todos. <br/>(They are now in the queue, so the delete button is not visible now)</li>
      <li>Reconnect internet. The todos will automatically synchronize.</li>
    </ol>
    <form id="add-form">
      <input type="text" id="new-todo" placeholder="Add a task here"/>
      <input type="text" id="priority" placeholder="Priority"/>
      <input type="submit" value="Add" />
    </form>
    <table id="todos">
    </table>
  </section>

service-worker.js文件中,我们引入了两个第三方库,ServiceWorkerWare.jslocalforage.js

importScripts('./vendor/ServiceWorkerWare.js');
importScripts('./vendor/localforage.js');

确定路由的根:

var root = (function() {
  var tokens = (self.location + '').split('/');
  tokens[tokens.length - 1] = '';
  return tokens.join('/');
})();

我们正在使用 Mozilla 的ServiceWorkerWare库为虚拟服务器构建快速路由:

var worker = new ServiceWorkerWare();

为了模拟响应,我们将原始请求入队:

    function tryOrFallback(fakeResponse) { 
      return function(req, res) {
        if (!navigator.onLine) {
          console.log('No network availability, enqueuing');
          return enqueue(req).then(function() {
        return fakeResponse.clone();
      });
    }

相关内容

  • 第六章的日志 API 分析食谱,使用高级库

第六章。使用高级库

本章将涵盖以下主题:

  • 与全局 API 协同工作

  • 实现电路断路器

  • 实现死信队列

  • 记录 API 分析

  • 与 Google Analytics 协同工作

简介

在本章中,你将了解一些可用于与服务工作者接口的高级库。这些主题将非常实用,你将在实际软件开发中使用高级库。我们还将学习一些高级主题,例如电路断路器和死信队列,这些可能在日常编程中不会遇到,但却是新学的知识。

让我们从查看服务工作者可用的全局 API 开始这一章。

与全局 API 协同工作

服务工作者可以访问一些非常实用的全局 API 方法。让我们看看其中的一些方法;你可能觉得它们很有用,并且可以在你的项目中使用。这些全局 API 方法包括CachecachesgetAllRequestResponsefetch

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个菜谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章的以下菜谱,学习服务工作者基础设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置复制index.htmlindex.jsservice-worker.jsstyle.css文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/06/01/

  2. 打开浏览器并转到index.html如何操作...

  3. 打开开发者工具栏(Cmd + Alt + IF12)。现在刷新页面,查看控制台中的消息。你将看到全局 API 函数已记录到控制台:如何操作...

的工作原理...

我们只是将可用的服务工作者 API 打印到控制台。我们的service-worker.js文件如下所示:

'use strict';

console.log(
  'Cache', this.Cache, '\n',
  'caches', this.caches,'\n',
  'fetch', this.fetch,'\n',
  'getAll', this.getAll,'\n',
  'Request', this.Request,'\n',
  'Response', this.Response
);

让我们更详细地讨论一些这些 API 方法。

缓存

Cache接口对服务工作者和窗口作用域都可用。其主要目的是为缓存的RequestResponse对象对提供存储机制。

caches

服务工作者使用CacheStorage对象将资产离线存储,该对象由window.caches只读属性启用。

fetch

全局 fetch 在网络中执行异步获取。

getAll

这是 Chromium 命令 API 的一部分。它作为参数传递给 Promise.then()

实现断路器

假设你运行的应用程序每 5 秒轮询一个 API,但出于某种原因,服务中断了,你继续轮询并得到超时。你需要快速而优雅地处理错误。断路器模式检测故障并防止你的应用程序执行注定要失败的操作。

在这个菜谱中,我们将探讨如何使用服务工作者实现断路器库。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个菜谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章的以下菜谱,学习服务工作者基础设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

如何做到这一点...

按照以下说明设置你的文件结构:

  1. 从以下位置下载所有文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/06/02/

  2. 打开浏览器并转到 index.html 文件:如何做到这一点...

  3. 现在打开开发者工具 (Cmd + Alt + IF12) 并确保点击了 Preserve log 复选框。现在刷新页面,你将看到断路器的日志消息:如何做到这一点...

如何工作...

在查看实现之前,让我们尝试理解断路器是如何工作的。

断路器监控故障。每当故障达到阈值时,断路器跳闸,任何对断路器的调用都将返回错误。在适当的间隔后,如果错误不再发生,断路器将重置断路器。

如您所见,我们需要两个阈值来处理错误和在一定时间后重置断路器:

如何工作...

图片来源:martinfowler.com

我们的大部分工作将在 circuit-breaker.js 文件中完成。如果你想了解更多关于断路器的信息,请点击此链接:

martinfowler.com/bliki/CircuitBreaker.html

首先,我们需要配置断路器。让我们创建 10 个块,每个块的超时时间为 3 秒,阈值为五个。我们还定义错误阈值为百分之五十:

var TEN_SECONDS = 10000,
    TEN_BLOCKS = 10,
    THREE_SECONDS = 3000,
    FIFTY_PERCENT = 50,
    FIVE = 5;

  var CB = function(opts) {
    opts = opts || {};

    this.errorThreshold = opts.errorThreshold || FIFTY_PERCENT;
    this.numBlocks = opts.numBlocks || TEN_BLOCKS;
    this.timeoutDuration = opts.timeoutDuration || THREE_SECONDS;
    this.volumeThreshold = opts.volumeThreshold || FIVE;
    this.windowDuration = opts.windowDuration || TEN_SECONDS;

    this.hanldeCircuitOpen = opts.hanldeCircuitOpen || function() {};
    this.handleCircuitClose = opts.handleCircuitClose || function() {};

    this.$buckets = [this.$createBlock()];
    this.$state = CB.CLOSED;

    this.$startTicker();
  };

我们然后定义run方法如下:

CB.prototype.run = function(command, fallback) {
    if (this.isOpen()) {
      this.$executeFallback(fallback || function() {});
    } else {
      this.$execCmd(command);
    }
  };

如果电路开启,此方法将执行作为参数传入的回退函数。否则,它将执行命令。在service-worker.js文件中,我们将fetch请求作为run方法的回退函数传入:

CircuitBreaker.prototype.fetch = function(request) {
    var unavailableRes = Response.error();

    return new Promise(function(resolve, reject) {
        this.run(function(success, fail) {
            fetch(request).then(function(res) {
                if(res.status < 400) {
                    success();
                    console.log('FETCH: successful');
                } else {
                    fail();
                    console.log('FETCH: failed');
                }
                resolve(res);
            }).catch(function(err) {
                fail();
                reject(unavailableRes);
                console.log('FETCH: unavailable');
            });
        }, function() {
            resolve(unavailableRes);
        });
    }.bind(this));
};

forceCloseforceOpenunforce方法相应地改变状态:

  CB.prototype.forceClose = function() {
    this.$forced = this.$state;
    this.$state = CB.CLOSED;
  };

  CB.prototype.forceOpen = function() {
    this.$forced = this.$state;
    this.$state = CB.OPEN;
  };

  CB.prototype.unforce = function() {
    this.$state = this.$forced;
    this.$forced = null;
  };

isOpen函数返回一个值,表示电路是开启还是关闭:

    CB.prototype.isOpen = function() {
        return this.$state === CB.OPEN;
    };

首先查询缓存;如果请求不匹配,将请求发送到网络:

self.onfetch = function(evt) {
  evt.respondWith(openCache().then(function(cache) {
    var request = evt.request;

    return cache.match(request).then(function(res) {
      return res || fetch(request);
    });
  }));
};

我们用$前缀表示我们的私有函数。$startTicker函数为我们启动计时器:

CB.prototype.$startTicker = function() {
    var me = this,
      bucketIndex = 0,
      bucketDuration = this.windowDuration / this.numBlocks;

    var tick = function() {
      if (me.$buckets.length > me.numBlocks) {
        me.$buckets.shift();
      }

      bucketIndex++;

      if (bucketIndex > me.numBlocks) {
        bucketIndex = 0;

        if (me.isOpen()) {
          me.$state = CB.HALF_OPEN;
        }
      }

      me.$buckets.push(me.$createBlock());
    };

    setInterval(tick, bucketDuration);
};

$createBlock函数给我们提供了一个新的块来工作,而$lastBlock函数则如预期地给出了最后一个块:

  CB.prototype.$createBlock = function() {
    return {
      successes: 0,
      failures: 0,
      shortCircuits: 0,
      timeouts: 0
    };
  };

  CB.prototype.$lastBlock = function() {
    var numBlocks = this.$buckets.length,
      lastBlock = this.$buckets[numBlocks - 1];

    return lastBlock;
  };

$execCmd方法通过增加成功和失败次数来更新状态:

  CB.prototype.$execCmd = function(command) {
    var me = this,
      increment,
      timeout;

    increment = function(prop) {
      return function() {
        var bucket;

        if (!timeout) {
          return;
        }

        bucket = me.$lastBlock();
        bucket[prop]++;

        if (me.$forced === null) {
          me.$updateState();
        }

        clearTimeout(timeout);
        timeout = null;
      };
    };

    timeout = setTimeout(increment('timeouts'), this.timeoutDuration);

    command(increment('successes'), increment('failures'));
  };

$executeFallback函数运行我们之前讨论过的回退方法:

CB.prototype.$executeFallback = function(fallback) {
    var bucket;

    fallback();

    bucket = this.$lastBlock();
    bucket.shortCircuits++;
};

$calcMetrics函数返回错误总数以及成功次数:

CB.prototype.$calcMetrics = function() {
    var totalCount = 0,
      totalErrors = 0,
      errorPerc = 0,
      bucket,
      errors,
      i;

    for (i = 0, len = this.$buckets.length; i < len; i++) {
      bucket = this.$buckets[i];
      errors = (bucket.failures + bucket.timeouts);

      totalErrors += errors;
      totalCount += (errors + bucket.successes);
    }

    errorPerc = (totalErrors / (totalCount > 0 ? totalCount : 1)) * 100;

    return {
      totalErrors: totalErrors,
      errorPerc: errorPerc,
      totalCount: totalCount
    };
};

$updateState方法在一系列计算后更新状态:

CB.prototype.$updateState = function() {
    var metrics = this.$calcMetrics();

    if (this.$state == CB.HALF_OPEN) {
      var lastCmdFailed = !this.$lastBlock().successes && metrics.totalErrors > 0;

      if (lastCmdFailed) {
        this.$state = CB.OPEN;
      } else {
        this.$state = CB.CLOSED;
        this.handleCircuitClose(metrics);
      }
    } else {
      var overErrorThreshold = metrics.errorPerc > this.errorThreshold,
        overVolumeThreshold = metrics.totalCount > this.volumeThreshold,
        overThreshold = overVolumeThreshold && overErrorThreshold;

      if (overThreshold) {
        this.$state = CB.OPEN;
        this.hanldeCircuitOpen(metrics);
      }
    }
};

在我们的service-worker.js文件中,我们通过传递 fetch 请求通过 circuitBreaker 对象来使用我们的断路器库:

self.addEventListener('fetch', function(evt) {
    var url = evt.request.url;

    if(!circuitBreakers[url]) {
        circuitBreakers[url] = new CircuitBreaker(opt);
    }

    evt.respondWith(circuitBreakers[url].fetch(evt.request));
});

实现死信队列

死信队列是由于以下一个或多个原因系统生成的队列:存储无法投递的消息、队列长度限制超出、消息长度限制超出或消息被另一个队列交换拒绝。

在这个食谱中,我们正在服务工作者中实现死信队列。

准备中

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下食谱:设置 GitHub 页面以支持 SSL设置 Windows 的 SSL设置 Mac 的 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置下载所有文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/06/03/

  2. 打开浏览器并转到index.html如何操作...

  3. 现在打开开发者工具(Cmd + Alt + IF12)并确保点击了保留日志复选框。

  4. 网络选项卡上选择离线选项:如何操作...

  5. 现在刷新页面,你会看到失败请求的消息,这些消息被我们实现的死信队列排队:如何操作...

它是如何工作的...

服务工作者安装事件处理程序将我们传递到 service-worker.js 文件中的 addAll 方法的文件缓存起来:

self.addEventListener('install', function(evt) {
  evt.waitUntil(
    caches.open(cacheName)
      .then(function(cache) {
        return cache.addAll([
          'style.css',
          'index.html',
          'index.js',
          'style.css'
        ]);
      })
      .then(function() {
        return self.skipWaiting();
      })
  );
});

当我们在网络上请求文件时,fetch 事件处理程序会查询缓存以确定请求是否匹配,如果匹配,则从缓存中提供它们:

self.addEventListener('fetch', function(evt) {
    evt.respondWith(
        caches.match(evt.request)
          .then(function(res) {
            if(res.status >= 500) {
                console.log('RESPONSE: error');
                return Response.error();
            } else {
                console.log('RESPONSE: success');
                replayQueuedRequests();
                return res;
            }
        }).catch(function() {
            queueFailedRequest(evt.request);
        })
    );
});

此外,如果响应成功,则调用 replayQueuedRequests() 来运行队列中的任何挂起请求:

function replayQueuedRequests() {
    Object.keys(queue).forEach(function(evt) {
        fetch(queue[evt]).then(function(){
            if(res.status >= 500) {
                console.log('RESPONSE: error');
                return Response.error();
            }
            console.log('DELETE: queue');
            delete queue[error];
        }).catch(function() {
            if (Date.now() - evt > expiration) {
                delete queue[error];
                console.log('DELETE: queue');
            }
        });
    });
} 

如果获取失败,我们将请求排队:

function queueFailedRequest(request) {
    queue[Date.now()] = request.url;
    console.log('QUEUED: failed request');
} 

记录 API 分析

如果你被要求为现有应用程序实现 API 日志记录,你的方法会是什么?最常见的方法是更改客户端代码或服务器端代码,或者两者都更改。

通过使用服务工作者,我们可以拦截客户端请求并收集信息,然后将它们发送到日志 API。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一道菜谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下菜谱:设置 GitHub pages for SSL设置 Windows 的 SSL设置 Mac 的 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置下载所有文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/06/04/

  2. 在命令行上运行 npm install

    npm install
    
    
  3. 在命令行上运行 npm start

    npm start
    
    
  4. 打开浏览器并转到 index.html 文件:如何操作...

  5. 添加任务和优先级:如何操作...

  6. 现在通过点击 报告 链接转到报告页面:如何操作...

它是如何工作的...

index.html 文件中,我们添加了一个带有表单的章节:

<section id="todo-area">
    <p>Try to add and delete some todos.</p>
    <form id="add-form">
      <input type="text" id="new-todo" placeholder="Add a task here"/>
      <input type="text" id="priority" placeholder="Priority"/>
      <input type="submit" value="Add" />
    </form>
    <table id="todos">
    </table>
    <p>Go to <a href="https://localhost:3011/report/" target="_blank">report</a></p>
  </section>

service-worker.js 文件中,每次发起获取请求时,我们都会记录它:

self.onfetch = function(evt) {
  evt.respondWith(
    logRequest(evt.request).then(fetch)
  );
};

function logRequest(req) {
  var retRequest = function() {
    return req;
  };

  var data = {
    method: req.method,
    url: req.url
  };

  return fetch(URL, {
    method: 'POST',
    body: JSON.stringify(data),
    headers: { 'content-type': 'application/json' }
  }).then(retRequest, retRequest);
}

index.js 文件包含添加和删除待办事项的逻辑。我们首先在注册点显示待办事项列表:

navigator.serviceWorker.oncontrollerchange = function() {
    this.controller.onstatechange = function() {
      if (this.state === 'activated') {
        loadTodos();
      }
    };
};

通过点击添加按钮,创建一个新的待办事项并将其发送到服务器:

document.querySelector('#add-form').onsubmit = function(event) {

如果提供了待办事项,则跳过。如果没有提供优先级,则默认为 次要

if (!newTodo) {
    return;
   }

priority = document.querySelector('#priority').value.trim()
                    || 'Minor';

我们随后发送 API 请求,一个待办事项集合的 POST 请求:

fetch(URL, {
    method: 'POST',
    body: JSON.stringify(todo),
    headers: headers,
  }).then(function(response) {
      return response.json();
    }).then(function(addedTodo) {
      document.querySelector('#todos').appendChild(getRowFor(addedTodo));
    });
};

为了检索待办事项集合,我们使用 GET 方法发起一个获取请求:

function loadTodos() {
  fetch(URL).then(function(res) {
      return res.json();
    }).then(showTodos);
}

然后我们填充待办事项表:

function showTodos(items) {
  var table = document.querySelector('#todos');

  table.innerHTML = '';
  for (var i = 0, len = items.length, todo; i < len; i++) {
    todo = items[i];
    table.appendChild(getRowFor(todo));
  }

  if (window.parent !== window) {
    window.parent.document.body.dispatchEvent(new CustomEvent('iframeresize'));
  }
}

一个函数对于创建表格行很有用:

function getRowFor(todo) {
  var tr = document.createElement('TR'),
    id = todo.id;

  tr.id = id;

  tr.appendChild(getCell(todo.todo));
  tr.appendChild(getCell(todo.priority));
  tr.appendChild(todo.isSticky ? getCell('') : getDeleteButton(id));

  return tr;
}

为表格数据构建一个辅助函数:

function getCell(todo) {
  var td = document.createElement('TD');

  td.textContent = todo;
  return td;
}

构建删除按钮:

function getDeleteButton(id) {
  var td = document.createElement('TD'),
    btn = document.createElement('BUTTON');

  btn.textContent = 'Delete';
  btn.onclick = function() {
    deleteTodo(id).then(function() {
      var tr = document.getElementById(id);
      tr.parentNode.removeChild(tr);
    });
  };

  td.appendChild(btn);
  return td;
}

发起删除待办事项的 DELETE 请求:

function deleteTodo(id) {
  return fetch(URL + '/' + id, { method: 'DELETE' });
}

server.js 文件包含两个 API,其中一个用于待办事项管理,另一个用于日志。

我们提供了一套默认的待办事项以开始。这些待办事项将作为示例出现在我们的列表顶部。

var todos = [
  {
    todo: 'Buy milk',
    priority: 'Minor'
  },
  {
    todo: 'Refill car',
    priority: 'Medium'
  },
  {
    todo: 'Learn service worker',
    priority: 'Major'
  }
].map(function(todo, index) {
  todo.id = index + 1;
  todo.isSticky = true;

  return todo;
});

粘性标志将确保这些待办事项不可删除。

REST API 端点将管理添加和删除待办事项以及日志的请求:

app.get('/report', function(req, res) {
  var stats = getLogSummary();
  var buffer = report({ stats: stats });
  res.send(buffer);
});

app.post('/report/logs', function(req, res) {
  var logEntry = logRequest(req.body);
  res.status(201).json(logEntry);
});

app.get('/api/todos', function(req, res) {
  res.json(todos.filter(function(item) {
    return item !== null;
  }));
});

app.get('/api/todos', function(req, res) {
  res.json(todos.filter(function(item) {
    return item !== null;
  }));
});

app.delete('/api/todos/:id', function(req, res) {
  var id = parseInt(req.params.id, 10) - 1;
  if (!todos[id].isSticky) {
    todos[id] = null;
  }
  res.sendStatus(204);
});

app.post('/api/todos', function(req, res) {
  var todo = req.body;
  todo.id = todos.length + 1;
  todos.push(todo);
  res.status(201).json(todo);
});

我们为GETDELETEPOST请求创建一个用于日志报告的聚合函数:

function getLogSummary() {
  var aggr = requestsLog.reduce(function(subSummary, entry) {
    if (!(entry.url in subSummary)) {
      subSummary[entry.url] = {
        url: entry.url,
        GET: 0,
        POST: 0,
        DELETE: 0,
      };
    }
    subSummary[entry.url][entry.method]++;
    return subSummary;
  }, {});

  return Object.keys(aggr).map(function(url) {
    return aggr[url];
  });
}

report.html文件中,我们有一个用于渲染日志数据的模板。我们在服务器端使用 SWIG 来渲染此模板:

<table id="todos">
    <tr>
      <th>url</th>
      <th>GET</th>
      <th>POST</th>
      <th>DELETE</th>
    </tr>
    {% for entry in stats %}
    <tr>
      <td>{{ entry.url }}</td>
      <td class="counter">{{ entry.GET }}</td>
      <td class="counter">{{ entry.POST }}</td>
      <td class="counter">{{ entry.DELETE }}</td>
    </tr>
    {% endfor %}
  </table>

与 Google Analytics 协作

Google Analytics 是今天广泛使用的工具,大多数网站都使用它来收集访客的各种数据。在本菜谱中,我们将探讨在实现 Google Analytics 时如何从服务工作者中获益。

准备工作

要开始使用服务工作者,您需要在浏览器设置中开启服务工作者实验功能。如果您还没有这样做,请参阅第一章的第一个菜谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参阅以下菜谱:设置 GitHub 页面以支持 SSL设置 Windows 的 SSL设置 Mac 的 SSL

如何操作...

按照以下说明设置您的文件结构:

  1. 从以下位置下载所有文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/06/05/

  2. 打开浏览器并转到index.html文件:如何操作...

  3. 现在打开开发者工具(Cmd + Alt + IF12),并转到网络选项卡。您将看到/collect 请求的状态为 200,这意味着它们已成功:如何操作...

  4. 刷新页面。您将看到以下屏幕:如何操作...

  5. 转到开发者工具的控制台选项卡:如何操作...

工作原理...

service-worker.js文件中,在激活点,caches.delete(cacheName)将通过检查缓存名称来查找并删除冗余的过时缓存:

var version = 1,
  currentCaches = {
    'google-analytics': 'google-analytics-v' + version
  };

self.addEventListener('activate', function(event) {
  var cacheNamesExpected = Object.keys(currentCaches).map(function(key) {
    return currentCaches[key];
  });

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheNamesExpected.indexOf(cacheName) === -1) {
            console.log('DELETE: Out of date cache:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

服务工作者将缓存初始请求;对同一资源的后续请求将由服务工作者的fetch()事件处理器处理。fetch事件处理器在currentCaches缓存中查询请求,并将响应发送回浏览器:

event.respondWith(
    caches.open(currentCaches['google-analytics']).then(function(cache) {
      return cache.match(event.request).then(function(res) {
        if (res) {
          console.log(
            '%c ✓ RESPONSE: %c Found in cache: %s',
            'color: #5EBD00', 'color: #000000', res
          );

          return res;
        }

如果响应未找到,它将向网络发送 fetch 请求:

 return fetch(event.request.clone()).then(function(res) {
          console.log('%c ✓ RESPONSE: %c For %s from network: %O',
            'color: #5EBD00', 'color: #000000',
            event.request.url, res);

          if (res.status < 400) {
            cache.put(event.request, res.clone());
          } 

如果前一个请求的响应成功,响应将被克隆并添加到缓存中,请求作为键,响应作为值:

promises = promises.map(function(promise) {
      return Promise.resolve(promise);
    });

接下来,我们确保在数组中的另一个承诺解决后立即解决当前承诺:

if (res.status < 400) {
    cache.put(event.request, res.clone());
}

参见

  • 在第四章的实现读取缓存配方中,使用高级技术访问离线内容

第七章。获取资源

在本章中,我们将涵盖以下主题:

  • 获取远程资源

  • 使用 FetchEvent 获取

  • 在服务工作者安装期间获取 JSON 文件

  • 代理

  • 预加载

简介

在本章中,我们将探讨如何从不同的来源请求文件和资源,这在更技术性的术语中是对不同资源执行获取操作。我们将查看在服务工作者安装期间获取 JSON 文件,使用服务工作者作为代理中间件,并在安装过程中预加载一系列特定的资源 URL,以便在加载页面之前手头就有这些资源。

让我们从查看一个简单的例子开始,这个例子展示了从两个不同的资源中获取资源。

获取远程资源

获取远程资源可以通过不同的方式完成。在本食谱中,我们将探讨使用服务工作者以两种标准方式获取远程资源,一种是有跨源 HTTP 请求CORS),另一种则没有。

你可以在以下链接中了解更多关于 CORS 的信息:

developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章的设置 GitHub 页面以支持 SSL食谱。

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置复制 index.htmlindex.jsservice-worker.jsstyle.css 文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/07/01/

  2. 打开浏览器并转到 index.html。你会看到两个来自不同协议(httpshttps-acao)的图片:如何操作...

它是如何工作的...

index.js 文件的开始部分,我们正在测试两种不同的协议来加载资源:

  • https:带有安全套接字层SSL)协议的 HTTP

  • https-acao: 带有 Access-Control-Origin=* 头的 SSL 协议

我们将使用两个不同的 URL,这些 URL 将被多次加载:

var protocols = {
  'https-acao': 'https://i942.photobucket.com/albums/ad261/szaranger/Packt/packt-logo.png',
  'https': 'https://dz13w8afd47il.cloudfront.net/sites/all/themes/packt_v4/images/packtlib-logo-dark.png'
};

我们还将使用两种不同的方法来获取资源,一种是有 CORS,另一种则没有:

navigator.serviceWorker.getRegistration().then(function(registration) {
  var fetchModes = ['cors', 'no-cors'];

接下来,我们将检查服务工作者是否已注册:

if (!registration || !navigator.serviceWorker.controller) {  
    navigator.serviceWorker.register(
    './service-worker.js').then(function() {
      console.log('Service worker registered, reloading the page');
      window.location.reload();
    });

如果不是这样,我们将注册它并重新加载页面以确保客户端处于服务工作者的控制之下:

for (var protocol in protocols) {
  if (protocols.hasOwnProperty(protocol)) {
    buildImage(protocol, protocols[protocol]);

    for (var i = 0; i < fetchModes.length; i++) {
      var fetchMode = fetchModes[i],
      init = { 
        method: 'GET',
        mode: fetchMode,
        cache: 'default' 
      };

    }
  }
}

两个 for 循环遍历提供的协议数组并对每个协议进行请求。它们还使用每个 URL 构建一个 DOM 图像元素,并遍历 fetchModes 数组的每个模式。

init 对象包含你可能想要应用于请求的任何自定义设置。让我们看看 init 对象的属性:

  • method: 请求方法,例如,GETPOST

  • mode: 你想要用于请求的模式,例如,corsno-corssame-origin

  • cache: 你想要用于请求的缓存模式,例如 defaultno-storereloadno-cacheforce-cacheonly-if-cached

buildImage 函数接受两个参数,protocolurl。它动态创建一个图像元素,并将 URL 作为该图像的源。然后它继续将该图像添加到 DOM 树中,其中 ID 是 https-acao-imagehttps-imagehttp–image 之一。在此点,JavaScript 对 URL 处理没有控制权;浏览器处理 URL:

function buildImage(protocol, url) {
  var element = protocol + '-image',
    image = document.createElement('img');

  image.src = url;
  document.getElementById(element).appendChild(image);
}

只有 HTTPS 请求的图像将被渲染,因为服务工作者只支持通过 SSL 的连接。

带有 Access-Control-Origin=* 头部(访问控制允许源)的 SSL 请求将成功返回结果。

默认情况下,如果第三方 URL 不支持 CORS,则从第三方 URL 获取资源将失败。你可以在请求中添加非 CORS 选项来克服这一点。然而,这将导致一个不透明的响应,这意味着你将无法判断响应是否成功。

fireRequest 函数接受三个参数,fetchModeprotocolinit。此函数返回另一个函数,我们可以将其作为组合调用。我们从直接从远程资源获取指定的资源开始:

 fetch(url, init).then(function(response) {
    printSuccess(response, url, section);
 }).catch(function(error) {
    printError(error, url, section);
 });

如果获取成功,我们将它打印到控制台并在网页上记录。如果请求失败,也适用同样的方法,只是这次我们打印错误。

辅助函数 log 通过 ID 查找 DOM 元素,并添加一个段落元素以及一个类属性来描述消息的类型:

function log(id, message, type) {
  var type = type || 'success',
    sectionElement = document.getElementById(id),
    logElement = document.createElement('p');

  if (type) {
    logElement.classList.add(type);
  }
  logElement.textContent = message;
  sectionElement.appendChild(logElement);
}

index.html 文件中,我们在头部部分有样式声明:

<style>
.error {
     color: #FF0000;
   }
   .success {
     color: #00FF00;
   }
</style>

在我们的 log() 函数中,我们将未定义类型设置为成功,以便当我们将其添加到 classList 时,它将显示绿色。错误类型将显示红色,如前面声明的样式。

使用 FetchEvent 进行获取

FetchEvent 类是一个在服务工作者上分发的获取动作。它包含有关请求以及响应的详细信息。它提供了所有重要的 FetchEvent.reponseWith() 方法,我们可以使用这些方法向由服务工作者控制页面提供响应。

准备中

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个菜谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章的以下菜谱,学习服务工作者基础设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置复制index.htmlindex.jsservice-worker.jsadeobe-log.pngstyle.css文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/07/02/

  2. 打开浏览器并转到index.html如何操作...

  3. 打开开发者工具栏(Cmd + Alt + IF12)。现在刷新页面并查看控制台中的消息。你将看到 fetch 请求被记录在控制台中:如何操作...

它是如何工作的...

我们只是将服务工作者 fetch 事件处理程序中的不同阶段打印到控制台。我们的service-worker.js文件的 fetch 方法看起来如下:

var cacheName= 'fetch-event-cache';

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheName)
      .then(function(cache) {
        return cache.addAll([
          'adobe-logo.png',
          'style.css',
          'index.html',
          'index.js',
          'style.css'
        ]);
      })
      .then(function() {
        return self.skipWaiting();
      })
  );
});

self.addEventListener('fetch', function(event) {
  console.log('Handling fetch event for', event.request.url);

  event.respondWith(
    caches.match(event.request).then(function(res) {
      if (res) {
        console.log('Fetching from cache:', res);

        return res;
      }
      console.log('No response from cache. Fetching from network...');

      return fetch(event.request).then(function(res) {
        console.log('Response from network:', res);

        return res;
      }).catch(function(error) {
        console.error('ERROR: Fetching failed:', error);

        throw error;
      });
    })
  );
});

让我们更详细地讨论一些这些 API 方法。

Cache.addAll()

此方法接受一个 URL 数组,检索响应,然后将该结果添加到指定的缓存。在我们的例子中,指定的缓存是'fetch-event-cache'

var cacheName= 'fetch-event-cache';

ExtendableEvent.waitUntil()

此方法延长了事件的寿命。在我们的例子中,我们等待资源被缓存:

.then(function(cache) {
        return cache.addAll([
          'adobe-logo.png',
          'style.css',
          'index.html',
          'index.js',
          'style.css'
        ]);
      })

FetchEvent.respondWith()

此方法通过返回一个Response对象或网络错误到Fetch对象来解析:

event.respondWith(
    caches.match(event.request).then(function(res) {

在服务工作者安装期间获取 JSON 文件

在这个菜谱中,我们将学习如何通过指定 JSON 文件中资源文件的名称来使用 JSON 文件缓存资源文件。通常,这是通过在服务工作者 JavaScript 文件中保持一个数组来完成的,但你可能希望它们位于单独的位置,例如出于版本控制的原因。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个配方,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章的以下配方:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,以及在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置下载所有文件:

  2. github.com/szaranger/szaranger.github.io/blob/master/service-workers/07/03/

  3. 打开浏览器并转到index.html文件:如何操作...

  4. 现在打开开发者工具(Cmd + Alt + IF12)并确保点击了保留日志复选框。现在刷新页面,你会看到从缓存检索文件的日志消息:如何操作...

  5. 如果你查看资源选项卡,你会看到缓存的资源:如何操作...

它是如何工作的...

我们只需要执行一个动作,即在初始加载时缓存资源和注册服务工作者。因此,在安装时,我们加载 JSON 文件,解析 JSON,并将文件添加到缓存中。我们的service-worker.js文件看起来像这样:

var cacheName= 'fetch-json';

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheName)
      .then(function(cache) {
        return fetch('files.json').then(function(response) {
          return response.json();
        }).then(function(files) {
          console.log('Installing files from JSON file: ', files);
          return cache.addAll(files);
        });
      })
      .then(function() {
        console.log(
          'All resources cached'
        );

        return self.skipWaiting();
      })
  );
});

如果在缓存中找到响应,则返回:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        if (response) {
          console.log('Fetching from the cache: ', event.request.url);
          return response;
        } else {
          console.log('Fetching from server: ', event.request.url);
        }
       return fetch(event.request);
     }
   )
 );
});

客户端声明服务工作者:

self.addEventListener('activate', function(event) {
   console.log('Activating the service worker!');
   event.waitUntil(self.clients.claim());
}); 

我们将在files.json文件中列出资源文件名:

[
  "adobe-logo.png",
  "apple-logo.png",
  "google-logo.png",
  "style.css",
  "index.html",
  "index.js",
  "style.css"
]

我们将在index.html文件中添加一个用于图像的部分:

<section>
    <h2>Assets from JSON</h2>
    <img src="img/adobe-logo.png" alt="adobe logo">
    <img src="img/apple-logo.png" alt="apple logo">
    <img src="img/google-logo.png" alt="google logo">
</section>

代理

代理是网络浏览器和互联网之间的中介。在本教程中,我们将学习如何使用服务工作者作为代理中间件。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个配方,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下配方:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,以及在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置下载所有文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/07/04/

  2. 打开浏览器并转到index.html如何操作...

  3. 现在点击第一个链接导航到/hello链接:如何操作...

  4. 现在打开开发者工具(Cmd + Alt + IF12),在控制台标签页查看日志消息:如何操作...

  5. 现在点击第一个链接导航到/hello/world链接:如何操作...

它是如何工作的...

我们在计划创建代理的index.html文件中添加了两个链接:

<section >
    <h1>Proxy</h1>
    <p>Click the links below for navigating to fetch handlers:</p>
    <div class="links">
      <a href="/service-workers/07/04/hello">/hello</a><br />
      <a href="/service-workers/07/04/hello/world">/hello/world</a>
    </div>
</section>

我们在service-worker.js文件中为请求本地 URL 创建了一个代理,包含一个hello字符串以及hello/world。客户端会将其识别为本地资源:

var helloFetchHandler = function(event) {
  if (event.request.url.indexOf('/hello') > 0) {
    console.log('DEBUG: Inside the /hello handler.');
    event.respondWith(new Response('Fetch handler for /hello'));
  }
};

var helloWorldFetchHandler = function(event) {
  if (event.request.url.endsWith('/hello/world')) {
    console.log('DEBUG: Inside the /hello/world handler.');
    event.respondWith(new Response('Fetch handler for /hello/world'));
  }
};

我们将这些处理程序作为回调函数传递给 fetch 事件监听器:

var fetchHandlers = [helloWorldFetchHandler, helloFetchHandler];

fetchHandlers.forEach(function(fetchHandler) {
  self.addEventListener('fetch', fetchHandler);
}); 

预取

在服务工作者安装阶段预取资源可以轻松实现网站的离线查看。在本食谱中,我们将探讨预取包括页面和图像在内的资源。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下食谱:设置 GitHub 页面以支持 SSL设置 Windows 的 SSL,和设置 Mac 的 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置下载所有文件:

  2. github.com/szaranger/szaranger.github.io/blob/master/service-workers/07/05/

  3. 打开浏览器并转到index.html文件:如何操作...

  4. 打开开发者工具栏(Cmd + Alt + IF12),查看控制台中的消息。你会看到资源已被成功缓存:如何操作...

  5. 现在通过在开发者工具的网络标签页选择离线来离线:如何操作...

  6. 现在点击prefetched.txt链接。链接的文本文件将在新标签页中打开:如何操作...

  7. 点击prefetched.html链接。链接的页面将在新标签页中打开:如何操作...

  8. 点击apple-log.png链接。链接的图片将在新标签页中打开:如何操作...

  9. 如果你检查控制台,你会找到链接是从缓存中提供的:如何操作...

它是如何工作的...

index.html文件中,我们添加了一个包含指向预取文件的链接的部分:

  <section id="prefetched">
    <ul>
      <li><a href="prefetched.txt" target="_blank">prefetched.txt</a></li>
      <li><a href="prefetched.html" target="_blank">prefetched.html</a></li>
      <li><a href="apple-logo.png" target="_blank">apple-logo.png</a></li>
    </ul>
  </section>

service-worker.js 文件中,我们在顶部声明缓存版本,以防您需要强制服务工作者控制的页面使用新的缓存:

var cacheName= 'cache';
var currentCaches = {
  prefetch: 'prefetch-' + cacheName
};

我们还列出了要缓存的资源:

var prefetchedURLs = [
    'prefetched.txt',
    'prefetched.html',
    'apple-logo.png'
];

以下行将根据我们提供的列表构建一个新的 URL,使用服务工作者脚本的当前位置作为其基础:

  var url = new URL(prefetchedURLs, location.href);

接下来,我们在查询字符串中添加一个缓存破坏时间戳:

url.search += (url.search ? '&' : '?') + 'cache-bust=' + Date.now();

如果有可能服务器不支持 CORS,则必须确保指定 {mode: 'no-cors'}

var request = new Request(url, {mode: 'no-cors'});

接下来,我们获取资源并将它们缓存起来:

return fetch(request).then(function(res) {
    if (res.status >= 400) {
        throw new Error('FAIL: request for ' + prefetchedURLs +
        ' failed, status ' + res.statusText);
    }
    console.log('CACHING: Caching');
    return cache.put(prefetchedURLs, res);
}).catch(function(err) {
    console.error('CACHING: Not caching ' + prefetchedURLs + ' due to ' + err);
});

现在让我们看看激活事件处理器。我们确保删除所有不在我们最初声明的 currentChaches 对象中的缓存:

    self.addEventListener('activate', function(evt) {
        var expectedCacheNames = Object.keys(currentCaches).map(function(key) {
            return currentCaches[key];
        });

        evt.waitUntil(
            caches.keys().then(function(cacheNames) {
              return Promise.all(
                cacheNames.map(function(cacheName) {
                  if (expectedCacheNames.indexOf(cacheName) === -1) {
                    console.log('DELETE: out of date cache:', cacheName);
                    return caches.delete(cacheName);
                  }
                })
              );
            })
        );
    });

获取事件监听器是服务工作者查找缓存资源的地方:

self.addEventListener('fetch', function(evt) {
  console.log('FETCH: Handling fetch event for ', evt.request.url);

  evt.respondWith(
    caches.match(evt.request).then(function(res) {
      if (res) {
        console.log('RESPONSE: found in cache:', res);

        return res;
      }

      console.log('RESPONSE: not found in cache. Fetching from network.');

      return fetch(evt.request).then(function(res) {
        console.log('RESPONSE: from network:', res);

        return res;
      }).catch(function(error) {
        console.error('FAIL: fetching :', error);

        throw error;
      });
    })
  );
});

第八章. 尝试 Web 推送

在本章中,我们将涵盖以下主题:

  • 实现简单的推送通知

  • 显示丰富的推送通知

  • 使用通知标签

  • 实现推送客户端

  • 订阅推送通知

  • 管理推送通知配额

简介

在过去几年中,推送通知因其手机应用程序中的功能而受到欢迎。无论你是否打开了应用程序,在前台运行,或者根本未运行,推送通知都会在你的手机上弹出一个消息。类似地,在编写本文时,有一个新的 Web API 可用,称为 Push API,它是一种实验性技术。为了使 Push API 工作,我们需要有一个活跃的服务工作者运行,并且必须已订阅推送通知。

让我们从本章开始,看看如何实现一个简单的推送通知。

实现简单的推送通知

获取远程资源可以通过不同的方式完成。在这个菜谱中,我们将探讨使用服务工作者获取远程资源的两种标准方式,包括带有和不带有 跨源 HTTP 请求CORS)。

如果你想了解更多关于 CORS 的信息,请点击以下链接:

developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考 第一章 的 设置服务工作者 菜谱,学习服务工作者基础。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考 第一章 的 设置 GitHub pages for SSL 菜谱,学习服务工作者基础

如何实现...

按照以下说明设置你的文件结构:

  1. 从以下位置复制 index.htmlindex.jsservice-worker.jsmanifest.jsonserver.jspackage.jsonstyle.css 文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/08/01/

  2. 在命令行中运行 npm install

  3. 前往 console.developers.google.com/project 创建一个 API 项目。获取一个发送者 ID(项目编号)并在 manifest.json 文件中替换 gcm_sender_id。同时替换 server.js 文件中的 <GCM API KEY> 占位符。

    webPush.setGCMAPIKey(/*GCM API KEY*/);
    
  4. 运行 npm start 以启动服务器。

  5. 打开浏览器并前往 index.html。确保你不在隐身模式下打开浏览器。点击 发送通知! 按钮发送通知。如何实现...

  6. 打开开发者工具栏(Cmd + Alt + IF12)。现在刷新页面并查看控制台中的消息。你会看到 fetch 请求被记录到控制台中。如何操作...

  7. 浏览器会提示你允许通知。如何操作...

  8. 很快你就会收到通知。(这可能会根据你的配置需要一些时间。)如何操作...

它是如何工作的...

index.js文件的开始处,我们指定用于服务器的基准 URL。

var baseURL = 'https://localhost:3012/';

接下来,为了获取用户的推送服务订阅,我们使用pushManager

return registration.pushManager.getSubscription()

如果找到了订阅,它将被返回。否则,用户将被订阅。

return registration.pushManager.getSubscription()
  .then(function(subscription) {
    if (subscription) {
      return subscription;
    }

    return registration.pushManager.subscribe({ userVisibleOnly: true });
  });

接下来,我们将向服务器发送订阅详情。

fetch(baseURL + 'register', {
    method: 'post',
    headers: {
      'Content-type': 'application/json'
    },
    body: JSON.stringify({
      endpoint: subscription.endpoint,
    }),
  });

这将使服务器向客户端发送通知。

document.querySelector('#send').onclick = function() {
  var delay = document.querySelector('#notification-delay').value;
  var ttl = document.querySelector('#notification-ttl').value;

  fetch(baseURL + 'sendNotification?endpoint=' + endpoint + '&delay=' + delay +
        '&ttl=' + ttl,
    {
      method: 'post',
    }
  );
};

我们将在index.html文件中添加一个用于延迟时间和活动时间的输入字段部分。

<section id="notification-input">
    <form>
      <h1>Notification</h1>
      Delay Time&nbsp;<input id='notification-delay' type='number' value='2'></input> <small>s</small><br/>
      <br/>
      Active Time&nbsp;<input id='notification-ttl' type='number' value='0'></input> <small>s</small><br/>
    </form>
    <button id="send">Send Notification!</button>
</section>

我们将使用manifest.json文件以支持 Google Chrome。

{
  "name": "Simple Push Notification",
  "short_name": "push-simple",
  "start_url": "./index.html",
  "display": "standalone",
  "gcm_sender_id": "46143029380",
  "gcm_user_visible_only": true
}

service-worker.js文件中,我们将添加一个用于注册推送事件的监听器。

'use strict';

self.addEventListener('push', function(event) {
  event.waitUntil(
    self.registration.showNotification('SW Push Notification', {
      body: 'Notification received!',
    })
  );
}); 

显示丰富通知

丰富的推送通知可以发送图片、振动模式和本地化通知。让我们看看我们如何实现这一点。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章中的设置服务工作者配方,学习服务工作者基础。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章中的设置 GitHub 页面以支持 SSL配方,学习服务工作者基础

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置复制index.htmlindex.jsservice-worker.jsmanifest.jsonserver.jspackage.jsonamazon-logo.pngstyle.css文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/08/03/

  2. 打开浏览器并转到index.html如何操作...

  3. 你可以通过更改输入字段来更改延迟时间和活动时间。如何操作...

  4. 你可能会被提示允许推送通知。如何操作...

  5. 很快你就会收到通知。(这可能会根据你的配置需要一些时间。)如何操作...

  6. 打开开发者工具栏(Cmd + Alt + IF12)。现在刷新页面并查看控制台中的消息。如何操作...

它是如何工作的...

index.js文件中,我们将处理用户点击按钮时的点击事件。

这将使服务器向客户端发送通知。

document.querySelector('#send').onclick = function() {
  var delay = document.querySelector('#notification-delay').value;
  var ttl = document.querySelector('#notification-ttl').value;

  fetch(baseURL + 'sendNotification?endpoint=' + endpoint + '&delay=' + delay +
        '&ttl=' + ttl,
    {
      method: 'post',
    }
  );
};

service-worker.js文件中,我们将添加一个事件监听器来注册推送事件。

'use strict';

self.addEventListener('push', function(event) {
  event.waitUntil(
    self.registration.showNotification('SW Rich Push Notification', {
      body: 'Richer than richest',
      icon: 'amazon-logo.png',
      vibrate: [300, 100, 300]
    })
  );
});

使用通知标签

为了替换旧的通知,我们可以使用通知标签。这将帮助我们向用户展示最新的信息。

这个食谱将展示如何管理通知队列,并丢弃之前的通知或将它们合并为一个单一的通知。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的开发生态系统,请参考第一章的设置 GitHub 页面以支持 SSL食谱。

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置复制index.htmlindex.jsservice-worker.jsmanifest.jsonserver.jspackage.jsonstyle.css文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/08/04/

  2. 打开浏览器并转到index.html。如何操作...

  3. 很快你将收到通知。(这可能会根据你的配置需要一些时间。)如何操作...

它是如何工作的...

service-worker.js文件中,我们将添加一个事件监听器来注册推送事件。注意传递给showNotification方法的tag元素。

'use strict';

self.addEventListener('push', function(event) {
  event.waitUntil(
     self.registration.showNotification('SW Push Notification', {
      body: 'Notification ' + count++,
      tag: 'swc'
    })  );
});

实现推送客户端

推送客户端使我们能够在用户点击通知消息时专注于我们的应用程序正在运行的标签页。我们甚至可以在应用程序关闭后重新打开它。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的开发生态系统,请参考第一章的以下食谱:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置复制index.htmlindex.jsservice-worker.jsmanifest.jsonserver.jspackage.jsonstyle.css文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/08/05/

  2. 从命令行运行npm install

  3. 前往console.developers.google.com/project并创建一个 API 项目。获取一个发送者 ID(项目编号)并在manifest.json文件中替换gcm_sender_id。同时替换server.js文件中的<GCM API KEY>占位符。

    webPush.setGCMAPIKey(/*GCM API KEY*/);
    
  4. 运行npm start以启动服务器。

  5. 打开浏览器并前往index.html。确保您没有在隐身模式下打开浏览器。在有效载荷字段中输入一些文本,然后点击发送通知按钮以发送通知。如何操作...

  6. 打开开发者工具栏(Cmd + Alt + IF12)。现在刷新页面并查看控制台中的消息。您将看到 fetch 请求被记录到控制台中。如何操作...

  7. 浏览器将提示您允许通知。如何操作...

  8. 很快您将开始收到通知。(这可能会根据您的配置需要一些时间。)如何操作...

  9. 点击这个第一个通知将带您到应用程序运行所在的页面。如何操作...

  10. 点击第二个通知将不会执行任何操作。如何操作...

  11. 点击第三个通知将打开一个新页面,其中包含运行中的应用程序。

工作原理...

index.js文件的开始处,我们将指定我们用于服务器的基准 URL。

var baseURL = 'https://localhost:3012/'; 

接下来,为了获取用户对推送服务的订阅,我们使用pushManager

return registration.pushManager.getSubscription()

如果找到订阅,这将返回。否则,用户将被订阅。

return registration.pushManager.getSubscription()
  .then(function(subscription) {
    if (subscription) {
      return subscription;
    }

    return registration.pushManager.subscribe({ userVisibleOnly: true });
});

接下来,我们将发送订阅详情到服务器。

fetch(baseURL + 'register', {
    method: 'post',
    headers: {
      'Content-type': 'application/json'
    },
    body: JSON.stringify({
      endpoint: subscription.endpoint,
    }),
});

发送按钮使服务器能够向客户端发送我们在表单中指定的有效载荷的通知。

document.querySelector('#send').onclick = function() {
  fetch(baseURL + 'sendNotification?endpoint=' + endpoint, {
      method: 'post',
  });
};

我们将在index.html文件中添加一个部分,用于指导用户关于通知消息。

<section id="notification-input">
    <form>
      <h1>Notification</h1>
      <p><strong>Click 'Send notification' &amp; try:</strong></p>
      <ul>
        <li>Close this page: Click the notification, and it will be reopened.</li>
        <li>Switch to another page: Click the notification, and it will be re-focused</li>
        <li>Remain on current page: A different notification will be shown, Clicking will not do anything.</li>
      </ul>
    </form>
    <button id="send">Send Notification!</button>
</section>

我们使用manifest.json文件以支持 Google Chrome。

{
  "name": "SW Push Clients Notification",
  "short_name": "push-clients",
  "start_url": "./index.html",
  "display": "standalone",
  "gcm_sender_id": "46143029380",
  "gcm_user_visible_only": true
}

service-worker.js文件中,我们将接收有效载荷并为注册推送事件添加事件监听器。

'use strict';

self.addEventListener('push', function(event) {
  event.waitUntil(
    self.clients.matchAll().then(function(clients) {

      var focused = clients.some(function(client) {
        return client.focused;
      });

      var notificationMessage;

      if (focused) {
        notificationMessage = 'Same Page';
      } else if (clients.length > 0) {
        notificationMessage = 'Diffrerent Page, ' +
                              'click here to gain focus';
      } else {
        notificationMessage = 'Page Closed, ' +
                              'click here to re-open it!';
      }

      return self.registration.showNotification('ServiceWorker Cookbook', {
        body: notificationMessage,
      });
    })
  );
});

订阅推送通知

这个菜谱将教会您如何使用带有订阅管理的推送通知,使用户能够订阅应用程序将公开的功能,以保持联系。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章的以下食谱,学习服务工作者基础设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置复制index.htmlindex.jsservice-worker.jsmanifest.jsonserver.jspackage.jsonstyle.css文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/08/06/

  2. 从命令行运行npm install

  3. 前往console.developers.google.com/project创建一个 API 项目。获取一个发送者 ID(项目编号)并在manifest.json文件中替换gcm_sender_id。同时替换server.js文件中的<GCM API KEY>占位符。

    webPush.setGCMAPIKey(/*GCM API KEY*/);
    
  4. 运行npm start以启动服务器。

  5. 打开浏览器并转到index.html。确保你不在隐身模式下打开浏览器。点击订阅按钮。按钮将变为取消订阅如何操作...

  6. 打开开发者工具栏(Cmd + Alt + IF12)。现在刷新页面并查看控制台中的消息。你会看到 fetch 请求被记录到控制台中。如何操作...

  7. 浏览器可能会提示你允许通知。如何操作...

  8. 很快你就会收到通知。(这可能会根据你的配置需要一些时间。)一旦你点击取消订阅按钮,通知将不再显示。如何操作...

它是如何工作的...

index.js文件的开始处,我们将指定我们用于服务器的基准 URL。

var baseURL = 'https://localhost:3012/'; 

接下来,为了获取用户对推送服务的订阅,我们使用pushManager

return registration.pushManager.getSubscription()

如果找到了订阅,它将被返回。否则,用户将被订阅。

return registration.pushManager.getSubscription()
  .then(function(subscription) {
    if (subscription) {
      return subscription;
    }

    return registration.pushManager.subscribe({ userVisibleOnly: true });
});

接下来,我们将向服务器发送订阅详情。

fetch(baseURL + 'register', {
    method: 'post',
    headers: {
      'Content-type': 'application/json'
    },
    body: JSON.stringify({
      endpoint: subscription.endpoint,
    }),
  });

该按钮使服务器能够向客户端发送我们以表单形式指定的有效载荷的通知。

document.querySelector('#subscription-button').onclick = function() {
  fetch(baseURL + 'sendNotification?endpoint=' + endpoint, {
      method: 'post',
  });
};

为了管理订阅,我们将添加切换逻辑。

function unsubscribe() {
  getSubscription().then(function(subscription) {
    return subscription.unsubscribe()
      .then(function() {
        console.log('Unsubscribed', subscription.endpoint);
        return fetch('unregister', {
          method: 'post',
          headers: {
            'Content-type': 'application/json'
          },
          body: JSON.stringify({
            endpoint: subscription.endpoint
          })
        });
      });
  }).then(setSubscribeButton);
}

function setSubscribeButton() {
  subscriptionBtn.onclick = subscribe;
  subscriptionBtn.textContent = 'Subscribe!';
}

function setUnsubscribeButton() {
  subscriptionBtn.onclick = unsubscribe;
  subscriptionBtn.textContent = 'Unsubscribe!';
}

我们将在index.html文件中添加一个部分,用于指导用户了解通知消息。

<section id="notification-input">
    <form>
      <h1>Notification</h1>
      <p>Click to subscribe</p>
    </form>
    <button id="subscription-button" disabled=true></button>
</section>

我们将使用manifest.json文件以支持 Google Chrome。

{
  "name": "SW Push Notification Subscription Management",
  "short_name": "push-with_subscription",
  "start_url": "./index.html",
  "display": "standalone",
  "gcm_sender_id": "46143029380",
  "gcm_user_visible_only": true
}

service-worker.js文件中,我们将接收有效载荷并为注册推送事件添加事件监听器。

'use strict';

self.addEventListener('push', function(event) {
  event.waitUntil(self.registration.showNotification('ServiceWorker Cookbook', {
    body: 'Push Notification Subscription Management'
  }));
});

self.addEventListener('pushsubscriptionchange', function(event) {
  console.log('Subscription expired');
  event.waitUntil(
    self.registration.pushManager.subscribe({ userVisibleOnly: true })
    .then(function(subscription) {
      console.log('Subscribed after expiration', subscription.endpoint);
      return fetch('register', {
        method: 'post',
        headers: {
          'Content-type': 'application/json'
        },
        body: JSON.stringify({
          endpoint: subscription.endpoint
        })
      });
    })
  );
});

server.js文件中,我们将向推送服务发送通知。

function sendNotification(endpoint) {
  webPush.sendNotification(endpoint).then(function() {
    console.log('Push Application Server - Notification sent to ' + endpoint);
  }).catch(function() {
    console.log('ERROR in sending Notification, endpoint removed ' + endpoint);
    subscriptions.splice(subscriptions.indexOf(endpoint), 1);
  });
}

为了演示目的,我们将通过在每pushInterval内向注册的端点发送通知来模拟已发生的事件。因此,您将看到通知快速到来。

setInterval(function() {
  subscriptions.forEach(sendNotification);
}, pushInterval * 1000);

function isSubscribed(endpoint) {
  return (subscriptions.indexOf(endpoint) >= 0);
}

管理推送通知配额

在这个配方中,我们将对不同浏览器的配额管理策略进行实验。我们将尝试发送尽可能多的通知来测试打开和关闭标签页、点击通知以及忽略通知的情况。

准备工作

要开始使用服务工作线程,您需要在浏览器设置中开启服务工作线程实验功能。如果您还没有这样做,请参考第一章的设置服务工作线程配方,学习服务工作线程基础。服务工作线程仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考以下第一章的配方,学习服务工作线程基础设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

如何操作...

按照以下说明设置您的文件结构:

  1. 从以下位置复制index.htmlindex.jsservice-worker.jsmanifest.jsonserver.jspackage.jsonstyle.css文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/08/07/

  2. 从命令行运行npm install

  3. 前往console.developers.google.com/project并创建一个 API 项目。获取一个发送者 ID(项目编号)并在manifest.json文件中替换gcm_sender_id。同时,在server.js文件中替换<GCM API KEY>占位符。

    webPush.setGCMAPIKey(/*GCM API KEY*/);
    
  4. 运行npm start以启动服务器。

  5. 打开浏览器并转到index.html。确保您没有在隐身模式下打开浏览器。点击可见通知不可见通知按钮。如何操作...

  6. 打开开发者工具栏(Cmd + Alt + IF12)。现在刷新页面并查看控制台中的消息。您将看到 fetch 请求被记录到控制台中。如何操作...

  7. 您的浏览器可能会提示您允许通知。如何操作...

它是如何工作的...

index.js文件的开始处,我们将指定我们用于服务器的基准 URL。

var baseURL = 'https://localhost:3012/'; 

接下来,为了获取用户对推送服务的订阅,我们使用pushManager

return registration.pushManager.getSubscription()

如果找到订阅,则将其返回。否则,用户将被订阅。

return registration.pushManager.getSubscription()
  .then(function(subscription) {
    if (subscription) {
      return subscription;
    }

  return registration.pushManager.subscribe({ userVisibleOnly: true });
});

我们还将检索用户的公钥。

var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
key = rawKey ?
btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
authSecret = rawAuthSecret ?
btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';

  endpoint = subscription.endpoint;

接下来,我们将发送订阅详情到服务器。

fetch(baseURL + 'register', {
    method: 'post',
    headers: {
      'Content-type': 'application/json'
    },
    body: JSON.stringify({
      endpoint: subscription.endpoint,
      key: key,
      authSecret: authSecret
    }),
  });

我们将请求服务器向客户端发送通知以进行测试。

fetch(baseURL + 'sendNotification', {
    method: 'post',
    headers: {
      'Content-type': 'application/json'
    },
    body: JSON.stringify({
      endpoint: endpoint,
      key: key,
      visible: visible,
      num: notificationNum,
    }),
  });

清除按钮清除通知缓存,该缓存存储收到的通知数量。

document.querySelector('#clear').onclick = function() {
  window.caches.open('notifications').then(function(cache) {
    Promise.all([
      cache.put(new Request('invisible'), new Response('0', {
        headers: {
          'content-type': 'application/json'
        }
      })),
      cache.put(new Request('visible'), new Response('0', {
        headers: {
          'content-type': 'application/json'
        }
      })),
    ]).then(function() {
      updateNotificationNumbers();
    });
  });
};

通过读取收到的通知数量来更新用户界面。

function updateNotificationNumbers() {
  window.caches.open('notifications').then(function(cache) {
    ['visible', 'invisible'].forEach(function(type) {
      cache.match(type).then(function(res) {
        if(res) {
          res.text().then(function(text) {
            document.getElementById('sent-' + type).textContent = text;
          });
        }
      });
    });
  });
}

此外,定期更新收到的通知数量。

window.onload = function() {
  updateNotificationNumbers();
  setInterval(updateNotificationNumbers, 1000);
};

我们将在index.html文件中添加一个部分,用于指导用户关于通知消息。

<section id="notification-input">
    <ul>
      <li>Select <strong>Visible notifications</strong> - They will appear one after another on your screen. The quota will not be enforced;</li>
      <li>Select <strong>Invisible notifications</strong> - They won't appear on your screen, but the number at the bottom of the page will update once the page is re-opened, to indicate the number of invisible notifications sent before reaching the quota.</li>
    </ul>

    <form>
    Notifications to send: <input id="notification-count" type="number" value="30"></input>
    </form>

    <button id="visible">Visible notifications</button>
    <button id="invisible">Invisible notifications</button>

    <p>Received <span id="sent-visible">0</span> visible notifications<br />
    Received <span id="sent-invisible">0</span> invisible notifications</p>
    <button id="clear">Clear</button>
  </section> id="subscription-button" disabled=true></button>
</section>

我们将使用manifest.json文件以支持 Google Chrome。

{
  "name": "SW Push Quota",
  "short_name": "push-quota",
  "start_url": "./index.html",
  "display": "standalone",
  "gcm_sender_id": "46143029380",
  "gcm_user_visible_only": true
}

service-worker.js文件中,我们保持服务工作者(service worker)处于活跃状态,直到通知缓存更新。

self.addEventListener('push', function(event) {

  var visible = event.data ? event.data.json() : false;

  if (visible) {
    event.waitUntil(updateNumber('visible').then(function(num) {
      return self.registration.showNotification('SW', {
        body: 'Received ' + num + ' visible notifications',
      });
    }));
  } else {
    event.waitUntil(updateNumber('invisible'));
  }
});

我们将创建一个通知缓存来存储收到的通知。

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheName).then(function(cache) {
      return Promise.all([
        cache.put(new Request('invisible'), new Response('0', {
          headers: {
            'content-type': 'application/json'
          }
        })),
        cache.put(new Request('visible'), new Response('0', {
          headers: {
            'content-type': 'application/json'
          }
        })),
      ]);
    })
  );
});

server.js文件中,我们将向推送服务发送通知。

app.post('/sendNotification', function(req, res) {
  var num = 1;
  var promises = [];

  var intervalID = setInterval(function() {
     promises.push(webPush.sendNotification(req.body.endpoint, {
       TTL: 200,
       payload: JSON.stringify(req.body.visible),
       userPublicKey: req.body.key,
       userAuth: req.body.authSecret,
     }));

     if (num++ === Number(req.body.num)) {
       clearInterval(intervalID);

       Promise.all(promises)
       .then(function() {
         res.sendStatus(201);
       });
     }
   }, 1000);
});

第九章. 查看通用用法

在本章中,我们将涵盖以下主题:

  • 立即控制页面

  • 处理缓慢的响应

  • 传递消息

  • 使用服务工作者作为代理中间件

  • 使用带有实时流程图的服务工作者

简介

在本章中,我们将探讨一些服务工作者可能变得有用的场景。这些场景可能属于通用类别。我们将在此处查看的示例可以用作构建其他服务工作者功能的基础。

立即控制页面

这个简单的配方将演示如何让服务工作者立即控制页面,而无需等待导航事件。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章中的设置服务工作者配方,学习服务工作者基础。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章中的设置 GitHub 页面以支持 SSL配方,学习服务工作者基础

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置复制 index.htmlindex.jsservice-worker.jsstyle.css 文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/09/01/

  2. 打开浏览器并访问 index.html如何操作...

  3. 刷新页面。服务工作者的状态将显示在屏幕上。

它是如何工作的...

在我们的 index.html 文件中,我们将添加一个部分来显示服务工作者的状态。

<section>
    <h3>Immediate Control</h3>
    <p>Status: 
        <strong><span id="control-status"></span></strong>
    </p>
</section>

index.js 文件的开始处,我们将检查注册状态并将其打印到屏幕上。

if (registration.installing) {
    serviceWorker = registration.installing;
    document.querySelector('#control-status')
    .textContent = 'Installing';
} else if (registration.waiting) {
    serviceWorker = registration.waiting;
    document.querySelector('#control-status')
    .textContent = 'Waiting';
} else if (registration.active) {
    serviceWorker = registration.active;
    document.querySelector('#control-status')
    .textContent = 'Active';
}

接下来,在 service-worker.js 文件中,我们将调用 skipWaiting() 以启用更新的服务工作者在存在与更新版本不同的现有服务工作者时立即激活。

if (typeof self.skipWaiting === 'function') {
  console.log('self.skipWaiting()');
  self.addEventListener('install', function(evt) {
    evt.waitUntil(self.skipWaiting());
  });
} else {
  console.log('self.skipWaiting() is unsupported.');
}

if (self.clients && (typeof self.clients.claim === 'function')) {
  console.log('self.clients.claim()');
  self.addEventListener('activate', function(evt) {
    evt.waitUntil(self.clients.claim());
  });
} else {
  console.log('self.clients.claim() is unsupported.');
}

处理缓慢的响应

服务工作者缓慢的更新是模拟服务器缓慢响应时间的好方法。在这个配方中,我们将使用超时来模拟缓慢的响应。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章中的第一个配方,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章中的以下配方:为 GitHub 页面设置 SSL为 Windows 设置 SSL为 Mac 设置 SSL

如何做...

按照以下说明设置你的文件结构:

  1. 从以下位置复制 index.htmlindex.jsservice-worker.jsmanifest.jsonserver.jspackage.jsonstyle.css 文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/09/02/

  2. 打开浏览器并转到 index.html。如何做...

  3. 打开开发者工具栏(Cmd + Alt + IF12)。现在刷新页面,查看控制台中的消息。你会看到服务工作者生命周期方法被记录到控制台中。如何做...

  4. 刷新页面。服务工作者将显示在屏幕上的消息。如何做...

它是如何工作的...

index.js 文件中,我们将在服务工作者注册的点将控制台消息附加到屏幕上。

function printStatus(status) {
    document.querySelector('#status').innerHTML = status;
    document.body.appendChild(document.createTextNode(Array.prototype.join.call(arguments, ", ") + '\n'));

    console.log.apply(console, arguments); 
}

接下来,在 service-worker.js 文件中,我们将创建一个函数来延迟我们接收到的任何新承诺。

function wait(ms) {
  return new Promise(function(resolve) {
    setTimeout(resolve, ms);
  });
}

常规的生命周期方法将调用 wait 函数来延迟 installactivatefetch 状态。

self.addEventListener('install', function(evt) {
  console.log('INSTALL: In progress ..');
  evt.waitUntil(
    wait(DELAY).then(function() {
      console.log('INSTALL: Complete');
    })
  );
});

self.addEventListener('activate', function(evt) {
  console.log('ACTIVATION: In progress ..');
  evt.waitUntil(
    wait(DELAY).then(function() {
      console.log('ACTIVATION: Complete');
    })
  );
});

self.addEventListener('fetch', function(evt) {
  evt.respondWith(new Response("Service workers says Hello!"));
});

中继消息

服务工作者可以用来在你的浏览器中构建一个小型聊天消息功能。这个配方展示了如何通过在将要用于构建我们的小型聊天应用之间的页面中中继消息,通过页面和服务工作者之间进行通信。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章中的设置服务工作者配方,学习服务工作者基础。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章中的为 GitHub 页面设置 SSL配方,学习服务工作者基础

如何做...

按照以下说明设置你的文件结构:

  1. 从以下位置复制 index.htmlindex.jsservice-worker.jsmanifest.jsonpackage.jsonstyle.css 文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/09/03/

  2. 打开浏览器并访问 index.html如何操作...

  3. 现在打开另一个浏览器并访问 index.html。在文本区域中输入一些内容。如何操作...

  4. 现在打开另一个浏览器并访问 index.html。在文本区域中输入一些内容。如何操作...

它是如何工作的...

index.js 文件的开始处,我们查询 UI 的 DOM 节点。

  if (navigator.serviceWorker) {
    var message = document.querySelector('#message');
    var received = document.querySelector('#received');
    var status = document.querySelector('#status');
    var inbox = {};

我们然后监听任何服务工作者消息。当收到消息时,我们指定一个 DOM 元素来显示它。

navigator.serviceWorker.addEventListener('message', function(evt) {

      var userId = evt.data.client;
      var node;

      if (!inbox[userId]) {
        node = document.createElement('div');
        received.appendChild(node);
        inbox[userId] = node;
      }

      node = inbox[userId];
      node.textContent = 'User ' + userId + ' says: ' + evt.data.message;
});

当页面强制重新加载时,例如,服务工作者将不会发送任何消息。

message.addEventListener('input', function() {
      if (!navigator.serviceWorker.controller) {
        status.textContent = 'ERROR: no controller';
        return;
      }

      navigator.serviceWorker.controller.postMessage(message.value);
});

service-worker.js 文件包含消息的事件处理器。

self.addEventListener('message', function(event) {

  var promise = self.clients.matchAll()
  .then(function(clientList) {
    var senderID = event.source ? event.source.id : 'unknown';

    if (!event.source) {
      console.log('Unsure about the sender');
    }

    clientList.forEach(function(client) {
      if (client.id === senderID) {
        return;
      }
      client.postMessage({
        client: senderID,
        message: event.data
      });
    });
  });

  if (event.waitUntil) {
    event.waitUntil(promise);
  }
});

立即声明将确保用户不必刷新页面。

self.addEventListener('activate', function(event) {
  event.waitUntil(self.clients.claim());
});

index.html 文件中,我们添加一个 div、一个文本区域和一个段落标签用于消息。

<section class="message">
    <div id="received"></div>

    <textarea id="message" style="width: 90%;padding: 10px;" rows="5"></textarea>
    <p>Message</p>
</section>

使用服务工作者作为代理中间件

代理是网络浏览器和互联网之间的中介。在这个菜谱中,你将学习如何使用服务工作者作为代理中间件。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考 第一章 的第一个菜谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考 第一章 中的以下菜谱,学习服务工作者基础设置 GitHub pages for SSL设置 Windows 的 SSL设置 Mac 的 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置下载所有文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/09/04/

  2. 打开浏览器并访问 index.html如何操作...

  3. 现在点击第一个链接以导航到 /hello 链接。如何操作...

  4. 现在打开开发者工具 (Cmd + Alt + IF12),在 控制台 选项卡上查看日志消息。如何操作...

  5. 现在点击第一个链接以导航到 /hello/world 链接。如何操作...

它是如何工作的...

我们将在计划创建代理的 index.html 文件中添加两个链接。

<section >
    <h1>Proxy</h1>
    <p>Click the links below for navigating to fetch handlers:</p>
    <div class="links">
      <a href="/service-workers/07/04/hello">/hello</a><br />
      <a href="/service-workers/07/04/hello/world">/hello/world</a>
    </div>
</section>

我们将在 service-worker.js 文件中创建一个代理,该文件包含一个 hello 字符串以及 hello/world。客户端将将其识别为本地资源。

var helloFetchHandler = function(event) {
  if (event.request.url.indexOf('/hello') !== -1) {
    console.log('DEBUG: Inside the /hello handler.');
    event.respondWith(new Response('Fetch handler for /hello'));
  }
};

var helloWorldFetchHandler = function(event) {
  if (event.request.url.endsWith('/hello/world')) {
    console.log('DEBUG: Inside the /hello/world handler.');
    event.respondWith(new Response('Fetch handler for /hello/world'));
  }
};

我们将这些处理程序作为回调传递给 fetch 事件监听器。

var fetchHandlers = [helloWorldFetchHandler, helloFetchHandler];

fetchHandlers.forEach(function(fetchHandler) {
  self.addEventListener('fetch', fetchHandler);
}); 

使用带有实时流程图的服务工人

在本配方中,你将通过演示工作流程并记录步骤来学习如何使用服务工人,以便我们可以跟随流程。

我们将要实现的功能如下:

  • 一个用于注册服务工人的按钮

  • 一个用于重新加载文档的按钮

  • 一个用于注销服务工人的按钮

按钮可以按任何顺序按下。你也可以指定服务工人脚本 URL 和作用域来模拟不同的测试用例。

准备工作

要开始使用服务工人,你需要在浏览器设置中开启服务工人实验功能。如果你还没有这样做,请参考第一章的设置服务工人配方,学习服务工人基础。服务工人仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章的设置 GitHub 页面以支持 SSL配方,学习服务工人基础

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置下载文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/08/05/

  2. 打开浏览器并转到index.html如何操作...

  3. 现在打开开发者工具 (Cmd + Alt + IF12) 以查看控制台选项卡上的日志消息。如何操作...

  4. 现在点击注册指定 SW按钮。如何操作...

  5. 现在打开开发者工具 (Cmd + Alt + IF12) 以查看控制台选项卡上的日志消息。如何操作...

  6. 现在点击重新加载文档按钮。如何操作...

  7. 现在打开开发者工具 (Cmd + Alt + IF12) 以查看控制台选项卡上的日志消息。如何操作...

  8. 现在点击注销活动 SW按钮。如何操作...

  9. 现在打开开发者工具 (Cmd + Alt + IF12) 以查看控制台选项卡上的日志消息。如何操作...

它是如何工作的...

app.js文件的开始处,我们初始化服务工人的辅助工具。

this.serviceWorkerUtil = new ServiceWorkerUtil();

要注册服务工人,我们执行以下步骤:

document.getElementById('reloadapp').addEventListener('click', function() {
    window.location.reload();
  });

然后,我们检查是否支持服务工人。

if (this.serviceWorkerUtil.isServiceWorkerSupported()) {
    document.getElementById('swinstall').addEventListener('click',
      function() {
        self.enableFeatures();
      }
    );

    document.getElementById('swuninstall').addEventListener('click',
      function() {
        self.disableFeatures();
      }
    );

接下来,我们检查服务工人是否处于控制状态。

if (this.serviceWorkerUtil.isServiceWorkerControllingThisApp()) {
      Logger.info('App code run as expected');

      this.disableSWRegistration();
    } else {
      this.enableSWRegistration();
    }

现在我们尝试注册服务工人以启用功能。

App.prototype.enableFeatures = function enableFeatures() {
  var scriptURL;
  var scope;

  Logger.newSection();
  Logger.log('ENABLE: Features enabled.');

  scriptURL = document.getElementById('swscripturl');
  scope = document.getElementById('swscope');

  Logger.debug(
    'Configuring the following service worker ' + scriptURL.value +
    ' with scope ' + scope.value
  );

  if (scriptURL.value !== '') {
    Logger.debug('scriptURL: ' + scriptURL.value);
  } else {
    Logger.error('No SW scriptURL specified');
    return;
  }

  if (scope.value !== '') {
    Logger.debug('SCOPE: ' + scope.value);
  } else {
    Logger.warn('SCOPE: not specified');
  }

  this.serviceWorkerUtil.registerServiceWorker(scriptURL.value, scope.value).then(
      this.disableSWRegistration,
      this.enableSWRegistration
  );
};

我们将禁用用户注销服务工人的可能性。

App.prototype.enableSWRegistration = function() {
  document.getElementById('swinstall').disabled = false;
  document.getElementById('swuninstall').disabled = true;
};

App.prototype.disableSWRegistration = function() {
  document.getElementById('swinstall').disabled = true;
  document.getElementById('swuninstall').disabled = false;
};

因此,应用程序开始运行。

var app = new App();

console.debug(app);

service-worker.js文件中,我们接收有效负载并为注册推送事件添加事件监听器。

'use strict';

self.addEventListener('push', function(event) {
  event.waitUntil(
    self.clients.matchAll().then(function(clients) {

      var focused = clients.some(function(client) {
        return client.focused;
      });

      var notificationMessage;

      if (focused) {
        notificationMessage = 'Same Page';
      } else if (clients.length > 0) {
        notificationMessage = 'Diffrerent Page, ' +
                              'click here to gain focus';
      } else {
        notificationMessage = 'Page Closed, ' +
                              'click here to re-open it!';
      }

      return self.registration.showNotification('ServiceWorker Cookbook', {
        body: notificationMessage,
      });
    })
  );
});

index.html文件中,我们将添加一个部分来显示按钮和控制台。

<section class="playground">
    <div class="inputs">
        <span class="title">Service Worker</span>
        <input id="swscripturl" placeholder="SW path" value="./service-worker.js" />
        <span class="title">Options </span>
        <input id="swscope" placeholder="scope (optional)" />
    </div>
    <div class="actions">
        <button id="swinstall" disabled>Register specified SW</button>
        <button id="reloadapp">Reload document</button>
        <button id="swuninstall" disabled>Unregister active SW</button>
    </div>
    <div id="log" class="log"></div>
</section>

第十章. 提高性能

在本章中,我们将涵盖以下主题:

  • 从缓存执行网络请求

  • 从网络执行网络请求

  • 测试waitUntil

  • 实现后台同步

  • 发送转发请求

  • 避免模型获取和渲染时间

简介

在本章的最后,我们将探讨如何通过服务工作者来提高性能。现在,我们将探讨提高从缓存和网络中执行网络请求的领域,实现后台同步,发送转发请求,以及避免模型获取和渲染时间。

从缓存中执行网络请求

如果你经常访问某个网站,那么你可能会从你的缓存而不是服务器本身加载大部分资源,如 CSS 和 JavaScript 文件。这为我们节省了服务器必要的带宽以及网络请求。控制我们从缓存和服务器中提供哪些内容是一个巨大的优势。服务器工作者通过给我们程序性地控制内容来提供这个强大的功能。在本食谱中,我们将探讨通过创建性能艺术事件查看器网络应用程序来实现这一目标的方法。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章中的设置服务工作者食谱,学习服务工作者基础。服务工作者仅在 HTTPS 上运行。要了解如何设置开发环境以支持此功能,请参考以下第一章中的食谱:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,以及在 Mac 上设置 SSL

如何做...

按照以下说明设置你的文件结构。

  1. 从以下位置下载文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/10/01/

  2. 打开浏览器并转到index.html如何做...

  3. 首先,我们将启用缓存从网络请求数据。点击获取按钮。如何做...

  4. 现在,我们将选择禁用选项卡下的标签下的禁用复选框,然后再次点击获取按钮,以便只从缓存中获取数据。页面上将显示错误。由于缓存已禁用,无法获取数据。如何做...

它是如何工作的...

index.js文件中,我们将为缓存设置一个特定页面的名称,因为缓存是基于源的,并且没有其他页面应该使用相同的缓存名称。

var CACHE_NAME = cache-only';

如果你检查开发工具的 资源 选项卡,你可以在 缓存存储 中找到缓存。

如何工作...

我们将存储缓存的经过时间到变量中。

var cacheFetchStartTime;

源 URL,例如,通过 RawGit 指向 GitHub 中的文件位置。

var SOURCE_URL = 'https://cdn.rawgit.com/szaranger/szaranger.github.io/master/service-workers/10/01/events';

如果你想要设置自己的源 URL,你可以通过在 GitHub 中创建一个 gist 或仓库,并创建一个包含你的数据的 JSON 格式文件(不需要 .json 扩展名)来轻松地做到这一点。一旦完成,复制文件的 URL 并前往 rawgit.com

将链接粘贴到那里以获取一个包含内容类型头部的链接,如下截图所示:

如何工作...

在我们点击获取按钮和所有数据接收之间,我们必须确保用户不会更改搜索标准或再次点击获取按钮。为了处理这种情况,我们将禁用控件。

function clear() {
  outlet.textContent = '';
  cacheStatus.textContent = '';
}

function disableEdit(enable) {
  fetchButton.disabled = enable;
  cacheDelayText.disabled = enable;
  cacheDisabledCheckbox.disabled = enable;

  if(!enable) {
    clear();
  }
}

返回的数据将以行形式渲染到屏幕上。

function displayEvents(events) {

  events.forEach(function(event) {
    var tickets = event.ticket ?
      '<a href="' + event.ticket + '" class="tickets">Tickets</a>' : '';

    outlet.innerHTML = outlet.innerHTML +
      '<article>' +
      '<span class="date">' + formatDate(event.date) + '</span>' +
      ' <span class="title">' + event.title + '</span>' +
      ' <span class="venue"> - ' + event.venue + '</span> ' +
      tickets +
      '</article>';
  });

}

events 数组的每个项目都将作为行打印到屏幕上。

如何工作...

handleFetchComplete 函数是缓存的回调。

因为请求体只能读取一次,我们必须克隆响应。

cloned = response.clone();

我们将使用 cache.put 将克隆的响应作为键值对放入缓存。这有助于后续的缓存获取找到这些更新的数据。

caches.open(CACHE_NAME).then(function(cache) {
   cache.put(SOURCE_URL, cloned); // cache.put(URL, response)
});

现在我们将读取以 JSON 格式返回的响应。

response.json().then(function(data) {
    displayEvents(data);
});

当用户点击获取按钮时,我们将几乎同时从缓存中请求数据。这在现实世界的应用程序中会在页面加载时发生,而不是由用户操作引起的。

fetchButton.addEventListener('click', function handleClick() {
...
}

为了模拟缓存延迟,我们在调用缓存获取回调之前等待。如果回调出错,我们将确保从原始调用中得到的 Promise 被拒绝以匹配。

return new Promise(function(resolve, reject) {
        setTimeout(function() {
          try {
            handleCacheFetchComplete(response);
            resolve();
          } catch (err) {
            reject(err);
          }
        }, cacheDelay);
});

formatDate 函数是我们将接收到的响应中的日期格式转换为屏幕上更易读格式的辅助函数。

function formatDate(date) {
  var d = new Date(date),
      month = (d.getMonth() + 1).toString(),
      day = d.getDate().toString(),
      year = d.getFullYear();
  if (month.length < 2) month = '0' + month;
  if (day.length < 2) day = '0' + day;

  return [month, day, year].join('-');
}

如果你喜欢不同的日期格式,你可以通过在返回语句中调整数组的顺序来达到你喜欢的格式。

相关内容

  • 在第三章“访问离线内容”的 显示缓存内容首先 菜谱中,访问离线内容

从网络执行网络请求

在上一个菜谱中,我们探讨了如何从缓存中获取请求。在这个菜谱中,我们将演示如何使用服务工作者从服务器/网络中获取请求。为了演示网络获取,我们将构建一个类似于上一个菜谱的界面,但专门用于展示网络交互。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个食谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章的以下食谱:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,以及在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置下载文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/10/02/

  2. 打开浏览器并转到index.html如何操作...

  3. 首先,我们将启用缓存从网络请求数据。点击获取按钮。如何操作...

  4. 现在我们将选择标签下的禁用复选框,然后再次点击获取按钮,以便只从缓存中获取数据。页面上将显示错误。由于缓存已禁用,无法获取数据。如何操作...

工作原理...

index.js文件中,我们将为缓存设置一个特定于页面的名称,因为缓存是基于源的,并且没有其他页面应该使用相同的缓存名称。

var CACHE_NAME = 'network-only';

如果你检查开发工具的资源标签页,你可以在缓存存储中找到缓存。

我们将存储两个网络的时间差到变量中。

var networkFetchStartTime;

源 URL,例如,通过 RawGit 指向 GitHub 上的文件位置。

var SOURCE_URL = 'https://cdn.rawgit.com/szaranger/szaranger.github.io/master/service-workers/10/02/events';

如果你想要设置自己的源 URL,你可以通过在 GitHub 中创建一个 gist 或仓库并创建一个包含你的数据的 JSON 格式的文件(不需要.json扩展名)轻松地做到这一点。一旦你这样做,复制文件的 URL 并转到rawgit.com。将链接粘贴到那里以获取另一个带有内容类型头的链接,如下面的截图所示:

工作原理...

在我们点击获取按钮和所有数据接收之间,我们必须确保用户不会更改搜索标准或再次点击获取按钮。为了处理这种情况,我们将禁用控件。

function clear() {
  outlet.textContent = '';
  networkStatus.textContent = '';
  networkDataReceived = false;
}

function disableEdit(enable) {
  fetchButton.disabled = enable;
  networkDelayText.disabled = enable;
  networkDisabledCheckbox.disabled = enable;

  if(!enable) {
    clear();
  }
}

返回的数据将以行形式渲染到屏幕上。

function displayEvents(events) {

  events.forEach(function(event) {
    var tickets = event.ticket ?
      '<a href="' + event.ticket + '" class="tickets">Tickets</a>' : '';

    outlet.innerHTML = outlet.innerHTML +
      '<article>' +
      '<span class="date">' + formatDate(event.date) + '</span>' +
      ' <span class="title">' + event.title + '</span>' +
      ' <span class="venue"> - ' + event.venue + '</span> ' +
      tickets +
      '</article>';
  });

}

events数组中的每个项目都将作为行打印到屏幕上。

工作原理...

handleFetchComplete函数是缓存和网络的双向回调。

如果勾选了禁用复选框,我们将通过抛出错误来模拟网络错误。

var shouldNetworkError = networkDisabledCheckbox.checked,
    cloned;

  if (shouldNetworkError) {
    throw new Error('Network error');
  }

由于请求体只能读取一次,我们必须克隆响应。

cloned = response.clone();

现在我们将以 JSON 格式读取响应。

response.json().then(function(data) {
    displayEvents(data);
    networkDataReceived = true;
});

当用户点击获取按钮时,我们将从网络和缓存几乎同时请求数据。这在现实世界的应用程序中会在页面加载时发生,而不是用户操作的结果。

fetchButton.addEventListener('click', function handleClick() {
...
}

我们将开始禁用在网络获取请求发起期间的所有用户输入。

disableEdit(true);

networkStatus.textContent = 'Fetching events...';
networkFetchStartTime = Date.now();

我们将使用带有缓存破坏 URL 以及无缓存选项的 fetch API 请求数据,以支持尚未实现缓存选项的 Firefox。

networkFetch = fetch(SOURCE_URL + '?cacheBuster=' + now, {
   mode: 'cors',
   cache: 'no-cache',
   headers: headers
})

为了模拟网络延迟,我们在调用网络获取回调之前会等待。在回调出错的情况下,我们必须确保我们拒绝从原始获取中接收到的Promise

return new Promise(function(resolve, reject) {
      setTimeout(function() {
        try {
          handleFetchComplete(response);
          resolve();
        } catch (err) {
          reject(err);
        }
      }, networkDelay);
});

formatDate函数是我们将接收到的响应中的日期格式转换为屏幕上更易读格式的辅助函数。

function formatDate(date) {
  var d = new Date(date),
      month = (d.getMonth() + 1).toString(),
      day = d.getDate().toString(),
      year = d.getFullYear();

  if (month.length < 2) month = '0' + month;
  if (day.length < 2) day = '0' + day;

  return [month, day, year].join('-');
}

如果你更喜欢不同的日期格式,你可以调整返回语句中数组的顺序以符合你的偏好格式。

相关内容

  • 第三章中的首先显示缓存内容配方,访问离线内容

测试 waitUntil

在这个配方中,我们将使用服务工作者来测试waitUntil方法,这将延迟服务工作者生命周期的安装方法,直到打开缓存并将页面保存到缓存的过程。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的设置服务工作者配方,学习服务工作者基础。服务工作者仅在 HTTPS 上运行。要了解如何设置开发环境以支持此功能,请参考以下第一章的配方:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,和在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置下载文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/10/03/

  2. 打开浏览器并转到index.html。你会看到注册状态:成功的消息。如何操作...

  3. 现在打开开发者工具(Cmd + Alt + IF12),转到网络标签页,点击显示无限制的下拉菜单,并选择离线如何操作...

  4. 现在刷新你的浏览器,你会看到离线消息和图片。如何操作...

工作原理...

waitUntil事件延长了安装事件的生存期,直到所有缓存都已填充。换句话说,它将处理安装中的工作者视为已安装的操作延迟到我们指定的所有资源都已缓存并且传递的承诺成功解决。

我们看到当我们的网站离线时,HTML 文件和图片文件被缓存,然后被检索。我们还可以缓存其他资源,包括 CSS 和 JavaScript 文件。

caches.open(currentCache.offline)
.then(function(cache) {
        return cache.addAll([
              'offline.html',
              '/assets/css/style.css',
              '/assets/js/index.js'
            ]);
        })
);

当注册成功时,我们将指示服务工作者拦截请求,并使用 fetch 事件从缓存内容中提供资源。

index.html文件内部,当注册成功时,我们将检查注册状态并将其打印到浏览器。否则,我们将打印服务工作者返回的错误信息。

navigator.serviceWorker.register(
      'service-worker.js',
      { scope: './' }
   ).then(function(serviceWorker) {
      document.getElementById('status').innerHTML = 
          'successful';
   }).catch(function(error) {
      document.getElementById('status').innerHTML = error;
});

服务工作者脚本文件将拦截网络请求,检查连接性,并为用户提供内容。

我们将在安装事件上添加事件监听器,并在回调函数中发起一个请求以获取此离线页面及其资源,当结果成功时,这些资源将被添加到缓存中。

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(currentCache.offline)
    .then(function(cache) {
         return cache.addAll([
        offlineUrl
          ]);
          })
    );
});

现在我们可以随时检索此页面,因为离线页面已存储在缓存中。如果我们没有连接性,我们需要在同一个服务工作者中添加逻辑以返回离线页面。

self.addEventListener('fetch', function(event) {
  var request = event.request,
    isRequestMethodGET = request.method === 'GET';

  if (request.mode === 'navigate' || isRequestMethodGET) {
    event.respondWith(
      fetch(createRequestWithCacheBusting(request.url)).catch(function(error) {
        console.log('OFFLINE: Returning offline page.', error);
        return caches.match(offlineUrl);
      })
    );
  } else {
    event.respondWith(caches.match(request)
        .then(function (response) {
        return response || fetch(request);
      })
    );
  }
});

我们正在监听前面源代码中的 fetch 事件,如果我们检测到用户试图导航到另一个页面并导致错误,我们简单地从缓存中返回离线页面。现在我们的离线页面已经工作。

实现背景同步

服务工作者(service worker)的背景同步功能负责管理后台同步过程。截至撰写本书时,此功能仍是非标准的,你应该避免在生产环境中使用它。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参阅第一章中的设置服务工作者配方,学习服务工作者基础。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参阅第一章中的设置 GitHub 页面以支持 SSL配方,学习服务工作者基础

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置复制文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/10/04/

  2. 打开浏览器并访问index.html如何操作...

  3. 点击注册背景同步按钮。底部将出现一条消息显示同步已注册如何操作...

  4. 屏幕上也会出现通知消息。如何操作...

它是如何工作的...

在我们的index.html文件中,我们将添加一个用于按钮和消息的部分。

  <section>
    <p>Registration status: <strong id="status"></strong></p>
    <button id="register">Register Background Sync</button>
    <div id="console"></div>
  </section>

我们将在index.js文件中处理按钮点击。通知需要权限,因此我们也将在这里处理。

document.getElementById('register').addEventListener('click', function(event) {
      event.preventDefault();

      new Promise(function(resolve, reject) {
        Notification.requestPermission(function(result) {
          if (result !== 'granted') {
            return reject(Error('Notification permission denied'));
          }
          resolve();
        })
      }).then(function() {
        return navigator.serviceWorker.ready;
      }).then(function(reg) {
        return reg.sync.register('syncTest');
      }).then(function() {
        print('Sync registered');
      }).catch(function(err) {
        print('It broke');
        print(err.message);
      });
});

我们的service-worker.js文件相当简单。当同步事件处理器被调用时,我们显示通知。

self.addEventListener('sync', function(event) {
  self.registration.showNotification('Sync\'d');
});

发送转发请求

在这个菜谱中,我们将实现一个发送转发请求的服务工作者。当你想要临时将请求转发到不同的资源时,请求转发非常有用。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章的第一个菜谱,学习服务工作者基础设置服务工作者。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章的以下菜谱:设置 GitHub 页面以支持 SSL在 Windows 上设置 SSL,以及在 Mac 上设置 SSL

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置下载所有文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/10/05/

  2. 打开浏览器并访问index.html如何操作...

  3. 现在打开开发者工具(Cmd + Alt + IF12),在控制台标签页上查看日志消息。转发消息将在一秒后出现。如何操作...

它是如何工作的...

我们将在index.html文件中添加一个div元素,我们计划在这里记录一条消息。

<section>
     <div id="console"></div>
</section>

我们将在service-worker.js文件中处理activefetch事件。

self.addEventListener('activate', _ => {
  clients.claim();
});

self.addEventListener('fetch', event => {
  console.log(event.request);
  event.respondWith(fetch(event.request));
});

将消息记录到屏幕上的辅助函数在index.js文件中。

var consoleEl = document.getElementById('console');

function print(message) {
  var p = document.createElement('p');

  p.textContent = message;
  consoleEl.appendChild(p);
  console.log(message);
}

避免模型获取和渲染时间

为了避免在连续请求时模型获取和渲染时间,我们保存了一个包含顺序插值模板的缓存,我们称之为渲染存储

根据 Mozilla 的说法,渲染存储的目的是保存/恢复特定视图的序列化版本,主要是为了性能。

准备工作

要开始使用服务工作者,你需要在浏览器设置中开启服务工作者实验功能。如果你还没有这样做,请参考第一章中的设置服务工作者配方,学习服务工作者基础。服务工作者仅在 HTTPS 上运行。要了解如何设置支持此功能的发展环境,请参考第一章中的设置 GitHub 页面以支持 SSL配方,学习服务工作者基础

如何操作...

按照以下说明设置你的文件结构:

  1. 从以下位置下载文件:

    github.com/szaranger/szaranger.github.io/blob/master/service-workers/10/06/

  2. 打开浏览器并访问index.html。如何操作...

  3. 点击列表中的任何链接进入商店。

它是如何工作的...

index.js文件的开始处,我们指定了宝可梦 API 端点以及一个安全源。

var proxy = 'https://crossorigin.me/';
var pokedex = proxy + 'http://pokeapi.co/api/v1/pokedex/1/';
…

接下来,从pokedex获取宝可梦列表并创建一个链接列表。

function fetchPokemonList() {
  fetch(pokedex)
    .then(function(response) {
      return response.json();
    })
    .then(function(info) {
      populatePokemonList(info.pokemon);

      if (window.parent !== window) {
        window.parent.document.body
          .dispatchEvent(new CustomEvent('iframeresize'));
      }
    });
}

随后,填充列表并为宝可梦列表创建链接。这些链接将被服务工作者拦截。

function populatePokemonList(pokemonList) {
  var el = document.querySelector('#pokemon');
  var buffer = pokemonList.map(function(pokemon) {
    var tokens = pokemon.resource_uri.split('/');
    var id = tokens[tokens.length - 2];
    return '<li><a href="pokemon.html?id=' + id + '">' + pokemon.name +
           '</a></li>';
  });
  el.innerHTML = buffer.join('\n');
} 

接下来,让我们看看service-worker.js文件。它试图恢复文档的缓存副本。如果没有找到,它将从网络响应。

function getResponse(request) {
  return self.caches.open('render-store').then(function(cache) {
    return cache.match(request).then(function(match) {
      return match || fetch(request);
    });
  });
}

cacheResponseInRenderStore函数从pokemon.js获取PUT请求的插值 HTML 内容,并为插值结果创建 HTML 响应。

function cacheResponseInRenderStore(request) {
  return request.text().then(function(contents) {
    var headers = { 'Content-Type': 'text/html' };
    var response = new Response(contents, { headers: headers });

    return self.caches.open('render-store').then(function(cache) {
      return cache.put(request.referrer, response);
    });
  });
}

pokemon.js文件中,我们初始化计时器以开始。

var startTime = performance.now();
var interpolationTime = 0;
var fetchingModelTime = 0;

我们将创建一个用于渲染的宝可梦模板,该模板通过从 URL 的查询字符串中获取数据来负责渲染。这个模板将获取指定的宝可梦并填充模板。一旦模板被填充,我们将文档标记为已缓存,然后通过将内容发送到服务工作者来将其发送到渲染存储。

if (document.documentElement.dataset.cached) {
  logTime();
} else {
  var pokemonId = window.location.search.split('=')[1];

  getPokemon(pokemonId).then(fillCharSheet).then(logTime).then(cache);
}

function getPokemon(id) {
  var fetchingModelStart = getStartTime();

  return fetch(getURL(id)).then(function(response) {
    fetchingModelTime = getStartTime() - fetchingModelStart;
    return response.json();
  });
}

接下来,我们将文档标记为已缓存,获取所有 HTML 内容,并使用PUT请求将内容发送到./render-store/ URL 以发送到服务工作者。

function interpolateTemplate(template, pokemon) {
  var interpolationStart = performance.now();
  var result = template.replace(/{{(\w+)}}/g, function(match, field) {
    return pokemon[field];
  });
  interpolationTime = performance.now() - interpolationStart;
  return result;
}
posted @ 2025-10-26 08:50  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报