目录
实战分享:利用 Service Worker 实现首页秒开
大家好!在前端开发的过程中,我们都在不断追求网页性能的优化,期望给用户带来更流畅、快速的浏览体验。今天,我想和大家分享一个我在实践中探索的成果 —— 使用 Service Worker 让首页秒开的实战案例。
在这个项目中,我将详细展示从搭建基础环境到逐步实现 Service Worker 缓存策略的全过程,包括如何处理页面和接口请求的缓存,如何应对版本更新带来的变化,以及在这个过程中遇到的各种棘手问题,像资源路径错误导致的缓存失败、接口请求出现 404 找不到资源的情况、缓存状态无法正确获取和显示等,我都将一一为大家呈现是如何解决的。
这不仅仅是一个简单的代码示例,更是一个充满实战经验和解决方案的分享。无论你是初涉前端领域的新手,还是已经有一定经验的开发者,相信都能从这个案例中获得启发和帮助,让我们一起深入探究如何借助 Service Worker 提升网页的性能表现吧!
demo目录
// demo 目录
.
├── app
│ └── index.js
├── package.json
└── public
├── index.html
└── sw.js
实现
package.json里我没有家很多东西,只是加了一个启用服务端的命令和三个 服务端需要用到的
{
"name": "service_worker",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node app/index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"path": "^0.12.7"
}
}
因为这是一个demo, 所以index.js 文件是用express临时写的一个服务端,
const express = require('express');
const path = require('path');
const app = express();
const cors = require('cors');
const port = 3000;
// 模拟接口数据
const apiData = {
message: "这是接口返回的数据",
version: 1
};
app.use(cors());
// 处理接口请求,返回模拟的JSON数据,并设置页面版本头信息
app.get('/api/data', (req, res) => {
// 这里模拟接口数据版本变化,可根据实际情况调整逻辑
apiData.version = Math.ceil(Date.now() / 5000);
res.set('x-page-version', apiData.version);
res.json(apiData);
});
app.use(express.static(path.join(__dirname, '../public'), {
setHeaders: (res) => {
// 这里模拟页面版本变化,每隔5秒version就发生变化
res.set('x-page-version', Math.ceil(Date.now() / 5000));
}
}));
// 启动服务器
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service Worker Demo</title>
</head>
<body>
<h1>Service Worker Demo</h1>
<p id="cache-status">缓存状态:未获取</p>
<p id="page-version">页面版本:未获取</p>
<button id="fetchDataButton">获取接口数据</button>
<div id="data-display"></div>
<script>
// 注册Service Worker
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("./sw.js").then(registration => {
console.log(`Service Worker registered with scope: ${registration.scope}`);
}).catch(error => {
console.log(`Service Worker registration failed: ${error}`);
});
}
// 处理Service Worker消息
navigator.serviceWorker.addEventListener("message", event => {
console.log('Received a message from Service Worker:', event.data);
if (event.data.action === "update") {
if (event.data.url === window.location.href) {
console.log('load lasted version');
location.reload(true);
}
}
// 接收并处理初始状态消息
if (event.data.action === 'setInitialStatus') {
updateCacheStatus(event.data.cacheStatus, event.data.pageVersion);
}
});
const fetchDataButton = document.getElementById('fetchDataButton');
const dataDisplay = document.getElementById('data-display');
fetchDataButton.addEventListener('click', () => {
// 发起接口请求
fetchData();
});
async function fetchData() {
try {
const response = await fetch('http://127.0.0.1:3000/api/data');
if (response.ok) {
const data = await response.json();
dataDisplay.textContent = JSON.stringify(data);
} else {
console.error('接口请求失败,状态码:', response.status);
}
} catch (error) {
console.error('接口请求出现错误:', error);
}
}
// 更新页面上显示的缓存状态和版本信息
function updateCacheStatus(status, version) {
document.getElementById('cache-status').textContent = `缓存状态:${status}`;
document.getElementById('page-version').textContent = `页面版本:${version}`;
}
</script>
</body>
</html>
接下来是最主要的也是最核心的代码sw.js
1. 缓存配置
const CACHE_NAME = 'HOMEPAGE_CACHE_v1';
const urlsToCache = ['/'];
CACHE_NAME
:定义了缓存的名称,每次更新Service Worker时可以改变这个名称来触发缓存更新。urlsToCache
:定义了需要缓存的资源列表,这里只缓存了主页。
2. 安装事件
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(urlsToCache);
}).catch((error) => {
console.error('[Service Worker] Failed to cache resources during install:', error);
})
);
});
- 当Service Worker安装时,会触发
install
事件。这里使用event.waitUntil
确保缓存操作完成后再进行下一步。 caches.open(CACHE_NAME)
打开缓存,cache.addAll(urlsToCache)
将预定义的资源添加到缓存中。
3. 激活事件
self.addEventListener('activate', (event) => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (!cacheWhitelist.includes(cacheName)) {
return caches.delete(cacheName);
}
})
);
}).then(() => self.clients.claim())
);
});
- 当Service Worker激活时,会触发
activate
事件。这里使用event.waitUntil
确保清理旧缓存操作完成后再进行下一步。 caches.keys()
获取所有缓存名称,cacheNames.map
遍历所有缓存,如果不在白名单中则删除。
4. 拦截请求事件
self.addEventListener('fetch', (event) => {
const requestUrl = new URL(event.request.url);
if (urlsToCache.includes(requestUrl.pathname)) {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
const cachedVersion = cachedResponse.headers.get('x-page-version');
event.waitUntil(
fetch(event.request).then((networkResponse) => {
if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {
const networkVersion = networkResponse.headers.get('x-page-version');
if (networkVersion !== cachedVersion) {
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse.clone());
if (requestUrl.pathname === '/') {
return sendMessage({
version: networkVersion,
action: 'update',
url: requestUrl.href
});
}
});
}
}
}).catch((error) => {
console.error(`[Service Worker] Background fetch failed for: ${event.request.url}`, error);
})
);
return cachedResponse;
}
return fetch(event.request).then((networkResponse) => {
if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {
const networkVersion = networkResponse.headers.get('x-page-version');
if (requestUrl.pathname === '/') {
return sendMessage({
action: 'setInitialStatus',
cacheStatus: '从网络获取并缓存',
pageVersion: networkVersion
}).then(() => networkResponse);
}
return networkResponse;
}
}).catch((error) => {
console.error(`[Service Worker] Fetch failed for: ${event.request.url}`, error);
});
})
);
} else {
event.respondWith(fetch(event.request));
}
});
- 当有网络请求时,会触发
fetch
事件。 - 如果请求的资源在
urlsToCache
中,则尝试从缓存中获取资源。- 如果缓存中有资源,则返回缓存内容,并在后台发起网络请求以更新缓存。
- 如果缓存中没有资源,则从网络获取最新资源。
- 对于不在
urlsToCache
中的请求,直接转发请求。
5. 辅助函数:发送消息给客户端
function sendMessage(data) {
return self.clients.matchAll().then((clients) => {
clients.forEach((client) => {
client.postMessage(data);
});
});
}
sendMessage
函数用于向所有客户端发送消息,这里用于通知客户端缓存状态或页面版本更新。
完整代码:
const CACHE_NAME = 'HOMEPAGE_CACHE_v1'; // 缓存 key,sw.js 更新了可以升级版本
// 配置需要缓存的资源,demo 中只缓存主文档,静态资源浏览器自己就会缓存
const urlsToCache = [
'/',
];
// 安装事件:预缓存一些关键资源
self.addEventListener('install', (event) => {
console.log('[Service Worker] Install Event');
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[Service Worker] Caching pre-defined resources');
return cache.addAll(urlsToCache);
}).catch((error) => {
console.error('[Service Worker] Failed to cache resources during install:', error)
})
);
});
// 激活事件:清理旧版本的缓存
self.addEventListener('activate', (event) => {
console.log('[Service Worker] Activate Event');
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (!cacheWhitelist.includes(cacheName)) {
console.log(`[Service Worker] Deleting old cache: ${cacheName}`);
return caches.delete(cacheName);
}
})
);
}).then(() => self.clients.claim()) // 确保 SW 控制所有客户端
);
});
// 拦截页面请求,实现Stale-While-Revalidate策略
self.addEventListener('fetch', (event) => {
const requestUrl = new URL(event.request.url);
// 处理首页及接口请求的缓存逻辑
if (urlsToCache.includes(requestUrl.pathname)) {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
// 如果缓存存在,立即返回缓存内容,并在后台更新缓存(发起请求)
console.log(`[Service Worker] Serving from cache: ${event.request.url}`);
const cachedVersion = cachedResponse.headers.get('x-page-version');
event.waitUntil(
fetch(event.request).then((networkResponse) => {
if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {
const networkVersion = networkResponse.headers.get('x-page-version');
console.log(`[Service Worker] Cached Version: ${cachedVersion}`);
console.log(`[Service Worker] Network Version: ${networkVersion}`);
// 如果页面或接口数据版本已更新
if (networkVersion !== cachedVersion) {
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse.clone());
console.log(`[Service Worker] Fetched and cached (background): ${event.request.url}`);
// 通知客户端刷新,展示最新内容(如果是首页),或者更新缓存数据(如果是接口请求)
if (requestUrl.pathname === '/') {
return sendMessage({
version: networkVersion,
action: 'update',
url: requestUrl.href
});
}
});
}
}
}).catch((error) => {
console.error(`[Service Worker] Background fetch failed for: ${event.request.url}`, error);
})
);
return cachedResponse;
}
// 如果缓存不存在,从网络获取最新资源(发起请求)
return fetch(event.request).then((networkResponse) => {
if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {
const networkVersion = networkResponse.headers.get('x-page-version');
// 向客户端发送初始状态消息,并返回网络响应
if (requestUrl.pathname === '/') {
return sendMessage({
action: 'setInitialStatus',
cacheStatus: '从网络获取并缓存',
pageVersion: networkVersion
}).then(() => networkResponse);
}
return networkResponse;
}
}).catch((error) => {
console.error(`[Service Worker] Fetch failed for: ${event.request.url}`, error);
});
})
);
} else {
// 对于其他非缓存资源请求,直接转发请求,不做额外处理
event.respondWith(fetch(event.request));
}
});
// 辅助函数:发送消息给客户端
function sendMessage(data) {
return self.clients.matchAll().then((clients) => {
clients.forEach((client) => {
client.postMessage(data);
});
});
}