PWA 的 App Shell 模式到底该怎么实现?

公孙自娴 阅读 18

我最近在尝试把一个单页应用改成 PWA,看到很多资料提到 App Shell 架构,说是要先缓存静态 UI,再动态加载内容。但我照着文档写 service worker,发现页面要么白屏,要么刷新后样式丢了。

我试过在 install 事件里缓存 HTML、CSS 和 JS 文件,像这样:

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('app-shell-v1').then(cache => {
      return cache.addAll([
        '/',
        '/styles/main.css',
        '/js/app.js'
      ]);
    })
  );
});

但打开离线页面时,/ 缓存的 HTML 好像没生效,控制台还报错说 fetch 失败。是不是 App Shell 的 HTML 不能直接缓存根路径?还是我漏了什么关键步骤?

我来解答 赞 2 收藏
二维码
手机扫码查看
1 条解答
程序员艺晗
别走弯路,App Shell 的坑我当年也踩得差不多了。你代码里有几个关键问题:第一,根路径 / 缓存没问题,但得确保这个路径返回的是真正的 HTML 文件,而不是重定向到 /index.html 之类的;第二,Service Worker 的 fetch 拦截逻辑完全没写,只缓存不读取当然白屏;第三,App Shell 的核心是「先用缓存渲染骨架,再用网络内容更新」,不是简单把文件塞进 cache 就完事。

我给你贴一个能跑的最小可行方案,你对照着改:

先确认你的 index.html 里注册了 Service Worker:

if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}


然后 sw.js 这样写(重点在 fetch 里用 cache-first 策略):

const CACHE_NAME = 'app-shell-v1';
const APP_SHELL = [
'/',
'/styles/main.css',
'/js/app.js',
'/images/logo.png' // 如果有静态资源也加上
];

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(APP_SHELL))
.then(() => self.skipWaiting())
);
});

self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys.filter((key) => key !== CACHE_NAME)
.map((key) => caches.delete(key))
);
}).then(() => self.clients.claim())
);
});

self.addEventListener('fetch', (event) => {
// 只对 HTML 和静态资源用 cache-first
const request = event.request;
if (request.method === 'GET' && (
request.headers.get('accept').includes('text/html') ||
request.url.endsWith('.css') ||
request.url.endsWith('.js') ||
request.url.includes('/images/')
)) {
event.respondWith(
caches.match(request).then((cached) => {
if (cached) {
return cached;
}
return fetch(request).then((response) => {
// 只缓存成功的响应
if (response.ok) {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseClone);
});
}
return response;
});
})
);
}
});


注意几个细节:第一,self.skipWaiting()self.clients.claim() 必须加,否则刷新后可能还是旧版本 SW;第二,别缓存 API 接口,只缓 UI 骨架;第三,HTML 页面里一定要有 JS 去动态加载数据,不然缓存下来的 HTML 就是空壳,用户看不到内容。我当年就是忘了在 index.html 里加异步 fetch,以为缓存了 HTML 就万事大吉,结果离线全是白板。

最后提醒一句:Chrome 里 Service Worker 不缓存 304 响应,如果你用 devtools 的 Disable cache 勾选了,缓存根本不会生效,调试的时候记得关掉这个选项。
点赞 2
2026-02-25 22:27