Service Worker拦截所有网络请求

Service Worker 的 fetch 事件是一个非常强大的机制,它允许 Service Worker 拦截由其控制的页面发出的所有网络请求 。这意味着,无论是通过 fetch() API、XMLHttpRequest (XHR) 发起的请求,还是浏览器在加载页面时自动发出的对 HTML、CSS、JavaScript、图片、字体等文件资源的请求,甚至是用户导航到新页面的请求,Service Worker 都能捕获并进行处理。

Service Worker fetch 事件的拦截范围

fetch 事件监听器会接收到一个 FetchEvent 对象,其中包含了 request 属性,这个 request 对象代表了被拦截的网络请求。

Service Worker 可以拦截以下类型的请求:

  1. 导航请求 (Navigation Requests): 当用户在地址栏输入 URL、点击链接或通过表单提交导致页面导航时,会触发导航请求。这些请求通常返回 HTML 文档。

  2. 子资源请求 (Subresource Requests): 页面加载过程中,浏览器会自动请求各种子资源,例如:

    • HTML 文档 (除了导航请求,例如通过 <iframe> 或动态加载的 HTML 片段)。
    • CSS 文件 (<link rel="stylesheet">)。
    • JavaScript 文件 (<script src="...">)。
    • 图片 (<img>, CSS background-image)。
    • 字体文件 (@font-face)。
    • 视频/音频文件 (<video>, <audio>)。
    • Web Workers / Shared Workers 脚本
    • Manifest 文件
    • 数据文件 (JSON, XML, TXT 等)。
  3. 通过 fetch() API 发起的请求: 现代 Web 应用中常用的异步数据请求方式。

  4. 通过 XMLHttpRequest (XHR) 发起的请求: 传统的异步数据请求方式,Service Worker 同样可以拦截。

  5. 通过 <img> 标签的 srcset 属性或 <picture> 元素发起的响应式图片请求。

  6. 通过 <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');
            });
        })
    );
});

如何测试和观察

  1. 搭建本地服务器: Service Worker 必须在 HTTPS 或 localhost 环境下运行。

    • 使用 http-server (Node.js):

      vbscript 复制代码
      npm install -g http-server
      cd my-intercept-app/
      http-server

      然后访问 http://localhost:8080

    • 或者使用 VS Code 的 Live Server 插件。

  2. 打开开发者工具: 在 Chrome 浏览器中按 F12 打开开发者工具。

    • 切换到 Console (控制台) 面板,观察 Service Worker 打印的日志。
    • 切换到 Network (网络) 面板,观察请求的来源(Size 列会显示 (from ServiceWorker)(ServiceWorker))。
    • 切换到 Application (应用) 面板,在 Service Workers 选项卡中确保你的 Service Worker 已激活并运行。在 Cache Storage 中查看缓存内容。
  3. 首次加载页面: 观察控制台和网络面板,你会看到 index.html, style.css, app.js, 占位图片等资源的请求被 Service Worker 拦截并缓存。

  4. 点击"使用 Fetch API 获取数据"按钮: 观察控制台,你会看到 Service Worker 拦截了对 jsonplaceholder.typicode.comfetch 请求。

  5. 点击"使用 XMLHttpRequest 获取数据"按钮: 观察控制台,你会看到 Service Worker 同样拦截了对 jsonplaceholder.typicode.comXHR 请求。

  6. 点击"导航到另一个页面"链接: 观察控制台,你会看到 Service Worker 拦截了 /another-page.html 的导航请求,并返回了我们 Service Worker 中定义的模拟 HTML 内容。

  7. 模拟离线:Network 面板勾选 Offline

    • 刷新页面,你会发现页面仍然能加载,因为所有资源都从 Service Worker 缓存中获取。
    • 再次点击 Fetch 或 XHR 按钮,你会看到 Service Worker 返回了我们预设的离线错误响应。
    • 再次点击导航链接,你会看到 Service Worker 仍然能返回模拟的 another-page.html

request.destinationrequest.mode 属性

在 Service Worker 的 fetch 事件中,event.request 对象提供了两个非常有用的属性来帮助你识别请求的类型:

  • request.destination 这是一个字符串,表示请求的目的地类型。它可以是以下值之一:

    • document:HTML 文档(导航请求或 <iframe>)。
    • script:JavaScript 脚本(<script> 标签或 Web Worker)。
    • style:CSS 样式表(<link rel="stylesheet">)。
    • image:图片(<img>, CSS background-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.destinationrequest.mode,Service Worker 能够非常精确地识别和处理不同类型的网络请求,从而实现精细化的缓存策略和离线体验。

相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax