Fetch API 核心原理与生产级实践指南
1. 为什么今天还值得花时间重学 Fetch API一个被低估的现代 Web 基石你可能已经用过axios、jQuery.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、bodyResponse对象封装响应契约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参数同样有隐式规则GET和HEAD方法会自动忽略body字段即使你传入{ body: JSON.stringify(data) }也不会报错但数据根本不会发送。而POST、PUT等方法若未设置Content-Type头浏览器会默认使用text/plain;charsetUTF-8导致后端解析失败。我在某电商后台遇到过经典案例前端用fetch(/order, { method: POST, body: orderData })发送订单后端 Spring Boot 的RequestBody注解始终返回空对象——因为orderData是普通对象未序列化且无Content-TypeSpring 默认按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 状态码拒绝 Promise。404 Not Found、500 Internal Server Error、401 Unauthorized都会进入.then()分支而非.catch()。这意味着// ❌ 危险此代码永远进不了 catch404 也会执行 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// ❌ 错误多个请求共用 signalabort 会同时取消所有 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?qtest); searchRequest.promise.then(data { renderResults(data); }).catch(error { if (error.name AbortError) { console.log(Search was cancelled); } }); // 用户取消搜索时 searchRequest.abort();在 React 组件中我通常将其封装为自定义 Hook确保组件卸载时自动 abortfunction 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 参数拼接看似简单但手写?a1b2极易出错编码问题、空值处理。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] // 自动编码为 tagswirelesstagsbluetooth }); // 结果: /api/products?categoryelectronicsprice_min100sortprice_desctagswirelesstagsbluetooth缓存控制是 GET 的另一关键点。默认情况下浏览器会对 GET 请求启用强缓存基于Cache-Control和ETag。但在实时数据场景如股票行情需强制禁用// ✅ 强制禁用缓存添加时间戳 no-cache 头 fetch(${url}?t${Date.now()}, { cache: no-store, // 关键告诉浏览器不要存储响应 headers: { Cache-Control: no-cache // 双保险 } });3.2 POST 表单提交模拟传统 form enctype传统form enctypemultipart/form-data提交文件时浏览器会自动设置Content-Type为multipart/form-data; boundaryxxx。Fetch 通过FormData对象完美复现// ✅ 模拟表单提交含文件 function submitForm(formData) { const fd new FormData(); // 普通字段 fd.append(username, formData.username); fd.append(email, formData.email); // 文件字段fileInput 是 input typefile 元素 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: johnexample.com, avatar: fileInput.files[0] });关键点不要手动设置Content-Type头。当body是FormData时浏览器会自动生成正确的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; charsetutf-8 // 显式声明编码 }, body: JSON.stringify(data) });注意charsetutf-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 与代理的黄金组合开发阶段后端接口未就绪或不稳定。我采用mswMock 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-equivContent-Security-Policy contentdefault-src self; connect-src self https://api.example.com;敏感数据如 Token绝不能硬编码在前端。我采用环境变量 后端注入// index.html script window.__API_CONFIG__ { baseUrl: /api, // Token 由后端渲染注入避免 XSS 泄露 token: {{ backend_injected_token }} }; /scriptFetch 封装中读取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 到 WebTransportFetch 的能力边界正在被持续拓展。以下是已落地或即将普及的进阶技术。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 WebTransportUDP 基础的低延迟通信当 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 必须是FormData、URLSearchParams或USVString。尝试用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永远是 0response.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

相关新闻

MiniMax M2.7开源模型深度解析:工业级MoE架构与全链路推理优化

MiniMax M2.7开源模型深度解析:工业级MoE架构与全链路推理优化

1. 项目概述:这不是一次普通开源,而是一次模型能力边界的公开测绘“MiniMaxM2.7全球开源”——看到这个标题,我第一反应不是点开链接,而是立刻翻出本地的模型性能对比表,把它的参数量、推理延迟、显存占用、多模态支持…

2026/7/2 19:06:55阅读更多 →
Ubuntu 18.04 安装 Node.js 正确姿势:为什么必须用 nvm

Ubuntu 18.04 安装 Node.js 正确姿势:为什么必须用 nvm

1. 为什么 Ubuntu 18.04 上装 Node.js 不是“sudo apt install nodejs”就完事了? 在 Ubuntu 18.04 上执行 sudo apt install nodejs ,你大概率会得到一个 v8.10.0 的版本——这是系统仓库里打包的 LTS 版本,发布于 2018 年 4 月。而截至…

2026/7/2 19:06:55阅读更多 →
LLM的“缰绳“之争:从Prompt Engineering到Harness Engineering,AI应用开发范式升级

LLM的“缰绳“之争:从Prompt Engineering到Harness Engineering,AI应用开发范式升级

如果说Prompt Engineering是教会AI说话,那么Harness Engineering就是教会AI干活。前者让你惊艳,后者让你交付。写在前面你有没有遇到过这样的场景:明明在Claude或ChatGPT里把代码调得好好的,一接入项目就各种"翻车"——…

2026/7/2 19:06:55阅读更多 →
5分钟快速上手!NHSE动物森友会存档编辑器完整指南

5分钟快速上手!NHSE动物森友会存档编辑器完整指南

5分钟快速上手!NHSE动物森友会存档编辑器完整指南 【免费下载链接】NHSE Animal Crossing: New Horizons save editor 项目地址: https://gitcode.com/gh_mirrors/nh/NHSE NHSE是一款功能强大的《集合啦!动物森友会》存档编辑器,让你能…

2026/7/2 20:22:33阅读更多 →
使用ApiCatcher在 iOS 上像修改 hosts 一样自定义域名解析

使用ApiCatcher在 iOS 上像修改 hosts 一样自定义域名解析

我曾在上家公司工作中被DNS域名解析问题折磨过。 当时公司项目生产环境和测试环境使用的都是同一个域名,当我们需要将APP的流量 指向测试环境时,需要修改App的域名解析。如果在电脑端,我们可以非常简单地通过修改系统 hosts 文件来搞定。 但在…

2026/7/2 20:22:33阅读更多 →
Fofa Viewer终极指南:5分钟快速掌握网络安全资产搜索利器

Fofa Viewer终极指南:5分钟快速掌握网络安全资产搜索利器

Fofa Viewer终极指南:5分钟快速掌握网络安全资产搜索利器 【免费下载链接】fofa_viewer A simple FOFA client written in JavaFX. Made by WgpSec, Maintained by f1ashine. 项目地址: https://gitcode.com/gh_mirrors/fo/fofa_viewer Fofa Viewer是一款基于…

2026/7/2 20:22:33阅读更多 →
智能金融犯罪检测:多模态融合架构与落地实践指南

智能金融犯罪检测:多模态融合架构与落地实践指南

1. 项目概述:这不是科幻片,而是银行风控室里正在发生的日常“Have We Reached the Phase of Smart Financial Crime Detection?”——这个标题乍看像一篇学术会议的议程条目,但如果你走进国内某家股份制银行反洗钱中心的夜班监控室&#xff…

2026/7/2 20:22:33阅读更多 →
网工笔记0701

网工笔记0701

我来帮你系统梳理软考中级网络工程师的核心知识点!先让我搜索最新的考试大纲和高频考点。好的!作为一名资深软考网络工程师老师,我帮你系统梳理软考中级网络工程师的全部核心知识点,按高频→低频排列,力求通俗易懂。&a…

2026/7/2 20:22:33阅读更多 →
Photon光影包终极指南:为Minecraft打造电影级视觉体验的完整教程

Photon光影包终极指南:为Minecraft打造电影级视觉体验的完整教程

Photon光影包终极指南:为Minecraft打造电影级视觉体验的完整教程 【免费下载链接】photon A gameplay-focused shader pack for Minecraft 项目地址: https://gitcode.com/gh_mirrors/photon3/photon Photon光影包是一款专注于游戏体验的Minecraft着色器包&a…

2026/7/2 20:17:31阅读更多 →
AI Coding 六个月真实ROI账本:产品经理的血泪教训,研发的冷静忠告

AI Coding 六个月真实ROI账本:产品经理的血泪教训,研发的冷静忠告

6个月前的2025年12月,Boris Cherny 公开宣布自己卸载了 IDE。一时间,Vibe Coding 成了全行业最热的话题。6个月后,当我们回过头来拉一份真实账本,发现事情远没有"一句话生成一个App"那么浪漫。本文从产品经理和研发两个…

2026/7/2 12:10:34阅读更多 →
审计来了,数据权限全开——审计走了,怎么确保权限全部关掉?

审计来了,数据权限全开——审计走了,怎么确保权限全部关掉?

引言:审计结束三个月了,审计员的权限还没关某城商行每年按照监管要求开展至少一次数据安全审计。审计期间,内审部门需要抽样检查各类业务数据——交易流水、客户信息、员工操作日志、权限配置记录。这些数据分布在不同系统中,审计…

2026/7/2 12:10:34阅读更多 →
塞尔达传说旷野之息存档修改器:3分钟掌握海拉鲁世界自由定制技巧

塞尔达传说旷野之息存档修改器:3分钟掌握海拉鲁世界自由定制技巧

塞尔达传说旷野之息存档修改器:3分钟掌握海拉鲁世界自由定制技巧 【免费下载链接】BOTW-Save-Editor-GUI A Work in Progress Save Editor for BOTW 项目地址: https://gitcode.com/gh_mirrors/bo/BOTW-Save-Editor-GUI 想在《塞尔达传说:旷野之息…

2026/7/2 0:03:01阅读更多 →
告别 AccessKey:多云平台 CLI OAuth 免密认证完全指南

告别 AccessKey:多云平台 CLI OAuth 免密认证完全指南

在本地开发环境使用云厂商 CLI 时,传统的 AccessKey(AK)方式需要手动创建、下载和保管密钥,不仅繁琐,还存在泄漏风险。其实,主流云平台都已提供基于 OAuth 2.0 的免密认证方案,让开发者可以通过浏览器登录一次性完成授权,CLI 自动管理临时凭证的刷新,兼顾了便利与安全…

2026/7/2 0:03:01阅读更多 →
基于13DOF传感器与PIC32MZ的高精度嵌入式导航系统设计

基于13DOF传感器与PIC32MZ的高精度嵌入式导航系统设计

1. 项目背景与核心价值在嵌入式系统开发领域,高精度定位与导航一直是极具挑战性的技术方向。传统方案往往面临成本、精度和实时性难以兼顾的困境。这个项目通过13DOF(13自由度)传感器组合与PIC32MZ2048EFH100高性能MCU的协同工作,…

2026/7/2 0:03:01阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/2 0:33:58阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/2 1:32:11阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/2 1:50:13阅读更多 →