资源缓存
资源缓存是提升 Web 应用性能的关键技术之一。通过合理的缓存策略,可以显著减少网络请求,提升页面加载速度,改善用户体验。本章将深入探讨各种缓存机制的实现和优化策略。
强缓存
强缓存是一种缓存机制,当浏览器请求资源时,如果命中强缓存,则直接从缓存中获取资源,不会向服务器发送请求。
强缓存的工作流程如下:
graph TD
A["浏览器请求资源"] --> B{检查本地缓存}
B -->|缓存不存在| C["向服务器发送请求"]
B -->|缓存存在| D{检查缓存是否过期}
D --> E["检查 Expires 头部"]
D --> F["检查 Cache-Control 头部"]
E --> G{当前时间 < Expires?}
F --> H{当前时间 < max-age?}
G -->|是| I["缓存有效"]
G -->|否| J["缓存过期"]
H -->|是| I
H -->|否| J
I --> K["直接使用本地缓存<br/>🚀 不发送网络请求"]
J --> C
C --> L["服务器处理请求"]
L --> M["服务器返回资源<br/>+ 200 OK<br/>+ Expires/Cache-Control 头部"]
M --> N["浏览器缓存资源"]
N --> O["返回资源给用户"]
K --> O
subgraph "强缓存特点"
P["✅ 不发送网络请求"]
Q["✅ 响应速度快"]
R["✅ 节省带宽"]
S["⚠️ 可能返回过期内容"]
end
subgraph "缓存控制方式"
T["Expires: 绝对时间<br/>HTTP/1.0"]
U["Cache-Control: 相对时间<br/>HTTP/1.1 (推荐)"]
end
style A fill:#e3f2fd
style I fill:#c8e6c9
style K fill:#c8e6c9
style O fill:#f3e5f5
style C fill:#fff3e0
style M fill:#fff3e0
Expire
Expire 是 HTTP/1.0 中的缓存控制头,用于指定资源的过期时间。
中文释义为:到期,表示缓存的过期时间。expire 是 HTTP 1.0 提出的,它描述的是一个绝对时间,该时间由服务端返回。因为 expire 值是一个固定时间,因此会受本地时间的影响,如果在缓存期间我们修改了本地时间,可能会导致缓存失效。
通常表示如下:
Expires: Wed, 11 May 2018 07:20:00 GMT
// Express.js 中设置 Expire 头
const express = require('express');
const app = express();
// Expire 缓存中间件
function setExpireHeaders(req, res, next) {
const oneYear = 365 * 24 * 60 * 60 * 1000; // 一年的毫秒数
const expireDate = new Date(Date.now() + oneYear);
// 设置静态资源的过期时间
if (req.url.match(/\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$/)) {
res.set('Expires', expireDate.toUTCString());
res.set('Cache-Control', `max-age=${oneYear / 1000}`);
}
next();
}
// 应用中间件
app.use(setExpireHeaders);
// 静态文件服务
app.use('/static', express.static('public', {
maxAge: '1y', // 设置一年的缓存时间
etag: true,
lastModified: true
}));
// 不同类型资源的缓存策略
const cacheStrategies = {
// 长期缓存资源(带版本号的静态资源)
longTerm: {
pattern: /\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)\?v=[\w\d]+$/,
maxAge: 365 * 24 * 60 * 60, // 1年
cacheControl: 'public, max-age=31536000, immutable'
},
// 中期缓存资源
mediumTerm: {
pattern: /\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$/,
maxAge: 30 * 24 * 60 * 60, // 30天
cacheControl: 'public, max-age=2592000'
},
// 短期缓存资源
shortTerm: {
pattern: /\.(html|htm)$/,
maxAge: 5 * 60, // 5分钟
cacheControl: 'public, max-age=300, must-revalidate'
},
// 不缓存资源
noCache: {
pattern: /\/api\//,
maxAge: 0,
cacheControl: 'no-cache, no-store, must-revalidate'
}
};
// 智能缓存中间件
function intelligentCacheMiddleware(req, res, next) {
for (const [strategyName, strategy] of Object.entries(cacheStrategies)) {
if (strategy.pattern.test(req.url)) {
const expireDate = new Date(Date.now() + strategy.maxAge * 1000);
res.set('Expires', expireDate.toUTCString());
res.set('Cache-Control', strategy.cacheControl);
// 添加缓存策略标识
res.set('X-Cache-Strategy', strategyName);
console.log(`应用缓存策略 [${strategyName}] 到资源: ${req.url}`);
break;
}
}
next();
}
app.use(intelligentCacheMiddleware);
Cache-Control
Cache-Control 是 HTTP/1.1 中更强大和灵活的缓存控制头,可以精确控制缓存行为。
中文释义为:缓存控制。cache-control 是 HTTP 1.1提出的,它描述的是一个相对时间,该相对时间由服务端返回.
表示如下:
Cache-Control: max-age=315360000
该属性还包括访问性及缓存方式设置,列举如下;
-
no-cache 存储在本地缓存取中,只是在与服务器进行新鲜度再验证之前,缓存无法使用。
-
no-store 不缓存资源到本地
-
public 可被所有用户缓存,多用户进行共享,包括终端或 CDN 等中间代理服务器
-
private 仅能被浏览器客户端缓存,属于私有缓存,不允许中间代理服务器缓存相关资源
// Cache-Control 指令管理器
class CacheControlManager {
constructor() {
this.directives = new Map();
this.presets = this.initializePresets();
}
initializePresets() {
return {
// 静态资源预设
static: {
public: true,
maxAge: 31536000, // 1年
immutable: true
},
// API 响应预设
api: {
private: true,
maxAge: 300, // 5分钟
mustRevalidate: true
},
// HTML 页面预设
html: {
public: true,
maxAge: 3600, // 1小时
mustRevalidate: true
},
// 无缓存预设
noCache: {
noCache: true,
noStore: true,
mustRevalidate: true
}
};
}
// 构建 Cache-Control 头
buildCacheControl(options = {}) {
const directives = [];
// 可见性指令
if (options.public) directives.push('public');
if (options.private) directives.push('private');
// 过期指令
if (options.maxAge !== undefined) {
directives.push(`max-age=${options.maxAge}`);
}
if (options.sMaxAge !== undefined) {
directives.push(`s-maxage=${options.sMaxAge}`);
}
// 重新验证指令
if (options.mustRevalidate) directives.push('must-revalidate');
if (options.proxyRevalidate) directives.push('proxy-revalidate');
// 缓存控制指令
if (options.noCache) directives.push('no-cache');
if (options.noStore) directives.push('no-store');
if (options.noTransform) directives.push('no-transform');
// HTTP/2 Push 指令
if (options.immutable) directives.push('immutable');
return directives.join(', ');
}
// 应用预设配置
applyPreset(presetName, customOptions = {}) {
const preset = this.presets[presetName];
if (!preset) {
throw new Error(`未知的预设配置: ${presetName}`);
}
const mergedOptions = { ...preset, ...customOptions };
return this.buildCacheControl(mergedOptions);
}
// 动态缓存策略
getDynamicCacheStrategy(resourceType, userAgent, requestTime) {
const strategies = {
// 移动端优化策略
mobile: {
css: { public: true, maxAge: 86400, immutable: true }, // 1天
js: { public: true, maxAge: 86400, immutable: true }, // 1天
image: { public: true, maxAge: 604800 } // 7天
},
// 桌面端策略
desktop: {
css: { public: true, maxAge: 2592000, immutable: true }, // 30天
js: { public: true, maxAge: 2592000, immutable: true }, // 30天
image: { public: true, maxAge: 2592000 } // 30天
}
};
const deviceType = this.detectDeviceType(userAgent);
const strategy = strategies[deviceType] || strategies.desktop;
return this.buildCacheControl(strategy[resourceType] || strategy.css);
}
detectDeviceType(userAgent) {
const mobilePattern = /Mobile|Android|iPhone|iPad/i;
return mobilePattern.test(userAgent) ? 'mobile' : 'desktop';
}
// 条件缓存
getConditionalCacheControl(conditions) {
const { isAuthenticated, resourceSize, bandwidth } = conditions;
let options = { public: true, maxAge: 3600 };
// 认证用户使用私有缓存
if (isAuthenticated) {
options.public = false;
options.private = true;
options.maxAge = 1800; // 30分钟
}
// 大文件减少缓存时间
if (resourceSize > 1024 * 1024) { // 1MB
options.maxAge = Math.max(options.maxAge / 2, 300);
}
// 低带宽用户增加缓存时间
if (bandwidth && bandwidth < 1000000) { // 1Mbps
options.maxAge *= 2;
}
return this.buildCacheControl(options);
}
}
// 使用示例
const cacheManager = new CacheControlManager();
// Express 中间件实现
function advancedCacheMiddleware(req, res, next) {
const userAgent = req.get('User-Agent') || '';
const resourceType = getResourceType(req.url);
// 根据资源类型和用户代理设置缓存
const cacheControl = cacheManager.getDynamicCacheStrategy(
resourceType,
userAgent,
Date.now()
);
res.set('Cache-Control', cacheControl);
// 添加调试信息
if (process.env.NODE_ENV === 'development') {
res.set('X-Cache-Debug', JSON.stringify({
resourceType,
userAgent: userAgent.substring(0, 50),
cacheControl
}));
}
next();
}
function getResourceType(url) {
if (/\.(css)$/.test(url)) return 'css';
if (/\.(js)$/.test(url)) return 'js';
if (/\.(png|jpg|jpeg|gif|webp|svg)$/.test(url)) return 'image';
if (/\.(woff|woff2|ttf|eot)$/.test(url)) return 'font';
return 'html';
}
协商缓存
协商缓存是一种需要向服务器确认的缓存机制。当强缓存失效时,浏览器会向服务器发送请求,通过比较资源的标识符来判断是否需要更新缓存。
协商缓存的工作流程如下:
graph TD
A["客户端首次请求资源"] --> B["服务器处理请求"]
B --> C["服务器返回资源内容<br/>+ 200 OK<br/>+ Last-Modified: 文件修改时间<br/>+ ETag: 内容哈希值"]
C --> D["客户端缓存资源<br/>及验证信息"]
D --> E["客户端再次请求<br/>同一资源"]
E --> F["发送条件请求头<br/>If-Modified-Since: 缓存的修改时间<br/>If-None-Match: 缓存的ETag值"]
F --> G["服务器验证资源"]
G --> H{资源是否<br/>已更新?}
H -->|未更新| I["304 Not Modified<br/>资源未修改"]
H -->|已更新| J["200 OK<br/>返回新资源<br/>+ 新的Last-Modified<br/>+ 新的ETag"]
I --> K["客户端使用缓存<br/>节省带宽和时间"]
J --> L["客户端更新缓存<br/>使用新资源"]
K --> M["完成请求"]
L --> M
subgraph "验证方式"
N["Last-Modified 验证<br/>比较文件修改时间"]
O["ETag 验证<br/>比较内容哈希值"]
end
G -.-> N
G -.-> O
style A fill:#e3f2fd
style C fill:#e8f5e8
style I fill:#c8e6c9
style J fill:#fff3e0
style K fill:#c8e6c9
style L fill:#fff3e0
style M fill:#f3e5f5
Last-Modified 与 If-Modified-Since
基于文件修改时间的协商缓存机制。
Last-Modified 由上一次请求的响应头返回,且该值会在本次请求中,通过请求头 If-Modified-Since 传递给服务端,服务端通过If-Modified-Since 与资源的修改时间进行对比,若在此日期后资源有更新,则将新的资源发送给客户端。
不过,通过文件的修改时间来判断资源是否更新是不明智的,因为很多时候文件更新时间变了,但文件内容未发生更改。
这样一来,就出现了 ETag 与If-None-Match。
// Last-Modified 缓存实现
class LastModifiedCache {
constructor(options = {}) {
this.enabled = options.enabled !== false;
this.precision = options.precision || 1000; // 精度为秒
}
// 设置 Last-Modified 头
setLastModified(res, filePath) {
if (!this.enabled) return;
try {
const fs = require('fs');
const stats = fs.statSync(filePath);
const lastModified = stats.mtime;
// 去掉毫秒精度,避免精度问题
const roundedTime = new Date(
Math.floor(lastModified.getTime() / this.precision) * this.precision
);
res.set('Last-Modified', roundedTime.toUTCString());
return roundedTime;
} catch (error) {
console.error('设置 Last-Modified 失败:', error);
return null;
}
}
// 检查 If-Modified-Since
checkIfModifiedSince(req, filePath) {
if (!this.enabled) return true;
const ifModifiedSince = req.get('If-Modified-Since');
if (!ifModifiedSince) return true;
try {
const fs = require('fs');
const stats = fs.statSync(filePath);
const lastModified = stats.mtime;
const clientTime = new Date(ifModifiedSince);
// 去掉毫秒精度进行比较
const serverTime = new Date(
Math.floor(lastModified.getTime() / this.precision) * this.precision
);
return serverTime > clientTime;
} catch (error) {
console.error('检查 If-Modified-Since 失败:', error);
return true;
}
}
// 中间件实现
middleware() {
return (req, res, next) => {
const originalSendFile = res.sendFile;
res.sendFile = function(path, options, callback) {
// 设置 Last-Modified
const lastModified = this.setLastModified(res, path);
// 检查是否需要返回 304
if (!this.checkIfModifiedSince(req, path)) {
res.status(304).end();
return;
}
// 调用原始的 sendFile 方法
return originalSendFile.call(this, path, options, callback);
}.bind(this);
next();
};
}
}
// 文件监控缓存管理器
class FileWatcherCache {
constructor() {
this.fileWatchers = new Map();
this.fileModificationTimes = new Map();
this.setupFileWatcher();
}
setupFileWatcher() {
const chokidar = require('chokidar');
// 监控静态文件目录
this.watcher = chokidar.watch(['public/**/*', 'dist/**/*'], {
ignored: /node_modules/,
persistent: true
});
this.watcher.on('change', (filePath) => {
this.updateFileModificationTime(filePath);
this.notifyClients(filePath);
});
this.watcher.on('add', (filePath) => {
this.updateFileModificationTime(filePath);
});
}
updateFileModificationTime(filePath) {
try {
const fs = require('fs');
const stats = fs.statSync(filePath);
this.fileModificationTimes.set(filePath, stats.mtime);
console.log(`文件更新: ${filePath} at ${stats.mtime}`);
} catch (error) {
console.error('更新文件修改时间失败:', error);
}
}
getFileModificationTime(filePath) {
return this.fileModificationTimes.get(filePath);
}
// 通知客户端文件已更新(用于热更新)
notifyClients(filePath) {
// 实现 WebSocket 通知逻辑
if (this.wsServer) {
this.wsServer.clients.forEach(client => {
if (client.readyState === 1) { // WebSocket.OPEN
client.send(JSON.stringify({
type: 'fileChanged',
filePath,
timestamp: Date.now()
}));
}
});
}
}
setWebSocketServer(wsServer) {
this.wsServer = wsServer;
}
destroy() {
if (this.watcher) {
this.watcher.close();
}
}
}
ETag 与 If-None-Match
基于内容哈希的协商缓存机制,更加精确和可靠。
不同于 Last-Modified,Etag 通过计算文件指纹,与请求传递过来的If-None-Match 进行对比,若值不等,则将新的资源发送给客户端。
值得一提的是,通常为了减轻服务器压力,并不会完整计算文件 hash 值作为 Etag,并且有些时候 Etag 的表现会退化为 Last-Modified(当指纹计算为文件更新时间时)。那为什么我们通常还是要选用 Etag 呢?原因有一下几点:
-
文件也许会发生周期性的更改,但内容并无变化,这时我们希望客户端认为这个文件是未变的;
-
文件修改频繁,比如在秒以内的时间进行修改,由于If-Modified-Since 能读取到的时间精度为S,因此这种场景下If-Modified-Since 无法正常使用;
-
某些服务器不能精确获得文件的最后修改时间。
// ETag 缓存管理器
class ETagCache {
constructor(options = {}) {
this.algorithm = options.algorithm || 'md5';
this.weak = options.weak !== false;
this.cache = new Map();
this.maxCacheSize = options.maxCacheSize || 1000;
}
// 生成 ETag
generateETag(content, filePath = null) {
const crypto = require('crypto');
// 如果是文件路径,读取文件内容
if (filePath && !content) {
try {
const fs = require('fs');
content = fs.readFileSync(filePath);
} catch (error) {
console.error('读取文件失败:', error);
return null;
}
}
// 生成哈希
const hash = crypto
.createHash(this.algorithm)
.update(content)
.digest('hex');
// 返回弱 ETag 或强 ETag
return this.weak ? `W/"${hash}"` : `"${hash}"`;
}
// 缓存 ETag
cacheETag(key, etag) {
// 限制缓存大小
if (this.cache.size >= this.maxCacheSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, {
etag,
timestamp: Date.now()
});
}
// 获取缓存的 ETag
getCachedETag(key) {
const cached = this.cache.get(key);
return cached ? cached.etag : null;
}
// 检查 If-None-Match
checkIfNoneMatch(req, etag) {
const ifNoneMatch = req.get('If-None-Match');
if (!ifNoneMatch) return false;
// 处理多个 ETag
const clientETags = ifNoneMatch.split(',').map(tag => tag.trim());
// 检查是否匹配
return clientETags.includes(etag) || clientETags.includes('*');
}
// Express 中间件
middleware(options = {}) {
return (req, res, next) => {
const originalSend = res.send;
const originalSendFile = res.sendFile;
// 拦截 send 方法
res.send = function(body) {
if (res.get('ETag')) {
return originalSend.call(this, body);
}
const etag = this.generateETag(body);
if (etag) {
res.set('ETag', etag);
if (this.checkIfNoneMatch(req, etag)) {
res.status(304).end();
return;
}
}
return originalSend.call(this, body);
}.bind(this);
// 拦截 sendFile 方法
res.sendFile = function(path, options, callback) {
const cacheKey = `file:${path}`;
let etag = this.getCachedETag(cacheKey);
if (!etag) {
etag = this.generateETag(null, path);
if (etag) {
this.cacheETag(cacheKey, etag);
}
}
if (etag) {
res.set('ETag', etag);
if (this.checkIfNoneMatch(req, etag)) {
res.status(304).end();
return;
}
}
return originalSendFile.call(this, path, options, callback);
}.bind(this);
next();
};
}
}
// 增量 ETag 计算器
class IncrementalETagCalculator {
constructor() {
this.chunks = new Map();
}
// 为大文件计算增量 ETag
calculateIncrementalETag(filePath, chunkSize = 1024 * 1024) {
return new Promise((resolve, reject) => {
const crypto = require('crypto');
const fs = require('fs');
const hash = crypto.createHash('md5');
const stream = fs.createReadStream(filePath, {
highWaterMark: chunkSize
});
stream.on('data', (chunk) => {
hash.update(chunk);
});
stream.on('end', () => {
const etag = `"${hash.digest('hex')}"`;
resolve(etag);
});
stream.on('error', reject);
});
}
// 流式 ETag 计算
createETagStream() {
const crypto = require('crypto');
const { Transform } = require('stream');
const hash = crypto.createHash('md5');
return new Transform({
transform(chunk, encoding, callback) {
hash.update(chunk);
this.push(chunk);
callback();
},
flush(callback) {
const etag = `"${hash.digest('hex')}"`;
this.emit('etag', etag);
callback();
}
});
}
}
// 智能缓存验证器
class SmartCacheValidator {
constructor() {
this.validators = new Map();
this.setupDefaultValidators();
}
setupDefaultValidators() {
// 文件大小验证器
this.addValidator('filesize', (filePath, cachedETag) => {
const fs = require('fs');
try {
const stats = fs.statSync(filePath);
return stats.size > 0;
} catch {
return false;
}
});
// 内容类型验证器
this.addValidator('contentType', (filePath, cachedETag) => {
const path = require('path');
const ext = path.extname(filePath).toLowerCase();
const textExtensions = ['.txt', '.html', '.css', '.js', '.json'];
// 文本文件需要更精确的验证
return !textExtensions.includes(ext);
});
}
addValidator(name, validatorFn) {
this.validators.set(name, validatorFn);
}
validate(filePath, cachedETag) {
for (const [name, validator] of this.validators) {
if (!validator(filePath, cachedETag)) {
console.log(`缓存验证失败: ${name} for ${filePath}`);
return false;
}
}
return true;
}
}
状态码
HTTP 缓存相关的状态码处理和优化。
// HTTP 状态码缓存管理器
class HTTPStatusCacheManager {
constructor() {
this.statusHandlers = new Map();
this.setupDefaultHandlers();
}
setupDefaultHandlers() {
// 304 Not Modified 处理
this.addStatusHandler(304, (req, res, context) => {
// 设置适当的缓存头
if (context.etag) {
res.set('ETag', context.etag);
}
if (context.lastModified) {
res.set('Last-Modified', context.lastModified);
}
if (context.cacheControl) {
res.set('Cache-Control', context.cacheControl);
}
// 添加性能指标
res.set('X-Cache-Status', 'HIT');
res.set('X-Response-Time', `${Date.now() - context.startTime}ms`);
res.status(304).end();
});
// 200 OK with Cache 处理
this.addStatusHandler(200, (req, res, context) => {
if (context.etag) {
res.set('ETag', context.etag);
}
if (context.lastModified) {
res.set('Last-Modified', context.lastModified);
}
if (context.cacheControl) {
res.set('Cache-Control', context.cacheControl);
}
res.set('X-Cache-Status', 'MISS');
res.set('X-Response-Time', `${Date.now() - context.startTime}ms`);
});
// 412 Precondition Failed 处理
this.addStatusHandler(412, (req, res, context) => {
res.set('X-Cache-Status', 'PRECONDITION_FAILED');
res.status(412).json({
error: 'Precondition Failed',
message: 'The resource has been modified',
timestamp: new Date().toISOString()
});
});
}
addStatusHandler(statusCode, handler) {
this.statusHandlers.set(statusCode, handler);
}
handleStatus(statusCode, req, res, context) {
const handler = this.statusHandlers.get(statusCode);
if (handler) {
handler(req, res, context);
} else {
console.warn(`未处理的状态码: ${statusCode}`);
res.status(statusCode).end();
}
}
// 条件请求处理器
processConditionalRequest(req, res, resourceInfo) {
const context = {
startTime: Date.now(),
etag: resourceInfo.etag,
lastModified: resourceInfo.lastModified,
cacheControl: resourceInfo.cacheControl
};
// 检查 If-None-Match
const ifNoneMatch = req.get('If-None-Match');
if (ifNoneMatch && resourceInfo.etag) {
const clientETags = ifNoneMatch.split(',').map(tag => tag.trim());
if (clientETags.includes(resourceInfo.etag) || clientETags.includes('*')) {
this.handleStatus(304, req, res, context);
return true;
}
}
// 检查 If-Modified-Since
const ifModifiedSince = req.get('If-Modified-Since');
if (ifModifiedSince && resourceInfo.lastModified) {
const clientTime = new Date(ifModifiedSince);
const resourceTime = new Date(resourceInfo.lastModified);
if (resourceTime <= clientTime) {
this.handleStatus(304, req, res, context);
return true;
}
}
// 检查 If-Match (强制匹配)
const ifMatch = req.get('If-Match');
if (ifMatch && resourceInfo.etag) {
const clientETags = ifMatch.split(',').map(tag => tag.trim());
if (!clientETags.includes(resourceInfo.etag) && !clientETags.includes('*')) {
this.handleStatus(412, req, res, context);
return true;
}
}
// 检查 If-Unmodified-Since
const ifUnmodifiedSince = req.get('If-Unmodified-Since');
if (ifUnmodifiedSince && resourceInfo.lastModified) {
const clientTime = new Date(ifUnmodifiedSince);
const resourceTime = new Date(resourceInfo.lastModified);
if (resourceTime > clientTime) {
this.handleStatus(412, req, res, context);
return true;
}
}
// 资源需要返回
this.handleStatus(200, req, res, context);
return false;
}
}
// 缓存性能监控器
class CachePerformanceMonitor {
constructor() {
this.metrics = {
hits: 0,
misses: 0,
errors: 0,
responseTimeSum: 0,
requestCount: 0
};
this.detailedMetrics = new Map();
}
recordCacheHit(resourceType, responseTime) {
this.metrics.hits++;
this.metrics.responseTimeSum += responseTime;
this.metrics.requestCount++;
this.updateDetailedMetrics(resourceType, 'hit', responseTime);
}
recordCacheMiss(resourceType, responseTime) {
this.metrics.misses++;
this.metrics.responseTimeSum += responseTime;
this.metrics.requestCount++;
this.updateDetailedMetrics(resourceType, 'miss', responseTime);
}
recordCacheError(resourceType, error) {
this.metrics.errors++;
console.error('缓存错误:', error);
}
updateDetailedMetrics(resourceType, type, responseTime) {
if (!this.detailedMetrics.has(resourceType)) {
this.detailedMetrics.set(resourceType, {
hits: 0,
misses: 0,
totalResponseTime: 0,
requestCount: 0
});
}
const typeMetrics = this.detailedMetrics.get(resourceType);
typeMetrics[type === 'hit' ? 'hits' : 'misses']++;
typeMetrics.totalResponseTime += responseTime;
typeMetrics.requestCount++;
}
getHitRatio() {
const total = this.metrics.hits + this.metrics.misses;
return total > 0 ? (this.metrics.hits / total * 100).toFixed(2) : 0;
}
getAverageResponseTime() {
return this.metrics.requestCount > 0
? (this.metrics.responseTimeSum / this.metrics.requestCount).toFixed(2)
: 0;
}
generateReport() {
const report = {
summary: {
hitRatio: this.getHitRatio() + '%',
averageResponseTime: this.getAverageResponseTime() + 'ms',
totalRequests: this.metrics.requestCount,
cacheHits: this.metrics.hits,
cacheMisses: this.metrics.misses,
errors: this.metrics.errors
},
byResourceType: {}
};
// 按资源类型统计
for (const [resourceType, metrics] of this.detailedMetrics) {
const total = metrics.hits + metrics.misses;
report.byResourceType[resourceType] = {
hitRatio: total > 0 ? (metrics.hits / total * 100).toFixed(2) + '%' : '0%',
averageResponseTime: metrics.requestCount > 0
? (metrics.totalResponseTime / metrics.requestCount).toFixed(2) + 'ms'
: '0ms',
requests: total
};
}
return report;
}
reset() {
this.metrics = {
hits: 0,
misses: 0,
errors: 0,
responseTimeSum: 0,
requestCount: 0
};
this.detailedMetrics.clear();
}
}
策略缓存
Service Worker
Service Worker 是现代 Web 应用中实现高级缓存策略的强大工具,它运行在浏览器后台,可以拦截网络请求并实现自定义的缓存逻辑。
注册
// Service Worker 注册管理器
class ServiceWorkerManager {
constructor(options = {}) {
this.swPath = options.swPath || '/sw.js';
this.scope = options.scope || '/';
this.updateViaCache = options.updateViaCache || 'imports';
this.registration = null;
this.isSupported = 'serviceWorker' in navigator;
}
// 注册 Service Worker
async register() {
if (!this.isSupported) {
console.warn('Service Worker 不被支持');
return false;
}
try {
this.registration = await navigator.serviceWorker.register(this.swPath, {
scope: this.scope,
updateViaCache: this.updateViaCache
});
console.log('Service Worker 注册成功:', this.registration.scope);
// 设置事件监听器
this.setupEventListeners();
// 检查更新
this.checkForUpdates();
return true;
} catch (error) {
console.error('Service Worker 注册失败:', error);
return false;
}
}
setupEventListeners() {
if (!this.registration) return;
// 监听控制器变化
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('Service Worker 控制器已更改');
this.onControllerChange();
});
// 监听消息
navigator.serviceWorker.addEventListener('message', (event) => {
this.handleMessage(event.data);
});
// 监听注册状态变化
this.registration.addEventListener('updatefound', () => {
console.log('发现 Service Worker 更新');
this.handleUpdate();
});
}
async checkForUpdates() {
if (!this.registration) return;
try {
await this.registration.update();
console.log('已检查 Service Worker 更新');
} catch (error) {
console.error('检查更新失败:', error);
}
}
handleUpdate() {
if (!this.registration.installing) return;
const newWorker = this.registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 新的 Service Worker 已安装,但旧的仍在控制
this.promptUserToUpdate();
}
});
}
promptUserToUpdate() {
if (confirm('发现新版本,是否立即更新?')) {
this.skipWaiting();
}
}
async skipWaiting() {
if (!this.registration.waiting) return;
// 发送跳过等待消息
this.registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
onControllerChange() {
// 重新加载页面以使用新的 Service Worker
window.location.reload();
}
handleMessage(data) {
switch (data.type) {
case 'CACHE_UPDATED':
console.log('缓存已更新:', data.payload);
break;
case 'OFFLINE_READY':
console.log('离线功能已准备就绪');
break;
case 'ERROR':
console.error('Service Worker 错误:', data.payload);
break;
default:
console.log('收到 Service Worker 消息:', data);
}
}
// 向 Service Worker 发送消息
async sendMessage(message) {
if (!navigator.serviceWorker.controller) {
console.warn('没有活动的 Service Worker');
return;
}
return new Promise((resolve, reject) => {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event) => {
if (event.data.error) {
reject(new Error(event.data.error));
} else {
resolve(event.data);
}
};
navigator.serviceWorker.controller.postMessage(message, [messageChannel.port2]);
});
}
// 获取缓存统计信息
async getCacheStats() {
try {
return await this.sendMessage({ type: 'GET_CACHE_STATS' });
} catch (error) {
console.error('获取缓存统计失败:', error);
return null;
}
}
// 清理缓存
async clearCache(cacheName) {
try {
return await this.sendMessage({
type: 'CLEAR_CACHE',
cacheName
});
} catch (error) {
console.error('清理缓存失败:', error);
return false;
}
}
// 预缓存资源
async precacheResources(urls) {
try {
return await this.sendMessage({
type: 'PRECACHE_RESOURCES',
urls
});
} catch (error) {
console.error('预缓存失败:', error);
return false;
}
}
}
// 使用示例
const swManager = new ServiceWorkerManager({
swPath: '/service-worker.js',
scope: '/',
updateViaCache: 'none'
});
// 页面加载时注册
window.addEventListener('load', async () => {
const success = await swManager.register();
if (success) {
console.log('Service Worker 注册成功');
// 预缓存关键资源
await swManager.precacheResources([
'/css/critical.css',
'/js/app.js',
'/fonts/main.woff2'
]);
}
});
Installing、Activated、Idle、Fetch 生命周期
// Service Worker 实现 (sw.js)
const CACHE_NAME = 'app-cache-v1';
const RUNTIME_CACHE = 'runtime-cache-v1';
const PRECACHE_URLS = [
'/',
'/css/app.css',
'/js/app.js',
'/images/logo.png',
'/manifest.json'
];
// Service Worker 生命周期管理器
class ServiceWorkerLifecycleManager {
constructor() {
this.cacheNames = {
precache: CACHE_NAME,
runtime: RUNTIME_CACHE,
images: 'images-cache-v1',
api: 'api-cache-v1'
};
this.setupEventListeners();
}
setupEventListeners() {
// 安装事件
self.addEventListener('install', (event) => {
console.log('Service Worker: Installing...');
event.waitUntil(this.handleInstall());
});
// 激活事件
self.addEventListener('activate', (event) => {
console.log('Service Worker: Activating...');
event.waitUntil(this.handleActivate());
});
// 网络请求拦截
self.addEventListener('fetch', (event) => {
event.respondWith(this.handleFetch(event.request));
});
// 消息处理
self.addEventListener('message', (event) => {
this.handleMessage(event);
});
// 后台同步
self.addEventListener('sync', (event) => {
this.handleBackgroundSync(event);
});
// 推送通知
self.addEventListener('push', (event) => {
this.handlePush(event);
});
}
// Installing 阶段处理
async handleInstall() {
try {
console.log('开始预缓存资源...');
const cache = await caches.open(this.cacheNames.precache);
// 预缓存关键资源
await cache.addAll(PRECACHE_URLS);
console.log('预缓存完成');
// 跳过等待,立即激活
await self.skipWaiting();
// 通知客户端安装完成
this.broadcastMessage({
type: 'INSTALL_COMPLETE',
timestamp: Date.now()
});
} catch (error) {
console.error('安装阶段失败:', error);
throw error;
}
}
// Activated 阶段处理
async handleActivate() {
try {
console.log('开始激活 Service Worker...');
// 清理旧缓存
await this.cleanupOldCaches();
// 立即获取控制权
await self.clients.claim();
console.log('Service Worker 激活完成');
// 通知客户端激活完成
this.broadcastMessage({
type: 'ACTIVATE_COMPLETE',
timestamp: Date.now()
});
} catch (error) {
console.error('激活阶段失败:', error);
throw error;
}
}
// 清理旧缓存
async cleanupOldCaches() {
const cacheWhitelist = Object.values(this.cacheNames);
const cacheNames = await caches.keys();
const deletePromises = cacheNames
.filter(cacheName => !cacheWhitelist.includes(cacheName))
.map(cacheName => {
console.log('删除旧缓存:', cacheName);
return caches.delete(cacheName);
});
await Promise.all(deletePromises);
}
// Fetch 事件处理
async handleFetch(request) {
const url = new URL(request.url);
try {
// 根据请求类型选择缓存策略
if (this.isHTMLRequest(request)) {
return await this.handleHTMLRequest(request);
}
if (this.isStaticAsset(request)) {
return await this.handleStaticAsset(request);
}
if (this.isAPIRequest(request)) {
return await this.handleAPIRequest(request);
}
if (this.isImageRequest(request)) {
return await this.handleImageRequest(request);
}
// 默认网络优先策略
return await this.networkFirst(request);
} catch (error) {
console.error('请求处理失败:', error);
return await this.handleFetchError(request, error);
}
}
// HTML 请求处理(网络优先,缓存回退)
async handleHTMLRequest(request) {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// 更新缓存
const cache = await caches.open(this.cacheNames.runtime);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
// 网络失败,返回缓存
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// 返回离线页面
return await caches.match('/offline.html');
}
}
// 静态资源处理(缓存优先)
async handleStaticAsset(request) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(this.cacheNames.precache);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
throw error;
}
}
// API 请求处理(网络优先,短时间缓存)
async handleAPIRequest(request) {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok && request.method === 'GET') {
const cache = await caches.open(this.cacheNames.api);
// 添加时间戳用于缓存过期检查
const responseWithTimestamp = new Response(networkResponse.body, {
status: networkResponse.status,
statusText: networkResponse.statusText,
headers: {
...networkResponse.headers,
'sw-cached-at': Date.now().toString()
}
});
cache.put(request, responseWithTimestamp.clone());
return responseWithTimestamp;
}
return networkResponse;
} catch (error) {
// 检查缓存
const cachedResponse = await this.getCachedAPIResponse(request);
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
}
// 获取缓存的 API 响应(检查过期时间)
async getCachedAPIResponse(request) {
const cachedResponse = await caches.match(request);
if (!cachedResponse) return null;
const cachedAt = cachedResponse.headers.get('sw-cached-at');
if (!cachedAt) return cachedResponse;
const age = Date.now() - parseInt(cachedAt);
const maxAge = 5 * 60 * 1000; // 5分钟
if (age > maxAge) {
// 缓存过期,删除
const cache = await caches.open(this.cacheNames.api);
cache.delete(request);
return null;
}
return cachedResponse;
}
// 图片请求处理(缓存优先,延迟加载)
async handleImageRequest(request) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(this.cacheNames.images);
// 图片压缩处理(如果需要)
const processedResponse = await this.processImageResponse(networkResponse);
cache.put(request, processedResponse.clone());
return processedResponse;
}
return networkResponse;
} catch (error) {
// 返回占位图片
return await caches.match('/images/placeholder.png');
}
}
// 图片响应处理
async processImageResponse(response) {
// 这里可以添加图片压缩、格式转换等逻辑
return response;
}
// 请求类型判断
isHTMLRequest(request) {
return request.headers.get('accept')?.includes('text/html');
}
isStaticAsset(request) {
return /\.(css|js|woff|woff2|ttf|eot)$/.test(new URL(request.url).pathname);
}
isAPIRequest(request) {
return new URL(request.url).pathname.startsWith('/api/');
}
isImageRequest(request) {
return /\.(png|jpg|jpeg|gif|webp|svg)$/.test(new URL(request.url).pathname);
}
// 网络优先策略
async networkFirst(request) {
try {
const networkResponse = await fetch(request);
return networkResponse;
} catch (error) {
const cachedResponse = await caches.match(request);
return cachedResponse || new Response('Network error', { status: 408 });
}
}
// 错误处理
async handleFetchError(request, error) {
console.error('Fetch 错误:', error);
// 尝试返回缓存
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// 返回错误页面
return new Response('资源不可用', {
status: 503,
statusText: 'Service Unavailable'
});
}
// 消息处理
handleMessage(event) {
const { type, ...payload } = event.data;
switch (type) {
case 'SKIP_WAITING':
self.skipWaiting();
break;
case 'GET_CACHE_STATS':
this.getCacheStats().then(stats => {
event.ports[0].postMessage(stats);
});
break;
case 'CLEAR_CACHE':
this.clearSpecificCache(payload.cacheName).then(success => {
event.ports[0].postMessage({ success });
});
break;
case 'PRECACHE_RESOURCES':
this.precacheResources(payload.urls).then(success => {
event.ports[0].postMessage({ success });
});
break;
default:
console.log('未知消息类型:', type);
}
}
// 获取缓存统计
async getCacheStats() {
const stats = {};
for (const [name, cacheName] of Object.entries(this.cacheNames)) {
try {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
stats[name] = {
name: cacheName,
count: keys.length,
urls: keys.map(request => request.url)
};
} catch (error) {
stats[name] = { error: error.message };
}
}
return stats;
}
// 清理特定缓存
async clearSpecificCache(cacheName) {
try {
return await caches.delete(cacheName);
} catch (error) {
console.error('清理缓存失败:', error);
return false;
}
}
// 预缓存资源
async precacheResources(urls) {
try {
const cache = await caches.open(this.cacheNames.runtime);
await cache.addAll(urls);
return true;
} catch (error) {
console.error('预缓存失败:', error);
return false;
}
}
// 后台同步处理
handleBackgroundSync(event) {
if (event.tag === 'background-sync') {
event.waitUntil(this.doBackgroundSync());
}
}
async doBackgroundSync() {
console.log('执行后台同步...');
// 实现后台同步逻辑
}
// 推送通知处理
handlePush(event) {
const options = {
body: event.data ? event.data.text() : 'No payload',
icon: '/images/icon-192.png',
badge: '/images/badge-72.png'
};
event.waitUntil(
self.registration.showNotification('App Notification', options)
);
}
// 广播消息给所有客户端
async broadcastMessage(message) {
const clients = await self.clients.matchAll();
clients.forEach(client => {
client.postMessage(message);
});
}
}
// 初始化 Service Worker 生命周期管理器
const lifecycleManager = new ServiceWorkerLifecycleManager();
现在我来为这些缓存策略生成相应的 Mermaid 流程图:
首先是完整的HTTP缓存请求流程图:
graph TD
A["用户请求资源"] --> B{是否存在缓存?}
B -->|no| C["向服务器请求"]
B -->|yes| D{判断缓存是否过期?}
D -->|no| E["直接使用缓存内容"]
D -->|yes| F{判断ETag}
F -->|no| G{判断Last-Modified}
F -->|yes| H["向服务器请求<br/>If-None-Match"]
G -->|no| C
G -->|yes| I["向服务器请求<br/>If-Modified-Since"]
H --> J{服务器返回200还是304}
I --> J
J -->|304| K["服务器返回304,读取本地缓存"]
J -->|200| L["向服务器请求"]
L --> M["请求响应,缓存防腐"]
C --> M
K --> N["返回展示资源"]
M --> N
E --> N
style A fill:#e3f2fd
style E fill:#c8e6c9
style K fill:#c8e6c9
style N fill:#f3e5f5
style M fill:#fff3e0
HTTP缓存策略的组成结构:
graph TD
A["HTTP 缓存策略"] --> B["强缓存"]
A --> C["协商缓存"]
B --> D["Expires<br/>指定过期时间"]
B --> E["Cache-Control<br/>更灵活的缓存控制"]
E --> E1["public/private<br/>缓存可见性"]
E --> E2["max-age<br/>最大存活时间"]
E --> E3["no-cache/no-store<br/>缓存控制"]
E --> E4["must-revalidate<br/>强制重新验证"]
C --> F["Last-Modified<br/>文件修改时间"]
C --> G["ETag<br/>内容哈希值"]
F --> F1["If-Modified-Since<br/>条件请求头"]
G --> G1["If-None-Match<br/>条件请求头"]
F1 --> H["304 Not Modified<br/>资源未修改"]
G1 --> H
F1 --> I["200 OK<br/>返回新资源"]
G1 --> I
H --> J["从缓存加载<br/>节省带宽"]
I --> K["更新缓存<br/>下载新资源"]
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#e8f5e8
style H fill:#c8e6c9
style I fill:#ffecb3
style J fill:#c8e6c9
style K fill:#ffecb3
实现策略
Service Worker 提供了多种缓存策略,每种策略都有其适用的场景:
graph TD
A["Service Worker<br/>拦截网络请求"] --> B{确定缓存策略}
B --> C["仅缓存<br/>Cache Only"]
B --> D["仅网络<br/>Network Only"]
B --> E["缓存优先<br/>Cache First"]
B --> F["网络优先<br/>Network First"]
C --> C1["检查缓存"]
C1 --> C2{缓存存在?}
C2 -->|是| C3["返回缓存内容"]
C2 -->|否| C4["返回错误"]
D --> D1["直接网络请求"]
D1 --> D2{请求成功?}
D2 -->|是| D3["返回网络响应"]
D2 -->|否| D4["返回网络错误"]
E --> E1["检查缓存"]
E1 --> E2{缓存存在?}
E2 -->|是| E3["返回缓存内容<br/>后台更新缓存"]
E2 -->|否| E4["网络请求"]
E4 --> E5["缓存响应并返回"]
F --> F1["网络请求"]
F1 --> F2{请求成功?}
F2 -->|是| F3["返回网络响应<br/>更新缓存"]
F2 -->|否| F4["检查缓存"]
F4 --> F5{缓存存在?}
F5 -->|是| F6["返回缓存内容"]
F5 -->|否| F7["返回错误"]
subgraph "适用场景"
S1["静态资源:仅缓存"]
S2["API数据:仅网络"]
S3["图片资源:缓存优先"]
S4["HTML页面:网络优先"]
end
style A fill:#e3f2fd
style C3 fill:#c8e6c9
style D3 fill:#fff3e0
style E3 fill:#c8e6c9
style E5 fill:#fff3e0
style F3 fill:#fff3e0
style F6 fill:#c8e6c9
仅缓存
缓存优先策略,只从缓存中获取资源,适用于不经常变化的静态资源。
// 仅缓存策略实现
class CacheOnlyStrategy {
constructor(cacheName) {
this.cacheName = cacheName;
}
async handle(request) {
const cache = await caches.open(this.cacheName);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
// 如果缓存中没有,返回错误
throw new Error(`资源不在缓存中: ${request.url}`);
}
// 预缓存资源
async precache(urls) {
const cache = await caches.open(this.cacheName);
const cachePromises = urls.map(async (url) => {
try {
const response = await fetch(url);
if (response.ok) {
await cache.put(url, response);
console.log(`预缓存成功: ${url}`);
}
} catch (error) {
console.error(`预缓存失败: ${url}`, error);
}
});
await Promise.all(cachePromises);
}
}
// 使用示例
const staticAssetsCache = new CacheOnlyStrategy('static-assets-v1');
// 在 Service Worker 中使用
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// 对静态资源使用仅缓存策略
if (url.pathname.match(/\.(css|js|woff2?|ttf|eot)$/)) {
event.respondWith(staticAssetsCache.handle(event.request));
}
});
仅限网络
网络优先策略,总是从网络获取最新资源,适用于实时性要求高的动态内容。
// 仅限网络策略实现
class NetworkOnlyStrategy {
constructor(options = {}) {
this.timeout = options.timeout || 10000; // 10秒超时
this.retryAttempts = options.retryAttempts || 1;
}
async handle(request) {
let lastError;
for (let attempt = 0; attempt <= this.retryAttempts; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
const response = await fetch(request, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (response.ok) {
return response;
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} catch (error) {
lastError = error;
if (attempt < this.retryAttempts) {
// 指数退避重试
const delay = Math.pow(2, attempt) * 1000;
await this.sleep(delay);
console.log(`重试请求 (${attempt + 1}/${this.retryAttempts}): ${request.url}`);
}
}
}
throw lastError;
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 网络状态检测
isOnline() {
return navigator.onLine;
}
// 带网络检测的请求处理
async handleWithNetworkCheck(request) {
if (!this.isOnline()) {
throw new Error('设备离线,无法访问网络');
}
return await this.handle(request);
}
}
先缓存,然后回退到网络
缓存优先策略,先尝试从缓存获取,失败后再请求网络。
// 缓存优先策略实现
class CacheFirstStrategy {
constructor(cacheName, options = {}) {
this.cacheName = cacheName;
this.maxAge = options.maxAge || 86400000; // 24小时
this.updateCache = options.updateCache !== false;
}
async handle(request) {
const cache = await caches.open(this.cacheName);
const cachedResponse = await cache.match(request);
// 检查缓存是否存在且有效
if (cachedResponse && this.isCacheValid(cachedResponse)) {
// 后台更新缓存(可选)
if (this.updateCache) {
this.updateCacheInBackground(request, cache);
}
return cachedResponse;
}
// 缓存失效或不存在,从网络获取
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// 更新缓存
const responseWithTimestamp = this.addTimestamp(networkResponse);
cache.put(request, responseWithTimestamp.clone());
return responseWithTimestamp;
}
// 网络请求失败,如果有过期缓存也返回
if (cachedResponse) {
return cachedResponse;
}
throw new Error(`网络请求失败: ${networkResponse.status}`);
} catch (error) {
// 网络失败,返回缓存(即使过期)
if (cachedResponse) {
console.warn('网络失败,返回过期缓存:', request.url);
return cachedResponse;
}
throw error;
}
}
// 检查缓存是否有效
isCacheValid(response) {
const cachedAt = response.headers.get('sw-cached-at');
if (!cachedAt) return true; // 没有时间戳的认为有效
const age = Date.now() - parseInt(cachedAt);
return age < this.maxAge;
}
// 添加时间戳到响应
addTimestamp(response) {
const headers = new Headers(response.headers);
headers.set('sw-cached-at', Date.now().toString());
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: headers
});
}
// 后台更新缓存
async updateCacheInBackground(request, cache) {
try {
const response = await fetch(request);
if (response.ok) {
const responseWithTimestamp = this.addTimestamp(response);
await cache.put(request, responseWithTimestamp);
console.log('后台缓存更新成功:', request.url);
}
} catch (error) {
console.warn('后台缓存更新失败:', error);
}
}
}
网络优先,回退到缓存
网络优先策略,优先从网络获取最新内容,失败后使用缓存。
// 网络优先策略实现
class NetworkFirstStrategy {
constructor(cacheName, options = {}) {
this.cacheName = cacheName;
this.timeout = options.timeout || 3000; // 3秒超时
this.maxCacheAge = options.maxCacheAge || 3600000; // 1小时
}
async handle(request) {
const cache = await caches.open(this.cacheName);
try {
// 尝试网络请求
const networkResponse = await this.fetchWithTimeout(request);
if (networkResponse.ok) {
// 更新缓存
const responseToCache = this.addCacheHeaders(networkResponse);
cache.put(request, responseToCache.clone());
return responseToCache;
}
throw new Error(`网络响应错误: ${networkResponse.status}`);
} catch (error) {
console.warn('网络请求失败,尝试缓存:', error.message);
// 网络失败,尝试缓存
const cachedResponse = await cache.match(request);
if (cachedResponse) {
// 检查缓存是否过期
if (this.isCacheExpired(cachedResponse)) {
console.warn('缓存已过期,但仍返回:', request.url);
}
return cachedResponse;
}
// 缓存也没有,抛出错误
throw new Error(`资源不可用: ${request.url}`);
}
}
// 带超时的网络请求
async fetchWithTimeout(request) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(request, {
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
// 添加缓存头信息
addCacheHeaders(response) {
const headers = new Headers(response.headers);
headers.set('sw-cached-at', Date.now().toString());
headers.set('sw-strategy', 'network-first');
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: headers
});
}
// 检查缓存是否过期
isCacheExpired(response) {
const cachedAt = response.headers.get('sw-cached-at');
if (!cachedAt) return false;
const age = Date.now() - parseInt(cachedAt);
return age > this.maxCacheAge;
}
}
综合缓存策略路由器
在实际项目中,我们通常需要根据不同的资源类型和业务需求选择合适的缓存策略:
// 综合缓存策略路由器
class CacheStrategyRouter {
constructor() {
this.strategies = new Map();
this.setupDefaultStrategies();
}
setupDefaultStrategies() {
// 静态资源缓存策略
this.strategies.set('static-assets', {
pattern: /\.(css|js|woff2?|ttf|eot)$/,
strategy: new CacheOnlyStrategy('static-assets-v1'),
description: '静态资源使用仅缓存策略'
});
// 图片资源缓存策略
this.strategies.set('images', {
pattern: /\.(png|jpg|jpeg|gif|webp|svg)$/,
strategy: new CacheFirstStrategy('images-cache-v1', { maxAge: 86400000 }),
description: '图片资源使用缓存优先策略'
});
// HTML页面缓存策略
this.strategies.set('pages', {
pattern: /\.html?$/,
strategy: new NetworkFirstStrategy('pages-cache-v1', { timeout: 2000 }),
description: 'HTML页面使用网络优先策略'
});
// API接口缓存策略
this.strategies.set('api', {
pattern: /\/api\//,
strategy: new NetworkOnlyStrategy({ timeout: 5000, retryAttempts: 2 }),
description: 'API接口使用仅网络策略'
});
// 字体文件缓存策略
this.strategies.set('fonts', {
pattern: /\.(woff|woff2|ttf|eot)$/,
strategy: new CacheFirstStrategy('fonts-cache-v1', { maxAge: 31536000000 }), // 1年
description: '字体文件使用长期缓存策略'
});
}
// 路由请求到对应的缓存策略
async route(request) {
const url = new URL(request.url);
// 遍历所有策略,找到匹配的
for (const [name, config] of this.strategies) {
if (config.pattern.test(url.pathname)) {
console.log(`使用缓存策略 [${name}]: ${config.description}`);
try {
return await config.strategy.handle(request);
} catch (error) {
console.error(`缓存策略 [${name}] 失败:`, error);
// 回退到基本网络请求
return await this.fallbackToNetwork(request);
}
}
}
// 默认策略:网络优先
console.log('使用默认缓存策略:网络优先');
const defaultStrategy = new NetworkFirstStrategy('default-cache-v1');
return await defaultStrategy.handle(request);
}
// 回退到基本网络请求
async fallbackToNetwork(request) {
try {
return await fetch(request);
} catch (error) {
return new Response('资源不可用', {
status: 503,
statusText: 'Service Unavailable'
});
}
}
// 添加自定义策略
addStrategy(name, pattern, strategy, description) {
this.strategies.set(name, {
pattern,
strategy,
description
});
}
// 移除策略
removeStrategy(name) {
return this.strategies.delete(name);
}
// 获取所有策略信息
getStrategiesInfo() {
const info = {};
for (const [name, config] of this.strategies) {
info[name] = {
pattern: config.pattern.toString(),
description: config.description
};
}
return info;
}
}
// Service Worker 中的使用示例
const cacheRouter = new CacheStrategyRouter();
// 自定义策略示例:视频文件使用特殊缓存策略
cacheRouter.addStrategy(
'videos',
/\.(mp4|webm|avi)$/,
new CacheFirstStrategy('videos-cache-v1', { maxAge: 604800000 }), // 7天
'视频文件使用缓存优先策略,7天有效期'
);
// 在 Service Worker 的 fetch 事件中使用
self.addEventListener('fetch', (event) => {
// 只处理 GET 请求
if (event.request.method !== 'GET') {
return;
}
event.respondWith(
cacheRouter.route(event.request).catch((error) => {
console.error('缓存路由失败:', error);
return fetch(event.request);
})
);
});
// 缓存预热示例
self.addEventListener('install', (event) => {
event.waitUntil(
Promise.all([
// 预缓存关键静态资源
cacheRouter.strategies.get('static-assets').strategy.precache([
'/css/app.css',
'/js/app.js',
'/js/vendor.js'
]),
// 预缓存关键图片
cacheRouter.strategies.get('images').strategy.precache([
'/images/logo.png',
'/images/hero-banner.jpg'
])
])
);
});
缓存技术方案实践
静态资源优化方案与思考
// 静态资源缓存优化方案
class StaticResourceCacheOptimizer {
constructor() {
this.cacheConfigs = this.initializeCacheConfigs();
this.versionManager = new AssetVersionManager();
this.compressionManager = new CompressionManager();
}
initializeCacheConfigs() {
return {
// 长期缓存资源(带版本号)
longTerm: {
pattern: /\.(css|js|png|jpg|jpeg|gif|woff|woff2|ttf|eot)\?v=[\w\d]+$/,
maxAge: 31536000, // 1年
cacheControl: 'public, max-age=31536000, immutable',
strategy: 'cache-first'
},
// 中期缓存资源
mediumTerm: {
pattern: /\.(css|js|png|jpg|jpeg|gif|woff|woff2|ttf|eot)$/,
maxAge: 2592000, // 30天
cacheControl: 'public, max-age=2592000',
strategy: 'cache-first'
},
// 短期缓存资源
shortTerm: {
pattern: /\.(html|htm)$/,
maxAge: 300, // 5分钟
cacheControl: 'public, max-age=300, must-revalidate',
strategy: 'network-first'
},
// API 缓存
api: {
pattern: /\/api\//,
maxAge: 60, // 1分钟
cacheControl: 'private, max-age=60',
strategy: 'network-first'
}
};
}
// 生成缓存策略
generateCacheStrategy(request) {
const url = new URL(request.url);
for (const [name, config] of Object.entries(this.cacheConfigs)) {
if (config.pattern.test(url.href)) {
return {
name,
config,
headers: this.generateCacheHeaders(config)
};
}
}
// 默认策略
return {
name: 'default',
config: this.cacheConfigs.shortTerm,
headers: this.generateCacheHeaders(this.cacheConfigs.shortTerm)
};
}
generateCacheHeaders(config) {
const headers = new Headers();
headers.set('Cache-Control', config.cacheControl);
if (config.maxAge) {
const expireDate = new Date(Date.now() + config.maxAge * 1000);
headers.set('Expires', expireDate.toUTCString());
}
return headers;
}
// 版本化资源处理
processVersionedAssets(manifest) {
const versionedAssets = new Map();
Object.entries(manifest).forEach(([file, hash]) => {
const versionedUrl = `${file}?v=${hash}`;
versionedAssets.set(file, versionedUrl);
});
return versionedAssets;
}
// 资源预加载策略
async implementPreloadStrategy(criticalResources) {
const preloadPromises = criticalResources.map(async (resource) => {
try {
const link = document.createElement('link');
link.rel = 'preload';
link.href = resource.url;
link.as = resource.type;
if (resource.crossorigin) {
link.crossOrigin = resource.crossorigin;
}
document.head.appendChild(link);
// 等待预加载完成
return new Promise((resolve, reject) => {
link.onload = resolve;
link.onerror = reject;
});
} catch (error) {
console.error('预加载失败:', resource.url, error);
}
});
await Promise.allSettled(preloadPromises);
}
// 智能缓存清理
async performIntelligentCacheCleanup() {
const cacheNames = await caches.keys();
const currentTime = Date.now();
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
for (const request of requests) {
const response = await cache.match(request);
const cachedAt = response.headers.get('sw-cached-at');
if (cachedAt) {
const age = currentTime - parseInt(cachedAt);
const maxAge = this.getMaxAgeForRequest(request);
if (age > maxAge * 2) { // 超过最大年龄的2倍
await cache.delete(request);
console.log('清理过期缓存:', request.url);
}
}
}
}
}
getMaxAgeForRequest(request) {
const strategy = this.generateCacheStrategy(request);
return strategy.config.maxAge * 1000; // 转换为毫秒
}
}
充分利用浏览器缓存机制
// 浏览器缓存机制优化器
class BrowserCacheOptimizer {
constructor() {
this.memoryCache = new MemoryCache();
this.diskCache = new DiskCache();
this.httpCache = new HTTPCache();
this.setupCacheCoordination();
}
setupCacheCoordination() {
// 内存缓存优先级最高
this.cacheHierarchy = [
this.memoryCache,
this.diskCache,
this.httpCache
];
}
// 分层缓存查找
async findInCache(request) {
for (const cache of this.cacheHierarchy) {
const response = await cache.match(request);
if (response) {
// 更新上级缓存
await this.updateUpperCaches(request, response, cache);
return response;
}
}
return null;
}
// 更新上级缓存
async updateUpperCaches(request, response, sourceCache) {
const sourceCacheIndex = this.cacheHierarchy.indexOf(sourceCache);
for (let i = 0; i < sourceCacheIndex; i++) {
const upperCache = this.cacheHierarchy[i];
await upperCache.put(request, response.clone());
}
}
// 智能缓存存储
async storeInOptimalCache(request, response) {
const resourceInfo = this.analyzeResource(request, response);
const optimalCache = this.selectOptimalCache(resourceInfo);
await optimalCache.put(request, response);
// 根据访问频率决定是否存储到内存缓存
if (resourceInfo.accessFrequency > 0.8) {
await this.memoryCache.put(request, response.clone());
}
}
analyzeResource(request, response) {
const url = new URL(request.url);
const contentType = response.headers.get('content-type') || '';
const contentLength = parseInt(response.headers.get('content-length') || '0');
return {
url: url.href,
path: url.pathname,
contentType,
size: contentLength,
isStatic: this.isStaticResource(url.pathname),
accessFrequency: this.getAccessFrequency(url.href),
cacheability: this.assessCacheability(response)
};
}
isStaticResource(pathname) {
return /\.(css|js|png|jpg|jpeg|gif|webp|svg|woff|woff2|ttf|eot)$/.test(pathname);
}
getAccessFrequency(url) {
// 从访问统计中获取频率
const stats = this.getAccessStats(url);
return stats ? stats.frequency : 0;
}
getAccessStats(url) {
// 模拟访问统计数据
return { frequency: Math.random() };
}
assessCacheability(response) {
const cacheControl = response.headers.get('cache-control') || '';
if (cacheControl.includes('no-store')) return 0;
if (cacheControl.includes('no-cache')) return 0.2;
if (cacheControl.includes('private')) return 0.5;
if (cacheControl.includes('public')) return 1;
return 0.7; // 默认可缓存性
}
selectOptimalCache(resourceInfo) {
// 小的静态资源优先存储到内存缓存
if (resourceInfo.isStatic && resourceInfo.size < 100 * 1024) { // 100KB
return this.memoryCache;
}
// 大文件存储到磁盘缓存
if (resourceInfo.size > 1024 * 1024) { // 1MB
return this.diskCache;
}
// 默认使用HTTP缓存
return this.httpCache;
}
// 缓存预热
async warmupCache(urls) {
const preloadPromises = urls.map(async (url) => {
try {
const request = new Request(url);
const response = await fetch(request);
if (response.ok) {
await this.storeInOptimalCache(request, response);
console.log(`缓存预热成功: ${url}`);
}
} catch (error) {
console.error(`缓存预热失败: ${url}`, error);
}
});
await Promise.allSettled(preloadPromises);
}
// 缓存失效处理
async invalidateCache(pattern) {
const invalidationPromises = this.cacheHierarchy.map(async (cache) => {
await cache.invalidate(pattern);
});
await Promise.all(invalidationPromises);
}
// 生成缓存报告
async generateCacheReport() {
const reports = await Promise.all(
this.cacheHierarchy.map(cache => cache.getReport())
);
return {
summary: {
totalCaches: this.cacheHierarchy.length,
totalSize: reports.reduce((sum, report) => sum + report.size, 0),
totalEntries: reports.reduce((sum, report) => sum + report.entries, 0)
},
details: reports,
recommendations: this.generateRecommendations(reports)
};
}
generateRecommendations(reports) {
const recommendations = [];
// 检查内存缓存使用率
const memoryReport = reports[0];
if (memoryReport.usageRatio > 0.9) {
recommendations.push({
type: 'memory_cache_full',
message: '内存缓存使用率过高,建议清理或增加容量',
priority: 'high'
});
}
// 检查磁盘缓存大小
const diskReport = reports[1];
if (diskReport.size > 500 * 1024 * 1024) { // 500MB
recommendations.push({
type: 'disk_cache_large',
message: '磁盘缓存过大,建议定期清理',
priority: 'medium'
});
}
return recommendations;
}
}
// 使用示例
const cacheOptimizer = new BrowserCacheOptimizer();
// 应用启动时预热缓存
window.addEventListener('load', async () => {
const criticalResources = [
'/css/critical.css',
'/js/vendor.js',
'/js/app.js',
'/images/logo.png'
];
await cacheOptimizer.warmupCache(criticalResources);
// 生成缓存报告
const report = await cacheOptimizer.generateCacheReport();
console.log('缓存状态报告:', report);
});
资源缓存最佳实践
缓存策略选择指南
根据不同类型的资源选择合适的缓存策略是优化Web应用性能的关键:
graph TD
A["资源类型分析"] --> B{资源特征}
B --> C["静态不变资源<br/>CSS/JS/字体"]
B --> D["更新频繁资源<br/>HTML/API"]
B --> E["大文件资源<br/>图片/视频"]
B --> F["实时数据<br/>用户状态/通知"]
C --> C1["强缓存 + 版本控制<br/>Cache-Control: max-age=31536000<br/>文件名带hash值"]
D --> D1["协商缓存<br/>ETag + Last-Modified<br/>短期缓存 + 重新验证"]
E --> E2["缓存优先策略<br/>Cache First<br/>延迟加载 + 压缩"]
F --> F1["仅网络策略<br/>Network Only<br/>实时获取"]
style C1 fill:#c8e6c9
style D1 fill:#fff3e0
style E2 fill:#e1f5fe
style F1 fill:#fce4ec
实际应用场景
电商网站缓存策略
// 电商网站综合缓存策略
class ECommerceCacheStrategy {
constructor() {
this.setupCacheRoutes();
}
setupCacheRoutes() {
// 商品静态资源 - 长期缓存
this.addRoute('product-assets', {
pattern: /\/(css|js|fonts)\//,
strategy: 'cache-first',
maxAge: 31536000, // 1年
description: '静态资源长期缓存,配合版本控制'
});
// 商品图片 - 缓存优先
this.addRoute('product-images', {
pattern: /\/images\/products\//,
strategy: 'cache-first',
maxAge: 2592000, // 30天
description: '商品图片缓存优先,定期更新'
});
// 商品列表API - 短期缓存
this.addRoute('product-api', {
pattern: /\/api\/products/,
strategy: 'network-first',
maxAge: 300, // 5分钟
description: '商品API网络优先,短期缓存防止重复请求'
});
// 用户相关API - 仅网络
this.addRoute('user-api', {
pattern: /\/api\/user\//,
strategy: 'network-only',
description: '用户数据实时获取,不使用缓存'
});
// 购物车API - 仅网络
this.addRoute('cart-api', {
pattern: /\/api\/cart/,
strategy: 'network-only',
description: '购物车数据实时同步'
});
}
// 实现具体的缓存逻辑
addRoute(name, config) {
console.log(`配置缓存路由 [${name}]: ${config.description}`);
}
}
// 使用示例
const ecommerceCache = new ECommerceCacheStrategy();
新闻网站缓存策略
// 新闻网站缓存策略
class NewsCacheStrategy {
constructor() {
this.setupNewsCache();
}
setupNewsCache() {
// 新闻文章 - 协商缓存
this.configureRoute('articles', {
pattern: /\/articles\//,
headers: {
'Cache-Control': 'public, max-age=3600, must-revalidate',
'ETag': 'article-version',
'Last-Modified': 'article-update-time'
},
description: '新闻文章使用协商缓存,保证时效性'
});
// 新闻图片 - 长期缓存
this.configureRoute('news-images', {
pattern: /\/news-images\//,
headers: {
'Cache-Control': 'public, max-age=86400, immutable'
},
description: '新闻图片长期缓存,不会修改'
});
// 首页内容 - 短期缓存
this.configureRoute('homepage', {
pattern: /^\/$/,
headers: {
'Cache-Control': 'public, max-age=300, must-revalidate'
},
description: '首页短期缓存,频繁更新'
});
}
configureRoute(name, config) {
console.log(`配置新闻缓存 [${name}]: ${config.description}`);
}
}
缓存性能监控与优化
// 缓存性能监控器
class CachePerformanceMonitor {
constructor() {
this.metrics = {
hitRate: 0,
missRate: 0,
averageResponseTime: 0,
cacheSize: 0,
errorRate: 0
};
this.thresholds = {
minHitRate: 0.8, // 最低命中率80%
maxResponseTime: 200, // 最大响应时间200ms
maxCacheSize: 100, // 最大缓存100MB
maxErrorRate: 0.05 // 最大错误率5%
};
}
// 实时监控缓存性能
startMonitoring() {
setInterval(() => {
this.collectMetrics();
this.analyzePerformance();
this.generateRecommendations();
}, 60000); // 每分钟检查一次
}
collectMetrics() {
// 收集缓存指标
this.metrics = {
hitRate: this.calculateHitRate(),
missRate: this.calculateMissRate(),
averageResponseTime: this.calculateAverageResponseTime(),
cacheSize: this.calculateCacheSize(),
errorRate: this.calculateErrorRate()
};
}
analyzePerformance() {
const issues = [];
if (this.metrics.hitRate < this.thresholds.minHitRate) {
issues.push({
type: 'low_hit_rate',
message: `缓存命中率过低: ${(this.metrics.hitRate * 100).toFixed(1)}%`,
recommendation: '检查缓存策略配置,增加缓存时间或优化缓存键值'
});
}
if (this.metrics.averageResponseTime > this.thresholds.maxResponseTime) {
issues.push({
type: 'slow_response',
message: `平均响应时间过长: ${this.metrics.averageResponseTime}ms`,
recommendation: '优化缓存查找算法或增加内存缓存'
});
}
if (this.metrics.cacheSize > this.thresholds.maxCacheSize) {
issues.push({
type: 'large_cache',
message: `缓存大小超限: ${this.metrics.cacheSize}MB`,
recommendation: '清理过期缓存或调整缓存策略'
});
}
return issues;
}
generateRecommendations() {
const recommendations = [];
// 基于性能指标生成建议
if (this.metrics.hitRate > 0.9) {
recommendations.push('缓存策略效果良好,可以考虑增加缓存时间');
} else if (this.metrics.hitRate < 0.6) {
recommendations.push('考虑预加载关键资源或调整缓存策略');
}
if (this.metrics.averageResponseTime < 100) {
recommendations.push('响应时间优秀,当前缓存配置适合');
}
return recommendations;
}
// 模拟指标计算方法
calculateHitRate() { return Math.random() * 0.4 + 0.6; }
calculateMissRate() { return 1 - this.calculateHitRate(); }
calculateAverageResponseTime() { return Math.random() * 200 + 50; }
calculateCacheSize() { return Math.random() * 150 + 20; }
calculateErrorRate() { return Math.random() * 0.1; }
}
// 启动监控
const monitor = new CachePerformanceMonitor();
monitor.startMonitoring();
总结与建议
关键原则
-
分层缓存策略
- 内存缓存:高频访问的小文件
- 磁盘缓存:大文件和中频访问资源
- 网络缓存:HTTP缓存头控制
-
资源分类管理
- 静态资源:长期缓存 + 版本控制
- 动态内容:协商缓存 + 短期失效
- 实时数据:仅网络策略
-
性能监控
- 缓存命中率监控
- 响应时间分析
- 缓存大小管理
- 错误率跟踪
-
持续优化
- 定期清理过期缓存
- 根据访问模式调整策略
- 监控用户体验指标
- A/B测试不同缓存配置
通过合理的缓存策略设计和持续的性能监控,可以显著提升Web应用的加载速度和用户体验,同时减少服务器负载和带宽消耗。