进阶篇: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 文件清单内。

完整工作流程

  1. 首次访问带 manifest 属性的页面:浏览器加载页面,下载 manifest 清单内全部资源,生成第一版离线缓存。
  2. 再次访问该页面:优先读取本地缓存渲染页面;同时发起 HTTP 请求检查 manifest 文件是否更新,触发 checking 事件。
  3. 如果 manifest 文件内容没有变化:触发 noupdate 事件,本次更新流程直接结束。注意:仅仅修改图片、JS、CSS 等缓存资源是不会触发更新的,必须修改 manifest 文件本身,浏览器才会重新拉取资源。
  4. 如果 manifest 文件发生变更:浏览器把清单内全部资源下载到临时缓存,下载过程持续抛出 progress 事件;下载出错抛出 error,直接终止更新。
  5. 全部资源下载完毕,资源移入正式离线缓存,触发 cachedupdateready 事件。

注意:页面 DOM 已经从旧缓存渲染完毕;新缓存不会自动生效,需要手动刷新页面,新资源才会生效。

manifest 缓存清单

文件后缀建议为 .appcache,服务器返回的 MIME 类型必须为 text/cache-manifest,否则不会生效。

AddType text/cache-manifest .appcache

清单文件分为三段:CACHENETWORKFALLBACK;文件第一行强制必须写 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;本地开发环境 localhost127.0.0.1 被浏览器视作安全源,可以 HTTP 调试。
  • 完全可控缓存:通过 JS 自由控制缓存读写;静态资源、接口请求都可以做缓存策略。- 扩展能力:除离线缓存外,还支持后台同步、Web 推送通知等能力。

完整生命周期

整体流程:注册 → 下载 → 安装 → 等待 → 激活 → 运行监听事件 → 终止

  1. 注册:主线程调用 navigator.serviceWorker.register(),传入 sw.js 脚本地址。
  2. 下载:浏览器下载 Service Worker 文件。
  3. 安装(install):脚本下载解析完成,触发 install;一般在这里预缓存需要的静态资源。install 失败,整个 Service Worker 注册直接作废。
  4. 等待(waiting):如果已经存在旧版本 Service Worker,新版本 install 成功后会进入等待状态,等待旧版本完全释放控制。
  5. 激活(activate):旧版本全部页面关闭之后,新版本触发 activate;通常在这里清理过期旧缓存。
  6. 运行阶段:激活成功,开始接管作用域内页面,监听 fetchmessagesync 等事件。页面全部关闭后,Service Worker 进入睡眠,节约内存;有事件到来再被唤醒。
  7. 终止(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:ApplicationService 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 字节改动,才会识别为新版本。

参考资料

A Beginner's Guide to Using the Application Cache

Service Worker: 简介

Service Worker 生命周期

Service Worker

MDN Service_Worker_API

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-08-22 21:44:20

results matching ""

    No results matching ""