做前后端也很多年了我有个很深的体会越基础的东西越容易在关键时刻翻车。JSON 获取数据六个字听起来一句话就能说完——发个请求、等响应、JSON.parse 一下完事。可真到了生产环境这一条链路里能出问题的地方比想象中多得多连接超时、对方服务器把错误包装成 200、返回的内容少了一个字段、跨域拦截、解析报错……任何一个环节崩了页面就是白屏脚本就是报错日志就是一片红色。这篇文章我想把这些年攒下的经验整理出来适合正在写前端页面的同学也适合刚接触服务端取数的朋友。内容会覆盖浏览器、Node.js、小程序和第三方接口对接尽量不聊空洞理论基本都落到能直接用的代码和处理思路上。如果你曾经被某个诡异的 JSON 获取问题卡过一下午看完应该能少走一段弯路。1. 状态码 200 不代表 JSON 到手很多同学刚接触接口时判断成功和失败只看 HTTP 状态码。这种习惯在联调阶段勉强够用但一上生产几乎必然出事。因为 HTTP 状态码描述的是这次网络交互是否完成而不是业务逻辑是否正确。1.1 我遇到过的一次成功返回有段时间我维护一个订单同步模块接口文档写得很规范返回结构是{code: 0, data: {...}}。为了省事代码里只判断了res.status 200只要不是 200 就抛错。某天夜里任务频繁失败我打开日志一看HTTP 状态码全是 200但data字段是null页面列表直接渲染崩掉。查到最后发现服务端在自己的异常处理里把所有业务错误也都统一返回 HTTP 200只在code字段里标了非 0 的数值。也就是说网络层面请求完全成功但业务层面早就失败了。这个经历让我后来形成两个习惯先区分 HTTP 层和应用层再决定怎么处理错误。HTTP 200 只代表请求被服务端收到并返回了内容绝不代表数据可用。1.2 三类最常见的拿不到 JSON现象我把实际开发里遇见的异常情况归纳成三类方便排查时按图索骥失败类型典型现象定位方式请求根本没发出去或被拦截控制台报跨域错误、网络层错误打开 Network 面板看请求是否到达服务器响应体不是合法 JSONresponse.json()抛出Unexpected token 用 Curl 看原始响应体业务失败伪装成成功HTTP 200但code非 0data为空打印完整响应体不能只看状态码第三类最阴险。很多后端框架的全局异常处理会把参数错误、权限不足、内部异常全部折成 HTTP 200然后在 JSON 里放一个错误码。如果前端代码只判断状态码用户看到的将是加载失败或者干脆白屏但日志里又查不到任何异常。1.3 给获取成功下一个准确的定义代码里写判断时可以按下面五条逐层确认网络可达TCP/TLS 握手完成没有超时和断连。HTTP 状态码符合预期通常是 200 或 304。响应体可以被解析成合法 JSON而不是 HTML 或空字符串。业务状态码为约定的成功值比如很多系统里的code 0。关键字段存在且类型正确比如data.list必须是数组。这五条里前两条是网络层第三条是解析层后两条是业务层。一个真正稳的获取逻辑应当把每层的失败都当成自己的失败来处理而不是把问题丢给下游。2. 浏览器里拿 JSONfetch 与 axios 的细节与取舍浏览器是 JSON 获取最密集的场景。从最早的XMLHttpRequest到现在的fetch再到周边生态里的axios其实核心机制没有变变的只是易用性和错误处理。2.1 fetch 的基本盘别把错误处理看简单fetch是当前浏览器原生支持的请求 API用它获取 JSON 的常规写法如下const res await fetch(https://api.example.com/weather/cities, { method: GET, headers: { Accept: application/json }, credentials: include }); if (!res.ok) { throw new Error(HTTP ${res.status}: ${res.statusText}); } const data await res.json();这段代码看起来简单但有两个点必须说清楚。第一fetch只有在网络层面失败时才会 reject比如断网、DNS 解析失败、连接被重置。对于 404、500 这类状态码它照样返回一个Response对象不会主动抛异常。所以你必须要手动检查res.ok或res.status否则一个 500 响应会被当成成功数据继续往下走。第二res.json()本质上是读取响应体文本再交给 JSON 解析器。如果响应体是空字符串、HTML 页面、或者带了 UTF-8 BOM 的怪异文本这里会抛出SyntaxError。这种情况最常见的来源是后端网关在出错时返回了一整张 HTML 报错页但状态码依然是 200。另外如果你的接口需要携带登录凭证记得加credentials: include。很多浏览器下跨域请求默认不带 Cookie导致接口总是返回未登录。2.2 axios拦截器和超时确实省事fetch原生 API 的能力在不断补齐但要说工程化体验我仍然更推荐axios。原因不是fetch不好而是axios把很多高频需求直接做成了配置项和拦截器。const instance axios.create({ baseURL: https://api.example.com, timeout: 6000, headers: { Content-Type: application/json } }); instance.interceptors.response.use( response response.data, error { if (error.response) { // 服务端返回了 4xx/5xx console.error(HTTP Error, error.response.status, error.response.data); } else if (error.code ECONNABORTED) { console.error(请求超时); } else { console.error(网络错误, error.message); } return Promise.reject(error); } );拦截器的作用是统一收口。团队项目里不同页面对错误的处理思路往往不一样如果不加拦截每个页面都要重复写超时、状态码判断、错误提示很容易漏。有了拦截器网络层的异常可以统一记录业务层再拿到response.data自己判断。还有个细节axios默认情况下2xx的状态码会走then其他状态码会走catch这比fetch需要手动判断res.ok要直观。配合timeout配置至少能保证请求不会无休止挂起。2.3 请求取消与并发防抖之外还差一步搜索框联动列表是 JSON 获取的高频场景。很多人只做了防抖没做请求取消结果就是输入关键字 A 发出请求又输入关键字 B 发出请求B 的响应先回来A 的响应后回来页面被旧数据覆盖。解决方案是AbortControllerconst controller new AbortController(); const timer setTimeout(() controller.abort(), 8000); try { const res await fetch(https://api.example.com/search, { signal: controller.signal }); const data await res.json(); // 只有本次请求是最新时才写入页面 if (requestId currentRequestId) { render(data); } } catch (err) { if (err.name AbortError) { console.log(请求已取消); } else { // 真正需要处理的异常 } } finally { clearTimeout(timer); }并发请求方面我建议尽量用Promise.allSettled而不是Promise.all。比如一个页面同时需要拉取用户信息、菜单配置和消息数这三个接口里只要有一个失败Promise.all就会整体 reject整页白屏。而allSettled可以让互不依赖的请求各自成功失败页面至少能渲染出可用的部分。3. 离开浏览器之后Node.js 与小程序里的 JSON 获取差异浏览器里 Fetch 和 XHR 的规则相对统一。跑到服务端和小程序里获取 JSON 的姿势完全变了个样尤其是定时任务和脚本场景很多坑跟浏览器里完全不一样。3.1 Node.js 18 的 fetch 和 axios 怎么选Node.js 从 18 版本开始原生提供了fetch底层由undici实现。对于简单脚本它已经够用const res await fetch(https://api.example.com/weather/city?city1001, { headers: { Authorization: Bearer ${token} } }); if (!res.ok) { throw new Error(HTTP ${res.status}); } const data await res.json();不过实际做服务端集成时我仍然倾向于用axios或got这类库。原因有三个Node 原生fetch没有内置超时机制必须自己包AbortController每个请求都写一遍很啰嗦。原生fetch对 cookie 的处理、连接池的配置不如专用 HTTP 客户端直观。服务端对接的场景往往需要自定义重试、流量控制、日志埋点axios的拦截器体系更好扩展。如果你的服务只是轻量级脚本原生fetch完全没问题。如果是给线上业务提供 JSON 数据采集能力建议直接上axios实例统一管理超时、重试和日志。3.2 小程序与客户端环境要注意的差异小程序里的 JSON 获取比普通网页多了几道限制。以我接触比较多的场景为例小程序要求接口域名必须配到后台白名单里开发环境的地址还要勾选不校验合法域名。很多新手在本地联调时一切正常预览到真机就全部请求失败八成是域名白名单没配齐或者用了 IP 地址而非 HTTPS 域名。代码层面小程序的wx.request返回的只是一段数据不会帮你做 JSON 自动解析。而且默认情况下请求头和浏览器端不一样如果服务端严格校验Content-Type必须手动设置wx.request({ url: https://api.example.com/weather/cities, method: POST, header: { content-type: application/json }, data: { keyword: test }, success(res) { // res.data 已经是解析后的对象 // 但我仍建议先确认状态码和业务码 if (res.statusCode 200 res.data.code 0) { console.log(res.data.data); } }, fail(err) { console.error(网络失败, err); } });另外小程序里对cookie的支持很弱很多登录态需要主动放到Authorization头里。如果你从前端复制的代码直接拿到小程序里跑大概率会踩到这个坑。3.3 定时任务与批量拉取时的并发控制服务端脚本里经常要批量拉取很多城市的数据。如果直接写一个Promise.all(cities.map(fetchCityWeather))在接口没有任何限流的情况下几十个请求瞬间打出去很容易触发对方服务的限流甚至封 IP。手动做并发控制也不复杂维护一个任务队列async function mapLimit(items, limit, handler) { const results []; let index 0; async function worker() { while (index items.length) { const current index; results[current] await handler(items[current]); } } const workers Array.from({ length: Math.min(limit, items.length) }, worker); await Promise.all(workers); return results; } const weatherList await mapLimit(cityList, 5, city fetchCityWeather(city) );把并发数控制在 3~5既能赶任务又不容易触发对方服务的防护策略。这个写法在很多语言里都有现成库但手写一遍能帮你真正理解并发控制的核心逻辑。4. 跨域获取 JSON老问题的新答案跨域大概是 JSON 获取里被问得最多的问题。我刚入行时也觉得它像玄学明明把接口地址粘到浏览器里能正常返回 JSON怎么放到页面请求就被拦了4.1 一个简单的跨域请求实际发生了什么浏览器有一个同源策略协议、域名、端口三者只要有一个不同就算跨域。跨域请求在发出之前浏览器会判断是否属于简单请求非简单请求会先发一个OPTIONS预检请求问服务端允不允许我这样请求。如果服务端没有返回对应的跨域响应头浏览器会在网络层面阻断本次请求。注意这里的措辞是浏览器拦的不是服务端拒绝的。服务端可能已经正常处理并返回了数据只是浏览器把这个数据按安全规则挡掉了。要让跨域请求成功服务端需要返回类似这样的响应头Access-Control-Allow-Origin: https://yourdomain.example Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Allow-Credentials: true在实际排查跨域问题时第一步永远是打开 Network 面板看请求的类型和响应头。如果发现OPTIONS请求返回 404 或 500问题往往出在服务端网关没有处理预检请求而不是前端代码写错了。4.2 开发环境用转发别把后端地址写进前端代码开发阶段最常见也最省事的方案是把请求地址改成相对路径然后让开发服务器把请求转发到真实后端。以 Vite 为例配置大致这样export default { server: { proxy: { /api: { target: https://api.example.com, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } }前端代码里就写fetch(/api/weather/cities)开发服务器会自动把它转发到真实服务。这样做的好处是本地没有跨域问题前端代码不需要关心后端地址是什么构建部署时也少了一个容易出错的环境变量。有个细节要注意changeOrigin: true很重要。它会把请求头里的Host改成目标服务的域名否则一些后端根据Host做路由或校验时会拒绝来自本地转发的请求。4.3 生产环境的 CORS 配置与 JSONP 的适用边界正式环境里如果前端和后端域名不同仍然需要后端配置正确的 CORS 响应头。常见做法是在网关或中间件层统一加而不是在每个业务接口里单独写。如果你对接的是一些老系统对方不支持 CORS也不方便改服务端可能会想到 JSONP。JSONP 的思路是利用script标签不受跨域限制只能支持GET请求且要求对方返回一段可执行的 JavaScript 包裹函数调用。JSONP 的缺点非常明显只能 GET、无法拿到 HTTP 状态码、存在脚本注入风险、调试困难。除非完全没有其他选择否则我不建议在新项目里引入。今天的主流方案CORS 足够解决绝大多数跨域 JSON 获取问题。5. JSON 到手之后解析、校验与字段保护很多人把拿到 JSON当作任务的终点。实际上从网络响应到页面可用中间还有解析、校验、规范化三道工序。省略任何一道线上都可能出现难以定位的隐性 Bug。5.1 我更喜欢先 text() 再 parse()fetch提供的res.json()很方便但它把读文本和解析 JSON两件事绑在一起。一旦解析失败你拿不到原始文本排查时只能靠猜。我现在更习惯读成文本再手动解析const text await res.text(); if (!text) { // 204 No Content 或网关吞掉响应体 throw new Error(响应体为空); } let data; try { data JSON.parse(text); } catch (err) { console.error(JSON 解析失败原文是, text.slice(0, 200)); throw new Error(响应体不是合法 JSON); }这样做有两个好处。一是解析失败时日志里能保留一部分原始内容方便定位是乱码、HTML 还是特殊符号导致的。二是可以提前处理空响应体而不是等json()抛出含糊的Unexpected end of JSON input。如果接口返回的 JSON 带了 UTF-8 BOM 头直接JSON.parse会报错。这种情况下最简单的处理方式是先去掉 BOMconst cleanText text.replace(/^\uFEFF/, );这个坑在 Windows 环境下生成的静态 JSON 文件里偶尔会出现签名后就能减少很多莫名其妙的问题。5.2 序列化的三个信息黑洞JSON 是一种轻量数据格式但它能表达的类型有限。对象里的一些常见类型在序列化和反序列化过程中会丢失或被改写原始类型JSON 序列化后原因undefined属性属性直接消失JSON 不支持undefinedNaN/Infinity变成nullJSON 数字格式不支持Date对象变成 ISO 字符串JSON 没有日期类型BigInt直接抛错JSON.stringify 无法处理 BigInt这意味着一份数据从接口拿到手看起来是个对象但里面嵌套的类型你可能并不完全了解。比如页面需要显示时间接口返回的是字符串2024-06-01T08:00:00Z前端如果只做浅渲染没问题但一旦要计算时间差、排序、格式化就得手动new Date()。还有undefined的问题。很多后端会把空字段直接省略而不是给null。前端做字段判断时item.cost ?? 0能同时覆盖null和undefined但如果你用typeof item.cost undefined判断又发现字段直接不存在逻辑就会跳进另一个分支。这里没有银弹只能靠约定和规范要么后端统一返回null要么前端用默认值兜底。5.3 给外部 JSON 加一道校验外部接口的数据格式说变就变。可能只是字段从list改成了data_list也可能是数组变成了对象。如果代码里不加校验坏数据会一路穿透到渲染层最后页面报一个非常诡异的错误。我现在处理第三方 JSON都会写一个小的规范化函数function normalizeWeatherList(raw) { if (!raw || typeof raw ! object) { throw new Error(响应不是对象); } if (raw.code ! 0) { throw new Error(业务错误${raw.message || 未知错误}); } const list raw.data?.list; if (!Array.isArray(list)) { throw new Error(data.list 缺失或不是数组); } return list.map(item ({ cityId: String(item.cityId), cityName: String(item.cityName), temp: Number(item.temp), humidity: Number(item.humidity) || 0 })); }这样的函数看起来啰嗦但它是整条 JSON 获取链路里值得写最仔细的地方。它能保证下游拿到的数据结构统一类型确定不会把脏数据带进业务逻辑。团队里如果项目复杂度更高可以引入 JSON Schema 校验或运行时类型校验库但原理都一样在进入业务代码之前把数据格式的合同检查一遍。6. 复盘一次第三方 JSON 接口接入从文档到稳定运行讲了一堆原理和代码不如完整回顾一次真实的接入过程。这个例子是给一个内部系统做城市气象数据同步需要定时拉取 200 个城市的数据并入库。听上去很简单实际复盘时长长记性。6.1 对接前我们先做了四件事接第三方接口之前先不要陷入写代码的冲动。我把以下四件事列在了第一步确认鉴权方式。是简单的 Key 还是需要签名Token 过期时间多久确认限流阈值。每分钟最多多少个请求超了返回什么状态码确认字段口径。温度单位是摄氏度还是华氏度空值用null还是缺字段确认失败模式。服务端code非 0 时message字段里有什么信息这四件事如果对接时没确认清楚后面每一条都是坑。限流阈值不清楚批量拉取就会被封字段口径不清楚入库数据就是错的失败模式不清楚告警系统形同虚设。6.2 一次线上告警的完整排查链路服务上线几天后某天凌晨我收到告警提示同步任务异常。日志显示的数据非常奇怪HTTP 200JSON 解析成功code也是 0但没有拿到任何城市列表。我当时的第一反应不是改代码而是把原始响应打印出来curl -i https://api.example.com/weather/list?citybeijingthemeall结果响应里确实有code:0但data.list为空数组。接着我又看了另一个城市有数据。这说明不是接口挂了而是某一个城市的参数写法有问题。翻文档时发现参数名规范要求cityCode不能写city。第三方接口对未知参数不加校验直接忽略然后返回一个空列表同时保持code: 0。这类问题是排查中最耗时间的HTTP 层、解析层、业务状态码全正常只有数据层悄悄空了。最后修正了参数拼接逻辑并把返回 200 但没有数据的情况也纳入告警判断这才算真正堵住了这个坑。6.3 重试、限速与缓存第三方接口再稳定也免不了偶发超时。于是我们在获取逻辑里加了带指数退避的重试async function fetchWithRetry(url, options, retries 3) { let lastErr; for (let i 0; i retries; i) { try { const res await fetch(url, options); if (!res.ok) { throw new Error(HTTP ${res.status}); } return await res.json(); } catch (err) { lastErr err; if (err.name AbortError || err.name TimeoutError) { await sleep(300 * Math.pow(2, i)); continue; } // 4xx 一般不需要重试重试只会加重对方压力 if (err.message.startsWith(HTTP 4)) { break; } await sleep(300 * Math.pow(2, i)); } } throw lastErr; }重试逻辑有个原则超时和 5xx 可以重试4xx 立刻停止。参数错了、鉴权失败了重试一百次结果一样还可能触发限流被拉黑。另外指数退避的间隔不能从 0 开始否则遇到瞬时抖动重试请求会跟连续攻击一样打在服务上。数据缓存同样重要。气象数据不需要每次任务都全量拉取新值我们把响应体按城市分别缓存设置五分钟过期时间。这样就算某个城市接口临时故障至少能提供上一次的可用数据不至于整个任务直接失败。接入这类接口多了之后我慢慢明白一件事真正让一个 JSON 获取链路稳定下来的不是什么高级框架而是一套把获取定义清楚的习惯。把 HTTP 状态、业务状态、字段校验分开看把超时和重试写在项目里把原始响应保留在日志里那些曾经让你抓头发的玄学问题就会一个个变成可定位、可复现、可预防的普通问题。希望这篇经验整理能让你下一次面对 JSON 获取时心态稳一点排查快一点。
阅读完成 · 觉得有帮助?