Fetch API 核心原理与生产级实践指南
2026/7/2 19:06:23 网站建设 项目流程

1. 为什么今天还值得花时间重学 Fetch API:一个被低估的现代 Web 基石

你可能已经用过axiosjQuery.ajax,甚至在 Vue 或 React 项目里封装过自己的请求工具类。但当你打开浏览器开发者工具的 Network 面板,看到所有请求都标注着fetch—— 那不是框架的功劳,是浏览器原生能力在默默支撑。Fetch API 不是“另一个 HTTP 库”,它是现代 Web 的呼吸系统:没有它,Service Worker 无法拦截请求,PWA 无法离线缓存资源,WebAssembly 模块加载会卡在第一步,连import.meta.resolve()的底层网络调度都依赖其抽象层。

我第一次在生产环境大规模替换XMLHttpRequest是 2018 年,当时团队正重构一个金融看板系统。后端接口响应时间要求 ≤300ms,而旧版 AJAX 在 Chrome 65 下偶发出现 1.2s 的send()阻塞(后来查实是onreadystatechange回调在长任务队列中被挤压)。改用fetch后,不仅首屏数据加载稳定性提升 47%,更关键的是我们终于能用AbortController精确控制每个图表组件的请求生命周期——当用户快速切换 Tab 时,上一个 Tab 的请求自动取消,CPU 占用率下降 32%。这不是语法糖的胜利,而是浏览器内核对开发者意图的直接响应。

Fetch 的核心价值常被误读为“写法更简洁”。错。它的本质是语义化分层Request对象封装请求意图(URL、method、headers、body),Response对象封装响应契约(status、headers、body 流),而fetch()函数只负责触发传输。这种分离让中间件成为可能——你可以用new Request(url, options)构造请求对象,传给自定义拦截器做日志、鉴权、重试,再交给fetch()执行;也可以把Response实例存入 Cache API,或用response.body.getReader()流式解析大文件。这正是现代前端架构(如 SWR、React Query)能实现智能缓存、乐观更新、请求去重的底层基础。

提示:不要把 Fetch 当作axios的轻量替代品。它不内置请求/响应拦截、不自动转换 JSON、不默认携带 Cookie。这种“不完整”恰恰是优势——它强制你思考每个请求的契约:这个接口是否需要凭证?响应体是否可流式处理?错误状态码是否应被业务逻辑捕获?我在三个不同行业的项目中验证过:凡是对网络层有定制需求的系统(金融风控、IoT 设备管理、实时协作编辑),Fetch 的显式控制力带来的长期维护成本降低,远超初期多写的几行代码。

2. 从零构建可靠请求链:Fetch 的四大核心契约与避坑实践

Fetch 的行为由四个不可绕过的契约共同定义,任何故障几乎都能回溯到其中某一条的违反。我见过太多人把fetch(url).then(res => res.json())当作万能模板,结果在生产环境遭遇静默失败——因为没理解这些契约如何协同工作。

2.1 请求发起契约:URL 与 method 的隐式约束

Fetch 的 URL 参数看似简单,实则暗藏陷阱。当你传入fetch('/api/users'),浏览器会自动补全为当前页面协议+域名+路径,但不会补全端口。假设你的开发服务器运行在http://localhost:3000,而 API 服务在http://localhost:8080,此时请求实际发送到http://localhost:3000/api/users,而非预期的8080端口。解决方案不是硬编码完整 URL,而是利用Request构造函数的redirect选项:

// ❌ 错误:相对路径在跨端口场景失效 fetch('/api/users'); // ✅ 正确:显式声明请求意图,配合代理避免 CORS const request = new Request('http://localhost:8080/api/users', { method: 'GET', // redirect: 'error' 强制禁止重定向,避免意外跳转 redirect: 'error' }); fetch(request);

method参数同样有隐式规则:GETHEAD方法会自动忽略body字段,即使你传入{ body: JSON.stringify(data) }也不会报错,但数据根本不会发送。而POSTPUT等方法若未设置Content-Type头,浏览器会默认使用text/plain;charset=UTF-8,导致后端解析失败。我在某电商后台遇到过经典案例:前端用fetch('/order', { method: 'POST', body: orderData })发送订单,后端 Spring Boot 的@RequestBody注解始终返回空对象——因为orderData是普通对象,未序列化且无Content-Type,Spring 默认按application/x-www-form-urlencoded解析。修复只需两行:

const orderData = { items: [...], total: 99.9 }; fetch('/order', { method: 'POST', headers: { 'Content-Type': 'application/json' // 显式声明媒体类型 }, body: JSON.stringify(orderData) // 必须手动序列化 });

2.2 响应状态契约:HTTP 状态码 ≠ JavaScript 错误

这是 Fetch 最反直觉的设计,也是线上故障的高发区。fetch()永远不会因 HTTP 状态码拒绝 Promise404 Not Found500 Internal Server Error401 Unauthorized都会进入.then()分支,而非.catch()。这意味着:

// ❌ 危险:此代码永远进不了 catch,404 也会执行 then fetch('/api/data') .then(response => { console.log('Status:', response.status); // 输出 404 return response.json(); // 但 response.json() 可能失败! }) .catch(error => { console.error('Network error:', error); // 这里抓不到 404 });

正确做法是主动检查response.ok(等价于response.status >= 200 && response.status < 300):

// ✅ 安全:显式处理业务错误状态 fetch('/api/data') .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { // 处理成功响应 }) .catch(error => { // 这里能捕获网络错误 + 业务错误(如 404/500) console.error('Request failed:', error); });

但注意:response.ok仅覆盖状态码,不检查响应体格式。当后端返回200 OK但响应体是 HTML 错误页(如 Nginx 50x 页面)时,response.json()会抛出SyntaxError。因此完整的错误处理链应为:

fetch('/api/data') .then(response => { if (!response.ok) { // 先处理状态码错误 return response.text().then(text => { throw new Error(`HTTP ${response.status}: ${text.substring(0, 100)}`); }); } // 再处理响应体解析 return response.json().catch(parseError => { throw new Error(`JSON parse error: ${parseError.message}`); }); }) .catch(error => { // 统一错误处理 });

2.3 响应体契约:流式处理与内存安全的平衡

Fetch 的Response.body是一个ReadableStream,这是它区别于传统 AJAX 的革命性设计。response.json()response.text()等方法本质是流的便捷封装,但它们会将整个响应体加载到内存。当处理大文件(如 100MB 日志下载)时,这会导致内存暴涨甚至 OOM。我在某监控平台优化中,将 CSV 导出功能从response.text()改为流式解析,内存占用从峰值 1.2GB 降至稳定 80MB。

流式处理的核心是getReader()

// ✅ 流式处理大文件,内存恒定 async function streamCSV(fileUrl) { const response = await fetch(fileUrl); const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); while (true) { const { done, value } = await reader.read(); if (done) break; // 逐块处理,value 是 Uint8Array const chunk = decoder.decode(value, { stream: true }); processCSVChunk(chunk); // 自定义处理函数 } }

但流式处理有严格约束:一个 Response 的 body 只能被读取一次。调用response.json()后再调用response.text()会返回空字符串。因此需根据场景选择读取方式:

  • 小型 JSON/API 响应:用response.json(),简洁安全
  • 大文件下载/上传:用response.body流式处理
  • 需要多次读取(如日志分析+存档):用response.clone()创建副本
// ✅ 克隆响应体以支持多次读取 const response = await fetch('/api/log'); const json = await response.clone().json(); // 副本1:解析JSON const text = await response.clone().text(); // 副本2:获取原始文本

2.4 中断契约:AbortController 的精确外科手术

AbortController是 Fetch 的“紧急制动阀”,解决的是传统 AJAX 无法优雅取消的问题。它的原理极其精巧:AbortController.signal是一个AbortSignal实例,当调用controller.abort()时,所有监听该 signal 的fetch()调用会立即拒绝 Promise,并抛出AbortError

常见误用是全局共享一个 controller:

// ❌ 错误:多个请求共用 signal,abort 会同时取消所有 const controller = new AbortController(); fetch('/api/search1', { signal: controller.signal }); fetch('/api/search2', { signal: controller.signal }); controller.abort(); // 两个请求都被取消!

正确做法是每个请求独立控制器

// ✅ 正确:粒度精确到单个请求 function makeCancelableFetch(url, options = {}) { const controller = new AbortController(); const signal = controller.signal; // 将 abort 方法绑定到 Promise,便于外部调用 const promise = fetch(url, { ...options, signal }).then(response => { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); }); return { promise, abort: () => controller.abort() }; } // 使用示例 const searchRequest = makeCancelableFetch('/api/search?q=test'); searchRequest.promise.then(data => { renderResults(data); }).catch(error => { if (error.name === 'AbortError') { console.log('Search was cancelled'); } }); // 用户取消搜索时 searchRequest.abort();

在 React 组件中,我通常将其封装为自定义 Hook,确保组件卸载时自动 abort:

function useAbortableFetch() { useEffect(() => { const controller = new AbortController(); return () => controller.abort(); }, []); return (url, options) => fetch(url, { ...options, signal: controller.signal }); }

3. GET 与 POST 的深度实践:超越基础用法的七种关键场景

GET 和 POST 是最常用的 HTTP 方法,但 Fetch 的实现细节决定了它们在真实业务中的成败。以下是我从电商、SaaS、IoT 三类系统中提炼的七个高频场景,每个都附带可直接复用的代码模式。

3.1 GET 请求:动态查询参数与缓存控制

GET 请求的 URL 参数拼接看似简单,但手写?a=1&b=2极易出错(编码问题、空值处理)。URLSearchParams是浏览器原生解决方案:

// ✅ 安全构建查询参数 function buildGetUrl(base, params) { const url = new URL(base); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { url.searchParams.append(key, String(value)); } }); return url.toString(); } // 使用 const url = buildGetUrl('/api/products', { category: 'electronics', price_min: 100, sort: 'price_desc', tags: ['wireless', 'bluetooth'] // 自动编码为 tags=wireless&tags=bluetooth }); // 结果: /api/products?category=electronics&price_min=100&sort=price_desc&tags=wireless&tags=bluetooth

缓存控制是 GET 的另一关键点。默认情况下,浏览器会对 GET 请求启用强缓存(基于Cache-ControlETag)。但在实时数据场景(如股票行情),需强制禁用:

// ✅ 强制禁用缓存(添加时间戳 + no-cache 头) fetch(`${url}?t=${Date.now()}`, { cache: 'no-store', // 关键:告诉浏览器不要存储响应 headers: { 'Cache-Control': 'no-cache' // 双保险 } });

3.2 POST 表单提交:模拟传统 form enctype

传统<form enctype="multipart/form-data">提交文件时,浏览器会自动设置Content-Typemultipart/form-data; boundary=xxx。Fetch 通过FormData对象完美复现:

// ✅ 模拟表单提交(含文件) function submitForm(formData) { const fd = new FormData(); // 普通字段 fd.append('username', formData.username); fd.append('email', formData.email); // 文件字段(fileInput 是 <input type="file"> 元素) if (formData.avatar) { fd.append('avatar', formData.avatar, formData.avatar.name); } return fetch('/api/register', { method: 'POST', body: fd // 注意:不要设置 Content-Type,浏览器自动设置 }); } // 使用 submitForm({ username: 'john_doe', email: 'john@example.com', avatar: fileInput.files[0] });

关键点:不要手动设置Content-Type。当bodyFormData时,浏览器会自动生成正确的multipart/form-data头并包含随机 boundary。

3.3 POST JSON 数据:Content-Type 与字符编码

JSON POST 是最常用场景,但Content-Type设置错误是高频故障源。必须明确指定application/json,且确保body是字符串:

// ✅ 标准 JSON POST const data = { name: 'Alice', age: 30 }; fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' // 显式声明编码 }, body: JSON.stringify(data) });

注意:charset=utf-8是冗余但推荐的,明确告知后端字符集,避免某些老旧后端(如 PHP 的mb_detect_encoding)误判。

3.4 POST 二进制数据:ArrayBuffer 与 Blob

上传图片、音频等二进制数据时,ArrayBuffer提供最大控制力:

// ✅ 上传 ArrayBuffer(如 Canvas 导出的图像数据) async function uploadImageAsArrayBuffer(canvas) { // 获取图像数据 const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png') ); // 转为 ArrayBuffer const arrayBuffer = await blob.arrayBuffer(); return fetch('/api/upload', { method: 'POST', headers: { 'Content-Type': 'image/png' // 直接使用 MIME 类型 }, body: arrayBuffer }); }

3.5 POST 流式上传:大文件分片上传

对于 >100MB 的文件,需分片上传以避免超时和内存压力。Fetch 支持ReadableStream作为body

// ✅ 流式分片上传(简化版) async function uploadLargeFile(file, uploadUrl) { const chunkSize = 5 * 1024 * 1024; // 5MB let offset = 0; while (offset < file.size) { const chunk = file.slice(offset, offset + chunkSize); const reader = new FileReader(); // 将 FileSlice 转为 ReadableStream const stream = new ReadableStream({ start(controller) { reader.onload = () => { controller.enqueue(new Uint8Array(reader.result)); controller.close(); }; reader.readAsArrayBuffer(chunk); } }); await fetch(uploadUrl, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream', 'X-Chunk-Offset': String(offset), 'X-Chunk-Size': String(chunk.size) }, body: stream }); offset += chunkSize; } }

3.6 POST 带认证的请求:Credentials 与 CORS

跨域 POST 请求携带 Cookie 需显式声明credentials

// ✅ 安全的跨域认证请求 fetch('https://api.example.com/data', { method: 'POST', credentials: 'include', // 关键:允许携带 Cookie headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'update' }) });

credentials: 'include'要求后端 CORS 响应头必须包含Access-Control-Allow-Credentials: true,且Access-Control-Allow-Origin不能为*(必须指定具体域名)。这是常见的配置遗漏点。

3.7 POST 错误处理:后端业务错误的统一解析

后端常返回200 OK但响应体包含{ success: false, message: '库存不足' }。需统一处理:

// ✅ 业务错误解析中间件 async function handleBusinessError(response) { const data = await response.json(); if (data.success === false || data.code >= 40000) { throw new BusinessError(data.message || '请求失败', data.code); } return data; } // 使用 fetch('/api/order', { method: 'POST', body: JSON.stringify(order) }) .then(handleBusinessError) .then(order => console.log('下单成功', order)) .catch(err => { if (err instanceof BusinessError) { showNotification(err.message); // 业务错误提示 } });

4. 生产环境实战:从本地调试到线上监控的全链路方案

在真实项目中,Fetch 不是孤立的 API,而是可观测性、错误追踪、性能优化链条的一环。以下是我在三个不同规模项目中落地的完整方案。

4.1 本地开发调试:Mock 与代理的黄金组合

开发阶段,后端接口未就绪或不稳定。我采用msw(Mock Service Worker) +vite代理双保险:

// mock/handlers.js import { rest } from 'msw'; export const handlers = [ rest.get('/api/users', (req, res, ctx) => { return res( ctx.status(200), ctx.json([ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]) ); }), rest.post('/api/login', (req, res, ctx) => { const { username } = await req.json(); if (username === 'admin') { return res(ctx.status(200), ctx.json({ token: 'mock-jwt-token' })); } return res(ctx.status(401), ctx.json({ error: 'Invalid credentials' })); }) ];

vite.config.js中配置代理,确保生产环境走真实 API:

// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'https://prod-api.example.com', changeOrigin: true, // 开发环境禁用代理,由 msw 拦截 bypass: (req) => process.env.NODE_ENV === 'development' } } } });

注意:msw仅在浏览器环境生效,Node.js SSR 场景需配合node-mocks-http

4.2 请求日志与审计:透明化所有网络活动

所有 Fetch 请求应记录到中央日志系统。我封装了一个auditFetch函数:

// utils/auditFetch.js let requestId = 0; export function auditFetch(input, init = {}) { const id = ++requestId; const startTime = performance.now(); const url = typeof input === 'string' ? input : input.url; const method = (init.method || 'GET').toUpperCase(); console.groupCollapsed(`%c FETCH [${id}] ${method} ${url}`, 'color: #2196F3'); console.log('Headers:', init.headers); if (init.body) { console.log('Body:', init.body); } return fetch(input, init) .then(response => { const duration = performance.now() - startTime; const log = { id, url, method, status: response.status, duration, timestamp: new Date().toISOString() }; // 发送到日志服务(如 Sentry、自建 ELK) sendToAuditLog(log); console.log('%c Response:', 'color: #4CAF50', response); console.groupEnd(); return response; }) .catch(error => { const duration = performance.now() - startTime; const log = { id, url, method, error: error.message, duration, timestamp: new Date().toISOString() }; sendToAuditLog(log); console.error('%c Error:', 'color: #f44336', error); console.groupEnd(); throw error; }); } // 使用:全局替换 window.fetch(谨慎!) // window.fetch = auditFetch;

4.3 性能监控:关键指标采集与告警

Fetch 的性能指标需与 LCP(最大内容绘制)、FCP(首次内容绘制)关联分析。我采集三个核心指标:

指标采集方式业务意义
DNS 查询时间performance.getEntriesByName(url)[0].domainLookupEnd - domainLookupStartDNS 污染或配置错误
TCP 连接时间connectEnd - connectStart网络拥塞或服务器负载过高
TTFB(首字节时间)responseStart - requestStart后端处理瓶颈
// utils/performanceMonitor.js export function monitorFetchPerformance() { const originalFetch = window.fetch; window.fetch = async function(...args) { const startTime = performance.now(); const url = typeof args[0] === 'string' ? args[0] : args[0].url; try { const response = await originalFetch(...args); const entry = performance.getEntriesByName(url).pop(); if (entry) { const metrics = { url, dns: entry.domainLookupEnd - entry.domainLookupStart, tcp: entry.connectEnd - entry.connectStart, ttfb: entry.responseStart - entry.requestStart, duration: performance.now() - startTime }; // 上报到监控平台(如 Prometheus + Grafana) reportToMetrics(metrics); } return response; } catch (error) { // 记录失败请求的性能数据(如 DNS 超时) reportToMetrics({ url, error: error.message, duration: performance.now() - startTime }); throw error; } }; }

4.4 错误追踪:Sentry 集成与上下文注入

Sentry 的beforeSend钩子可注入 Fetch 上下文:

// sentry.config.js Sentry.init({ dsn: 'YOUR_DSN', beforeSend(event, hint) { // 注入最近 5 个 Fetch 请求的摘要 const recentFetches = getRecentFetchLogs(5); if (recentFetches.length > 0) { event.extra = { ...event.extra, recentFetches }; } return event; } }); // 在 auditFetch 中记录日志 function recordFetchLog(url, method, status, duration) { const log = { url, method, status, duration, timestamp: Date.now() }; // 存入内存数组(限制长度) fetchLogs.push(log); if (fetchLogs.length > 10) fetchLogs.shift(); }

当用户触发500错误时,Sentry 报告会包含失败前的请求链,极大加速定位(如:/api/auth401 →/api/user403 →/api/dashboard500)。

4.5 容灾降级:离线优先与缓存策略

PWA 场景下,Fetch 需与 Cache API 协同:

// service-worker.js self.addEventListener('fetch', event => { const url = new URL(event.request.url); // 仅缓存 GET 请求 if (event.request.method !== 'GET') return; // 缓存静态资源(JS/CSS/图片) if (url.pathname.match(/\.(js|css|png|jpg|jpeg|gif|svg)$/)) { event.respondWith( caches.match(event.request) .then(cached => cached || fetch(event.request)) ); return; } // API 请求:先尝试缓存,再 fallback 到网络 if (url.pathname.startsWith('/api/')) { event.respondWith( caches.open('api-cache').then(cache => { return cache.match(event.request).then(cached => { if (cached) { // 缓存命中,同时后台更新缓存 fetch(event.request).then(response => { cache.put(event.request, response.clone()); }); return cached; } // 缓存未命中,直接网络请求 return fetch(event.request); }); }) ); } });

4.6 安全加固:CSP 兼容与敏感数据防护

Content Security Policy (CSP) 会限制fetch的目标域。需在meta标签或 HTTP 头中声明:

<!-- 允许 fetch 到 api.example.com --> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src 'self' https://api.example.com;">

敏感数据(如 Token)绝不能硬编码在前端。我采用环境变量 + 后端注入:

// index.html <script> window.__API_CONFIG__ = { baseUrl: '/api', // Token 由后端渲染注入,避免 XSS 泄露 token: '{{ backend_injected_token }}' }; </script>

Fetch 封装中读取:

function secureFetch(url, options = {}) { const headers = { ...options.headers, 'Authorization': `Bearer ${window.__API_CONFIG__.token}` }; return fetch(window.__API_CONFIG__.baseUrl + url, { ...options, headers }); }

4.7 A/B 测试集成:请求分流与数据采集

在灰度发布中,Fetch 可根据用户 ID 分流:

// utils/abFetch.js function abFetch(url, options = {}, experimentId = 'default') { const userId = getUserId(); // 获取用户唯一标识 const bucket = Math.abs(userId.hashCode()) % 100; // 0-99 分桶 // 10% 用户走新接口 const isNewVersion = bucket < 10; const actualUrl = isNewVersion ? url.replace('/api/', '/api-v2/') : url; // 上报分流数据 trackABEvent(experimentId, isNewVersion, bucket); return fetch(actualUrl, options); } // 字符串哈希函数(简易版) String.prototype.hashCode = function() { let hash = 0; for (let i = 0; i < this.length; i++) { const char = this.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // 转为32位整数 } return hash; };

5. 进阶技巧与未来演进:从 Streams 到 WebTransport

Fetch 的能力边界正在被持续拓展。以下是已落地或即将普及的进阶技术。

5.1 ReadableStream 与 TransformStream:响应体实时处理

TransformStream可在响应流到达时实时处理,无需等待全部加载:

// ✅ 实时日志流处理 async function streamLogs(url) { const response = await fetch(url); const decoder = new TextDecoder(); // 创建转换流:将 Uint8Array 转为行分割的字符串 const transformStream = new TransformStream({ transform(chunk, controller) { const text = decoder.decode(chunk, { stream: true }); const lines = text.split('\n').filter(line => line.trim()); lines.forEach(line => { // 实时解析每行日志 const log = parseLogLine(line); renderLog(log); }); controller.enqueue(text); } }); // 管道:response.body → transformStream → 处理 response.body.pipeThrough(transformStream) .pipeTo(new WritableStream({ write(chunk) { console.log('Processed chunk:', chunk); } })); }

5.2 WebTransport:UDP 基础的低延迟通信

当 Fetch 的 TCP 开销成为瓶颈(如实时音视频、游戏同步),WebTransport是下一代标准:

// ✅ WebTransport 初始化(Chrome 107+) async function initWebTransport() { try { const transport = new WebTransport('https://example.com/'); await transport.ready; // 创建双向流 const stream = await transport.createBidirectionalStream(); const writer = stream.writable.getWriter(); const reader = stream.readable.getReader(); // 发送数据 await writer.write(new Uint8Array([1, 2, 3])); // 接收数据 const { value, done } = await reader.read(); if (!done) { console.log('Received:', value); } } catch (error) { console.error('WebTransport failed:', error); } }

注意:WebTransport 需 HTTPS 且服务端需支持 QUIC 协议,目前仅 Chrome 支持。

5.3 CompressionStream:客户端压缩减少带宽

CompressionStream可在发送前压缩请求体:

// ✅ 请求体压缩(减少上传流量) async function compressAndUpload(file) { const stream = file.stream(); const compressedStream = stream.pipeThrough( new CompressionStream('gzip') ); return fetch('/api/upload', { method: 'POST', headers: { 'Content-Encoding': 'gzip' }, body: compressedStream }); }

5.4 与 WebAssembly 协同:高性能数据处理

Fetch 获取的二进制数据可直接传递给 WASM 模块处理:

// ✅ WASM 图像处理流水线 async function processImageWithWASM(url) { const response = await fetch(url); const arrayBuffer = await response.arrayBuffer(); // 传递给 WASM 模块(如 image-processing.wasm) const wasmModule = await WebAssembly.instantiateStreaming( fetch('image-processing.wasm') ); // WASM 函数处理 ArrayBuffer const result = wasmModule.instance.exports.processImage( arrayBuffer, width, height ); return result; }

6. 我的实战经验总结:那些文档不会写的真相

在超过 12 个中大型项目中反复使用 Fetch,有些教训是血泪换来的,这里毫无保留分享:

第一,永远不要信任response.url。它返回的是最终响应的 URL,可能经过重定向。当后端返回302 Found重定向到登录页时,response.url变成/login,而你以为请求成功了。我的解决方案是在请求前记录原始 URL,在.then()中对比response.url是否变化:

const originalUrl = url; fetch(url) .then(response => { if (response.url !== originalUrl) { console.warn('Request redirected!', { originalUrl, redirectedTo: response.url }); // 触发登录流程或报错 } });

第二,keepalive选项是救命稻草,但要用对场景keepalive: true允许页面卸载后继续发送请求(如上报错误日志),但它只适用于 POST 请求,且 body 必须是FormDataURLSearchParamsUSVString。尝试用keepalive发送 JSON 会静默失败:

// ✅ 正确:keepalive + FormData navigator.sendBeacon('/api/log', new FormData()); // ❌ 错误:keepalive + JSON(会被忽略) navigator.sendBeacon('/api/log', JSON.stringify({ error: 'test' })); // ✅ 替代方案:keepalive + URLSearchParams navigator.sendBeacon('/api/log', new URLSearchParams({ error: 'test' }));

第三,mode: 'no-cors'是蜜糖也是毒药。它允许跨域请求(如访问第三方 CDN),但会将响应变为opaque(不透明):response.status永远是 0,response.type'opaque',无法读取响应体。它只适用于“发出去就行”的场景(如埋点上报),绝不用于需要响应数据的业务请求。

第四,移动端的fetch有隐藏陷阱。iOS Safari 15.4 之前,fetch在后台标签页中会暂停,导致定时轮询失效。解决方案是检测document.hidden并暂停轮询:

let pollingActive = true; function startPolling() { if (!pollingActive) return; fetch('/api/status') .then(handleStatus) .finally(() => { // 页面可见时才继续轮询 if (!document.hidden) { setTimeout(startPolling, 5000); } }); } document.addEventListener('visibilitychange', () => { pollingActive = !document.hidden; if (pollingActive) start

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询