pin_drop当前位置:知识文库 ❯ 图文
后端通信:18. 请求缓存策略 - 从入门到实践详解
教程简介
请求缓存是提升Web应用性能的关键手段。通过合理利用浏览器缓存、HTTP缓存头、Service Worker缓存以及前端内存缓存,可以显著减少网络请求次数、降低服务器负载、加快页面加载速度。本教程将全面讲解HTTP缓存机制、前端缓存策略、Service Worker缓存、缓存更新与失效等内容,帮助你构建高性能的缓存体系。
核心概念
缓存层次结构
代码示例
请求发出
↓
内存缓存(最快,容量最小)
↓
Service Worker缓存
↓
浏览器HTTP缓存(Disk Cache)
↓
CDN缓存
↓
服务器缓存(Redis/Memcached)
↓
数据库HTTP缓存类型
强缓存 vs 协商缓存流程
代码示例
强缓存流程:
浏览器请求 → 检查缓存 → 未过期 → 直接使用缓存(200 from cache)
→ 已过期 → 发送请求 → 获取新资源
协商缓存流程:
浏览器请求 → 检查缓存 → 发送验证请求(带If-None-Match/If-Modified-Since)
→ 服务器验证
→ 未修改 → 304 Not Modified(使用缓存)
→ 已修改 → 200 OK + 新资源Cache-Control指令详解
语法与用法
HTTP缓存头设置
代码示例
// 强缓存 - 1小时有效
Cache-Control: max-age=3600, public
// 强缓存 - 1年有效(配合内容哈希)
Cache-Control: max-age=31536000, immutable
// 协商缓存 - ETag
ETag: "abc123"
// 下次请求:If-None-Match: "abc123"
// 协商缓存 - Last-Modified
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
// 下次请求:If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT
// 禁用缓存
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache前端缓存API
代码示例
// Cache API(Service Worker中使用)
const cache = await caches.open('my-cache-v1');
await cache.put(request, response);
const response = await cache.match(request);
await cache.delete(request);
const keys = await cache.keys();
// 内存缓存(Map实现)
const memoryCache = new Map();
memoryCache.set('key', { data, timestamp });
const cached = memoryCache.get('key');代码示例
示例1:HTTP缓存头演示
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTTP缓存头演示</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #0f0f23;
color: #e0e0e0;
min-height: 100vh;
padding: 30px 20px;
}
.container { max-width: 900px; margin: 0 auto; }
h1 { text-align: center; color: #48dbfb; margin-bottom: 30px; }
.card {
background: #1a1a2e;
border: 1px solid #2d2d44;
border-radius: 12px;
padding: 24px;
margin-bottom: 16px;
}
.card h2 { font-size: 16px; color: #feca57; margin-bottom: 16px; }
.cache-flow {
background: #0a0a1a;
border-radius: 8px;
padding: 20px;
font-family: monospace;
font-size: 13px;
line-height: 2;
overflow-x: auto;
}
.flow-browser { color: #48dbfb; }
.flow-server { color: #2ecc71; }
.flow-cache { color: #feca57; }
.flow-header { color: #ff6b6b; }
.flow-result { color: #a6e3a1; font-weight: bold; }
.headers-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
margin: 12px 0;
}
.headers-table th, .headers-table td {
padding: 10px 12px;
border: 1px solid #2d2d44;
text-align: left;
}
.headers-table th { background: #0f3460; color: #48dbfb; }
.headers-table td { color: #cdd6f4; font-family: monospace; }
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin: 4px;
}
.btn-primary { background: #48dbfb; color: #0f0f23; }
.btn-primary:hover { background: #0abde3; }
.btn-success { background: #2ecc71; color: white; }
.btn-danger { background: #ff6b6b; color: white; }
.result-panel {
background: #0a0a1a;
border-radius: 8px;
padding: 16px;
margin-top: 12px;
font-family: monospace;
font-size: 12px;
max-height: 250px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.scenario-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 12px;
margin-top: 12px;
}
.scenario-item {
padding: 16px;
background: #0f3460;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.scenario-item:hover { transform: translateY(-2px); background: #162d50; }
.scenario-item h3 { font-size: 14px; color: #48dbfb; margin-bottom: 8px; }
.scenario-item p { font-size: 12px; color: #888; }
</style>
</head>
<body>
<div class="container">
<h1>HTTP 缓存头演示</h1>
<!-- 强缓存流程 -->
<div class="card">
<h2>强缓存流程</h2>
<div class="cache-flow">
<span class="flow-browser">浏览器</span> <span class="flow-cache">缓存</span> <span class="flow-server">服务器</span>
请求资源
>
检查Cache-Control
<span class="flow-cache">max-age未过期?</span>
是
<span class="flow-result">200 (from cache)</span>
< 直接返回
否
发送请求
>
200 + 新资源 + 新缓存头
<
返回新资源 存入缓存
< </div>
</div>
<!-- 协商缓存流程 -->
<div class="card">
<h2>协商缓存流程</h2>
<div class="cache-flow">
<span class="flow-browser">浏览器</span> <span class="flow-cache">缓存</span> <span class="flow-server">服务器</span>
请求资源
>
检查缓存是否存在
<span class="flow-cache">有缓存?</span>
否
发送请求
>
200 + 资源 + <span class="flow-header">ETag</span>/<span class="flow-header">Last-Modified</span>
<
存入缓存
是
发送验证请求
<span class="flow-header">If-None-Match: "abc"</span>
<span class="flow-header">If-Modified-Since: ...</span>
>
未修改
<span class="flow-result">304 Not Modified</span>
<
使用缓存
已修改
200 + 新资源
<
返回新资源 更新缓存 </div>
</div>
<!-- Cache-Control指令 -->
<div class="card">
<h2>Cache-Control 指令详解</h2>
<table class="headers-table">
<thead>
<tr>
<th>指令</th>
<th>缓存行为</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td>max-age=31536000</td>
<td>缓存1年,期间不发请求</td>
<td>带哈希的静态资源</td>
</tr>
<tr>
<td>max-age=0, must-revalidate</td>
<td>每次都验证</td>
<td>需要最新数据的API</td>
</tr>
<tr>
<td>no-cache</td>
<td>可缓存,但每次必须验证</td>
<td>HTML文档</td>
</tr>
<tr>
<td>no-store</td>
<td>完全不缓存</td>
<td>敏感数据</td>
</tr>
<tr>
<td>private</td>
<td>仅浏览器缓存,CDN不缓存</td>
<td>用户私有数据</td>
</tr>
<tr>
<td>public</td>
<td>允许所有缓存</td>
<td>公开资源</td>
</tr>
<tr>
<td>immutable</td>
<td>资源不会变化,无需验证</td>
<td>带内容哈希的URL</td>
</tr>
<tr>
<td>stale-while-revalidate=60</td>
<td>允许使用过期缓存60秒</td>
<td>非关键API数据</td>
</tr>
</tbody>
</table>
</div>
<!-- 缓存策略场景 -->
<div class="card">
<h2>不同资源的缓存策略</h2>
<div class="scenario-grid">
<div class="scenario-item">
<h3>HTML文档</h3>
<p>Cache-Control: no-cache<br>每次验证,确保获取最新版本</p>
</div>
<div class="scenario-item">
<h3>CSS/JS(带哈希)</h3>
<p>Cache-Control: max-age=31536000, immutable<br>文件名含哈希,内容变则URL变</p>
</div>
<div class="scenario-item">
<h3>图片资源</h3>
<p>Cache-Control: max-age=86400<br>缓存1天,适合不常变的图片</p>
</div>
<div class="scenario-item">
<h3>API数据</h3>
<p>Cache-Control: max-age=0, must-revalidate<br>每次验证,保证数据新鲜</p>
</div>
<div class="scenario-item">
<h3>用户数据</h3>
<p>Cache-Control: private, no-cache<br>私有数据,不经过CDN</p>
</div>
<div class="scenario-item">
<h3>敏感数据</h3>
<p>Cache-Control: no-store<br>完全不缓存,如银行信息</p>
</div>
</div>
</div>
<!-- 实时测试 -->
<div class="card">
<h2>缓存行为测试</h2>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<button class="btn btn-primary" onclick="testCache('normal')">普通请求</button>
<button class="btn btn-success" onclick="testCache('nocache')">no-cache请求</button>
<button class="btn btn-danger" onclick="testCache('nostore')">no-store请求</button>
<button class="btn btn-primary" onclick="clearBrowserCache()">清除缓存后请求</button>
</div>
<div class="result-panel" id="cacheTestResult">点击按钮测试缓存行为</div>
</div>
</div>
<script>
async function testCache(mode) {
const result = document.getElementById('cacheTestResult');
const url = 'https://httpbin.org/cache/' + (mode === 'nocache' ? '60' : '30');
result.textContent = `发送${mode}模式请求...\n\n`;
try {
const options = { method: 'GET' };
if (mode === 'nocache') {
options.headers = { 'Cache-Control': 'no-cache' };
} else if (mode === 'nostore') {
options.headers = { 'Cache-Control': 'no-store' };
}
const startTime = performance.now();
const response = await fetch(url, options);
const elapsed = (performance.now() - startTime).toFixed(2);
result.textContent += `状态码: ${response.status}\n`;
result.textContent += `耗时: ${elapsed}ms\n`;
result.textContent += `来源: ${response.type}\n\n`;
result.textContent += '响应头:\n';
response.headers.forEach((value, key) => {
if (key.startsWith('cache') || key.startsWith('etag') ||
key.startsWith('last-') || key.startsWith('expires') ||
key.startsWith('pragma') || key.startsWith('content-')) {
result.textContent += ` ${key}: ${value}\n`;
}
});
const data = await response.json();
result.textContent += '\n响应数据:\n' + JSON.stringify(data, null, 2);
} catch (error) {
result.textContent += `请求失败: ${error.message}`;
}
}
async function clearBrowserCache() {
// 注意:前端无法直接清除浏览器HTTP缓存
// 可以通过添加随机参数绕过缓存
const result = document.getElementById('cacheTestResult');
const url = 'https://httpbin.org/cache/30?_t=' + Date.now();
result.textContent = '添加时间戳参数绕过缓存...\n\n';
try {
const startTime = performance.now();
const response = await fetch(url);
const elapsed = (performance.now() - startTime).toFixed(2);
result.textContent += `状态码: ${response.status}\n`;
result.textContent += `耗时: ${elapsed}ms\n`;
result.textContent += `\n提示:添加 ?_t=${Date.now()} 参数可以绕过浏览器缓存\n`;
result.textContent += `但每次都会下载完整资源,不适合生产环境\n`;
result.textContent += `更好的方案是使用内容哈希(如 app.abc123.js)`;
} catch (error) {
result.textContent += `请求失败: ${error.message}`;
}
}
</script>
</body>
</html>示例2:前端内存缓存
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>前端内存缓存</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f7fa;
min-height: 100vh;
padding: 30px 20px;
}
.container { max-width: 900px; margin: 0 auto; }
h1 { text-align: center; color: #2c3e50; margin-bottom: 30px; }
.card {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.card h2 { font-size: 16px; color: #2c3e50; margin-bottom: 16px; }
.code-block {
background: #2c3e50;
color: #a6e3a1;
padding: 16px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
white-space: pre;
margin: 12px 0;
}
.demo-area {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-top: 16px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin: 4px;
}
.btn-primary { background: #3498db; color: white; }
.btn-success { background: #2ecc71; color: white; }
.btn-danger { background: #e74c3c; color: white; }
.btn-warning { background: #f39c12; color: white; }
.cache-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 16px;
}
.stat-box {
background: #f0f0f0;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #2c3e50;
}
.stat-label {
font-size: 12px;
color: #888;
margin-top: 4px;
}
.log-area {
background: #2c3e50;
border-radius: 8px;
padding: 16px;
font-family: monospace;
font-size: 12px;
color: #2ecc71;
max-height: 250px;
overflow-y: auto;
white-space: pre-wrap;
}
.log-hit { color: #2ecc71; }
.log-miss { color: #e74c3c; }
.log-info { color: #3498db; }
</style>
</head>
<body>
<div class="container">
<h1>前端内存缓存</h1>
<!-- 缓存类实现 -->
<div class="card">
<h2>内存缓存类(支持TTL和LRU)</h2>
<div class="code-block">/**
* 内存缓存类
* 支持TTL过期、LRU淘汰、容量限制
*/
class MemoryCache {
constructor(options = {}) {
this.maxSize = options.maxSize || 100; // 最大缓存条数
this.defaultTTL = options.defaultTTL || 60000; // 默认TTL 60秒
this.cache = new Map(); // Map保持插入顺序,适合LRU
this.stats = { hits: 0, misses: 0, sets: 0, deletes: 0 };
}
// 设置缓存
set(key, value, ttl = this.defaultTTL) {
// 如果已存在,先删除(更新顺序)
if (this.cache.has(key)) {
this.cache.delete(key);
}
// 超过容量,删除最旧的(LRU)
if (this.cache.size >= this.maxSize) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, {
value,
expiry: ttl > 0 ? Date.now() + ttl : Infinity,
createdAt: Date.now()
});
this.stats.sets++;
}
// 获取缓存
get(key) {
if (!this.cache.has(key)) {
this.stats.misses++;
return null;
}
const entry = this.cache.get(key);
// 检查是否过期
if (Date.now() > entry.expiry) {
this.cache.delete(key);
this.stats.misses++;
return null;
}
// LRU:移到末尾(最近使用)
this.cache.delete(key);
this.cache.set(key, entry);
this.stats.hits++;
return entry.value;
}
// 检查是否存在
has(key) {
return this.get(key) !== null;
}
// 删除缓存
delete(key) {
const deleted = this.cache.delete(key);
if (deleted) this.stats.deletes++;
return deleted;
}
// 清空缓存
clear() {
this.cache.clear();
}
// 获取缓存信息
getInfo(key) {
const entry = this.cache.get(key);
if (!entry) return null;
return {
remainingTTL: Math.max(0, entry.expiry - Date.now()),
age: Date.now() - entry.createdAt
};
}
// 命中率
get hitRate() {
const total = this.stats.hits + this.stats.misses;
return total === 0 ? 0 : (this.stats.hits / total * 100).toFixed(1);
}
// 缓存大小
get size() {
return this.cache.size;
}
}
/**
* 带缓存的请求函数
*/
async function cachedFetch(url, options = {}) {
const cacheKey = `${options.method || 'GET'}:${url}`;
const ttl = options.cacheTTL || 60000;
const useCache = options.useCache !== false;
// 检查缓存
if (useCache) {
const cached = requestCache.get(cacheKey);
if (cached) {
console.log(`[Cache HIT] ${cacheKey}`);
return cached;
}
}
console.log(`[Cache MISS] ${cacheKey}`);
// 发送请求
const response = await fetch(url, options);
const data = await response.json();
// 存入缓存
if (useCache && response.ok) {
requestCache.set(cacheKey, data, ttl);
}
return data;
}
const requestCache = new MemoryCache({ maxSize: 50, defaultTTL: 30000 });</div>
</div>
<!-- 交互式演示 -->
<div class="card">
<h2>缓存交互演示</h2>
<div class="demo-area">
<div class="cache-stats">
<div class="stat-box">
<div class="stat-value" id="statHits">0</div>
<div class="stat-label">缓存命中</div>
</div>
<div class="stat-box">
<div class="stat-value" id="statMisses">0</div>
<div class="stat-label">缓存未命中</div>
</div>
<div class="stat-box">
<div class="stat-value" id="statHitRate">0%</div>
<div class="stat-label">命中率</div>
</div>
<div class="stat-box">
<div class="stat-value" id="statSize">0</div>
<div class="stat-label">缓存条目</div>
</div>
</div>
<div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px;">
<button class="btn btn-primary" onclick="demoFetch('/users')">请求 /users</button>
<button class="btn btn-primary" onclick="demoFetch('/posts')">请求 /posts</button>
<button class="btn btn-primary" onclick="demoFetch('/comments')">请求 /comments</button>
<button class="btn btn-success" onclick="demoFetch('/users')">再次请求 /users</button>
<button class="btn btn-warning" onclick="demoInvalidate('/users')">失效 /users</button>
<button class="btn btn-danger" onclick="demoClear()">清空缓存</button>
</div>
<div class="log-area" id="demoLog">等待操作...</div>
</div>
</div>
</div>
<script>
// 缓存实例
const demoCache = new MemoryCache({ maxSize: 50, defaultTTL: 30000 });
// MemoryCache类定义(与上面代码相同)
function MemoryCache(options = {}) {
this.maxSize = options.maxSize || 100;
this.defaultTTL = options.defaultTTL || 60000;
this.cache = new Map();
this.stats = { hits: 0, misses: 0, sets: 0, deletes: 0 };
}
MemoryCache.prototype.set = function(key, value, ttl) {
ttl = ttl || this.defaultTTL;
if (this.cache.has(key)) this.cache.delete(key);
if (this.cache.size >= this.maxSize) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, { value, expiry: Date.now() + ttl, createdAt: Date.now() });
this.stats.sets++;
};
MemoryCache.prototype.get = function(key) {
if (!this.cache.has(key)) { this.stats.misses++; return null; }
const entry = this.cache.get(key);
if (Date.now() > entry.expiry) { this.cache.delete(key); this.stats.misses++; return null; }
this.cache.delete(key);
this.cache.set(key, entry);
this.stats.hits++;
return entry.value;
};
MemoryCache.prototype.delete = function(key) {
return this.cache.delete(key);
};
MemoryCache.prototype.clear = function() { this.cache.clear(); };
Object.defineProperty(MemoryCache.prototype, 'hitRate', {
get: function() {
const total = this.stats.hits + this.stats.misses;
return total === 0 ? 0 : (this.stats.hits / total * 100).toFixed(1);
}
});
Object.defineProperty(MemoryCache.prototype, 'size', {
get: function() { return this.cache.size; }
});
// 模拟数据
const mockData = {
'/users': [{ id: 1, name: '张三' }, { id: 2, name: '李四' }],
'/posts': [{ id: 1, title: '文章1' }, { id: 2, title: '文章2' }],
'/comments': [{ id: 1, text: '评论1' }, { id: 2, text: '评论2' }]
};
async function demoFetch(path) {
const log = document.getElementById('demoLog');
const time = new Date().toLocaleTimeString();
// 检查缓存
const cached = demoCache.get(path);
if (cached) {
log.textContent += `[${time}] <span class="log-hit">CACHE HIT</span> ${path} → 返回缓存数据 (${Date.now() - cached._cachedAt}ms前缓存)\n`;
} else {
log.textContent += `[${time}] <span class="log-miss">CACHE MISS</span> ${path} → 发送网络请求...\n`;
// 模拟网络延迟
await new Promise(r => setTimeout(r, 300 + Math.random() * 500));
const data = { ...mockData[path], _cachedAt: Date.now() };
demoCache.set(path, data, 30000);
log.textContent += `[${time}] <span class="log-info">CACHED</span> ${path} → 数据已缓存 (TTL: 30s)\n`;
}
updateStats();
log.scrollTop = log.scrollHeight;
}
function demoInvalidate(path) {
demoCache.delete(path);
const log = document.getElementById('demoLog');
log.textContent += `[${new Date().toLocaleTimeString()}] <span class="log-miss">INVALIDATED</span> ${path} → 缓存已失效\n`;
updateStats();
}
function demoClear() {
demoCache.clear();
const log = document.getElementById('demoLog');
log.textContent += `[${new Date().toLocaleTimeString()}] <span class="log-miss">CLEARED</span> 所有缓存已清空\n`;
updateStats();
}
function updateStats() {
document.getElementById('statHits').textContent = demoCache.stats.hits;
document.getElementById('statMisses').textContent = demoCache.stats.misses;
document.getElementById('statHitRate').textContent = demoCache.hitRate + '%';
document.getElementById('statSize').textContent = demoCache.size;
}
</script>
</body>
</html>示例3:Service Worker缓存策略
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service Worker缓存策略</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #1a1a2e;
color: #e0e0e0;
min-height: 100vh;
padding: 30px 20px;
}
.container { max-width: 900px; margin: 0 auto; }
h1 { text-align: center; color: #48dbfb; margin-bottom: 30px; }
.card {
background: #16213e;
border: 1px solid #2d2d44;
border-radius: 12px;
padding: 24px;
margin-bottom: 16px;
}
.card h2 { font-size: 16px; color: #feca57; margin-bottom: 16px; }
.strategy-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.strategy-card {
background: #0f3460;
border-radius: 10px;
padding: 20px;
border: 1px solid #2d2d44;
}
.strategy-card h3 {
font-size: 15px;
margin-bottom: 8px;
}
.strategy-card .desc {
font-size: 13px;
color: #a6adc8;
line-height: 1.6;
margin-bottom: 12px;
}
.strategy-flow {
background: #0a0a1a;
border-radius: 6px;
padding: 12px;
font-family: monospace;
font-size: 11px;
line-height: 1.6;
}
.code-block {
background: #0a0a1a;
color: #a6e3a1;
padding: 16px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
white-space: pre;
margin: 12px 0;
}
.use-case {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
margin-top: 8px;
}
.uc-static { background: #2ecc7133; color: #2ecc71; }
.uc-dynamic { background: #3498db33; color: #3498db; }
.uc-api { background: #f39c1233; color: #f39c12; }
.uc-offline { background: #9b59b633; color: #9b59b6; }
</style>
</head>
<body>
<div class="container">
<h1>Service Worker 缓存策略</h1>
<!-- 六种策略 -->
<div class="card">
<h2>六种缓存策略</h2>
<div class="strategy-grid">
<div class="strategy-card">
<h3 style="color:#2ecc71;">1. Cache First</h3>
<div class="desc">优先从缓存获取,缓存未命中时从网络获取并更新缓存。</div>
<div class="strategy-flow">
请求 → 查缓存 → 命中 → 返回缓存
→ 未命中 → 网络请求 → 存缓存 → 返回</div>
<span class="use-case uc-static">静态资源</span>
<span class="use-case uc-offline">离线优先</span>
</div>
<div class="strategy-card">
<h3 style="color:#3498db;">2. Network First</h3>
<div class="desc">优先从网络获取,网络失败时回退到缓存。</div>
<div class="strategy-flow">
请求 → 网络请求 → 成功 → 存缓存 → 返回
→ 失败 → 查缓存 → 返回缓存</div>
<span class="use-case uc-dynamic">动态内容</span>
<span class="use-case uc-api">API请求</span>
</div>
<div class="strategy-card">
<h3 style="color:#f39c12;">3. Stale While Revalidate</h3>
<div class="desc">立即返回缓存(即使过期),同时后台更新缓存。</div>
<div class="strategy-flow">
请求 → 查缓存 → 命中 → 立即返回缓存
同时 → 网络请求 → 更新缓存</div>
<span class="use-case uc-api">非关键API</span>
<span class="use-case uc-dynamic">及时性要求不高</span>
</div>
<div class="strategy-card">
<h3 style="color:#e74c3c;">4. Network Only</h3>
<div class="desc">只从网络获取,不使用缓存。</div>
<div class="strategy-flow">
请求 → 网络请求 → 成功 → 返回
→ 失败 → 报错</div>
<span class="use-case uc-api">实时数据</span>
<span class="use-case uc-dynamic">非幂等请求</span>
</div>
<div class="strategy-card">
<h3 style="color:#9b59b6;">5. Cache Only</h3>
<div class="desc">只从缓存获取,不请求网络。</div>
<div class="strategy-flow">
请求 → 查缓存 → 命中 → 返回
→ 未命中 → 报错</div>
<span class="use-case uc-static">预缓存资源</span>
<span class="use-case uc-offline">离线页面</span>
</div>
<div class="strategy-card">
<h3 style="color:#1abc9c;">6. Cache + Network Update</h3>
<div class="desc">先返回缓存,网络响应后更新页面内容。</div>
<div class="strategy-flow">
请求 → 查缓存 → 命中 → 返回缓存数据
→ 网络请求 → 成功 → 更新缓存 → 更新页面</div>
<span class="use-case uc-api">列表数据</span>
<span class="use-case uc-dynamic">快速响应+最新数据</span>
</div>
</div>
</div>
<!-- Service Worker代码 -->
<div class="card">
<h2>Service Worker 缓存实现</h2>
<div class="code-block">// sw.js - Service Worker文件
const CACHE_NAME = 'app-cache-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/offline.html'
];
// 安装事件 - 预缓存静态资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_ASSETS))
.then(() => self.skipWaiting())
);
});
// 激活事件 - 清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
)
).then(() => self.clients.claim())
);
});
// 请求拦截 - 根据URL选择不同策略
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// 非GET请求:Network Only
if (request.method !== 'GET') return;
// API请求:Network First
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request));
return;
}
// 静态资源:Cache First
if (isStaticAsset(url.pathname)) {
event.respondWith(cacheFirst(request));
return;
}
// HTML页面:Network First
if (request.headers.get('accept').includes('text/html')) {
event.respondWith(networkFirst(request));
return;
}
// 其他:Stale While Revalidate
event.respondWith(staleWhileRevalidate(request));
});
// === 缓存策略实现 ===
// Cache First
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
}
// Network First
async function networkFirst(request) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
} catch (error) {
const cached = await caches.match(request);
if (cached) return cached;
// 返回离线页面
if (request.headers.get('accept').includes('text/html')) {
return caches.match('/offline.html');
}
throw error;
}
}
// Stale While Revalidate
async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
// 后台更新
const fetchPromise = fetch(request).then(response => {
if (response.ok) {
cache.put(request, response.clone());
}
return response;
});
// 如果有缓存,立即返回;否则等待网络
return cached || fetchPromise;
}
function isStaticAsset(pathname) {
return /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$/.test(pathname);
}</div>
</div>
<!-- 注册Service Worker -->
<div class="card">
<h2>注册 Service Worker</h2>
<div class="code-block">// 在主页面中注册Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/'
});
console.log('SW注册成功:', registration.scope);
// 监听更新
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
// 新版本已激活,提示用户刷新
if (navigator.serviceWorker.controller) {
showUpdateNotification();
}
}
});
});
} catch (error) {
console.error('SW注册失败:', error);
}
});
}
// Cache API使用示例
async function cacheApiExample() {
// 打开缓存
const cache = await caches.open('api-cache-v1');
// 添加请求到缓存
await cache.add('/api/users');
// 手动添加请求和响应
const response = new Response(JSON.stringify({ hello: 'world' }), {
headers: { 'Content-Type': 'application/json' }
});
await cache.put('/api/hello', response);
// 匹配缓存
const cachedResponse = await cache.match('/api/hello');
const data = await cachedResponse.json();
// 删除缓存
await cache.delete('/api/hello');
// 查看所有缓存的键
const keys = await cache.keys();
console.log('缓存键:', keys);
}</div>
</div>
</div>
</body>
</html>示例4:缓存更新与版本管理
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>缓存更新与版本管理</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #fafafa;
min-height: 100vh;
padding: 30px 20px;
}
.container { max-width: 900px; margin: 0 auto; }
h1 { text-align: center; color: #2c3e50; margin-bottom: 30px; }
.card {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.card h2 { font-size: 16px; color: #2c3e50; margin-bottom: 16px; }
.code-block {
background: #2c3e50;
color: #a6e3a1;
padding: 16px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
white-space: pre;
margin: 12px 0;
}
.version-timeline {
position: relative;
padding-left: 30px;
margin: 20px 0;
}
.version-timeline::before {
content: '';
position: absolute;
left: 10px;
top: 0;
bottom: 0;
width: 2px;
background: #e0e0e0;
}
.version-item {
position: relative;
padding: 12px 16px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 12px;
}
.version-item::before {
content: '';
position: absolute;
left: -24px;
top: 16px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #3498db;
border: 2px solid white;
}
.version-item.current::before { background: #2ecc71; }
.version-item.old::before { background: #e0e0e0; }
.version-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.tag-current { background: #d4edda; color: #155724; }
.tag-old { background: #e2e3e5; color: #6c757d; }
.tag-pending { background: #fff3cd; color: #856404; }
p { font-size: 14px; color: #666; line-height: 1.8; margin-bottom: 12px; }
</style>
</head>
<body>
<div class="container">
<h1>缓存更新与版本管理</h1>
<!-- 缓存版本策略 -->
<div class="card">
<h2>缓存版本管理策略</h2>
<p>
当应用更新时,需要确保用户获取最新版本的资源。以下是几种常见的缓存更新策略:
</p>
<h3 style="font-size:14px; color:#2c3e50; margin:16px 0 8px;">1. 内容哈希(推荐)</h3>
<div class="code-block">// 构建工具自动在文件名中添加内容哈希
// app.abc123.js → 内容变化时哈希变化 → URL变化 → 绕过缓存
// webpack配置
module.exports = {
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
},
// CSS文件也加哈希
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css'
})
]
};
// Vite默认使用内容哈希
// dist/assets/index-abc123.js
// dist/assets/index-def456.css
// HTML引用带哈希的资源
// <script src="/assets/app.abc123.js"></script>
// 当app.js内容变化时,哈希变化,URL变化,浏览器请求新文件</div>
<h3 style="font-size:14px; color:#2c3e50; margin:16px 0 8px;">2. Service Worker版本更新</h3>
<div class="code-block">// sw.js
const CACHE_NAME = 'app-v2'; // 修改版本号触发更新
self.addEventListener('install', (event) => {
// 新SW安装,预缓存新资源
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_ASSETS))
);
});
self.addEventListener('activate', (event) => {
// 新SW激活,删除旧缓存
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys.filter(key => key !== CACHE_NAME)
.map(key => {
console.log('删除旧缓存:', key);
return caches.delete(key);
})
)
)
);
});
// 版本更新时间线:
// v1安装 → v1激活 → 缓存app-v1
// 修改CACHE_NAME → v2安装 → v2激活 → 删除app-v1 → 缓存app-v2</div>
<h3 style="font-size:14px; color:#2c3e50; margin:16px 0 8px;">3. HTML缓存策略</h3>
<div class="code-block">// HTML文件必须使用no-cache策略
// 因为HTML引用了带哈希的JS/CSS,如果HTML被缓存,
// 用户将无法获取新版本的JS/CSS引用
// Nginx配置
location / {
# HTML文件不缓存
if ($request_uri ~* \.html$) {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# 带哈希的静态资源长期缓存
if ($request_uri ~* \.[a-f0-9]{8,}\.(js|css|png|jpg|svg|woff2)$) {
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
// Express配置
app.get('*', (req, res) => {
if (req.path.endsWith('.html') || req.path === '/') {
res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
} else if (/\.[a-f0-9]{8,}\.(js|css)/.test(req.path)) {
res.set('Cache-Control', 'public, max-age=31536000, immutable');
}
res.sendFile(path.join(__dirname, 'dist', req.path));
});</div>
</div>
<!-- 缓存更新流程 -->
<div class="card">
<h2>应用更新流程</h2>
<div class="version-timeline">
<div class="version-item">
<span class="version-tag tag-old">v1.0.0</span>
<p style="margin-top:4px;">用户首次访问,下载所有资源,缓存 app-v1</p>
</div>
<div class="version-item">
<span class="version-tag tag-pending">部署v2</span>
<p style="margin-top:4px;">开发者部署新版本,修改CACHE_NAME为app-v2</p>
</div>
<div class="version-item">
<span class="version-tag tag-pending">SW更新</span>
<p style="margin-top:4px;">用户访问页面,浏览器检测到新SW,后台安装app-v2缓存</p>
</div>
<div class="version-item">
<span class="version-tag tag-pending">等待激活</span>
<p style="margin-top:4px;">新SW进入waiting状态,旧页面仍使用app-v1缓存</p>
</div>
<div class="version-item current">
<span class="version-tag tag-current">v2激活</span>
<p style="margin-top:4px;">用户关闭所有旧标签页后重新打开,新SW激活,删除app-v1,使用app-v2</p>
</div>
</div>
</div>
<!-- 缓存失效方案 -->
<div class="card">
<h2>主动缓存失效方案</h2>
<div class="code-block">// 方案1:版本号查询
async function checkForUpdates() {
const response = await fetch('/api/version?t=' + Date.now());
const { version } = await response.json();
const currentVersion = localStorage.getItem('app_version');
if (currentVersion && currentVersion !== version) {
// 版本不同,提示用户刷新
showUpdateBanner();
}
localStorage.setItem('app_version', version);
}
// 方案2:Service Worker消息通知
// sw.js
self.addEventListener('message', (event) => {
if (event.data.type === 'SKIP_WAITING') {
self.skipWaiting(); // 立即激活新SW
}
});
// 主页面
navigator.serviceWorker.addEventListener('controllerchange', () => {
// 新SW已接管,刷新页面
window.location.reload();
});
// 用户点击更新按钮
async function forceUpdate() {
const registration = await navigator.serviceWorker.getRegistration();
if (registration && registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
}
// 方案3:定时轮询检查
setInterval(checkForUpdates, 5 * 60 * 1000); // 每5分钟检查一次
// 方案4:WebSocket推送更新通知
const ws = new WebSocket('wss://example.com/updates');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'NEW_VERSION') {
showUpdateNotification(data.version);
}
};</div>
</div>
</div>
</body>
</html>浏览器兼容性
注意事项与最佳实践
1. 缓存策略选择
代码示例
// 不同类型资源的推荐缓存策略
// 静态资源(带内容哈希)
// Cache-Control: max-age=31536000, immutable
// 原因:文件名含哈希,内容变则URL变
// HTML文档
// Cache-Control: no-cache
// 原因:需要每次验证,确保引用最新的JS/CSS
// API数据
// Cache-Control: no-cache 或 max-age=0, must-revalidate
// 原因:需要最新数据
// 图片/字体
// Cache-Control: max-age=86400 (1天)
// 原因:不常变化,但可能更新
// 用户私有数据
// Cache-Control: private, no-cache
// 原因:不应被CDN缓存,需每次验证2. 避免缓存陷阱
代码示例
// 陷阱1:缓存了过期的API数据
// 解决:设置合理的TTL,关键数据使用no-cache
// 陷阱2:HTML被强缓存导致无法更新
// 解决:HTML必须使用no-cache
// 陷阱3:Service Worker缓存了错误响应
// 解决:只缓存成功的响应
async function cacheFirstSafe(request) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
// 只缓存成功响应
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
}
// 陷阱4:POST请求被缓存
// 解决:只缓存GET请求
if (request.method !== 'GET') return;3. 缓存调试
代码示例
// Chrome DevTools缓存调试
// 1. Network面板 - 查看缓存状态
// - from disk cache / from memory cache
// - 304 Not Modified
// 2. Application面板 - Cache Storage
// - 查看Service Worker缓存内容
// 3. 勾选"Disable cache" - 开发时禁用缓存
// 编程方式查看缓存
async function inspectCache() {
const cacheNames = await caches.keys();
for (const name of cacheNames) {
const cache = await caches.open(name);
const keys = await cache.keys();
console.log(`缓存: ${name} (${keys.length} 条)`);
for (const key of keys) {
const response = await cache.match(key);
console.log(` ${key.url} - ${response.headers.get('date')}`);
}
}
}代码规范示例
规范的缓存配置
代码示例
/**
* 缓存配置管理
* 统一管理HTTP缓存头和Service Worker缓存策略
*/
const CacheConfig = {
// HTTP缓存头配置
headers: {
// HTML文档 - 每次验证
html: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
},
// 带哈希的静态资源 - 长期缓存
hashedAssets: {
'Cache-Control': 'public, max-age=31536000, immutable'
},
// 不带哈希的静态资源 - 短期缓存
unhashedAssets: {
'Cache-Control': 'public, max-age=86400'
},
// API响应 - 不缓存或短期缓存
api: {
'Cache-Control': 'private, no-cache, must-revalidate'
},
// 敏感数据 - 完全不缓存
'Cache-Control': 'no-store',
'Pragma': 'no-cache'
}
},
// Service Worker缓存策略配置
swStrategies: {
'/': 'networkFirst', // HTML
'/api/': 'networkFirst', // API
'/assets/': 'cacheFirst', // 静态资源
'/images/': 'cacheFirst', // 图片
'/fonts/': 'cacheFirst', // 字体
},
// 内存缓存配置
memory: {
maxSize: 100,
defaultTTL: 30000, // 30秒
apiTTL: 10000, // API 10秒
staticTTL: 300000 // 静态数据 5分钟
}
};
// 根据请求路径自动选择缓存策略
function getCacheStrategy(pathname) {
for (const [pattern, strategy] of Object.entries(CacheConfig.swStrategies)) {
if (pathname.startsWith(pattern)) {
return strategy;
}
}
return 'staleWhileRevalidate'; // 默认策略
}常见问题与解决方案
问题1:用户看到旧版本页面
代码示例
// 问题:部署新版本后,部分用户仍看到旧页面
// 解决方案1:HTML使用no-cache
// 确保HTML不被强缓存,每次都验证
// 解决方案2:版本检测+提示更新
let currentVersion = __APP_VERSION__; // 构建时注入
async function checkVersion() {
const response = await fetch('/version.json?t=' + Date.now());
const { version } = await response.json();
if (version !== currentVersion) {
showUpdateNotification('发现新版本,点击刷新');
}
}
// 解决方案3:Service Worker skipWaiting
// 新SW安装后立即激活,不等待旧标签页关闭
self.addEventListener('install', (event) => {
self.skipWaiting(); // 跳过等待
});问题2:缓存雪崩
代码示例
// 问题:大量缓存同时过期,导致瞬间大量请求打到服务器
// 解决方案:给TTL添加随机偏移
function setCacheWithJitter(key, value, baseTTL) {
const jitter = Math.random() * baseTTL * 0.1; // 10%的随机偏移
const ttl = baseTTL + jitter;
cache.set(key, value, ttl);
}
// 使用示例
setCacheWithJitter('users', data, 60000); // 60秒 ± 6秒问题3:CDN缓存与浏览器缓存不一致
代码示例
// 问题:CDN缓存了旧版本,浏览器获取到过期数据
// 解决方案1:使用内容哈希URL
// CDN和浏览器都缓存带哈希的资源,内容变化时URL变化
// 解决方案2:CDN缓存清除API
// 部署后调用CDN API清除缓存
async function purgeCDNCache(paths) {
await fetch('https://api.cdn.com/purge', {
method: 'POST',
headers: { 'Authorization': 'Bearer xxx' },
body: JSON.stringify({ paths })
});
}
// 解决方案3:使用版本化API路径
// /api/v2/users 而不是 /api/users总结
请求缓存策略是前端性能优化的核心环节,本教程涵盖了以下内容:
HTTP缓存机制:深入理解强缓存(Cache-Control/max-age)和协商缓存(ETag/Last-Modified)的工作原理、判断流程和配置方法。
Cache-Control指令:掌握max-age、no-cache、no-store、public、private、immutable、stale-while-revalidate等指令的含义和适用场景。
前端内存缓存:实现支持TTL过期和LRU淘汰的内存缓存类,以及带缓存的请求函数封装。
Service Worker缓存:掌握六种缓存策略(Cache First、Network First、Stale While Revalidate等)的实现和适用场景。
缓存版本管理:学会使用内容哈希、Service Worker版本更新、HTML no-cache策略确保用户获取最新版本。
缓存更新与失效:了解主动缓存失效方案,包括版本号查询、SW消息通知、WebSocket推送等。
最佳实践:不同类型资源的缓存策略选择、缓存陷阱规避、缓存调试方法等生产环境必备知识。
常见问题
什么是18. 请求缓存策略?
18. 请求缓存策略是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。
如何学习18. 请求缓存策略的实际应用?
教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。
18. 请求缓存策略有哪些注意事项?
常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。
18. 请求缓存策略适合初学者吗?
本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。
18. 请求缓存策略的核心要点是什么?
核心要点包括教程简介等内容,建议按教程顺序逐步学习。
本文由小确幸生活整理发布,转载请注明出处
本文涉及AI创作
内容由AI创作,请仔细甄别