进阶篇:Web 离线应用
网络连接不稳定、断网,是 Web 应用长期存在的体验痛点。就算产品本身功能再完善,一旦失去网络,页面就无法访问,整体体验会大打折扣。Web 离线应用,就是让网页在设备没有网络连接的环境下依旧可以正常运行。
离线能力的核心是离线缓存:用户首次访问站点,把运行需要的静态资源保存到本地存储;后续访问优先读取本地缓存,就算网络断开,应用依旧可用。
离线缓存可以带来三方面收益:
- 离线浏览:断网环境依旧可以访问 Web 应用;
- 加载速度提升:资源直接读取本地磁盘,省去网络请求耗时;
- 降低服务器压力:浏览器仅下载更新变更后的资源,减少重复请求。
前端历史上有两套离线缓存技术方案:
- Application Cache(简称 AppCache):HTML5 早期离线方案,已经从 Web 标准废弃,新项目禁止使用,仅部分老旧浏览器还留有兼容实现。
- Service Worker:现代标准离线方案,属于 Web Worker 体系,通过 JS 完全自定义缓存逻辑,灵活性极强;是 PWA(渐进式 Web 应用)的核心底层技术。
检测设备在线离线状态
做离线应用开发,首先可以通过浏览器 API 判断当前网络状态,同时监听网络切换事件,给用户对应的提示。
注意:navigator.onLine 只能检测设备是否有本地网络连接,不能代表互联网真实连通,仅适合做体验辅助,不能作为业务强判断依据。
if (navigator.onLine) {
alert('联网中');
} else {
alert('离线中');
}
监听网络状态切换事件:
window.addEventListener('online', function () {
alert("网络重新连接!");
});
window.addEventListener('offline', function () {
alert("网络已断开!");
});
Application Cache(已废弃)
AppCache 是 HTML5 早期提供的离线缓存机制。通过在页面配置 manifest 清单文件,声明需要缓存的资源列表,浏览器自动完成资源缓存,实现页面离线访问。
核心缺陷:AppCache 属于声明式黑盒缓存,缓存规则全部定义在静态 manifest 清单中,开发者运行时几乎没有控制权,浏览器按固定逻辑执行,大量行为违背开发直觉。
缺陷列表:
- 更新机制反直觉:只根据
manifest文件本身的字节变化判断是否更新缓存; - 页面自动隐式缓存,不可控;
- 一旦有一个资源下载失败,整个缓存更新全部作废;
- URL 查询参数被视为完全不同资源;
manifest文件自身不能被缓存进 AppCache;- JS API 能力薄弱,只能做监听,不能细粒度干预;
- 无法处理动态接口请求:面向静态资源,AJAX、
fetch动态接口很难做灵活离线缓存。
注意:该 API 已经正式从 Web 标准移除,新项目不要使用,只做历史原理了解。
在页面引入 manifest 清单
需要做缓存的页面,在 <html> 标签添加 manifest 属性,指向缓存清单文件。
<html manifest="example.appcache">
<!-- 页面内容 -->
</html>
- 每一个需要被 AppCache 管理的页面,都必须添加
manifest属性; - 没有写
manifest属性的页面,不会被缓存; - 带有
manifest属性的 HTML 页面本身,会被浏览器自动加入缓存,不需要写进manifest文件清单内。
完整工作流程
- 首次访问带
manifest属性的页面:浏览器加载页面,下载manifest清单内全部资源,生成第一版离线缓存。 - 再次访问该页面:优先读取本地缓存渲染页面;同时发起 HTTP 请求检查
manifest文件是否更新,触发checking事件。 - 如果
manifest文件内容没有变化:触发noupdate事件,本次更新流程直接结束。注意:仅仅修改图片、JS、CSS 等缓存资源是不会触发更新的,必须修改manifest文件本身,浏览器才会重新拉取资源。 - 如果
manifest文件发生变更:浏览器把清单内全部资源下载到临时缓存,下载过程持续抛出progress事件;下载出错抛出error,直接终止更新。 - 全部资源下载完毕,资源移入正式离线缓存,触发
cached、updateready事件。
注意:页面 DOM 已经从旧缓存渲染完毕;新缓存不会自动生效,需要手动刷新页面,新资源才会生效。
manifest 缓存清单
文件后缀建议为 .appcache,服务器返回的 MIME 类型必须为 text/cache-manifest,否则不会生效。
AddType text/cache-manifest .appcache
清单文件分为三段:CACHE、NETWORK、FALLBACK;文件第一行强制必须写 CACHE MANIFEST。
CACHE:默认段,定义需要离线缓存的资源;NETWORK:网络白名单,这些资源永远走网络,离线无法访问;支持通配符*;FALLBACK:资源访问失败时,使用备选页面。
CACHE MANIFEST
# v1.0.0 2025-10-10 修改版本注释触发更新
CACHE:
/favicon.ico
index.html
style.css
js/main.js
NETWORK:
*
FALLBACK:
/ /offline.html
缓存失效条件
- 用户手动清除浏览器缓存数据;
- 服务器删除
manifest文件,触发obsolete事件,缓存整体作废; manifest文件本身发生修改(仅修改被缓存资源,不会触发更新)。
Service Worker(现代离线方案)
Service Worker 是 PWA 的核心技术,它是独立于页面主线程的事件驱动 JS 脚本,充当浏览器、网页、网络三者之间的代理。可以拦截页面所有网络请求,自由编写缓存读写逻辑,实现高度自定义的离线策略。
核心特性:
- 独立线程运行:独立于页面主线程,不能直接访问 DOM;页面标签关闭,Service Worker 可以睡眠驻留,后台依旧可以执行任务。
- Promise 异步优先:所有 IO、缓存操作均为异步,不阻塞页面。
- 安全限制:正式环境必须部署在 HTTPS;本地开发环境
localhost、127.0.0.1被浏览器视作安全源,可以 HTTP 调试。 - 完全可控缓存:通过 JS 自由控制缓存读写;静态资源、接口请求都可以做缓存策略。- 扩展能力:除离线缓存外,还支持后台同步、Web 推送通知等能力。
完整生命周期
整体流程:注册 → 下载 → 安装 → 等待 → 激活 → 运行监听事件 → 终止。
- 注册:主线程调用
navigator.serviceWorker.register(),传入sw.js脚本地址。 - 下载:浏览器下载 Service Worker 文件。
- 安装(install):脚本下载解析完成,触发 install;一般在这里预缓存需要的静态资源。install 失败,整个 Service Worker 注册直接作废。
- 等待(waiting):如果已经存在旧版本 Service Worker,新版本 install 成功后会进入等待状态,等待旧版本完全释放控制。
- 激活(activate):旧版本全部页面关闭之后,新版本触发 activate;通常在这里清理过期旧缓存。
- 运行阶段:激活成功,开始接管作用域内页面,监听
fetch、message、sync等事件。页面全部关闭后,Service Worker 进入睡眠,节约内存;有事件到来再被唤醒。 - 终止(Terminated):注销、版本更新、长时间空闲,会被浏览器终止。
注意:首次注册成功后,当前页面不会被新 Service Worker 接管;需要刷新页面,fetch 拦截才会生效。
```javascriptself.addEventListener('install', event => {}) self.addEventListener('activate', event => {}) self.addEventListener('fetch', event => {}) self.addEventListener('message', event => {}) self.addEventListener('error', event => {})
### 注册
在页面主线程注册 `sw.js`,`scope` 参数用来限定 Service Worker 的管控 URL 范围。
```javascript
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('DOMContentLoaded', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/'
});
console.log('注册成功,作用域:', registration.scope);
} catch (err) {
console.error('Service Worker 注册失败:', err);
}
});
}
调试工具地址:
- Chrome:
chrome://inspect/#service-workers - Chrome:
Application→Service Workers(开发者工具面板)
安装(install)
在 sw.js 脚本内部监听 install,使用 event.waitUntil() 等待缓存初始化完成。caches 是浏览器提供的 CacheStorage API,用来管理命名缓存库。
// sw.js
const CACHE_VERSION = 'v1-assets-202510';
const preCacheUrls = [
'/',
'/index.html',
'/style.css',
'/app.js',
'/favicon.ico'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches
.open(CACHE_VERSION)
.then(cache => cache.addAll(preCacheUrls))
);
});
备注:cache.addAll() 只要任意一个资源请求失败,整个 install 就失败,Service Worker 安装作废。
跳过等待阶段
新版本 install 之后,默认等待旧 Service Worker 全部页面关闭才激活。self.skipWaiting() 可以跳过 waiting,立刻激活新版本。
// sw.js
self.addEventListener('install', (event) => {
self.skipWaiting();
event.waitUntil(
caches
.open(CACHE_VERSION)
.then(cache => cache.addAll(preCacheUrls))
);
});
注意:已经打开的旧页面,不会立刻被新 SW 接管,容易出现页面代码和 SW 逻辑版本不一致。
激活:清理旧缓存
当新版本 Service Worker 激活时,在 activate 事件中清理旧版本缓存,避免存储膨胀。
// sw.js
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_VERSION) {
return caches.delete(cacheName);
}
})
);
})
);
});
运行:fetch 拦截请求
激活之后,页面发出的网络请求会被 fetch 事件拦截;在这里编写缓存策略(缓存优先、网络优先等)。
缓存新的网络响应:Response 对象是可读流,只能消费一次,必须调用 .clone() 复制副本,一份交给浏览器渲染,一份存入缓存。
// sw.js
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedRes) => {
if (cachedRes) {
return cachedRes;
}
return fetch(event.request).then((netRes) => {
const isSuccess = netRes && netRes.status === 200;
const isGet = event.request.method === 'GET'; if (isSuccess && isGet) {
const cloneRes = netRes.clone();
caches.open(CACHE_VERSION).then(cache => {
cache.put(event.request, cloneRes);
});
}
return netRes;
});
})
);
});
注销
主线程可以注销已经注册的 Service Worker:
navigator.serviceWorker.getRegistrations().then((regs) => {
regs.forEach(reg => reg.unregister());
});
更新触发条件
浏览器会重新下载 sw.js 脚本,字节对比发生变化就视为新版本:
- 访问 Service Worker 作用域内页面;
- 后台定时:最长 24 小时强制重新下载脚本;
- 手动调用 API 触发更新。
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
await registration.update();
}
注意:仅仅修改被缓存的图片、JS、CSS,不会触发 SW 更新;只有 sw.js 字节改动,才会识别为新版本。