# 与 Service Worker 通讯

ServiceWorkerModule导入到你的 AppModule中不仅会注册 Service Worker,还会提供一些服务,让你能和 Service Worker 通讯,并控制你的应用缓存。

# 前提条件

对下列知识有基本的了解:

  • Service Worker 快速上手

# SwUpdate服务

SwUpdate服务让你能访问一些事件,这些事件会指出 Service Worker 何时发现并安装了可用的更新

SwUpdate服务支持四个独立的操作:

  • 当在服务器上检测到新版本、已安装并可本地使用或安装失败时获得通知

  • 要求 Service Worker 检查服务器上是否有更新。

  • 要求 Service Worker 为当前标签页激活应用的最新版本

# 版本更新

versionUpdatesSwUpdate的一个 Observable属性,并且会发出四种事件类型:

事件类型 Event types 详情 Details
VersionDetectedEvent 当 Service Worker 在服务器上检测到应用程序的新版本并即将开始下载时发出。Emitted when the service worker has detected a new version of the app on the server and is about to start downloading it.
NoNewVersionDetectedEvent 当 Service Worker 检查了服务器上应用程序的版本并且没有找到新版本时发出。Emitted when the service worker has checked the version of the app on the server and did not find a new version.
VersionReadyEvent 当有新版本的应用程序可供客户端激活时发出。它可用于通知用户可用的更新或提示他们刷新页面。Emitted when a new version of the app is available to be activated by clients. It may be used to notify the user of an available update or prompt them to refresh the page.
VersionInstallationFailedEvent 在新版本安装失败时发出。它可用于日志/监控目的。Emitted when the installation of a new version failed. It may be used for logging/monitoring purposes.

log-update.service.ts

@Injectable()
export class LogUpdateService {

  constructor(updates: SwUpdate) {
    updates.versionUpdates.subscribe(evt => {
      switch (evt.type) {
        case 'VERSION_DETECTED':
          console.log(`Downloading new app version: ${evt.version.hash}`);
          break;
        case 'VERSION_READY':
          console.log(`Current app version: ${evt.currentVersion.hash}`);
          console.log(`New app version ready for use: ${evt.latestVersion.hash}`);
          break;
        case 'VERSION_INSTALLATION_FAILED':
          console.log(`Failed to install app version '${evt.version.hash}': ${evt.error}`);
          break;
      }
    });
  }
}

# 检查更新

可以要求 Service Worker 检查是否有任何更新已经发布到了服务器上。Service Worker 会在初始化和每次导航请求(也就是用户导航到应用中的另一个地址)时检查更新。不过,如果你的站点更新非常频繁,或者需要按计划进行更新,你可能会选择手动检查更新。

通过调用 checkForUpdate()方法来实现:

check-for-update.service.ts

import { ApplicationRef, Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
import { concat, interval } from 'rxjs';
import { first } from 'rxjs/operators';

@Injectable()
export class CheckForUpdateService {

  constructor(appRef: ApplicationRef, updates: SwUpdate) {
    // Allow the app to stabilize first, before starting
    // polling for updates with `interval()`.
    const appIsStable$ = appRef.isStable.pipe(first(isStable => isStable === true));
    const everySixHours$ = interval(6 * 60 * 60 * 1000);
    const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);

    everySixHoursOnceAppIsStable$.subscribe(async () => {
      try {
        const updateFound = await updates.checkForUpdate();
        console.log(updateFound ? 'A new version is available.' : 'Already on the latest version.');
      } catch (err) {
        console.error('Failed to check for updates:', err);
      }
    });
  }
}

该方法返回一个用来表示检查更新已经成功完成的 Promise<boolean&gt;。这种检查可能会失败,它会导致此 Promise被拒绝(reject)。

为了避免影响页面的首次渲染,在注册 ServiceWorker 脚本之前,ServiceWorkerModule默认会在应用程序达到稳定态之前等待最多 30 秒。如果不断轮询更新(比如调用 setInterval() 或 RxJS 的 interval() )就会阻止应用程序达到稳定态,则直到 30 秒结束之前都不会往浏览器中注册 ServiceWorker 脚本。

注意:应用中所执行的各种轮询都会阻止它达到稳定态。欲知详情,参阅 isStable 文档。

可以通过在开始轮询更新之前先等应用达到稳定态来消除这种延迟,如上述例子所示。另外,你还可以为 ServiceWorker 定义不一样的 注册策略

# 升级到最新版本

你可以通过在新版本就绪后立即重新加载页面来将现有选项卡更新到最新版本。为避免中断用户的进度,一般来说最好提示用户并让他们确认可以重新加载页面并更新到最新版本:

prompt-update.service.ts

@Injectable()
export class PromptUpdateService {

  constructor(swUpdate: SwUpdate) {
    swUpdate.versionUpdates
        .pipe(filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'))
        .subscribe(evt => {
          if (promptUser(evt)) {
            // Reload the page to update to the latest version.
            document.location.reload();
          }
        });
  }

}

调用 SwUpdate#activateUpdate() 会把某个页标签更新为最新版,而不必重新加载页面,但是这可能中断应用程序。

用不重新加载的方式更新可以创建一个应用外壳与其它页面资源(如惰性加载块)不匹配的版本,因为文件名在不同版本之间可能发生变化。

如果你确信对于你的特定用例,这种情况是安全的,你可以只用 activateUpdate()

# 处理不可恢复的状态

在某些情况下,Service Worker 用来为客户端提供服务的应用版本可能处于损坏状态,如果不重新加载整个页面,则无法恢复该状态。

比如,设想以下情形:

  • 用户首次打开该应用,Service Worker 会缓存该应用的最新版本。假设应用要缓存的资源包括 index.htmlmain.<main-hash-1>.jslazy-chunk.<lazy-hash-1>.js

  • 用户关闭该应用程序,并且有一段时间没有打开它。

  • 一段时间后,会将新版本的应用程序部署到服务器。新版本中包含文件 index.htmlmain.<main-hash-2>.jslazy-chunk.<lazy-hash-2>.js

注意:哈希值现在已经不同了,因为文件的内容已经改变)。服务器上不再提供旧版本。

旧版本在服务器上不再可用。

  • 同时,用户的浏览器决定从其缓存中清退 lazy-chunk.<lazy-hash-1>.js浏览器可能决定从缓存中清退特定(或所有)资源,以便回收磁盘空间。

  • 用户再次打开本应用。此时,Service Worker 将提供它所知的最新版本,当然,实际上对我们是旧版本(index.htmlmain.<main-hash-1>.js)。

  • 在稍后的某个时刻,该应用程序请求惰性捆绑包 lazy-chunk.<lazy-hash-1>.js

  • Service Worker 无法在缓存中找到该资产(请记住浏览器已经将其清退了)。它也无法从服务器上获取它(因为服务器现在只有较新版本的 lazy-chunk.<lazy-hash-2>.js)。

在上述情况下,Service Worker 将无法提供通常会被缓存的资产。该特定的应用程序版本已损坏,并且无法在不重新加载页面的情况下修复客户端的状态。在这种情况下,Service Worker 会通过发送 UnrecoverableStateEvent事件来通知客户端。可以订阅 SwUpdate#unrecoverable以得到通知并处理这些错误。

handle-unrecoverable-state.service.ts

@Injectable()
export class HandleUnrecoverableStateService {
  constructor(updates: SwUpdate) {
    updates.unrecoverable.subscribe(event => {
      notifyUser(
        'An error occurred that we cannot recover from:\n' +
        event.reason +
        '\n\nPlease reload the page.'
      );
    });
  }
}

# 关于 Angular Service Worker 的更多信息

你可能还对下列内容感兴趣:

  • Service Worker 通知
Last Updated: 5/13/2023, 10:57:08 AM