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 能够非常精确地识别和处理不同类型的网络请求,从而实现精细化的缓存策略和离线体验。

相关推荐
王者鳜錸1 小时前
VUE+SPRINGBOOT从0-1打造前后端-前后台系统-邮箱重置密码
前端·vue.js·spring boot
独泪了无痕3 小时前
深入浅析Vue3中的生命周期钩子函数
前端·vue.js
小白白一枚1113 小时前
vue和react的框架原理
前端·vue.js·react.js
字节逆旅3 小时前
从一次爬坑看前端的出路
前端·后端·程序员
若梦plus4 小时前
微前端之样式隔离、JS隔离、公共依赖、路由状态更新、通信方式对比
前端
若梦plus4 小时前
Babel中微内核&插件化思想的应用
前端·babel
若梦plus4 小时前
微前端中微内核&插件化思想的应用
前端
若梦plus4 小时前
服务化架构中微内核&插件化思想的应用
前端
若梦plus4 小时前
Electron中微内核&插件化思想的应用
前端·electron
若梦plus4 小时前
Vue.js中微内核&插件化思想的应用
前端