七、添加服务工作器

欢迎来到我们进入渐进式 Web 应用世界的第一大步。本章将致力于创建我们的第一个服务工作器,这反过来将解锁使 PWAs 如此特殊的许多功能。

我们之前已经讨论过 PWA 桥接 web 应用和本机应用。他们这样做的方式是通过服务工作器。服务工作器使推送通知和脱机访问成为可能。这是一项令人兴奋的新技术,有许多应用(每年都有越来越多的应用);如果有一项技术能在未来五年内改变 web 开发,那就是服务工作器。

然而,足够的炒作;让我们深入了解服务工作器到底是什么。

在本章中,我们将介绍以下主题:

  • 什么是服务工作器?
  • 服务工作器生命周期
  • 如何在我们的页面上注册服务工作器

什么是服务工作器?

服务工作器是介于我们的应用和网络之间的一种 JavaScript。

您可以考虑在应用上下文之外运行的脚本,但我们可以在代码范围内使用它进行通信。它是我们应用的一部分,但与其他部分是分开的。

最简单的例子是在缓存文件的上下文中(我们将在接下来的章节中探讨)。假设我们的应用,当用户导航到时 https://chatastrophe.com 去拿我们的icon.png文件。

如果我们配置了服务工作器,它将位于我们的应用和网络之间。当我们的应用请求图标文件时,服务工作器会拦截该请求并检查本地缓存中的文件。如果找到,它将返回它;没有网络请求。只有在缓存中找不到文件时,才会让网络请求通过;下载完成后,它会将文件放入缓存。

你可以看到“工人”这个词的由来——我们的服务工作器是一只忙碌的小蜜蜂。

让我们看另一个例子;推送通知(偷偷预览第 9 章使我们的应用可以通过清单安装)。大多数推送通知都是这样工作的——当某个事件发生时(用户发送新的聊天消息),消息服务会发出警报(在我们的例子中,消息服务由 Firebase 管理)。消息服务向相关注册用户(通过其设备注册的用户)发送警报,然后其设备创建通知(叮当!)。

在 web 应用上下文中,此流的问题在于,当用户不在页面上时,我们的应用停止运行,因此我们无法通知他们,除非他们的应用已经打开,这完全破坏了推送通知的作用。

服务工作器通过始终“打开”并倾听消息来解决此问题。现在,消息服务可以提醒我们的服务工作器,向用户显示消息。我们的应用代码实际上从未涉及,所以不管它是否在运行。

这是令人兴奋的事情,但任何新技术,都有一些粗糙的边缘,还有一些东西需要注意。

服务工作器生命周期

当用户第一次访问您的页面时,服务工作器的生命就开始了。服务工作器已下载并开始运行。它可能会在不需要时闲置一段时间,但在需要时可以重新启动。

这种始终开启的功能使服务工作器对推送通知非常有用。这也使得服务工作器对工作有点不感兴趣(后面会有更多)。然而,让我们深入地看看一个典型页面上的服务工作器的生死。

首先,如果可能,安装了 service worker。所有 service worker 安装都将首先检查用户的浏览器是否支持该技术。到目前为止,Firefox、Chrome 和 Opera 都有完全的支持,而其他浏览器则没有。例如,苹果公司将服务工作器视为实验性技术,表明他们对整个事件仍持怀疑态度。

如果用户的浏览器足够现代化,则开始安装。脚本(例如,sw.js)安装(或者更确切地说,注册)在某个范围内。范围在本例中是指其关注的网站路径。全局范围将采用'/',例如站点上的所有路径,但您也可以将您的服务工作器限制为'/users';例如,仅缓存应用的某些部分。我们将在缓存章节中详细讨论范围。

注册后,服务工作器即被激活。激活事件也会在需要服务工作器时发生,例如,当传入推送通知时。服务工作器的激活和停用意味着您无法在服务工作器内保持状态;它只是对事件做出反应而运行的一段代码,而不是它自己的完整应用。这是一个需要记住的重要区别,以免我们对员工要求太多。

在事件发生之前,服务工作器将处于空闲状态。到目前为止,服务工作器对两个事件做出反应:fetch事件(也称为来自应用的网络请求)和message(也称为来自应用代码或消息服务的交互)。我们可以在服务工作器中注册这些事件的侦听器,然后根据需要做出反应。

服务工作器代码将在两种情况下更新:24 小时已过(在这种情况下,它将停止并重新下载一种方法,以防止损坏的代码造成太多麻烦),或者用户访问页面并且sw.js文件已更改。每次用户访问应用时,服务工作器都会将其当前代码与网站提供的sw.js进行比较,如果有一个字节的差异,就会下载并注册新的sw.js

这是对服务工作器及其工作方式的基本技术概述。这可能看起来很复杂,但好消息是使用服务工作器相对简单;您可以在几分钟内完成一个简单的安装并运行,这正是我们下一步要做的!

注册我们的第一名服务工作器

记住服务工作器的区别——他们是我们网站的一部分,但在我们的应用代码之外运行。考虑到这一点,我们的服务工作器将住在public/ folder内,而不是src/ folder内。

然后,在public/ folder中创建一个名为sw.js的文件。现在我们将保持简单;只需在内部添加一个console.log

console.log("Service worker running!");

实际工作(登记服务工作器)将在我们的index.html内完成。对于此过程,我们希望执行以下操作:

  1. 检查浏览器是否支持服务工作器。
  2. 等待页面加载。
  3. 注册服务工作器。
  4. 注销结果。

让我们一个接一个地完成这些步骤。首先,让我们在 Firebase 初始化下方的public/index.html内创建一个空的script标记:

<body>
  <div id="root"></div>
  <script src="/secrets.js"></script>
  <script src="https://www.gstatic.com/firebasejs/4.1.2/firebase.js"></script>
  <script>
    // Initialize Firebase
    var config = {
      apiKey: window.apiKey,
      authDomain: "chatastrophe-77bac.firebaseapp.com",
      databaseURL: "https://chatastrophe-77bac.firebaseio.com",
      projectId: "chatastrophe-77bac",
      storageBucket: "chatastrophe-77bac.appspot.com",
      messagingSenderId: "85734589405"
    }; 
    window.firebase = firebase;
    firebase.initializeApp(config);
  </script>
  <script>
 // Service worker code here.
 </script>

检查浏览器支持

检查用户的浏览器是否可以处理服务工作器是非常容易的。在脚本标签中,我们将添加一个简单的if语句:

<script>
  if ('serviceWorker' in navigator) {
    // register
  } else {
    console.log('service worker is not supported');
  }
</script>

在这里,我们检查window.navigator对象是否有任何服务工作器支持。导航器也可以用来(通过它的userAgent属性)检查用户有什么浏览器,尽管这里不需要。

正在侦听页面加载

在页面加载完成之前,我们不想注册我们的服务工作器;这没有意义,而且可能会导致复杂性,因此我们将在'load'事件的窗口中添加一个事件侦听器:

<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', function() {

    });
  } else {
    console.log('service worker is not supported');
  }
</script>

注册服务工作器

正如我们前面提到的,window.navigator有一个serviceWorker属性,它的存在确认了浏览器对服务工作器的支持。我们还可以使用同一对象通过其register功能注册我们的服务工作器。我知道,这是令人震惊的事情。

我们调用navigator.serviceWorker.register,并将路径传递到我们的服务工作器文件:

<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
      navigator.serviceWorker.register('sw.js')
    });
  } else {
    console.log('service worker is not supported');
  }
</script>

注销结果

最后,让我们添加一些console.logs,以便我们知道注册的结果。幸运的是,navigator.serviceWorker.register返回了一个承诺:

<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
      navigator.serviceWorker.register('sw.js').then(function(registration) {
        // Registration was successful
        console.log('Registered!');
      }, function(err) {
        // registration failed :(
        console.log('ServiceWorker registration failed: ', err);
      }).catch(function(err) {
        console.log(err);
      });
    });
  } else {
    console.log('service worker is not supported');
  }
</script>

好的,让我们测试一下!重新加载页面,如果一切正常,您应该在控制台中看到以下内容:

您还可以通过导航到 DevTools 中的“应用”选项卡,然后导航到“服务工作器”选项卡进行检查:

我建议您趁此机会检查一下“重新加载时更新”按钮。这可以确保每次刷新页面时都会刷新服务工作器(请记住我们前面讨论的正常服务工作器生命周期)。为什么要采取这种预防措施?我们正在步入缓存代码的世界,浏览器可能会认为您的服务工作器没有改变,而实际上却改变了。此复选框仅确保您始终处理最新版本的sw.js

好的,我们已经登记了一名工人!好极了让我们花一点时间从服务工作器的生命周期中开始。

体验服务工作器的生命周期

服务工作器经历的第一个事件是'install'事件。这是用户第一次启动 PWA 时。标准用户只能体验一次。

要利用此事件,我们所要做的就是向服务工作器本身添加一个事件侦听器。为了在sw.js中实现这一点,我们使用self关键字:

self.addEventListener('install', function() {
 console.log('Install!');
});

重新加载页面时,控制台中会出现'Install!'。事实上,您应该在每次重新加载页面时看到它,除非您取消选中 Application | Service Workers 下的 Update on reload 选项。那么,你只会第一次看到它。

接下来是activate事件。此事件在服务工作器首次注册时触发,在注册完成之前。换句话说,它应在与安装相同的情况下发生,仅在以后发生:

self.addEventListener('activate', function() {
  console.log('Activate!');
});

我们要报道的最后一个事件是'fetch'事件。每当应用发出网络请求时,都会调用此事件。它由一个具有请求 URL 的事件对象调用,我们可以注销该 URL:

self.addEventListener('fetch', function(event) {
  console.log('Fetch!', event.request);
});

在添加此项后,我们将看到一个非常混乱的控制台:

您现在可以删除 service worker 中的所有console.logs,但我们将在将来使用这些事件侦听器中的每一个。

接下来,我们将研究如何连接 Firebase 消息服务,为推送通知奠定基础。

向我们的服务工作器添加 Firebase

本章其余部分的目标是将 Firebase 集成到我们的服务工作器中,以便它能够接收推送通知并显示它们。

这是一个大项目。在下一章结束之前,我们无法实际显示推送通知。然而,在这里我们将看到如何将第三方服务集成到服务工作器中,并进一步深入研究服务工作器背后的理论。

命名我们的服务工作器

我们将用于向用户设备发送推送通知的服务称为Firebase 云消息FCM。FCM 在 web 上的工作方式是查找服务工作器,然后向其发送消息(包含通知详细信息)。然后,服务工作器显示通知。

默认情况下,FCM 查找名为firebase-messaging-sw.js的服务工作器。您可以使用firebase.messaging().useServiceWorker并传递服务工作器注册对象来更改该设置。然而,出于我们的目的,更直接的方法是简单地重命名我们的服务工作器。让我们这样做;更改public/中的文件名和index.html中的注册:

<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
      navigator.serviceWorker.register('firebase-messaging-sw.js').then(function(registration) {
        // Registration was successful
        console.log('Registered!');
      }, function(err) {
        // registration failed :(
        console.log('ServiceWorker registration failed: ', err);
      }).catch(function(err) {
        console.log(err);
      });
    });
   } else {
     console.log('service worker is not supported');
   }
</script>

完成后,我们可以在服务工作器内部开始初始化 Firebase。

再说一遍;服务工作器未链接到您的应用代码。这意味着它无法访问我们当前的 Firebase 初始化。不过,我们可以在服务工作器内部重新初始化 Firebase,并且只保留相关的内容--messagingSenderId。您可以从 Firebase 控制台或您的secrets.js文件获取您的messagingSenderId

If you're concerned about security, ensure that you add public/firebase-messaging-sw.js to your .gitignore, though keeping your messagingSenderId private is not as important as keeping your API key secret.

// firebase-messaging-sw.js
firebase.initializeApp({
  'messagingSenderId': '85734589405'
});

我们还需要导入文件顶部需要的 Firebase 部分,包括app库和messaging库:

importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-messaging.js');

完成后,我们应该能够console.log输出firebase.messaging();

importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-messaging.js');
firebase.initializeApp({
  'messagingSenderId': '85734589405'
});console.log(firebase.messaging());

您应该看到以下内容:

这意味着我们的 Firebase 在我们的服务工作器内部启动并运行!

If you're still seeing the logs from our old sw.js, go to the Application | Service Workers tab of DevTools and Unregister it. This is a good example of how service workers will persist even if they're not being reregistered.

如前所述,服务工作器是一段始终在运行的代码(尽管不是完全准确的——想想这些工作器的生命周期——这是一种很好的思考他们的方式)。这意味着它将始终等待 FCM 通知它有消息传入。

但是,目前我们没有收到任何消息。下一步是开始配置何时发送推送通知以及如何显示它们!

总结

在本章中,我们学习了服务工作器的基本知识,并启动了一个服务工作器。我们的下一步是开始使用它。具体来说,我们希望使用它来侦听通知,然后将它们显示给用户。让我们通过设置推送通知,朝着让我们的 PWA 感觉像本机应用的方向迈出另一大步。