跳转到内容

应用外壳(app shell)模型

发布于 更新于

一句话: 应用外壳模型把渲染应用 UI 外框所需的标记、样式、脚本与图片缓存进 service worker,这样对已缓存路由的重复访问就能立即从缓存中得到响应,然后再 获取内容。

应用外壳是驱动页面 UI 的最小 HTML、CSS 与 JavaScript 集合——即页面头部、 导航、布局骨架等跨路由保持不变的部分,而非随页面变化的数据。 developer.chrome.com 描述了将这一外壳缓存进 service worker、再单独加载内容 的做法;MDN 则单独说明了 service worker 可以缓存页面、样式、脚本与图片等 通用资源,这正是应用外壳模式所依赖的缓存机制。

const SHELL_CACHE = 'app-shell-v1';
const SHELL_ASSETS = ['/', '/styles/app.css', '/scripts/app.js', '/icons/icon-192.png'];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL_ASSETS))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => cached ?? fetch(event.request))
);
});

检测 service worker 支持并提供兜底

Section titled “检测 service worker 支持并提供兜底”

这一缓存机制需要 service worker 拦截请求并提供缓存响应;没有它,这种模式就 没有缓存可供响应。developer.chrome.com 指出,对不支持 service worker 的浏览器, 站点仍可回退到普通的 HTTP 缓存(例如长期有效的 Cache-Control 响应头), 而不是完全失去缓存能力:

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
} else {
// 兜底:不支持 service worker,没有离线缓存的外壳。根据 developer.chrome.com
// 的说明,这类浏览器仍可获得带长期 Cache-Control 响应头的服务端渲染页面,
// 而非依赖这套基于 SW 的方案。
}

根据 Service Workers 规范与 developer.chrome.com 的说明,service worker 在 install 阶段填充缓存,并可以在 activate 阶段显式删除过期的缓存条目。 像上面 SHELL_CACHE 那样为缓存名加上版本号,让 install 填充一个新缓存, 再在 activate 中删除旧命名缓存的条目,就是让外壳标记、样式或脚本的变更 触达已安装用户的方式。将删除范围限定在外壳自身的缓存名前缀(app-shell-) 内,这样只会清理过期的外壳版本,而不会波及数据缓存、运行时缓存等其他同源 缓存:

self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys
.filter((key) => key.startsWith('app-shell-') && key !== SHELL_CACHE)
.map((key) => caches.delete(key))
)
)
);
});

应用外壳模型本身并不是一个浏览器 API——它是构建在 Service Worker API 与 Cache API 之上的一种缓存模式,这两者在当前的 Chromium、Firefox 与 Safari 中均已实现。这一模式 所依赖的更广泛预缓存 API 参见 预缓存。

  • 在 install 阶段只缓存外壳自身的标记、样式、脚本与图片——而不是随请求变化的 页面内容。
  • 对 'serviceWorker' in navigator 做特性检测,缺失时让页面照常加载。
  • 给缓存名加上版本后缀,并在外壳资源变更时于 activate 中删除旧版本缓存。
  • 内容单独填充进外壳(网络或数据缓存)——不要试图预缓存内容随请求变化的页面。