ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

前端HTTP请求组成全解析:从请求行到请求体

前端HTTP请求组成全解析:从请求行到请求体 1. 请求背后的四个组成部分从URL到请求体的一次完整拆解先问一个问题你平时写axios.get(/api/user)的时候有没有想过浏览器究竟往服务器发了些什么我面试过不少前端候选人能说出“GET/POST”的不少但能把请求结构说完整的十个里面两三个。这个看似基础的问题其实决定了你在排查网络问题、设计接口规范、做前端性能优化时能不能快速定位问题。HTTP请求整体上由四部分组成请求行Request Line、请求头Headers、空行、请求体Body。其中GET请求通常没有请求体但前三部分无论如何都存在。我把它们拆开讲每一部分对应到浏览器开发者工具里你能看到的东西方便你对照着理解。1.1 请求行方法、路径和HTTP版本三件套请求行是请求的第一行格式固定METHOD 路径 HTTP版本。实际抓包看到的模样是这样的GET /api/users?page1size20 HTTP/1.1 POST /api/user/login HTTP/1.1第一个要素是请求方法Method。GET、POST、PUT、DELETE、PATCH、OPTIONS、HEAD这些都是前端常用方法。关于方法选型我的建议很简单读操作用GET写操作用POST全量更新用PUT部分更新用PATCH。但现实项目里很多团队只用了GET和POST这没问题但要记住一个核心原则——GET请求参数会出现在URL和浏览器历史记录里永远不要用它传敏感信息。第二个要素是请求路径。这里有同学会混淆URL和URI的概念。用一句话区分URI是资源标识URL是资源定位。/api/users/123是URIhttps://api.example.com/api/users/123是URL。前端代码里的axios.get(/api/users/123)传入的是URI浏览器会基于当前页面域名自动拼接成完整URL。第三个要素是HTTP版本。现在主流的HTTP/1.1和HTTP/2之间有个关键差异——HTTP/1.1是纯文本协议HTTP/2是二进制分帧协议HTTP/1.1有队头阻塞问题HTTP/2通过多路复用解决了。你打开Chrome的Network面板能看到协议版本列大多数HTTPS站点现在都已经是HTTP/2了。1.2 请求头携带元数据的“快递面单”请求头是HTTP请求里信息密度最高的部分。它像快递面单——上面写满了发件人、收件人、包裹类型、重量、配送要求这些元信息包裹本身请求体还没出场。前端日常打交道最多的几个头字段我列个表说明请求头字段作用典型示例Content-Type声明请求体的媒体类型application/json、application/x-www-form-urlencoded、multipart/form-dataAuthorization身份认证凭证Bearer eyJhbGciOi...Accept告知服务器客户端期望的响应类型application/jsonOrigin标识请求来源页面跨域时必带https://example.comReferer请求来源地址https://example.com/loginCookie携带会话信息sessionIdabc123User-Agent客户端标识Mozilla/5.0 (Windows NT 10.0; Win64; x64)Cache-Control缓存控制指令no-cache、max-age3600我想展开说两个最容易被忽略的。第一个是Content-Type。前端传参时这里最容易出bug。你用原生XHR发POST如果直接send一个对象后端大概率收到的是[object Object]这个字符串。正确做法是根据Content-Type拼接对应的数据格式JSON格式就JSON.stringify()表单格式就用URLSearchParams或FormData。axios帮我们做了这层转换但你在抓包时还是要会判断body和Header是否匹配。第二个是Origin和Referer。跨域请求时浏览器会自动带上Origin头后端通过校验这个字段决定是否允许访问。注意它是不可伪造的浏览器管理所以服务端做跨域校验时优先信任Origin而不是Referer因为Referer在隐私模式下可能被省略也可能被自定义修改。1.3 空行和请求体那个容易被忽略的分隔符空行在HTTP协议里起的是分隔作用——告诉服务器“请求头到此结束后面是请求体”。实际开发里你不需要手动构造它浏览器的网络栈会处理好。但是理解这个分隔对调试有帮助有时候抓包工具显示请求异常你会看到body解析不完整实际上问题出在服务端对body的读取没有正确处理。请求体Body是真正交给服务器的数据。三种常见格式对应三种不同的Content-Typeapplication/json{name:张三,age:18}适合嵌套结构复杂的对象是前后端分离项目用最多的格式。application/x-www-form-urlencodedname张三age18表单的默认格式浏览器原生支持后端解析最简单。multipart/form-data用于文件上传每个字段都有独立的boundary分隔线支持二进制数据。实际项目中我见过不少同学在这三个格式上栽跟头。典型场景是后端接口明明要的是x-www-form-urlencoded前端用axios传了JSON对象导致后端取不到参数。排查的第一步一定是打开Network面板看Content-Type和Payload十有八九能找到问题。1.4 URL中的query请求参数的天然载体URL从?开始到#之前的部分是query查询参数。query在GET请求里是主要传参方式在POST请求里也时有出现。拼装query有几个容易踩坑的点。字符串拼接方式不可靠。最典型的坑是参数值里含有特殊字符——比如用户搜索的关键词是abc直接拼进URL会破坏参数结构。正确做法是用URLSearchParams或encodeURIComponent处理// 不推荐 const url /api/search?keyword${keyword}; // 推荐 const params new URLSearchParams({ keyword }); const url /api/search?${params.toString()};另外要区分query和URL编码的关系。URLSearchParams会把中文、空格、、等特殊字符自动转义后端拿到的时候会自动解码。如果后端拿到的中文是乱码大概率是你手动拼接时用了错误的编码方式或者后端没有配置正确的字符集。还有一个细节query参数有长度限制吗HTTP规范并没有限制query的长度但实际情况中浏览器、服务器、网关都可能有限制比如Nginx默认是8KB很多浏览器对超长URL会拦截或截断。所以大量数据不要往URL里塞这也是POST存在的重要原因之一。2. 从地址栏到Ajax前端发起HTTP请求的四种典型方式及选型思路理解了请求的组成结构之后下一个问题是前端代码里具体怎么把HTTP请求发出去这个问题的答案很丰富不同场景下选择不同。我分四类来说每一类都说明适用的场景和存在的限制。2.1 原生XMLHttpRequest老而弥坚的基础形态XMLHttpRequestXHR是浏览器第一个标准化的HTTP请求API从IE时代就存在现在依然在使用。它的核心用法const xhr new XMLHttpRequest(); xhr.open(POST, /api/user/login, true); xhr.setRequestHeader(Content-Type, application/json); xhr.timeout 10000; xhr.onreadystatechange function() { if (xhr.readyState 4) { if (xhr.status 200 xhr.status 300) { console.log(JSON.parse(xhr.responseText)); } } }; xhr.send(JSON.stringify({ username: admin, password: 123456 }));这里有三个细节值得展开。第一open()的第三个参数async默认是true代表异步请求。如果设成false请求会阻塞主线程页面冻结直到响应返回。我在老代码里见过这种写法体验极差现在强烈不建议使用同步XHR。第二onreadystatechange里的readyState枚举值有5个0未初始化、1已打开、2已获取响应头、3下载中、4完成。大多数时候你只需要判readyState 4因为此时响应已经完全接收。但调试大文件下载时可以监听到3状态来分析下载进度。第三XHR的setRequestHeader必须在open()之后、send()之前调用否则会抛异常。这是个尴尬的BOM顺序问题用封装库之后就不会遇到但还是要知道。对于实际开发我不推荐直接用XHR写业务代码因为它的API设计偏底层还需要处理各种边界情况。但读一些老旧库源码时你很可能遇到它。理解它可以帮你快速看懂那些维护了五六年以上的老项目。2.2 Fetch API现代浏览器的原生方案Fetch是ES6时代推出的原生HTTP请求API原生支持Promise。对比XHRFetch的语法简洁很多而且请求和响应都基于Stream可扩展性更强。async function fetchWithTimeout(url, options {}, timeout 10000) { const controller new AbortController(); const timer setTimeout(() controller.abort(), timeout); try { const response await fetch(url, { ...options, signal: controller.signal }); if (!response.ok) { throw new Error(HTTP error! status: ${response.status}); } return await response.json(); } finally { clearTimeout(timer); } }但Fetch有几个实际项目中容易忽略的坑。第一Fetch默认credentials是same-origin跨域请求不会携带Cookie。如果你的前后端是不同域名且靠Cookie维持登录态必须显式设置credentials: include。我知道很多SSO相关的bug都出在这个配置上。第二Fetch不会自动在HTTP状态码非2xx时reject Promise而是正常resolve只是response.ok为false。这意味着你必须手动检查状态码再决定是否抛出异常否则很容易出现“请求失败了但代码看起来正常”的诡异现象。第三Fetch的响应体只能用一次。response.json()、response.text()、response.blob()是互斥的调用了一个就不能再调另一个。如果需要同时看文本和解析JSON必须先拿text()再自行解析。这个限制在实际调试中经常坑人。第四和XHR不同Fetch没有内置超时机制。上面代码里我用AbortController实现了手动超时这是官方推荐的方案在业务代码里必须要有否则网络异常时请求会一直挂着。那到底选XHR还是Fetch我的实践结论是新项目优先Fetch老项目继续用XHR封装库比如axios底层还是XHR。Fetch的Promise风格更适合现代代码加上AbortController可以提供更细颗粒度的取消控制。2.3 axios工程化项目的事实标准axios是前端最流行的HTTP请求库底层是XHR浏览器端或Node.js的http模块但封装了层非常友好的API。它之所以成为工程标配核心原因是几个特性。第一请求/响应拦截器。这在统一处理Token、错误提示、日志上报时极其方便import axios from axios; const instance axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000, withCredentials: true, }); instance.interceptors.request.use((config) { const token localStorage.getItem(accessToken); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); instance.interceptors.response.use( (response) response.data, (error) { if (error.response?.status 401) { // 跳转登录页 } return Promise.reject(error); } ); export default instance;这是每个中大型前端项目里几乎一模一样的封装套路区别只是在细节上。我曾经在一个项目里见过没有拦截器封装的代码——每个接口调用都手动取token、手动处理错误三四十个API文件里相同的try/catch写了无数遍。后来重构时把所有逻辑集中到拦截器删掉了大约40%的重复代码。第二自动转换JSON。axios检测到请求体的Content-Type是application/json时会自动调用JSON.stringify检测到响应类型是JSON时自动JSON.parse。XHR时代你手动处理这些转换的细节在axios里完全不用操心。第三取消请求能力。基于XHR的abort加上AbortSignalaxios提供了CancelToken旧版和signal新版两种取消方式。页面跳转时取消未完成的请求是避免“在组件卸载后setState”警告的有效手段。不过axios也不是完美的。包体积约30KBgzip后对体量敏感的项目来说这是个负担。如果你的项目只需要简单的GET/POST完全可以考虑用Fetch封装轻量方案不必引入axios。2.4 浏览器原生导航请求表单提交与页面跳转的HTTP视角除了Ajax浏览器还有一种发起HTTP请求的方式——用户直接导航或表单提交。这种方式的特点是页面会整体刷新请求结果直接替换当前页面。表单提交其实可以手动构造请求。一个不为人知的小技巧是form的action和method决定请求目标和方式而表单字段会按x-www-form-urlencoded格式放入请求体。这对于实现某些无需JavaScript的登录场景是有用的但多数情况下我们不会走这条路。另有一种特殊请求是图片、脚本、样式等静态资源的HTTP请求。浏览器加载img、script、link时会自动发起GET请求这些请求由浏览器管理开发者无法在代码层面用Fetch模拟。理解这一点有助于你排查“为什么我的资源被多端缓存了”“为什么网站加载速度慢”这类问题。Performance面板里能看到这些请求的耗时分布而抓包工具里能看到它们的请求头与响应头。3. 实际项目中HTTP请求组成的落地拦截器、统一封装与参数处理的成熟实践这个章节聊一聊真实项目里请求相关代码的组织方式。既然标题叫“前端的http请求组成”那在工程里落地时就要考虑如何管理URL参数从哪里来统一错误处理怎么做我把这些经验拆成几条讲。3.1 API层集中管理与模块化拆分很多前端项目把所有的接口调用直接扔在组件里。小项目还行一旦上了几十个接口页面组件里到处是axios.get(...)的代码维护成本直线上升。我的推荐做法是单独建一个api目录按业务域拆分模块。比如一个商城系统可以分成auth.js、product.js、order.js、user.js。每个文件里导出该域下所有接口的调用函数// api/product.js import request from /utils/request; export function getProductList(params) { return request({ url: /api/products, method: get, params, }); } export function getProductDetail(id) { return request({ url: /api/products/${id}, method: get, }); }这样做的好处有两个一是调用方不需要感知HTTP细节组件只关心业务函数二是接口路径集中管理后端一旦改了URL前缀或路径只需改一处。我在重构这种代码时总结的经验是——把接口路径集中在API层改路径、改参数格式都在一个文件里完成粒度小且影响面可控。3.2 请求参数的三种来源与组装顺序一个复杂的页面请求参数往往来自三个地方路由参数比如从/order/123页面获取id、组件状态比如搜索框内容、全局状态比如用户信息。组装参数时遵循的顺序是先设置默认值再合并组件状态最后用路由参数覆盖。const params { page: 1, size: 20, ...searchForm, ...routeParams, };这段代码的逻辑很简单但实际项目中为了达成这个效果我见过很多种拼装方式。有些项目混用Object.assign、展开运算符、lodash.merge风格不一致容易出优先级问题。我建议全项目统一用展开运算符收口到API层去组装不要把拼参逻辑散落在各组件里。对DELETE和PUT这类方法参数放query还是body容易出现分歧。我的约定是删除类操作如果只传ID放query需要传复杂条件时放body。前后端约定好之后写进接口文档避免联调时反复扯皮。3.3 拦截器统一处理登录态过期、Token刷新与错误提示拦截器是工程化项目的必用功能我来详细展开三个常见场景。Token过期自动刷新。这是我处理过很多次的问题。流程是请求携带过期Token → 后端返回401 → 前端拦截器收到401 → 调用刷新Token接口用refreshToken→ 获取新Token → 重放原请求。这里涉及对pending请求的缓存我通常用一个数组保存失败的请求以及它们的resolve/reject刷新完成后统一重放let isRefreshing false; let pendingQueue []; instance.interceptors.response.use( (response) response.data, async (error) { const { response, config } error; if (response?.status 401 !config._retry) { if (isRefreshing) { return new Promise((resolve, reject) { pendingQueue.push({ resolve, reject, config }); }); } config._retry true; isRefreshing true; try { const { token } await refreshTokenApi(); localStorage.setItem(accessToken, token); pendingQueue.forEach(({ resolve, reject, config }) { instance(config).then(resolve).catch(reject); }); pendingQueue []; return instance(config); } catch (refreshError) { // 刷新失败跳转登录页 window.location.href /login; } finally { isRefreshing false; } } return Promise.reject(error); } );统一错误提示。我的经验是不要在拦截器里对所有错误都弹提示框需要分业务类型。网络断连error.message是Network Error或者状态码0统一提示“网络异常”并做断网重连逻辑超时ECONNABORTED提示“请求超时请重试”4xx/5xx按错误码表做映射。接口返回的业务错误HTTP状态码200但业务code非0不要放拦截器处理丢给具体页面决定要不要提示。3.4 超时与重试策略从前端角度控制请求的可靠性与体验超时和重试是请求组成里的增强功能。axios默认超时是0不超时在实际项目中一定要设置一个合理的阈值。怎么定我的经验是参考后端99%响应耗时。如果后端P99是2秒那超时时间设6~10秒是合理的如果后端P99是5秒那前端超时至少15秒否则会出现大量“用户还没等到响应就被我们掐断”的情况。const instance axios.create({ timeout: 15000, });重试策略要区分场景。对于查询类GET请求网络抖动时重试一次是有价值的对于创建类POST请求重试可能导致重复下单这时要依赖后端幂等键。前端重试的常规实现是给请求函数加一层循环async function requestWithRetry(config, retry 2) { try { return await instance(config); } catch (error) { if (retry 0 isRetryableError(error)) { await sleep(500); return requestWithRetry(config, retry - 1); } throw error; } } function isRetryableError(error) { const status error.response?.status; return !status || status 500 || error.code ECONNABORTED; }这里我加了个isRetryableError判断状态码500以上或请求超时值得重试而4xx参数错误重试没有意义。4. 请求头与请求体的核对排查从Network面板到Bug定位的完整链路HTTP请求组成的知识最实际的用途是在Bug定位时快速判断问题出在哪一层。我总结了一套从“抓包”到“定责”的排查思路。4.1 Network面板里应该看哪些字段当联调发现接口不通时打开Chrome开发者工具的Network面板按下面顺序查。第一Headers标签里确认请求方法、请求URL、Status Code。状态码直接决定责任方——4xx是前端传参有问题5xx是后端异常3xx是重定向配置问题。第二确认Request Headers里的Content-Type和后端接收格式一致。如果后端用RequestBody接收对象而前端发的是表单格式后端会直接报错或取到空对象。第三看Payload或Request Body与接口文档核对参数名、参数类型、嵌套结构。这里最常见的问题是前端传了userId但后端要的是id前端把数字123传成了字符串123后端要数组但前端传了逗号分隔的字符串。第四看Response里的内容区分是HTTP层错误还是业务层错误。HTTP 200但code: 500的意思是后端处理逻辑报错了HTTP 400才是真正的请求本身问题。4.2 拦截器、Proxy和Service Worker三层影响请求的隐形因素有些时候Network面板显示的请求和代码里写的不同这通常是三层因素造成的。第一层是拦截器。项目中全局配置的拦截器可能会修改请求头、URL或请求体。排查时先关掉自定义拦截器逻辑看原始请求是否正常。第二层是开发环境代理Proxy。Vite/webpack的proxy配置会让请求路径在本地被改写比如把/api代理到http://backend:8080。调试时要注意Network面板里显示的请求域名是后端实际地址这就是判定代理生效的依据。配了跨域代理又发现400错误建议直接看代理日志后端接到的URL是否和预期一致。第三层是Service Worker这是最隐蔽的。如果网站注册了Service Worker所有请求都可能被拦截并由缓存返回Network面板里会显示from ServiceWorker。这种环境下请求根本没有到达服务器而后端日志里也看不到该请求。排查思路是先确认站点是否注册了Service WorkerApplication面板 → Service Workers必要时可以在开发环境绕过它。4.3 常见联调问题的分类定位表根据我几年一线的联调经验整理一份“问题 → 可能原因 → 排查方向”的表单现象可能原因排查方向请求404路径拼错、代理未生效、路由前缀缺失比对URL与后端路由检查代理配置请求405方法不对用GET提交了POST接口检查方法如果是跨域OPTIONS需要后端配置允许该method请求400参数格式不对、必填字段缺失、Content-Type与Body不匹配核对Payload和后端接收方式请求401Token缺失、过期、无效检查Authorization头刷新Token请求403权限不足、IP白名单、CSRF校验失败检查角色权限确认是否漏带CSRF token请求500后端代码异常查后端日志比对入参是否合法请求超时后端耗时过长、网络异常、前端timeout过短用curl绕过前端确认后端响应时间CORS报错后端未配置跨域、预检请求失败检查Access-Control-Allow-Origin/Headers/Methods配置这套表格我每次面试前端也会聊到核心思想是HTTP请求问题永远先看“实际发的是什么”再推测“应该是怎么处理”最后对照“后端收到的是什么”。三方对上了问题就浮出水面了。5. 从HTTP规范到前端边界同源策略、跨域与预检请求的前端视角请求组成离不开“谁允许我发”的规则。这部分是浏览器安全模型的核心也是前端面试必问题目。我从请求组成的角度来讲重点是理解实际请求背后浏览器做了什么额外的动作。5.1 同源策略限制的到底是什么同源策略规定协议、域名、端口三者都相同才属于同源。前端代码发起请求时浏览器默认只允许请求同源资源。这不是HTTP协议本身的限制——HTTP协议并不关心你从哪来它可以正常传输请求。这是浏览器基于安全考虑加的客户端限制。前端发请求到不同源时请求实际到达了服务器服务器也正常处理了只是浏览器在拿到响应后被拦截了然后报一个CORS错误。所以很多年前端都在问“为什么后端明明返回了数据我在浏览器里看不到”。这个问题的本质就是浏览器拦截了跨域响应。因此排查跨域问题时应对方案要么由后端加响应头声明“我允许这个来源访问”要么由前端启动代理绕开浏览器的同源检查要么在开发阶段用浏览器插件临时关闭同源策略后者只适合本地调试。5.2 简单请求与预检请求OPTIONS请求为什么存在跨域请求分两种简单请求和预检请求。简单请求是指满足以下条件之一的GET、HEAD、POSTContent-Type只能是application/x-www-form-urlencoded、multipart/form-data或text/plain且不带自定义请求头。这类请求浏览器直接发出不经过预检。其余请求比如Content-Type为application/json、携带自定义Header、使用PUT等方法都会触发预检——浏览器先发一个OPTIONS请求询问服务器“我接下来的请求你是否允许”服务器响应允许后浏览器才发真正的请求。很多后端同事不理解为什么前端老是发OPTIONS请求。这个问题的根源在于预检机制。后端需要在OPTIONS请求上返回正确跨域响应头即ACAM等字段。前端排查CORS问题时如果Network里只有OPTIONS没有后续请求多半是预检没通过——后端没有对OPTIONS返回正确的响应头或返回了非2xx状态码。注意预检请求本身不带业务数据它只是“探路”。也就是说在跨域POST场景下你会发现后端打印的访问日志里多了一堆OPTIONS请求但实际业务请求只有一条。这是正常现象不是入侵。5.3 实际项目中的跨域方案选择开发环境和生产环境的跨域处理方式不同。开发环境用vite/server的proxy代理是最省事的前端配置// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: https://api.example.com, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ), }, }, }, });这样浏览器里的请求是同源的都指向localhost:5173由Vite在服务端转发到目标域名规避了跨域。注意changeOrigin: true的作用是让后端看到的Host头变成目标域名否则有些后端会拒绝请求。生产环境一般靠后端配CORS响应头解决前端不需要也不能在代码层面绕过。设计后端CORS头时Access-Control-Allow-Origin是重点如果前端带了自定义header如Authorization还要配合Access-Control-Allow-Headers声明允许携带的请求头。6. 请求大小限制与性能优化从请求组成角度谈数据瘦身请求组成里的最大隐私是很多前端根本不知道自己发出去的数据有多大也没想过怎么优化。我见过一个页面打开时同时发十几个请求每个请求还带了一整套用户信息对象——数据量完全是浪费的。这一部分来讲请求组成的“物理极限”和优化策略。6.1 常见限制请求头大小与请求体大小服务器对请求头和请求体一般都有大小限制。以Nginx默认值为例large_client_header_buffers默认是8KB4个4KBclient_max_body_size默认是1MB。超过这个值Nginx会直接返回413 Request Entity Too Large。前端在请求设计时要注意几个点不要把大对象丢进query。有一个需要避免的操作是把整个筛选条件对象序列化后放进URL筛选条件多时URL很容易超过几KB。不要重复携带冗余信息。比如每一个请求都带上完整的用户信息对象而不是只带user_id。请求体超过服务器限制时需要考虑分片上传方案或者协商后端调整client_max_body_size。6.2 从HTTP请求头的角度做性能优化减少体积、合理使用缓存前端请求性能优化的核心思路是“少发、发小”。从这个角度出发有几个和请求头相关的优化策略。合并请求。几个独立的接口如果可以合并成一个聚合接口可以减少请求头重复携带的固定开销每个请求都有Cookie、Authorization等头虽然单个不大但请求多了累积就很可观。当然这不是万能的需要权衡并行度和聚合度的关系。合理使用缓存头。浏览器会自动根据响应里的Cache-Control决定是否缓存。可缓存的GET请求发送一次后第二次直接命中本地缓存不再发出网络请求。这在Network面板显示为from disk cache或from memory cache。前端可以做的是为动态API设置合适的no-cache策略为静态资源设置长缓存并配合hash文件名避免重复下载。压缩请求体。目前普遍支持gzip/brotli压缩的是响应体请求体压缩并不常见需要服务器支持。项目中如果确实有非常大的POST请求可以考虑用浏览器端的CompressionStream压缩后再传输但这属于进阶优化一般项目用不到。6.3 请求放弃和取消前端如何优雅地终止一次请求请求发出后有时用户已经离开了页面或者搜索框里输入了新的关键词此时之前的请求还在跑它既浪费带宽又可能在返回后污染当前页面状态。正确的做法是取消它。axios前端取消请求有经典实现const CancelToken axios.CancelToken; let cancel; axios.get(/api/search, { cancelToken: new CancelToken(c { cancel c; }) }); // 需要取消时 cancel(用户取消请求);新版axios建议用AbortControllerconst controller new AbortController(); axios.get(/api/search, { signal: controller.signal, }); controller.abort();在React/Vue组件里我习惯在unmount或beforeUnmount时取消所有未完成的请求。注意取消请求不等同于关闭网络连接它只是让前端不再等待该请求的响应同时释放回调。请求如果已经到达服务器后端依然会处理——所以取消请求前端性能优化的一部分不要把它当作后端的“真正终止”。这里再分享一个关于请求并发控制的小技巧如果页面需要同时请求多个接口但网络带宽有限可以用Promise.all并行但限制并发数。前端没有自带的并发限制API我通常封装一个简易的异步Queue整理成一个带缓存的应用式发布订阅调度器限制最大并发并复用离线数据。这在数据看板这类页面同时拉取十几个图表数据非常有效。7. 关于请求组成的三个不常见但必须知道的细节写到这里正文主体已经足够详实。我最后补充三件我在实际工作中踩过坑、在常规文档里很少被写透的细节。7.1 Content-Type为application/x-www-form-urlencoded时Body的正确编码方式axios里这样传const params new URLSearchParams(); params.append(username, 张三); params.append(age, 18); axios.post(/api/user, params); // axios检测到URLSearchParams自动设置Content-Type为x-www-form-urlencoded注意不要传普通对象否则axios会把它序列化成JSON格式而Content-Type却还是表单形式后端按表单解析会取不到值。我见过有人手动设置Content-Type: application/x-www-form-urlencoded但body传的是一个JSON字符串结果后端ASP.NET、Spring都解析失败。现在的做法是构造好URLSearchParams再交给axios让框架自动做正确的处理。7.2 请求头Authorization和Cookie同时存在时的优先级如果服务器同时支持Authorization头和Cookie很多后端框架会先读Authorization再读Cookie但不同框架可能有差异。前端实际做单点登录时常见做法是优先使用一个避免两套逻辑都维护。我的建议是选Authorization头方案因为它在跨域请求中靠credentials控制显式传递更可靠Cookie在跨域时还需要设置SameSiteNone; Secure容易踩兼容性坑。另外Cookie方案在移动端原生webview里偶尔出现自动携带失败的问题而Authorization头完全由前端控制不会出现这种“带不上去”的情况。7.3 请求异常时如何区分“请求从未发出”和“响应异常”Fetch异常可能是网络层、HTTP层和业务层的错误这在控制台的报错信息里有明确的区分。网络层错误DNS解析失败、连接拒绝、断网表现为fetch本身reject错误说TypeError: Failed to fetchHTTP状态码错误需要手动检查response.ok和response.status业务错误则是HTTP 200但code非0。定位时建议用这个顺序排除网络层 → HTTP层 → 业务层。不是所有报错都值得前端处理网络层交给全局兜底业务层交给具体页面。在我实际处理的几起线上事故中很多看起来是“后端没返回数据”的问题最后定位到是前端网络层请求根本没发出去——比如浏览器拦截了跨域、Service Worker吞了请求、或者请求代理解析错了URL。所以排查异常的第一步永远是打开Network面板确认请求是否真实发出发出后状态码是多少Body内容是否符合预期。这个习惯养成了你会发现自己排查Bug的速度比同事快一半以上。HTTP请求组成这件事听起来很基础工作中全是用得上的。从组装的四个部分到发起的各种方式再到工程化的封装和排查链路整个知识体系就像一张地图——有了地图你才可能在各种网络问题面前不走弯路。
返回列表