嘿,别被标题吓到了,这其实是一个关于“断网也能跑”的魔法故事。
想象一下,你正在玩一款手机网页小游戏,正打到关卡通关 BOSS 的关键时刻,突然——4G 信号消失了,Wi-Fi 断了。如果是传统网页,屏幕大概率会给你展示一只可怜的恐龙,或者一片空白。但如果你做的网页支持离线缓存,恭喜你,你还能继续打完这一局,甚至把成绩保存到本地,等网络恢复后再上传。
再换个场景:你点了份外卖,外卖小哥正在送来的路上,你突然进了电梯,信号满格变叉。这时候如果外卖 App 只是普通网页,你可能连“骑手在哪”都看不到。但如果用了离线缓存技术,哪怕断网,你依然能查看订单状态、联系商家。
HTML5 提供的这套“离线生存包”,主要就靠两个核心技术:Application Cache(AppCache,已废弃但需了解) 和 Service Worker(现代标准)。今天咱们不聊枯燥的文档,直接上手写代码,从最简单的游戏存档到复杂的外卖订单,一步步把它玩明白。
一、先搞懂:为什么需要离线缓存?
在开始敲代码前,咱们得先理清一个概念。浏览器默认是“在线模式”,你每访问一个网页,它都要向服务器请求 HTML、CSS、JS、图片等资源。如果网络断了,或者服务器挂了,网页就废了。
离线缓存的核心目的就两个:
- 速度更快:同样的资源,第二次访问直接从本地硬盘读,比从服务器下载快得多。
- 断网可用:即使彻底没网,核心功能依然能跑。
二、技术选型:别碰 AppCache,直接用 Service Worker
这里要划重点:HTML5 早期有一个叫 Application Cache 的功能,现在已经被官方废弃了,因为它存在很多 Bug,比如缓存更新不及时、冲突问题等。如果你看到网上还有教用 manifest 文件的,请直接关掉,过时了。
现在的主流方案是 Service Worker。它是运行在浏览器后台的一个脚本,像一个大管家,拦截所有的网络请求,决定是从缓存拿数据,还是去服务器更新。
三、实例一:极简游戏存档(纯前端缓存)
对于一个简单的网页游戏,我们其实不需要复杂的 Service Worker,直接用 localStorage 或 IndexedDB 就够了。这适合存“游戏进度”这种小规模数据。
假设你的游戏叫《打地鼠》,玩家打完了 10 个地鼠,积分 100 分。这时候断网了,怎么保存成绩?
1. 保存存档(存入 localStorage)
// 游戏结束时的保存逻辑
function saveGameProgress(score, level) {
const gameState = {
score: score,
level: level,
lastPlayed: new Date().toISOString()
};
// 转为 JSON 字符串存入 localStorage
localStorage.setItem('muShuGameSave', JSON.stringify(gameState));
console.log('存档已保存!');
}
// 调用示例
saveGameProgress(100, 5);
2. 读取存档(断网也能读)
// 游戏加载时的读取逻辑
function loadGameProgress() {
const savedData = localStorage.getItem('muShuGameSave');
if (savedData) {
const gameState = JSON.parse(savedData);
console.log(`欢迎回来!上次得分:${gameState.score},等级:${gameState.level}`);
// 恢复游戏界面
document.getElementById('scoreDisplay').innerText = gameState.score;
document.getElementById('levelDisplay').innerText = gameState.level;
} else {
console.log('没有存档,开始新游戏');
}
}
// 页面加载时自动读取
window.onload = loadGameProgress;
3. 为什么这个算“离线缓存”?
因为 localStorage 是浏览器本地存储,完全不需要网络。哪怕手机彻底没信号,你打开网页,刚才存的成绩还在。这就是最基础的离线能力。
四、实例二:外卖订单页面(Service Worker + 缓存策略)
这个场景更复杂。外卖页面需要加载很多动态数据:订单列表、骑手位置、商家信息。如果每次都要从服务器请求,断网就废了。
我们要用 Service Worker 来实现“先缓存,后加载”,并配合一个“缓存优先,网络更新”的策略。
第一步:注册 Service Worker
在页面的主 JavaScript 文件中,注册 SW:
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('ServiceWorker 注册成功:', registration.scope);
})
.catch(error => {
console.log('ServiceWorker 注册失败:', error);
});
});
}
第二步:编写 Service Worker(核心!)
创建 sw.js 文件,放在网站根目录。这个文件负责拦截所有网络请求。
// sw.js - 外卖订单页面的离线缓存服务
const CACHE_NAME = 'waimai-order-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png',
'/fonts/icons.woff2'
];
// 1. 安装阶段:预缓存静态资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('缓存打开成功');
return cache.addAll(urlsToCache);
})
);
// 激活新的 Service Worker,旧的去死
self.skipWaiting();
});
// 2. 激活阶段:清理旧缓存
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.filter(cacheName => {
return cacheName !== CACHE_NAME;
}).map(cacheName => {
console.log('删除旧缓存:', cacheName);
return caches.delete(cacheName);
})
);
})
);
self.clients.claim();
});
// 3. 拦截请求:核心逻辑!
self.addEventListener('fetch', event => {
// 对于 HTML 页面,采用网络优先;对于其他资源,采用缓存优先
if (event.request.destination === 'document') {
// HTML 页面:先尝试网络,失败则返回缓存版本
event.respondWith(
fetch(event.request)
.catch(() => {
return caches.match(event.request);
})
);
} else {
// 静态资源(CSS、JS、图片等):先尝试缓存,没有则去网络
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
// 命中缓存,同时后台去更新缓存
fetch(event.request).then(networkResponse => {
if (networkResponse && networkResponse.ok) {
const clone = networkResponse.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, clone);
});
}
});
return response;
}
// 没命中缓存,去网络拿
return fetch(event.request);
})
);
}
});
4. 外卖订单数据缓存(进阶:缓存动态数据)
上面的例子只缓存了静态资源。但外卖订单是动态的(比如“骑手已取餐”),怎么缓存?
我们可以用 Cache API 在页面加载时,把订单数据也存起来。
// 在页面 JS 中,获取订单数据后存入缓存
async function fetchAndCacheOrder(orderId) {
const cache = await caches.open('waimai-order-cache-v1');
// 模拟从服务器获取订单数据
const response = await fetch(`/api/orders/${orderId}`);
const orderData = await response.json();
// 把 JSON 数据包装成 Response 对象,存入缓存
const cacheResponse = new Response(JSON.stringify(orderData), {
headers: {
'Content-Type': 'application/json'
}
});
await cache.put(`/api/orders/${orderId}`, cacheResponse);
return orderData;
}
然后在 Service Worker 中,拦截 /api/orders/ 的请求,优先返回缓存:
// 在 sw.js 的 fetch 事件中,增加对 API 请求的处理
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 如果是订单 API 请求
if (url.pathname.startsWith('/api/orders/')) {
event.respondWith(
caches.match(event.request).then(cacheResponse => {
// 如果有缓存,先返回缓存
if (cacheResponse) {
// 后台静默更新
fetch(event.request).then(networkResponse => {
if (networkResponse.ok) {
const clone = networkResponse.clone();
caches.open('waimai-order-cache-v1').then(cache => {
cache.put(event.request, clone);
});
}
});
return cacheResponse;
}
// 没有缓存,去网络
return fetch(event.request);
})
);
return;
}
// ... 其他请求的处理逻辑同上
});
5. 实际体验流程
- 用户首次访问外卖页面:Service Worker 安装,缓存静态资源。
- 用户查看订单:订单数据被缓存到
waimai-order-cache-v1。 - 用户进入电梯,断网:
- 刷新页面:HTML 和 CSS 从缓存加载,页面能显示。
- 查看订单:API 请求命中缓存,显示最新的订单状态(比如“骑手已取餐”)。
- 用户走出电梯,网络恢复:
- 下次访问时,Service Worker 后台静默更新缓存,显示最新的订单状态(比如“骑手已到达”)。
五、常见坑点 & 注意事项
1. Service Worker 作用域问题
Service Worker 只能控制它所在目录及子目录的文件。如果你的 sw.js 放在根目录,它能控制整个网站;如果放在 /js/ 目录下,它只能控制 /js/ 目录下的文件。
2. 缓存更新不生效?
浏览器可能会缓存 Service Worker 文件本身。修改 sw.js 后,需要:
- 清除浏览器缓存,或
- 打开开发者工具 -> Application -> Service Workers -> Update,或
- 修改
sw.js的内容(比如加个注释),让浏览器认为它是新文件。
3. HTTPS 要求
绝大多数现代浏览器要求 Service Worker 必须在 HTTPS 环境下运行(localhost 除外)。所以测试时可以开本地服务器,但上线必须用 HTTPS。
4. 动态数据的缓存时效
外卖订单这类数据,缓存时间不宜过长。可以在缓存时加一个时间戳,或者在 Service Worker 中设置“缓存有效性”,比如只缓存 5 分钟内的数据。
// 简单的缓存时效控制思路
const now = Date.now();
const fiveMinutesAgo = now - 5 * 60 * 1000;
const cacheResponse = await caches.match(event.request);
if (cacheResponse) {
const cachedTime = cacheResponse.headers.get('cached-at');
if (cachedTime && parseInt(cachedTime) > fiveMinutesAgo) {
return cacheResponse; // 缓存有效,返回
}
}
return fetch(event.request); // 缓存过期,去网络
六、总结:从游戏到外卖,离线缓存的核心思想
无论是保存游戏存档,还是缓存外卖订单,离线缓存的本质就一句话:
把可能用到的东西,提前存到本地;当网络不好时,先从本地找,找不到再去网络要。
- 小型数据(如游戏存档):用
localStorage或IndexedDB,简单直接。 - 静态资源(HTML/CSS/JS/图片):用 Service Worker 的
Cache API,实现秒开和离线可用。 - 动态数据(如订单信息):用 Service Worker 拦截 API 请求,优先返回缓存,后台静默更新。
掌握了这个思路,你不仅能做离线游戏、离线外卖 App,还能做任何需要“断网不瞎”的网页应用。下次再进电梯没信号,别慌,你做的网页依然能跑。
希望这个详解能帮到你!如果有具体的代码问题,欢迎继续交流。
