Service Worker 和应用 Shell 模型

单页 Web 应用 (SPA) 的常见架构特性是支持应用的全局功能所需的一组最少的 HTML、CSS 和 JavaScript。但在实际操作中,这往往是所有页面始终显示的页眉、导航栏和其他常见界面元素。当 Service Worker 预缓存这个最小界面的 HTML 和依赖资源时,我们称之为应用 Shell

App Shell 示意图。这是一张网页的屏幕截图,顶部是标题,底部是内容区域。标题标记为“应用 Shell”,底部标记为“内容”。

App Shell 在 Web 应用的感知性能方面发挥着重要作用。这是最先加载的内容,因此,也是用户在等待内容填充到界面中时最先看到的内容。

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

  • 以可靠、一致的效果吸引重复访问用户。首次访问没有安装 Service Worker 的应用时,必须先从网络加载应用的标记及其关联资源,然后 Service Worker 才能将其放入缓存中。但是,重复访问将从缓存中提取 App Shell,这意味着可以瞬时加载和呈现。
  • 在离线场景中可靠地使用功能。有时互联网接入不稳定,或根本无法上网,用户可怕的“我们找不到该网站”它不会消失。App Shell 模型通过使用缓存中的 App Shell 标记响应任何导航请求,解决此问题。即使有人在您的 Web 应用中访问他们之前从未访问过的网址,App Shell 也会从缓存中提供,并用有用内容填充。

何时应使用 App Shell 模型

如果您的常用界面元素不会因路由而异,但内容会更改,则 App Shell 最为合理。大多数 SPA 可能已经使用一种非常有效的 App Shell 模型。

如果项目就是这样描述的,而您想要添加 Service Worker 来增强其可靠性和性能,则 App Shell 应当:

  • 加载速度快
  • 使用 Cache 实例中的静态资源。
  • 添加常见的界面元素,如标题和边栏,这些元素与网页内容分开。
  • 检索并显示特定网页的内容。
  • 如果适用,可以选择缓存动态内容以供离线观看。

App Shell 通过 API 或 JavaScript 中绑定的内容动态加载特定于网页的内容。它还应该自更新,也就是说,如果 App Shell 的标记发生更改,Service Worker 更新应获取新的 App Shell 并自动对其进行缓存。

构建应用 Shell

App Shell 应与内容独立存在,但又能为在其中填充内容提供基础。理想情况下,它应尽可能精简,但应在初始下载时包含足够多的实用内容,以便用户了解体验会快速加载。

合适的平衡点取决于您的应用。Jake Archibald 的 Trained To Thrill 应用的应用 Shell 包含一个带有刷新按钮的标头,用于从 Flickr 提取新内容。

Trained to Thrill Web 应用在两种不同状态下的屏幕截图。在左侧,仅显示缓存的 App Shell,未填充任何内容。在右侧,内容(一些模块序列的几张图片)被动态加载到 App 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>

为项目构建 App Shell 时,它必须具有以下特征:

  • HTML 应明确为各个界面元素设置独立的区域。在上面的示例中,这包括应用的标题、导航、主要内容区域和用于加载“旋转图标”的空间仅在内容加载时显示。
  • 为应用程序 Shell 加载的初始 JavaScript 和 CSS 应尽量减少,并且只与应用程序 Shell 本身的功能(与内容无关)。这可确保应用能够尽快渲染其 shell,并最大限度地减少主线程工作,直到内容显示为止。
  • 用于注册 Service Worker 的内嵌脚本。

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

缓存 App 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 会预缓存 App Shell 标记及其依赖项:

<ph type="x-smartling-placeholder">
</ph> Chrome 开发者工具中网络面板的屏幕截图,显示了从网络下载的资源列表。由 Service Worker 预缓存的资源与行左侧一个齿轮的其他资源区分开来。在安装时,Service Worker 会预缓存一些 JavaScript 和 CSS 文件。
Service Worker 会在安装时预缓存 App Shell 的依赖项。预缓存请求为最后两行,请求旁边的齿轮图标表示 Service Worker 处理了请求。

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

总结

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