在 Google 上构建 PWA(第 1 部分)

公告团队在开发 PWA 时了解到的有关 Service Worker 的信息。

Douglas Parker
Douglas Parker
Joel Riley
Joel Riley
Dikla Cohen
Dikla Cohen

我们发布了一系列博文,介绍 Google 公告团队在这方面积累的经验,这是第一篇 构建面向外部的 PWA 时。在这些博文中,我们将分享我们面临的一些挑战, 我们克服错误的方法,以及有关如何避免误区的一般建议。绝不是 意味着能够全面了解 PWA。目的是分享我们团队的经验教训。

在这第一篇帖子中,我们先介绍一些背景信息,然后再深入探讨 关于 Service Worker 的知识。

背景

从 2017 年年中到 2019 年年中,本公告一直在积极开发。

我们选择开发 PWA 的原因

在深入了解开发过程之前,我们先来了解一下构建 PWA 为何有吸引力 选择以下选项:

  • 能够快速迭代。特别有用,因为公告是在 多个市场
  • 单一代码库。我们的用户在 Android 和 iOS 设备上的用户数大致均衡。PWA 意味着 我们可以构建一个同时支持这两个平台的 Web 应用。这提高了 团队影响力和影响力
  • 更新速度快,并且不依赖于用户行为。PWA 可以自动更新 可减少外部过时客户端的数量。我们成功部署了突破性后端 只需极短的迁移时间即可帮助客户进行各种更改
  • 与第一方和第三方应用轻松集成。此类集成是 。对于 PWA,通常只需打开一个网址即可。
  • 消除了安装应用的麻烦。

我们的框架

在公告中,我们使用的是 Polymer,但任何支持良好、现代的 框架才能正常运行。

关于 Service Worker 的知识

没有服务就无法使用 PWA worker。Service Worker 为您提供大量强大功能,例如高级缓存策略、离线功能、后台同步、 虽然 Service Worker 确实增加了复杂性,但我们发现,与其所带来的好处相比, 复杂性。

可以的话,生成验证码

避免手动编写 Service Worker 脚本。手动编写 Service Worker 需要手动编写 管理缓存的资源和重写大多数 Service Worker 库通用的逻辑,例如 显示为 Workbox

话虽如此,但由于我们的内部技术栈,我们无法使用库来生成和管理 我们的 Service Worker。我们在下文中了解到的信息会不时地反映这一点。转到 非生成的 Service Worker

并非所有库都与 Service Worker 兼容

某些 JS 库的假设在由 Service Worker 运行时无法按预期工作。对于 实例(假设 windowdocument 可用),或者使用服务不可用的 API 工作器(XMLHttpRequest、本地存储空间等)。确保您的应用需要 应用与 Service Worker 兼容对于这个特定的 PWA,我们希望使用 使用 gapi.js 进行身份验证,但 无法做到,因为它不支持 Service Worker。库作者还应减少或移除 尽可能对 JavaScript 上下文做出不必要的假设,以支持 Service Worker 的使用 例如避免使用与 Service Worker 不兼容的 API,并避免在全局范围内 状态

避免在初始化期间访问 IndexedDB

在以下情况下,不读取 IndexedDB 初始化 Service Worker 脚本,否则会出现以下意外情况:

  1. 用户拥有采用 IndexedDB (IDB) 版本 N 的 Web 应用
  2. 使用 IDB 版本 N+1 推送新的 Web 应用
  3. 用户访问 PWA,这会触发下载新的 Service Worker
  4. 新的 Service Worker 会在注册 install 事件处理程序之前从 IDB 中读取数据,从而触发 IDB 升级周期从 N 升级到 N+1
  5. 由于用户的旧客户端版本为 N,因此 Service Worker 升级过程挂起为活动状态 对旧版本数据库仍保持开放
  6. Service Worker 挂起,并且永远不会安装

在本例中,缓存在 Service Worker 安装时失效,因此,如果 Service Worker 从未 已安装,则用户从未收到过更新的应用。

提升弹性

尽管 Service Worker 脚本在后台运行,但它们也可以随时终止,即使 在 I/O 操作(网络、IDB 等)期间使用。任何长时间运行的进程 可随时恢复

如果同步过程将大型文件上传到服务器并保存到 IDB,我们的解决方案 需要充分利用我们内部上传库的可续传 系统时,在上传前将可续传上传网址保存到 IDB,并使用该网址恢复 如果第一次没有完成,则直接上传。同样,在执行任何长时间运行的 I/O 操作之前, 状态已保存到 IDB,以指明每条记录在流程中所处的位置。

不依赖于全局状态

由于 Service Worker 存在于不同的上下文中,因此您可能会希望存在的许多符号没有 存在。我们的很多代码既在 window 上下文又在 Service Worker 上下文(如 例如日志记录、标志、同步等)。代码需要对它使用的服务有一定的防御性,例如 或 Cookie您可以使用 globalThis 以适用于所有上下文的方式引用全局对象。同时使用存储的数据 因为无法保证脚本何时会终止, 状态被逐出

本地开发

Service Worker 的一个主要组成部分是在本地缓存资源。不过,在开发阶段, 与您需要的内容完全相反,尤其是在延迟完成更新时。你仍然想要 安装服务器工作器,以便您可以调试其问题或使用其他 API,例如 后台同步或通知。在 Chrome 上,您可以通过 Chrome 开发者工具实现此目的,方法是: 选中 Bypass for network 复选框(Application 面板 > Service worker 窗格) 除了选中网络面板中的停用缓存复选框外,还可以 禁用内存缓存。为了覆盖更多的浏览器,我们选择另一种解决方案 在 Service Worker 中包含一个用于停用缓存的标志,该标志在开发者上默认启用 build。这可确保开发者始终能获取最新的更改,而不会出现任何缓存问题。时间是 一定要添加 Cache-Control: no-cache 标头,防止浏览器 缓存任何资源

灯塔

Lighthouse 提供了多种调试功能, 可用于 PWA 的工具。它会扫描网站并生成涵盖 PWA、性能、 包括无障碍功能、搜索引擎优化 (SEO) 及其他最佳做法。 我们建议在持续运行 Lighthouse 集成,以便在您破坏其中一个产品 PWA 标准实际上,我们遇到过一次这种情况,Service Worker 没有安装 Service Worker 我们在正式版推送之前没有意识到这一点如果将 Lighthouse 纳入我们的持续集成 (CI) 中, 阻止了他们。

拥抱持续交付

由于 Service Worker 可以自动更新,因此用户无法限制升级。这个 显著减少外部过时客户端的数量。当用户打开我们的应用时 在延迟下载新客户端的同时,Service Worker 将为旧客户端提供服务。部署 下载新版客户端时,系统会提示用户刷新页面以使用新功能。即使 该用户忽略了此请求,那么下次刷新网页时,他们将收到新的 版本因此,用户很难在同一 对 iOS/Android 应用执行相同的操作

我们只需极短的迁移时间即可推送重要的后端更改, 客户。通常,我们会先让用户一个月来更新到较新的客户端,然后再进行 重大更改由于应用会在过时期间运行,因此老客户也可以 如果用户长时间未打开应用,该目录就处于实际存在的状态。在 iOS 上,Service Worker 在几周后被逐出 因此不会出现这种情况对于 Android,此问题可以通过在 过时,或在几周后手动过期内容。在实践中,我们从未遇到过 客户端可能遇到的各种问题特定团队对团队要求的严格程度取决于他们的具体用途 但 PWA 比 iOS/Android 应用提供了更大的灵活性。

在 Service Worker 中获取 Cookie 值

有时,有必要在 Service Worker 上下文中访问 Cookie 值。在本例中,我们 需要访问 Cookie 值以生成令牌来验证第一方 API 请求。在 Service Worker,则无法使用 document.cookies 等同步 API。您随时可以发送 从 Service Worker 发送给活动(窗口化的)客户端,以请求 Cookie 值,尽管 Service Worker 可以在后台运行,而没有任何窗口化的客户端 例如后台同步期间。为解决此问题,我们在 Google Cloud 控制台上 仅将 Cookie 值回显给客户端的前端服务器。Service Worker 并读取响应以获取 Cookie 值。

随着 Cookie Store API、 对支持此解决方法的浏览器而言,应该不再需要这种解决方法,因为它提供了 异步访问浏览器 Cookie,可由 Service Worker 直接使用。

非生成的 Service Worker 的误区

确保在任何静态缓存文件发生更改时,Service Worker 脚本发生更改

一种常见的 PWA 模式是 Service Worker 在其运行期间安装所有静态应用文件 install 阶段,可让所有客户端直接缓存 Cache Storage API 缓存 后续访问 。只有在浏览器检测到 Service Worker 时才安装 Service Worker Worker 脚本以某种方式发生了变化,因此我们必须确保 Service Worker 脚本文件本身 在缓存文件发生更改时以某种方式发生更改。我们手动完成此操作 静态资源文件集,这样每个版本都会生成不同的 Service Worker JavaScript 文件。Service Worker 库,例如 Workbox 可为您自动执行此过程。

单元测试

通过向全局对象添加事件监听器来运行 Service Worker API。例如:

self.addEventListener('fetch', (evt) => evt.respondWith(fetch('/foo')));

这可能很难测试,因为您需要模拟事件触发器、事件对象, respondWith() 回调,然后等待该 promise,最后对结果做出断言。一个 构建这种代码结构的更简单方法是将所有实现委托给另一个文件, 测试。

import fetchHandler from './fetch_handler.js';
self.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));

由于对 Service Worker 脚本进行单元测试比较困难,我们保留了核心 Service Worker 编写尽可能精简的脚本,将大部分实现分成其他模块。开始时间 这些文件只是标准 JS 模块,可以更轻松地通过标准测试 库。

敬请期待第 2 部分和第 3 部分

在此系列的第 2 和第 3 部分中,我们将讨论媒体管理和 iOS 特有的问题。如果您 想问我们有关在 Google 构建 PWA 的更多信息,请访问我们的作者个人资料, 如何与我们联系: