单页 Web 应用 (SPA) 的一项常见架构功能是为应用的全局功能提供支持所需的最小 HTML、CSS 和 JavaScript 集。实际上,这往往是指在所有网页中持续存在的标题、导航栏和其他常见界面元素。当 Service Worker 预缓存这个最小界面的 HTML 和相关资产时,我们将其称为 Application Shell。
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 提取新内容。
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 标记及其依赖项:
在几乎所有工作流中,都可以预缓存 App Shell 的 HTML、CSS 和 JavaScript,包括使用捆绑器的项目。随着本文档的深入,您将学习如何直接使用 Workbox 设置工具链,以构建最适合您的项目的 Service Worker(无论其是否为 SPA)。
总结
将 App Shell 模型与 Service Worker 结合使用非常适合离线缓存,尤其是当您将其预缓存功能与针对标记或 API 响应的网络优先、回退到缓存策略相结合时。因此,您可以获得可靠的快速体验,在重复访问时可以立即呈现应用 Shell,即使在离线条件下也是如此。