1. 为什么网页一断网就变白:离线应用要解决的核心问题
你有没有碰到过这种尴尬:地铁上信号忽明忽暗,打开自己做的 H5 页面直接白屏;或者用户明明昨天刚访问过你的活动页,今天断网后再点开,只剩一片空白。问题不一定出在你的代码写法上,而在你从来没有认真思考过 HTML 离线应用与缓存机制这个被大多数人忽略的角落。HTML 不是只能在线渲染的文档,配合浏览器提供的缓存能力,完全可以在断网状态下保持可用。
这篇文章我从实际项目落地的角度出发,把 HTTP 缓存、AppCache 的历史、Service Worker 与 Cache API、IndexedDB 这些关系掰开揉碎地讲清楚,并给出可以直接抄的缓存策略。无论你是用原生 HTML 写落地页,还是用 Vue/React 构建单页应用,只要理解了这条链路,断网白屏、更新不生效、缓存错乱这类问题都能定位到根因。
1.1 网页加载的本质:一切都是网络请求
很多人对“网页”的理解停留在“一个 HTML 文件”上,但实际上,浏览器打开一个页面时,同时要进行几十甚至上百次网络请求:请求 HTML 文档本身、CSS 样式表、JavaScript 脚本、图片、字体、视频,以及异步接口返回的数据。每一类资源都有自己独立的 URL 和生命周期。
一旦断网,这些请求会集体失败。浏览器拿不到 HTML,自然就没有文档结构;拿不到 CSS,即便有 HTML 也会像一份纯文本;拿不到 JS,动态渲染出来的单页应用就会停留在白屏或者 loading 状态。说白了,靠“在线请求”活着的页面,在网络断开的那一刻就丧失了全部能力。
离线应用要解决的核心问题,就是把“每次都要去服务器拿”的依赖,换成“本地已经有副本,网络断开也能直接读”的资源冗余。缓存机制本质上是在做一件很朴素的事:在服务器和页面之间,增加一个可以被控制的存储层。
1.2 离线应用需要解决的三件事
做离线应用,不是简单地把某个文件存下来就行。你在设计缓存方案时,至少要同时回答三个问题。
第一,静态资源怎么存。HTML、CSS、JS、图片这些文件体积大、数量多,如果每次都走网络,慢且浪费流量。这部分适合用 Cache API 做预缓存,按版本管理。
第二,接口数据怎么存。页面要展示的内容往往来自后端接口,断网后如果只缓存了静态资源,页面骨架有了,数据却是空的,用户看到的还是一个空壳。这里需要把 JSON 格式的接口数据本地化,存到 IndexedDB 或者 Cache Storage 里。
第三,版本怎么更新。缓存是把双刃剑:存得越狠,离线能力越强,但线上发了个新版本,用户却还在用旧缓存,你作为开发者会非常难受。所以离线方案必须包含一套明确的更新策略,让“何时用旧版”“何时拉新版”可控。
后面所有内容,都是围绕这三个问题展开的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. AppCache 的前车之鉴:HTML 离线缓存的演进逻辑
聊到 HTML 离线应用,绕不开一个已经进入历史垃圾堆但依然值得了解的东西:Application Cache,也就是 AppCache。它曾经以“HTML5 离线缓存”的身份被写进各种教材里,如今却被浏览器官方标记为废弃,这中间的坑,值得每一个做缓存的人记住。
2.1 Application Cache 曾经是怎么工作的
AppCache 的工作原理很简单:在一个 HTML 页面的 <html> 标签上挂一个 manifest 属性,指向一个 .appcache 后缀的清单文件。比如:
html复制<!doctype html>
<html lang="zh-CN" manifest="app.appcache">
然后在 app.appcache 文件里分区块声明缓存规则:
text复制CACHE MANIFEST
# v1
/index.html
/css/style.css
/js/main.js
NETWORK:
/api/refresh
FALLBACK:
/ /offline.html
CACHE MANIFEST 下面列出的资源会被浏览器本机缓存;NETWORK 下列出的地址保持在线访问,不被缓存;FALLBACK 则定义某个路径不可访问时,回退到哪个本地页面。
这套设计刚推出时确实很惊艳,因为它几乎不需要写 JavaScript,浏览器自己就把清单文件里的资源拉下来存好了。于是很多团队把它用在了移动端 H5 上,希望在弱网环境下能秒开页面。
2.2 它为什么被所有浏览器抛弃
AppCache 看起来省事,实际用起来全是坑。我当年在一个多页面的移动站上用过它,踩过几个印象深刻的雷。
第一个坑是更新不可控。只要清单文件里的内容发生变化,浏览器会自动重新拉取清单里所有资源,这个过程开发者无法精确干预,也没有任何 API 告诉你“更新到哪一步了”。想做个“先提示用户、再下载更新”的流程,完全做不到。
第二个坑是双缓存问题。AppCache 缓存的 HTML 文档,一旦被缓存,它和服务器上新的 HTML 会出现“两个版本打架”的情况。你改了 index.html 并更新了清单版本号,但浏览器可能仍然展示旧的页面,因为 AppCache 对页面本身有特殊处理。当时很多团队被迫在 HTML 里写一段“清除 AppCache”的脚本,体验非常糟糕。
第三个坑是存储空间限制和不可监控性。移动端浏览器对 AppCache 的容量限制通常只有 5MB 左右,超过后直接缓存失败。而且你无法代码里查看当前缓存了哪些资源、占了多少空间、是否能删除某个特定条目。这些问题叠加在一起,让 AppCache 成了一个“上线容易维护难”的定时炸弹。后来主流浏览器相继宣布废弃 AppCache,转而在 Service Worker 基础上重新构建离线能力。
2.3 我们还能从 AppCache 里学到什么
AppCache 失败的核心原因,不是“离线缓存”这个想法错了,而是它把缓存控制权从开发者手里拿走了。浏览器按照一套含糊的启发式规则帮你决定缓存什么、何时更新,你却只能在旁边干瞪眼。
Service Worker 接下来的设计思路完全是相反的:所有缓存行为都由开发者用 JavaScript 显式控制,缓存哪个请求、用哪种策略、什么时候清理、什么时候通知页面更新,全部变成代码。这种“把控制权还给开发者”的思路,正是离线应用走到今天的底层逻辑。所以你要做的第一件事,是放下 AppCache 那套“声明式”思维,拥抱 Service Worker 的“命令式”编程模型。
3. 先把地基打牢:HTTP 缓存与浏览器存储到底存了什么
在进入 Service Wor
