应用 Shell 是为界面提供支持的最小的 HTML、CSS 和 JavaScript。App Shell 应该:
- 快速加载
- 将被缓存
- 动态显示内容
应用 Shell 是确保实现可靠良好性能的秘诀。应用的 Shell 就好比您在构建原生应用时需要向应用商店发布的一组代码。它是开始实现所需的负载,但可能不是整个过程。它会将您的界面保留在本地,并通过 API 动态提取内容。
背景
Alex Russell 的渐进式 Web 应用文章介绍了 Web 应用可如何根据用户的使用和征求用户同意逐步调整,以提供更接近原生应用的体验,其中包括离线支持、推送通知以及被添加到主屏幕的功能。这在很大程度上取决于 Service Worker 的功能和性能优势及其缓存能力。这样,您就可以专注于速度,为 Web 应用提供与原生应用相同的免安装加载和定期更新。
为了充分利用这些功能,我们需要一种新的网站思维方式,即应用 Shell 架构。
下面我们来深入了解如何使用 Service Worker 增强的 App Shell 架构来构造您的应用。我们将着眼于客户端和服务器端呈现,并分享您可以立即试用的端到端示例。
为了强调这一点,以下示例展示了使用此架构的应用首次加载。请注意,屏幕底部会显示“应用已可离线使用”消息框。如果 shell 的更新稍后可用,我们可以通知用户刷新以获取新版本。
那么,什么是 Service Worker?
Service Worker 是一种独立于网页在后台运行的脚本。它会对事件作出响应,包括从其所提供页面发出的网络请求,以及从您的服务器推送通知。Service Worker 的生命周期特意较短。它在获得事件时被唤醒,并且仅在需要处理事件时运行。
与常规浏览上下文中的 JavaScript 相比,Service Worker 也有一组有限的 API。这是网络上的 Worker 的标准。Service Worker 无法访问 DOM,但可以访问 Cache API 等功能,它们可以使用 Fetch API 发出网络请求。IndexedDB API 和 postMessage() 也可用于在 Service Worker 与其控制的页面之间实现数据持久性和消息传递。从您的服务器发送的推送事件可以调用 Notification API,以提高用户互动度。
Service Worker 可以拦截从页面发出的网络请求(这会在 Service Worker 上触发 fetch 事件),并返回从网络检索的响应或从本地缓存检索的响应,甚至可以以编程方式构建。它实际上是浏览器中的可编程代理。简洁明了的是,无论响应来自何处,它在查询网页时都好像没有 Service Worker 参与。
如需深入了解 Service Worker,请阅读 Service Worker 简介。
性能优势
Service Worker 功能强大,可以进行离线缓存,另外,由于对网站或 Web 应用的重复访问,可以实现即时加载,大幅提升性能。您可以缓存 App Shell,使其能够离线使用,并使用 JavaScript 填充其内容。
当您反复访问时,这可让您在没有网络的情况下在屏幕上展示有意义的像素,即使内容最终来自网络也是如此。您可以将其视为立即显示工具栏和卡片,然后逐步加载其余内容。
为了在真实设备上测试此架构,我们在 WebPageTest.org 上运行了 App Shell 示例,结果如下所示。
测试 1:使用 Chrome 开发者版在 Nexus 5 的有线电视上进行测试
应用的第一个视图必须从网络中提取所有资源,并且要到 1.2 秒才能实现有意义的渲染。得益于 Service Worker 缓存,我们的重复访问可以在 0.5 秒内实现有意义的绘制并完全完成加载。
测试 2:使用 Chrome 开发者版在 Nexus 5 上通过 3G 网络进行测试
我们也可以通过速度略慢的 3G 连接测试我们的样本。这一次,首次有效绘制需要 2.5 秒的时间。完全加载网页需要 7.1 秒。借助 Service Worker 缓存,我们的重复访问可以在 0.8 秒内实现有意义的绘制并完全完成加载。
其他视图也有类似的故事。比较在 App Shell 中实现首次有效绘制所需的时间 3 秒:
至从我们的 Service Worker 缓存加载同一网页时所需经过的 0.9 秒。为我们的最终用户节省了超过 2 秒的时间。
使用 App Shell 架构,您自己的应用也可以取得类似且可靠的性能优势。
Service Worker 是否要求我们重新思考我们构建应用的方式?
Service Worker 意味着应用架构的一些细微更改。与其将所有应用程序压缩成一个 HTML 字符串,不如采用 AJAX 样式的操作。在这里,您可以
这种拆分的影响是巨大的。首次访问时,您可以在服务器上呈现内容,并在客户端上安装 Service Worker。在后续访问中,您只需请求数据。
如何进行渐进式增强?
虽然目前并非所有浏览器都支持 Service Worker,但应用内容 shell 架构使用渐进式增强来确保所有人都能访问内容。以我们的示例项目为例。
下图显示了 Chrome、Firefox Nightly 和 Safari 中呈现的完整版本。在左侧,您可以看到 Safari 版本,其中的内容在服务器上呈现时不使用 Service Worker。右侧是由 Service Worker 提供支持的 Chrome 和 Firefox Nightly 版本。
什么情况下适合使用此架构?
App Shell 架构最适合动态应用和网站。如果您的网站较小且是静态的,您可能不需要应用 Shell,只需在 Service Worker oninstall
步骤中缓存整个网站。请使用最适合您的项目的方法。许多 JavaScript 框架已经鼓励将应用逻辑与内容分离开来,使得此模式应用起来更加直接。
是否有使用此模式的正式版应用?
只需对整个应用界面进行一些更改,即可实现应用 Shell 架构,并且这种架构非常适合大型网站,例如 Google 的 2015 年 Google I/O 大会渐进式 Web 应用和 Google 的收件箱。
离线应用 shell 是一项重大性能优势,在 Jake Archibald 的离线 Wikipedia 应用和 Flipkart Lite 的渐进式 Web 应用中也有体现。
架构说明
在首次加载期间,您的目标是尽快将有意义的内容呈现到用户的屏幕上。
首次加载和加载其他网页
总体而言,应用 Shell 架构将:
优先处理初始加载,但让 Service Worker 缓存应用 Shell,这样重复访问就不需要从网络重新获取 Shell。
延迟加载或后台加载,所有其他加载项。一种很好的选择是使用直读缓存来提取动态内容。
例如,您可以使用 Service Worker 工具(如 sw-precache)可靠地缓存和更新管理静态内容的 Service Worker。(稍后会详细介绍 sw-precache。)
为此,请执行以下操作:
服务器将发送客户端可以呈现的 HTML 内容,并使用时间很久的 HTTP 缓存过期标头来应对不支持 Service Worker 的浏览器。它将使用哈希提供文件名,以实现“版本控制”和轻松更新,从而在应用生命周期的后期使用。
页面将在文档
<head>
内的<style>
标记中添加内嵌 CSS 样式,以便快速对 App Shell 进行首次绘制。每个网页都会异步加载当前视图所需的 JavaScript。因为 CSS 无法异步加载,所以我们可以使用 JavaScript 请求样式,因为它是异步的,而不是由解析器驱动的同步代码。我们还可以利用requestAnimationFrame()
来避免出现快速缓存命中,最终导致样式意外成为关键渲染路径的一部分的情况。requestAnimationFrame()
会强制先绘制第一帧,然后再加载样式。另一种方法是通过项目(如 Filament Group 的 loadCSS)使用 JavaScript 异步请求 CSS。Service Worker 将存储应用 Shell 的缓存条目,以便在重复访问时,可以完全从 Service Worker 缓存加载 Shell,除非网络上有可用更新。
实际实现
我们使用 App Shell 架构、适用于客户端的 vanilla ES2015 JavaScript 以及适用于服务器的 Express.js 编写了一个功能齐全的示例。当然,没有什么可以阻止您针对客户端或服务器部分(例如 PHP、Ruby、Python)使用自己的堆栈。
Service Worker 生命周期
对于我们的应用 Shell 项目,我们使用了 sw-precache,它可以提供以下 Service Worker 生命周期:
事件 | 行动 |
---|---|
安装 | 缓存应用 Shell 和其他单页应用资源。 |
激活 | 清除旧缓存。 |
提取 | 为网址提供一个单页 Web 应用,并将缓存用于资源和预定义的部分。将网络用于其他请求。 |
服务器位数
在此架构中,服务器端组件(在本例中使用 Express 编写)应该能够分别处理内容和展示。内容可添加到导致网页静态呈现的 HTML 布局中,也可单独提供并动态加载。
可以理解,您的服务器端设置可能与我们在演示版应用中使用的设置有很大不同。大多数服务器设置都可以实现这种 Web 应用模式,尽管需要进行一些重新设计。我们发现,以下模型的效果非常好:
端点为应用的三个部分定义:面向用户的网址(索引/通配符)、应用 Shell(Service Worker)以及 HTML 部分。
每个端点都有一个可以拉取 handlebars 布局的控制器,而该布局可以拉取手柄部分和视图。简单来说,部分视图是复制到最终网页的 HTML 块。 注意:执行更高级数据同步的 JavaScript 框架通常更容易移植到 Application Shell 架构。它们倾向于使用数据绑定和同步,而不是部分绑定。
用户最初会看到包含内容的静态页面。此页面会注册一个 Service Worker(如果受支持),它将缓存 App Shell 及其依赖的所有内容(CSS、JS 等)。
然后,App Shell 将作为单页 Web 应用,在特定网址的内容中使用 JavaScript 转换为 XHR。XHR 调用会发送到 /partials* 端点,该端点会返回显示相应内容所需的小块 HTML、CSS 和 JS。 注意:有很多方法可以解决这个问题,XHR 只是其中之一。一些应用将内嵌其数据(可能使用 JSON)进行初始呈现,因此在扁平化 HTML 中不是“静态”的。
对于不支持 Service Worker 的浏览器,应始终为其提供回退体验。在我们的演示中,我们会回退到基本的静态服务器端渲染,但这只是众多选项之一。Service Worker 为您利用缓存的应用 Shell 来提升单页应用样式应用性能提供了新机遇。
文件版本控制
有一个问题是,如何处理文件版本控制和更新。这是特定于应用的选项,可用选项包括:
先连接网络,否则使用缓存版本。
仅限网络,离线时会失败。
缓存旧版本并稍后更新。
对于 App Shell 本身,您的 Service Worker 设置应采用缓存优先方法。如果您没有缓存应用 Shell,则表示您没有正确采用架构。
工具
我们维护着多个不同的 Service Worker 帮助程序库,让预缓存应用的 shell 或处理常见缓存模式的过程更容易设置。
对 App Shell 使用 sw-precache
使用 sw-precache 缓存 App Shell 后,应该可以处理与 App Shell 的文件修订版本、安装/激活问题以及提取场景有关的问题。将 sw-precache 放入您应用的构建流程中,并使用可配置的通配符来选择静态资源。与其手动编写 Service Worker 脚本,不如让 sw-precache 生成一个使用缓存优先提取处理程序来安全高效地管理缓存的脚本。
对应用的初次访问会触发预缓存所需的全部资源。这类似于从应用商店安装原生应用的体验。当用户返回您的应用时,系统只会下载更新后的资源。在我们的演示中,我们会在有新 shell 可用时通过消息“应用更新。请刷新以查看新版本。”此模式可让用户知道他们可以刷新以获取最新版本,从而顺畅地启动。
使用 sw-toolbox 进行运行时缓存
使用 sw-toolbox 进行运行时缓存,并采用根据资源而异的策略:
cacheFirst(针对图片),以及一个自定义过期政策为 N maxEntries 的专用命名缓存。
networkFirst 或用于 API 请求的最快速度,具体取决于所需的内容新鲜度。最快的速度可能没什么问题,但如果某个 API Feed 经常更新,请使用 networkFirst。
总结
App Shell 架构具有诸多优势,但仅对某些类别的应用才有意义。该模型仍处于早期阶段,因此有必要评估此架构的工作和整体性能优势。
在我们的实验中,我们利用客户端和服务器之间的模板共享来尽可能减少构建两个应用层的工作。这样可以确保渐进式增强仍然是核心功能。
如果您已考虑在应用中使用 Service Worker,请查看该架构并评估它是否适合您自己的项目。
感谢评价者:Jeff Posnick、Paul Lewis、Alex Russell、Seth Thompson、Rob Dodson、Taylor Savage 和 Joe Medley。