Service Worker 的 fetch
事件是一个非常强大的机制,它允许 Service Worker 拦截由其控制的页面发出的所有网络请求 。这意味着,无论是通过 fetch()
API、XMLHttpRequest
(XHR) 发起的请求,还是浏览器在加载页面时自动发出的对 HTML、CSS、JavaScript、图片、字体等文件资源的请求,甚至是用户导航到新页面的请求,Service Worker 都能捕获并进行处理。
Service Worker fetch
事件的拦截范围
fetch
事件监听器会接收到一个 FetchEvent
对象,其中包含了 request
属性,这个 request
对象代表了被拦截的网络请求。
Service Worker 可以拦截以下类型的请求:
-
导航请求 (Navigation Requests): 当用户在地址栏输入 URL、点击链接或通过表单提交导致页面导航时,会触发导航请求。这些请求通常返回 HTML 文档。
-
子资源请求 (Subresource Requests): 页面加载过程中,浏览器会自动请求各种子资源,例如:
- HTML 文档 (除了导航请求,例如通过
<iframe>
或动态加载的 HTML 片段)。 - CSS 文件 (
<link rel="stylesheet">
)。 - JavaScript 文件 (
<script src="...">
)。 - 图片 (
<img>
, CSSbackground-image
)。 - 字体文件 (
@font-face
)。 - 视频/音频文件 (
<video>
,<audio>
)。 - Web Workers / Shared Workers 脚本。
- Manifest 文件。
- 数据文件 (JSON, XML, TXT 等)。
- HTML 文档 (除了导航请求,例如通过
-
通过
fetch()
API 发起的请求: 现代 Web 应用中常用的异步数据请求方式。 -
通过
XMLHttpRequest
(XHR) 发起的请求: 传统的异步数据请求方式,Service Worker 同样可以拦截。 -
通过
<img>
标签的srcset
属性或<picture>
元素发起的响应式图片请求。 -
通过
<a>
标签的ping
属性发起的请求。
简而言之,所有由浏览器或页面脚本发出的、需要通过网络获取资源的请求,Service Worker 理论上都可以拦截。
详细代码讲解可以拦截的类型
我们将创建一个 service-worker.js
文件,其中包含 fetch
事件监听器,并打印出被拦截请求的类型和 URL,以展示其拦截能力。
项目结构:
perl
my-intercept-app/
├── index.html
├── style.css
├── app.js
└── service-worker.js
1. index.html
(主页面)
html
<!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>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Service Worker 拦截请求示例</h1>
<p>打开开发者工具的 Console 和 Network 面板,观察 Service Worker 的拦截情况。</p>
<h2>子资源请求 (Subresource Requests)</h2>
<img src="https://via.placeholder.com/100" alt="占位图片" id="dynamicImage">
<div class="background-image-div"></div>
<h2>API 请求 (Fetch & XHR)</h2>
<button id="fetchDataBtn">使用 Fetch API 获取数据</button>
<button id="xhrDataBtn">使用 XMLHttpRequest 获取数据</button>
<pre id="output"></pre>
<h2>导航请求 (Navigation Request)</h2>
<p>点击 <a href="/another-page.html">这里</a> 导航到另一个页面。</p>
<script src="app.js"></script>
<script>
// 注册 Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker 注册成功:', registration.scope);
})
.catch(error => {
console.error('Service Worker 注册失败:', error);
});
});
} else {
console.warn('您的浏览器不支持 Service Worker。');
}
</script>
</body>
</html>
2. style.css
(样式文件)
css
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f4f4f4;
color: #333;
}
h1, h2 {
color: #0056b3;
}
button {
padding: 10px 15px;
margin: 5px;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #218838;
}
pre {
background-color: #e9ecef;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
img {
border: 1px solid #ccc;
margin: 10px 0;
}
.background-image-div {
width: 150px;
height: 150px;
background-image: url('https://via.placeholder.com/150/FF0000/FFFFFF?text=BG_IMG'); /* 背景图片 */
background-size: cover;
margin: 10px 0;
border: 1px solid #ccc;
}
3. app.js
(页面脚本)
js
document.addEventListener('DOMContentLoaded', () => {
const fetchDataBtn = document.getElementById('fetchDataBtn');
const xhrDataBtn = document.getElementById('xhrDataBtn');
const output = document.getElementById('output');
// 使用 Fetch API 发送请求
fetchDataBtn.addEventListener('click', async () => {
output.textContent = 'Fetch API 请求中...';
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json();
output.textContent = 'Fetch API 响应: ' + JSON.stringify(data, null, 2);
} catch (error) {
output.textContent = 'Fetch API 请求失败: ' + error.message;
console.error('Fetch API error:', error);
}
});
// 使用 XMLHttpRequest 发送请求
xhrDataBtn.addEventListener('click', () => {
output.textContent = 'XMLHttpRequest 请求中...';
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts/1', true);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
output.textContent = 'XMLHttpRequest 响应: ' + JSON.stringify(JSON.parse(xhr.responseText), null, 2);
} else {
output.textContent = 'XMLHttpRequest 请求失败: ' + xhr.statusText;
}
};
xhr.onerror = function() {
output.textContent = 'XMLHttpRequest 请求失败 (网络错误)';
console.error('XHR error:', xhr);
};
xhr.send();
});
});
4. service-worker.js
(Service Worker 脚本)
js
const CACHE_NAME = 'request-interceptor-v1';
const urlsToCache = [
'/', // 根路径,通常指 index.html
'/index.html',
'/style.css',
'/app.js',
'/another-page.html', // 导航目标页面
'https://via.placeholder.com/100', // <img> 标签的图片
'https://via.placeholder.com/150/FF0000/FFFFFF?text=BG_IMG' // CSS background-image
];
// 模拟另一个页面,用于导航请求拦截
// 在实际项目中,你可能需要创建一个真实的 another-page.html 文件
// 这里为了演示,我们直接在 Service Worker 中处理这个请求
self.addEventListener('install', (event) => {
console.log('[SW] 安装中...');
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('[SW] 缓存核心资源');
return cache.addAll(urlsToCache);
})
.then(() => self.skipWaiting()) // 强制新 SW 立即激活
.catch(error => {
console.error('[SW] 缓存失败:', error);
})
);
});
self.addEventListener('activate', (event) => {
console.log('[SW] 激活中...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('[SW] 删除旧缓存:', cacheName);
return caches.delete(cacheName);
}
})
);
}).then(() => self.clients.claim()) // 立即控制所有客户端
);
});
// 核心:fetch 事件监听器
self.addEventListener('fetch', (event) => {
// 获取请求对象
const request = event.request;
const url = new URL(request.url);
console.log(`\n--- [SW] 拦截请求 ---`);
console.log(`URL: ${url.href}`);
console.log(`Method: ${request.method}`);
console.log(`Type: ${request.destination}`); // 请求类型 (document, script, style, image, font, xhr, etc.)
console.log(`Mode: ${request.mode}`); // 请求模式 (navigate, cors, no-cors, same-origin)
// 示例 1: 拦截导航请求
if (request.mode === 'navigate') {
console.log(`[SW] 这是一个导航请求!`);
// 我们可以选择从缓存中返回离线页面,或者直接从网络获取
event.respondWith(
caches.match(request).then(cachedResponse => {
if (cachedResponse) {
console.log(`[SW] 从缓存返回导航页面: ${url.pathname}`);
return cachedResponse;
}
// 如果是 /another-page.html 且不在缓存中,可以返回一个自定义响应
if (url.pathname === '/another-page.html') {
console.log(`[SW] 动态生成 /another-page.html 响应`);
return new Response('<h1>这是另一个页面 (由 Service Worker 生成)</h1><p>你已成功导航到此页面,且 Service Worker 拦截了请求。</p><a href="/">返回主页</a>', {
headers: { 'Content-Type': 'text/html' }
});
}
console.log(`[SW] 从网络获取导航页面: ${url.href}`);
return fetch(request); // 否则从网络获取
}).catch(error => {
console.error(`[SW] 导航请求处理失败: ${error}`);
// 可以在这里返回一个通用的离线页面
return caches.match('/offline.html'); // 假设你有一个 offline.html
})
);
return; // 处理完导航请求后退出
}
// 示例 2: 拦截 API 请求 (fetch 和 XHR)
// 注意:request.destination 为 'xhr' 或 'empty' (对于 fetch)
// 更好的方式是根据 URL 模式来判断是否是 API 请求
if (url.hostname === 'jsonplaceholder.typicode.com') {
console.log(`[SW] 这是一个 API 请求 (Fetch 或 XHR)!`);
event.respondWith(
fetch(request)
.then(response => {
// 可以对 API 响应进行缓存,但通常 API 响应不适合长期缓存
console.log(`[SW] API 请求从网络获取: ${url.href}`);
return response;
})
.catch(error => {
console.error(`[SW] API 请求失败: ${error}`);
// 可以在离线时返回一个预设的错误响应
return new Response(JSON.stringify({ error: 'Offline API access failed.' }), {
headers: { 'Content-Type': 'application/json' },
status: 503,
statusText: 'Service Unavailable'
});
})
);
return;
}
// 示例 3: 拦截子资源请求 (CSS, JS, 图片等)
// 采用"缓存优先,网络回退"策略
event.respondWith(
caches.match(request).then(cachedResponse => {
if (cachedResponse) {
console.log(`[SW] 从缓存返回子资源: ${url.pathname}`);
return cachedResponse;
}
// 如果缓存中没有,从网络获取
console.log(`[SW] 从网络获取子资源: ${url.pathname}`);
return fetch(request).then(networkResponse => {
// 检查响应是否有效,例如状态码为200,并且不是不透明响应(opaque response)
if (!networkResponse || networkResponse.status !== 200 || networkResponse.type === 'opaque') {
return networkResponse;
}
// 克隆响应,因为响应流只能被消费一次
const responseToCache = networkResponse.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(request, responseToCache); // 将网络响应存入缓存
});
return networkResponse;
}).catch(error => {
console.error(`[SW] 子资源网络请求失败: ${url.pathname}, ${error}`);
// 可以在这里返回一个默认的离线图片或样式
// 例如:if (request.destination === 'image') return caches.match('/images/offline.png');
});
})
);
});
如何测试和观察
-
搭建本地服务器: Service Worker 必须在 HTTPS 或
localhost
环境下运行。-
使用
http-server
(Node.js):vbscriptnpm install -g http-server cd my-intercept-app/ http-server
然后访问
http://localhost:8080
。 -
或者使用 VS Code 的 Live Server 插件。
-
-
打开开发者工具: 在 Chrome 浏览器中按 F12 打开开发者工具。
- 切换到
Console
(控制台) 面板,观察 Service Worker 打印的日志。 - 切换到
Network
(网络) 面板,观察请求的来源(Size
列会显示(from ServiceWorker)
或(ServiceWorker)
)。 - 切换到
Application
(应用) 面板,在Service Workers
选项卡中确保你的 Service Worker 已激活并运行。在Cache Storage
中查看缓存内容。
- 切换到
-
首次加载页面: 观察控制台和网络面板,你会看到
index.html
,style.css
,app.js
, 占位图片等资源的请求被 Service Worker 拦截并缓存。 -
点击"使用 Fetch API 获取数据"按钮: 观察控制台,你会看到 Service Worker 拦截了对
jsonplaceholder.typicode.com
的fetch
请求。 -
点击"使用 XMLHttpRequest 获取数据"按钮: 观察控制台,你会看到 Service Worker 同样拦截了对
jsonplaceholder.typicode.com
的XHR
请求。 -
点击"导航到另一个页面"链接: 观察控制台,你会看到 Service Worker 拦截了
/another-page.html
的导航请求,并返回了我们 Service Worker 中定义的模拟 HTML 内容。 -
模拟离线: 在
Network
面板勾选Offline
。- 刷新页面,你会发现页面仍然能加载,因为所有资源都从 Service Worker 缓存中获取。
- 再次点击 Fetch 或 XHR 按钮,你会看到 Service Worker 返回了我们预设的离线错误响应。
- 再次点击导航链接,你会看到 Service Worker 仍然能返回模拟的
another-page.html
。
request.destination
和 request.mode
属性
在 Service Worker 的 fetch
事件中,event.request
对象提供了两个非常有用的属性来帮助你识别请求的类型:
-
request.destination
: 这是一个字符串,表示请求的目的地类型。它可以是以下值之一:document
:HTML 文档(导航请求或<iframe>
)。script
:JavaScript 脚本(<script>
标签或 Web Worker)。style
:CSS 样式表(<link rel="stylesheet">
)。image
:图片(<img>
, CSSbackground-image
)。font
:字体文件(@font-face
)。audio
:音频文件。video
:视频文件。manifest
:Web App Manifest 文件。report
:报告(如 CSP 报告)。object
:嵌入对象(如<embed>
,<object>
)。embed
:嵌入内容(如<embed>
)。worker
:Web Worker 脚本。sharedworker
:Shared Worker 脚本。serviceworker
:Service Worker 脚本本身。xslt
:XSLT 样式表。track
:<track>
元素使用的文本轨道。empty
:默认值,用于无法确定目的地的请求,例如通过fetch()
API 发起的通用数据请求(尽管有时fetch
请求也可能被推断为xhr
)。xhr
:通常用于XMLHttpRequest
请求,但fetch
请求也可能被归类为xhr
。unknown
:未知类型。
-
request.mode
: 这是一个字符串,表示请求的模式,它与 CORS 策略密切相关:navigate
:导航请求(用户在地址栏输入 URL 或点击链接)。cors
:跨域请求,遵循 CORS 规则。no-cors
:跨域请求,但不会触发 CORS 预检,响应会被视为"不透明"(opaque)。same-origin
:同源请求。websocket
:WebSocket 连接请求。
通过结合 request.destination
和 request.mode
,Service Worker 能够非常精确地识别和处理不同类型的网络请求,从而实现精细化的缓存策略和离线体验。