# 应用外壳（app shell）模型

> 应用外壳模式如何把页面的标记、样式、脚本与图片缓存进 service worker，让重复访问能即时渲染 UI 外框，再填充内容。

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

## 外壳是什么

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

## 在 install 阶段缓存外壳

```js
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 支持并提供兜底

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

```js
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-`）
内，这样只会清理过期的外壳版本，而不会波及数据缓存、运行时缓存等其他同源
缓存：

```js
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 参见 [预缓存](/zh/reference/performance/precaching/)。

## 实践清单

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

## 延伸阅读

- [预缓存](/zh/reference/performance/precaching/)
- [导航预加载](/zh/reference/performance/navigation-preload/)