前言
在Web应用中,性能优化不仅仅是关于代码执行速度,还与资源获取和数据持久化密切相关。合理的缓存策略可以显著减少网络请求,提升应用响应速度,同时有效降低服务器负载和用户流量消耗。离线优化则进一步解决了网络不稳定或断网场景下的用户体验问题,为Web应用提供类似原生应用的可靠性。
本文作为JavaScript性能优化实战系列的第八篇,将深入探讨前端缓存策略与离线优化技术。我们将从浏览器原生缓存机制出发,逐步深入到Service Worker、IndexedDB等现代Web API,并探讨PWA离线体验优化的实战应用,最终构建离线优先的Web应用架构。
浏览器缓存机制全面解析
浏览器缓存是前端性能优化的第一道防线,合理利用浏览器缓存机制可以大幅减少网络请求,加快页面加载速度。
HTTP缓存基础
HTTP缓存是最基础的缓存形式,通过HTTP头部控制资源的缓存行为。
强缓存
强缓存是指在缓存期间不需要请求服务器,直接使用缓存的资源。主要通过以下HTTP头部控制:
-
Cache-Control:HTTP/1.1的缓存控制头,优先级高于Expires
max-age
:缓存有效期(秒)public
:可以被任何缓存区缓存private
:只能被浏览器缓存no-cache
:每次使用缓存前必须先验证资源是否有效no-store
:完全不使用缓存
-
Expires:HTTP/1.0的缓存控制头,指定资源过期的具体时间
// 强缓存示例响应头
Cache-Control: max-age=3600, public
Expires: Wed, 21 Oct 2023 07:28:00 GMT
当浏览器发起请求时,会先检查是否命中强缓存:
- 如果命中,直接从缓存读取资源,此时Chrome开发者工具的Network面板会显示
(from disk cache)
或(from memory cache)
- 如果未命中,则进入协商缓存环节
协商缓存
协商缓存是指浏览器需要向服务器发送请求以确认缓存是否有效。主要通过以下HTTP头部控制:
-
ETag/If-None-Match:资源的唯一标识符
- 服务器返回资源时设置
ETag
- 浏览器请求时在
If-None-Match
中带上上次响应的ETag值
- 服务器返回资源时设置
-
Last-Modified/If-Modified-Since:资源的最后修改时间
- 服务器返回资源时设置
Last-Modified
- 浏览器请求时在
If-Modified-Since
中带上上次响应的Last-Modified值
- 服务器返回资源时设置
// 协商缓存示例
// 服务器响应头
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
// 浏览器后续请求头
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT
协商缓存的工作流程:
- 浏览器发送带有缓存校验字段的请求
- 服务器判断缓存是否有效
- 有效:返回304 Not Modified响应,无响应体
- 无效:返回200 OK响应,带有新的资源
完整的缓存判断流程
下面是一个完整的浏览器缓存判断流程:
发起请求 ---> 是否有缓存? ---> 否 ---> 向服务器请求 --> 返回200,缓存资源
|
是
|
强缓存是否有效? ---> 是 ---> 使用缓存,不发送请求 (200 from cache)
|
否
|
发送协商缓存请求 ---> 服务器判断缓存是否有效
|
是
|
返回304,使用缓存
|
否
|
返回200,更新缓存
常见资源类型的缓存策略
不同类型的资源适合不同的缓存策略:
HTML文档
HTML文档通常包含应用的结构和入口,建议使用协商缓存而非强缓存,确保用户能及时获取最新内容:
Cache-Control: no-cache
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
JavaScript和CSS文件
对于带有版本号或哈希的静态资源,可以使用长期的强缓存:
Cache-Control: max-age=31536000, public
在构建系统中可以这样实现:
// webpack配置示例
module.exports = {
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js'
}
};
图片和媒体文件
图片和媒体文件通常变化不频繁,适合使用长期缓存:
Cache-Control: max-age=86400, public
API响应数据
API响应通常是动态数据,不应该被强缓存,但可以根据业务需求使用短期缓存:
Cache-Control: max-age=60, private
对于不应该缓存的敏感数据:
Cache-Control: no-store
缓存控制最佳实践
版本化静态资源
对静态资源使用内容哈希作为文件名的一部分,确保内容变化时URL也随之变化:
// 在构建工具中的配置
{
output: {
// 使用contenthash确保内容变化时文件名变化
filename: '[name].[contenthash:8].js',
// 其他资源也使用类似策略
assetModuleFilename: 'assets/[name].[contenthash:8][ext]'
}
}
合理设置Cache-Control
根据资源类型和更新频率设置适当的Cache-Control:
// Node.js服务器示例
app.use('/static', express.static('public', {
etag: true,
lastModified: true,
setHeaders: (res, path) => {
if (path.endsWith('.html')) {
// HTML文件使用协商缓存
res.setHeader('Cache-Control', 'no-cache');
} else if (path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$/)) {
// 带版本号的静态资源使用长期缓存
res.setHeader('Cache-Control', 'max-age=31536000, public');
}
}
}));
使用Vary头处理不同的客户端请求
当响应内容根据请求头变化时,应使用Vary头指定这些头部:
Vary: User-Agent, Accept-Encoding
这确保缓存能正确区分不同客户端或不同压缩格式的响应。
避免缓存私人信息
对于包含用户私人信息的响应,应明确防止缓存:
Cache-Control: private, no-store, max-age=0
实战案例:构建高效的缓存系统
前端缓存层设计
以下是一个典型的前端应用缓存层设计:
[用户请求] --> [Service Worker缓存] --> [HTTP缓存] --> [服务器]
|
[浏览器存储]
(IndexedDB/localStorage)
实现多级缓存策略
下面是一个结合HTTP缓存和前端存储的多级缓存实现:
// 封装一个带缓存的API请求函数
async function fetchWithCache(url, options = {
}) {
const cacheKey = `api:${
url}`;
const cacheTime = options.cacheTime || 60000; // 默认缓存1分钟
// 尝试从localStorage获取缓存
try {
const cached = localStorage.getItem(cacheKey);
if (cached) {
const {
timestamp, data } = JSON.parse(cached);
// 检查缓存是否有效
if (Date.now() - timestamp < cacheTime) {
console.log('从本地缓存获取数据:', url);
return data;
}
}
} catch (e) {
console.error('读取缓存出错:', e);
}
// 发起网络请求,使用HTTP缓存
try {
const response = await fetch(url, {
...options,
headers: {
...options.headers,
// 对于GET请求可以使用协商缓存
...(options.method === 'GET' ? {
} : {
'Cache-Control': 'no-cache' })
}
});
if (!response.ok) {
throw new Error(`请求失败: ${
response.status}`);
}
const data = await response.json();
// 将数据存入localStorage缓存
try {
localStorage.setItem(cacheKey, JSON.stringify({
timestamp: Date.now(),
data
}));
} catch (e) {
console.error('写入缓存出错:', e);
}
return data;
} catch (error) {
console.error('网络请求失败:', error);
throw error;
}
}
// 使用示例
async function getUserProfile(userId) {
return fetchWithCache(`/api/users/${
userId}`, {
cacheTime: 300000 }); // 缓存5分钟
}
async function getProductList() {
return fetchWithCache('/api/products', {
cacheTime: 60000 }); // 缓存1分钟
}
浏览器缓存查看与调试技巧
使用Chrome DevTools检查缓存
-
查看HTTP缓存:
- 打开Chrome DevTools > Network面板
- 勾选"Disable cache"可临时禁用缓存
- 查看Size列,显示"(from disk cache)“或”(from memory cache)"表示命中缓存
- 查看Status列,304表示命中协商缓存
-
检查应用缓存存储:
- 打开Chrome DevTools > Application面板
- 在Storage部分可以查看和管理各种缓存和存储
- Cache Storage查看Service Worker缓存
- IndexedDB和Local Storage查看对应存储
缓存调试常用命令
// 清除localStorage缓存
localStorage.clear();
// 编程方式检查缓存头
fetch('/example.js')
.then(response => {
console.log('Cache-Control:', response.headers.get('Cache-Control'));
console.log('ETag:', response.headers.get('ETag'));
console.log('Last-Modified:', response.headers.get('Last-Modified'));
});
// 使用performance API分析缓存性能
performance.getEntriesByType('resource').forEach(resource => {
console.log(`${
resource.name}: ${
resource.duration}ms`);
});
通过深入理解和合理应用浏览器缓存机制,我们可以显著提升Web应用的性能和用户体验。在下一节中,我们将探讨更强大的Service Worker缓存策略,它不仅能实现更细粒度的缓存控制,还能为Web应用提供真正的离线能力。
Service Worker缓存策略与实现
Service Worker是一种运行在浏览器背后的脚本,它独立于网页,能够拦截和处理网络请求,实现离线缓存、推送通知等功能。相比传统的HTTP缓存,Service Worker提供了更精细的缓存控制能力和离线支持。
Service Worker基础
Service Worker生命周期
Service Worker有着明确的生命周期,理解这一点对于实现可靠的缓存策略至关重要:
- 注册(Register):告知浏览器Service Worker的位置
- 安装(Install):首次安装或更新时触发,通常用于缓存静态资源
- 激活(Activate):安装成功后激活,通常用于清理旧缓存
- 空闲(Idle):未处理任何事件时的状态
- 终止(Terminated):空闲一段时间后被终止,节省资源
- 获取(Fetch):拦截网络请求,可以返回缓存或发起新请求
注册Service Worker
首先需要在主页面注册Service Worker:
// 检查浏览器是否支持Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker注册成功,作用域为:', registration.scope);
})
.catch(error => {
console.error('Service Worker注册失败:', error);
});
});
}
创建基本的Service Worker
以下是一个基本的Service Worker示例(sw.js
):
// 定义缓存名称和缓存资源列表
const CACHE_NAME = 'app-v1';
const CACHE_ASSETS = [
'/',
'/index.html',
'/css/style.css',
'/js/main.js',
'/images/logo.png',
'/offline.html'
];
// 安装事件:预缓存静态资源
self.addEventListener('install', event => {
console.log('Service Worker: 安装中');
// 延长安装阶段直到缓存完成
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Service Worker: 缓存文件');
return cache.addAll(CACHE_ASSETS);
})
.then(() => self.skipWaiting()) // 强制激活
);
});
// 激活事件:清理旧缓存
self.addEventListener('activate', event => {
console.log('Service Worker: 已激活');
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cache => {
if (cache !== CACHE_NAME) {
console.log('Service Worker: 清理旧缓存', cache);
return caches.delete(cache);
}
})
);
})
);
});
// 拦截fetch请求
self.addEventListener('fetch', event => {
console.log('Service Worker: 拦截请求', event.request.url);
event.respondWith(
// 检查缓存
caches.match(event.request)
.then(response => {
// 如果在缓存中找到了响应,返回缓存的版本
if (response) {
return response;
}
// 没有缓存,发起网络请求
return fetch(event.request)
.then(response => {
// 检查是否得到有效响应
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 复制响应(因为响应是流,只能消费一次)
const responseToCache = response.clone();
// 将响应添加到缓存
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
// 网络请求失败时,返回离线页面
if (event.request.headers.get('accept').includes('text/html')) {
return caches.match('/offline.html');
}
});
})
);
});
高级缓存策略
Service Worker提供了灵活的缓存策略选择,根据不同资源类型和应用需求,可以实现多种缓存模式:
1. 缓存优先(Cache First)
优先从缓存获取资源,缓存未命中才发起网络请求。适合静态资源和不经常更改的内容。
function cacheFirst(request) {
return caches.match(request)
.then(cacheResponse => {
return cacheResponse || fetch(request).then(networkResponse => {
// 将网络响应存入缓存
return caches.open(CACHE_NAME).then(cache => {
cache.put(request, networkResponse.clone());
return networkResponse;
});
});
});
}
2. 网络优先(Network First)
优先从网络获取最新资源,网络请求失败才使用缓存。适合经常更新的内容,如API响应。
function networkFirst(request) {
return fetch(request)
.then(networkResponse => {
// 将网络响应复制一份存入缓存
caches.open(CACHE_NAME).then(cache => {
cache.put(request, networkResponse.clone());
});
return networkResponse;
})
.catch(() => {
// 网络请求失败,使用缓存
return caches.match(request);
});
}
3. 网络优先并更新缓存(Stale While Revalidate)
同时从缓存和网络获取资源,先返回缓存内容(如果有)以快速响应,同时更新缓存以备下次使用。适合内容更新不那么关键的场景。
function staleWhileRevalidate(request) {
return caches.open(CACHE_NAME).then(cache => {
return cache.match(request).then(cacheResponse => {
const fetchPromise = fetch(request).then(networkResponse => {
cache.put(request, networkResponse.clone());
return networkResponse;
});
// 返回缓存响应,或等待网络响应
return cacheResponse || fetchPromise;
});
});
}
4. 缓存与网络竞争(Cache and Network Race)
同时从缓存和网络获取资源,返回最先得到的响应。适合优化在不同网络环境下的体验。
function raceStrategy(request) {
// 创建Promise.race,返回最快的响应
return Promise.race([
caches.match(request).then(response => response),
fetch(request).then(response => {
// 更新缓存
const responseClone = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(request, responseClone);
});
return response;
})
]);
}
5. 缓存回退网络(Cache Falling Back to Network)
先尝试使用缓存,缓存未命中时再发起网络请求。网络请求失败时可以提供通用的离线页面。
function cacheFallbackToNetwork(request) {
return caches.match(request)
.then(cacheResponse => {
if (cacheResponse) {
return cacheResponse;
}
return fetch(request)
.catch(() => {
// 请求HTML内容时返回通用离线页面
if (request.headers.get('Accept').includes('text/html')) {
return caches.match('/offline.html');
}
// 对于图片可以返回占位图
if (request.url.match(/\.(jpg|png|gif|svg)$/)) {
return caches.match('/images/offline-image.png');
}
});
});
}
根据资源类型选择缓存策略
在实际应用中,通常需要根据不同的资源类型选择不同的缓存策略:
self.addEventListener('fetch', event => {
const request = event.request;
const url = new URL(request.url);
// 对同源请求应用缓存策略
if (url.origin === self.location.origin) {
// 静态资源使用缓存优先策略
if (url.pathname.startsWith('/static/') ||
url.pathname.match(/\.(css|js|png|jpg|jpeg|svg|gif)$/)) {
event.respondWith(cacheFirst(request));
return;
}
// HTML页面使用网络优先策略
if (request.headers.get('Accept').includes('text/html')) {
event.respondWith(networkFirst(request));
return;
}
}
// API请求使用staleWhileRevalidate策略
if (url.pathname.startsWith('/api/')) {
event.respondWith(staleWhileRevalidate(request));
return;
}
// 默认使用网络优先策略
event.respondWith(networkFirst(request));
});
定期更新缓存内容
有些情况下,我们需要定期更新缓存内容,而不仅依赖于用户的请求:
// 定义需要定期更新的资源
const PERIODIC_UPDATES = [
'/api/news',
'/api/notifications'
];
// 设置更新间隔(例如每小时更新一次)
const UPDATE_INTERVAL = 60 * 60 * 1000; // 1小时
// 在SW激活后设置定期更新
self.addEventListener('activate', event => {
// 其他激活代码...
// 设置定期更新任务
self.registration.periodicSync.register('update-cache', {
minInterval: UPDATE_INTERVAL
});
});
// 处理定期同步事件
self.addEventListener('periodicsync', event => {
if (event.tag === 'update-cache') {
event.waitUntil(updateCache());
}
});
// 更新缓存的函数
function updateCache() {
return Promise.all(
PERIODIC_UPDATES.map(url =>
fetch(url)
.then(response => {
if (!response.ok) return;
const clonedResponse = response.clone();
return caches.open(CACHE_NAME).then(cache => {
return cache.put(url, clonedResponse);
});
})
.catch(error => console.error('缓存更新失败:', url, error))
)
);
}
缓存失效与更新策略
管理缓存的更新和失效是Service Worker实现中的关键挑战。以下是几种常用的缓存更新策略:
基于版本号的缓存更新
使用缓存版本号,在Service Worker更新时清除旧缓存:
const CACHE_VERSION = 'v2';
const CACHE_NAME = `app-cache-${
CACHE_VERSION}`;
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
// 删除不匹配当前版本的缓存
if (cacheName.startsWith('app-cache-') && cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
基于内容哈希的缓存更新
对于静态资源,使用内容哈希作为文件名的一部分,确保内容变化时URL也会变化:
// 在构建过程中生成带哈希的文件名
// main.8e2d4a2.js 而不是 main.js
// 在Service Worker中预缓存这些资源
const CACHE_ASSETS = [
'/',
'/index.html',
'/css/style.5f3e9b1.css',
'/js/main.8e2d4a2.js',
'/images/logo.3a7c4d8.png'
];
使用ETag进行缓存验证
对于API响应,可以使用ETag进行缓存验证,只在内容变化时更新缓存:
function updateWithEtag(request) {
// 先检查缓存
return caches.match(request).then(cachedResponse => {
// 提取之前响应的ETag
const etag = cachedResponse ? cachedResponse.headers.get('ETag') : null;
// 创建一个新的请求,包含If-None-Match头
const newRequest = etag ?
new Request(request, {
headers: {
'If-None-Match': etag
}
}) : request;
// 发送带条件的请求
return fetch(newRequest).then(networkResponse => {
// 304表示内容未变化,使用缓存
if (networkResponse.status === 304) {
return cachedResponse;
}
// 内容已变化,更新缓存并返回新响应
const clonedResponse = networkResponse.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(request, clonedResponse);
});
return networkResponse;
}).catch(() => {
// 网络请求失败,返回缓存(如果有)
return cachedResponse;
});
});
}
处理Service Worker更新
Service Worker更新是一个需要谨慎处理的过程,以下是一种推荐的更新流程:
// 在主页面中监听Service Worker更新
if ('serviceWorker' in navigator) {
// 注册Service Worker
navigator.serviceWorker.register('/sw.js')
.then(registration => {
// 检查更新
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
// 当新的Service Worker变为activated状态
if (newWorker.state === 'activated') {
// 通知用户页面有更新
if (window.confirm('网站已更新,是否刷新页面以应用新版本?')) {
window.location.reload();
}
}
});
});
// 检查控制页面的Service Worker是否发生变化
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (!refreshing) {
refreshing = true;
window.location.reload();
}
});
});
// 定期检查更新
setInterval(() => {
navigator.serviceWorker.getRegistration().then(registration => {
if (registration) {
registration.update();
}
});
}, 60 * 60 * 1000); // 每小时检查一次
}
实战案例:构建离线可用的博客应用
以下是一个实际案例,展示如何为博客应用实现离线访问功能:
// sw.js - 博客应用的Service Worker
// 缓存名称定义
const STATIC_CACHE = 'blog-static-v1';
const PAGES_CACHE = 'blog-pages-v1';
const IMAGES_CACHE = 'blog-images-v1';
const API_CACHE = 'blog-api-v1';
// 需要缓存的静态资源
const STATIC_ASSETS = [
'/',
'/index.html',
'/css/main.css',
'/js/app.js',
'/js/vendor.js',
'/offline.html',
'/images/logo.svg',
'/images/offline.svg'
];
// 安装事件 - 预缓存静态资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then(cache => cache.addAll(STATIC_ASSETS))
.then(() => self.skipWaiting())
);
});
// 激活事件 - 清理旧缓存
self.addEventListener('activate', event => {
const currentCaches = [STATIC_CACHE, PAGES_CACHE, IMAGES_CACHE, API_CACHE];
event.waitUntil(
caches.keys()
.then(cacheNames => {
return cacheNames.filter(
cacheName => !currentCaches.includes(cacheName)
);
})
.then(cachesToDelete => {
return Promise.all(
cachesToDelete.map(cacheToDelete => caches.delete(cacheToDelete))
);
})
.then(() => self.clients.claim())
);
});
// 拦截请求
self.addEventListener('fetch', event => {
const request = event.request;
const url = new URL(request.url);
// 不处理非GET请求
if (request.method !== 'GET') {
return;
}
// 处理API请求
if (url.pathname.startsWith('/api/')) {
event.respondWith(handleApiRequest(request));
return;
}
// 处理HTML页面请求
if (request.headers.get('Accept').includes('text/html')) {
event.respondWith(handleHtmlRequest(request));
return;
}
// 处理图片请求
if (url.pathname.match(/\.(jpg|jpeg|png|gif|svg)$/)) {
event.respondWith(handleImageRequest(request));
return;
}
// 处理其他静态资源
event.respondWith(
caches.match(request)
.then(cachedResponse => cachedResponse || fetchAndCache(request, STATIC_CACHE))
);
});
// 处理API请求
function handleApiRequest(request) {
return fetchWithTimeout(request, 3000)
.then(response => {
// 复制响应以便缓存
const clonedResponse = response.clone();
// 只缓存成功的响应
if (response.ok) {
caches.open(API_CACHE)
.then(cache => cache.put(request, clonedResponse));
}
return response;
})
.catch(() => {
// 网络请求失败,尝试从缓存获取
return caches.match(request)
.then(cachedResponse => {
if (cachedResponse) {
// 给缓存响应添加标记,表明来自缓存
const responseOptions = {
headers: new Headers(cachedResponse.headers),
status: cachedResponse.status,
statusText: cachedResponse.statusText
};
responseOptions.headers.set('X-Data-Source', 'cache');
return cachedResponse.blob()
.then(body => new Response(body, responseOptions));
}
// 如果没有缓存,返回离线API响应
return new Response(
JSON.stringify({
error: 'offline',
message: '您当前处于离线状态,无法获取最新数据'
}),
{
status: 503,
headers: {
'Content-Type': 'application/json' }
}
);
});
});
}
// 处理HTML请求
function handleHtmlRequest(request) {
return fetchWithTimeout(request, 3000)
.then(response => {
// 复制响应并缓存
const clonedResponse = response.clone();
caches.open(PAGES_CACHE)
.then(cache => cache.put(request, clonedResponse));
return response;
})
.catch(() => {
// 网络请求失败,尝试从缓存获取
return caches.match(request)
.then(cachedResponse => cachedResponse || caches.match('/offline.html'));
});
}
// 处理图片请求
function handleImageRequest(request) {
// 使用缓存优先策略
return caches.match(request)
.then(cachedResponse => {
if (cachedResponse) {
// 同时在后台更新缓存
fetch(request)
.then(networkResponse => {
if (networkResponse.ok) {
caches.open(IMAGES_CACHE)
.then(cache => cache.put(request, networkResponse));
}
})
.catch(() => {
/* 静默失败 */});
return cachedResponse;
}
// 缓存中没有,发起网络请求
return fetch(request)
.then(networkResponse => {
// 缓存响应副本
const clonedResponse = networkResponse.clone();
caches.open(IMAGES_CACHE)
.then(cache => cache.put(request, clonedResponse));
return networkResponse;
})
.catch(() => {
// 返回占位图片
return caches.match('/images/offline.svg');
});
});
}
// 通用的获取并缓存函数
function fetchAndCache(request, cacheName) {
return fetch(request)
.then(response => {
if (response.ok) {
const clonedResponse = response.clone();
caches.open(cacheName)
.then(cache => cache.put(request, clonedResponse));
}
return response;
})
.catch(error => {
console.error('获取资源失败:', error);
throw error;
});
}
// 添加超时的fetch函数
function fetchWithTimeout(request, timeout) {
return new Promise((resolve, reject) => {
// 设置超时
const timeoutId = setTimeout(() => {
reject(new Error('请求超时'));
}, timeout);
fetch(request).then(
response => {
clearTimeout(timeoutId);
resolve(response);
},
err => {
clearTimeout(timeoutId);
reject(err);
}
);
});
}
通过上述Service Worker实现,博客应用能够实现:
- 离线浏览已访问过的页面
- 即使在线,也能快速加载缓存资源
- 针对不同资源类型应用不同的缓存策略
- 提供优雅的离线体验和失败处理
Service Worker强大的缓存控制能力和离线支持,使它成为现代Web应用性能优化和用户体验提升的关键技术。在下一节中,我们将探讨另一种客户端存储技术——IndexedDB,它为Web应用提供了更强大的本地数据库能力。
IndexedDB高性能客户端存储应用
IndexedDB基础概念
IndexedDB是一种低级API,用于在客户端存储大量结构化数据。与localStorage和sessionStorage不同,IndexedDB提供了完整的事务性数据库系统,支持索引、游标和事务,能够高效处理大量数据。
IndexedDB的主要特点
- 大容量存储:可以存储远超localStorage的数据量(通常为数百MB甚至GB)
- 结构化数据:支持JavaScript对象直接存储,无需序列化
- 事务支持:提供类似传统数据库的事务隔离
- 异步API:所有操作都是异步的,不会阻塞主线程
- 同源策略:遵循浏览器同源策略,保证安全性
- 索引查询:可以为数据创建索引,支持高效查询
IndexedDB的基本工作流程
- 打开数据库连接
- 在连接回调中创建对象仓库(object store)
- 启动一个事务并请求执行操作
- 通过事件监听操作结果
- 使用查询方法或游标获取数据
创建和管理IndexedDB数据库
打开数据库
function openDatabase() {
return new Promise((resolve, reject) => {
// 打开数据库(如果不存在则创建)
const request = indexedDB.open('MyAppDB', 1);
// 处理数据库升级事件
request.onupgradeneeded = event => {
const db = event.target.result;
// 创建对象仓库
if (!db.objectStoreNames.contains('users')) {
const usersStore = db.createObjectStore('users', {
keyPath: 'id' });
// 创建索引
usersStore.createIndex('email', 'email', {
unique: true });
usersStore.createIndex('name', 'name', {
unique: false });
}
if (!db.objectStoreNames.contains('articles')) {
const articlesStore = db.createObjectStore('articles', {
keyPath: 'id' });
articlesStore.createIndex('author', 'authorId', {
unique: false });
articlesStore.createIndex('date', 'publishDate', {
unique: false });
}
};
// 成功回调
request.onsuccess = event => {
const db = event.target.result;
resolve(db);
};
// 错误回调
request.onerror = event => {
console.error('打开数据库失败:', event.target.error);
reject(event.target.error);
};
});
}
添加数据
function addItem(db, storeName, item) {
return new Promise((resolve, reject) => {
// 创建读写事务
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
// 添加或更新记录
const request = store.put(item);
// 处理结果
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
// 事务完成处理
transaction.oncomplete = () => console.log('事务完成');
transaction.onerror = event => console.error('事务错误:', event.target.error);
});
}
获取数据
function getItem(db, storeName, key) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
查询数据
function queryByIndex(db, storeName, indexName, value) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const index = store.index(indexName);
const request = index.getAll(value);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
使用游标遍历数据
function iterateWithCursor(db, storeName, callback) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const results = [