Service Worker 和应用 Shell 模型

单页 Web 应用 (SPA) 的一项常见架构功能是为应用的全局功能提供支持所需的最小 HTML、CSS 和 JavaScript 集。实际上,这往往是指在所有网页中持续存在的标题、导航栏和其他常见界面元素。当 Service Worker 预缓存这个最小界面的 HTML 和相关资产时,我们将其称为 Application Shell

应用 Shell 示意图。所见网页的屏幕截图,顶部是页眉,底部是内容区域。标头标记为“Application Shell”,而底部则标记为“Content”。

App Shell 在 Web 应用的感知性能方面发挥着重要作用。这是最先加载的内容,因此,当用户等待内容填充到界面时,首先映入眼帘的就是它。

虽然应用 Shell 可以快速加载(前提是网络可用且至少速度较快),但预缓存应用 Shell 及其关联资源的 Service Worker 会为应用 Shell 模型提供以下附加优势:

  • 可靠、一致的重复光顾效果。首次访问未安装 Service Worker 的应用时,必须先从网络加载应用的标记及其关联的资源,然后 Service Worker 才能将其放入缓存中。不过,重复访问会从缓存中提取应用 Shell,这意味着加载和呈现是即时的。
  • 在离线场景中可靠地使用功能。有时,互联网访问不稳定或完全无法使用,可怕的“我们找不到该网站”屏幕让人们更加难以置信。App Shell 模型通过使用缓存中的 App Shell 标记来响应任何导航请求,从而解决了这个问题。即使有人在您的 Web 应用中访问了之前从未访问过的网址,应用 Shell 也将从缓存提供,并填充有用内容。

何时应使用 App Shell 模型

如果您有常见的界面元素,不会因路线而异,但内容会发生变化,那么 App Shell 最为合适。大多数 SPA 可能已经在使用已有效的应用 Shell 模型。

如果您的项目符合此要求,并且您想添加 Service Worker 以增强其可靠性和性能,则 App Shell 应:

  • 快速加载
  • 使用 Cache 实例中的静态资源。
  • 添加标题和边栏等常用界面元素,并与页面内容分开。
  • 检索并显示特定于页面的内容。
  • 在适当的情况下,您可以选择缓存动态内容以供离线观看。

应用 Shell 通过 API 或 JavaScript 捆绑的内容动态加载网页特定的内容。它应该会自动更新,因为如果 App Shell 的标记发生变化,Service Worker 更新应获取新的 App Shell 并自动缓存它。

构建 App Shell

App Shell 应独立于内容存在,同时又可以为在其中填充内容提供基础。理想情况下,它应该尽可能精简,但应在初始下载中包含足够的有意义内容,让用户知道体验的加载速度很快。

能否实现正确的平衡取决于您的应用。Jake Archibald 的 Trained To Thrill 应用的应用 Shell 包含一个标题,其中包含一个刷新按钮,可用于从 Flickr 提取新内容。

Trained to Thrill Web 应用处于两种不同状态的屏幕截图。在左侧,仅显示缓存的应用 Shell,没有填充任何内容。在右侧,内容(一些火车的几张照片)动态加载到应用 Shell 的内容区域。

App Shell 标记因项目而异,不过下面是一个提供应用样板的 index.html 文件示例:

​​<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>
      Application Shell Example
    </title>
    <link rel="manifest" href="/manifest.json">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="stylesheet" type="text/css" href="styles/global.css">
  </head>
  <body>
    <header class="header">
      <!-- Application header -->
      <h1 class="header__title">Application Shell Example</h1>
    </header>

    <nav class="nav">
      <!-- Navigation items -->
    </nav>

    <main id="app">
      <!-- Where the application content populates -->
    </main>

    <div class="loader">
      <!-- Spinner/content placeholders -->
    </div>

    <!-- Critical application shell logic -->
    <script src="app.js"></script>

    <!-- Service worker registration script -->
    <script>
      if ('serviceWorker' in navigator) {
        // Register a service worker after the load event
        window.addEventListener('load', () => {
          navigator.serviceWorker.register('/sw.js');
        });
      }
    </script>
  </body>
</html>

无论如何为项目构建应用 Shell,它都必须具有以下特征:

  • 对于各个界面元素,HTML 应该具有明显的隔离区域。在上面的示例中,这包括应用的标题、导航、主内容区域,以及仅在内容加载时显示的加载“旋转图标”空间。
  • 为 App Shell 加载的初始 JavaScript 和 CSS 应尽量少,并且仅与 App Shell 本身的功能相关,而不与内容有关。这样可确保应用尽快呈现其 shell,并最大限度地减少主线程工作,直到内容显示为止。
  • 用于注册 Service Worker 的内嵌脚本。

构建 App Shell 后,您可以构建 Service Worker 来缓存它及其资产。

缓存应用 Shell

App Shell 及其所需的资源是 Service Worker 在安装时应立即预缓存的内容。假设有一个与上例类似的 App Shell,让我们看看如何使用 workbox-build 在基本的 Workbox 示例中实现这一点:

// build-sw.js
import {generateSW} from 'workbox-build';

// Where the generated service worker will be written to:
const swDest = './dist/sw.js';

generateSW({
  swDest,
  globDirectory: './dist',
  globPatterns: [
    // The necessary CSS and JS for the app shell
    '**/*.js',
    '**/*.css',
    // The app shell itself
    'shell.html'
  ],
  // All navigations for URLs not precached will use this HTML
  navigateFallback: 'shell.html'
}).then(({count, size}) => {
  console.log(`Generated ${swDest}, which precaches ${count} assets totaling ${size} bytes.`);
});

存储在 build-sw.js 中的此配置会导入应用的 CSS 和 JavaScript,包括 shell.html 中包含的 App Shell 标记文件。该脚本是使用 Node 执行的,如下所示:

node build-sw.js

生成的 Service Worker 将写入 ./dist/sw.js,并将在完成时记录以下消息:

Generated ./dist/sw.js, which precaches 5 assets totaling 44375 bytes.

页面加载时,Service Worker 会预缓存应用 Shell 标记及其依赖项:

Chrome 开发者工具中“Network”面板的屏幕截图,其中显示了从网络下载的资源列表。由 Service Worker 预缓存的资产与行左侧带有齿轮图标的其他资产区分开来。Service Worker 在安装时预缓存多个 JavaScript 和 CSS 文件。
Service Worker 在安装时预缓存应用 Shell 的依赖项。预缓存请求是最后两行,请求旁边的齿轮图标表示 Service Worker 处理了请求。

在几乎所有工作流中,都可以预缓存 App Shell 的 HTML、CSS 和 JavaScript,包括使用捆绑器的项目。随着本文档的深入,您将学习如何直接使用 Workbox 设置工具链,以构建最适合您的项目的 Service Worker(无论其是否为 SPA)。

总结

将 App Shell 模型与 Service Worker 结合使用非常适合离线缓存,尤其是当您将其预缓存功能与针对标记或 API 响应的网络优先、回退到缓存策略相结合时。因此,您可以获得可靠的快速体验,在重复访问时可以立即呈现应用 Shell,即使在离线条件下也是如此。