1. 这不是技术演进史,而是一份前端网络请求的“生存指南”
你写过多少次axios.get('/api/user')?又在控制台里见过多少次Failed to fetch或CORS error?这些报错背后,从来不是某一行代码写错了,而是你对浏览器和服务器之间那条看不见的“数据通道”理解得不够深。我做前端开发十二年,从 jQuery 时代手写$.ajax({ type: 'POST', url: '/login', data: { u: 'a', p: 'b' } })开始,经历过 XHR 手动管理状态、Fetch API 初期兼容性踩坑、Axios 拦截器被滥用导致内存泄漏,再到如今在 Next.js App Router 中用原生fetch做服务端数据获取——每一次技术切换,表面是 API 变了,本质是开发者对“如何可靠、可控、可维护地发起一次 HTTP 请求”的认知在升级。
标题里说的“前世今生”,不是怀旧,而是复盘:Ajax 不是某个库,而是一种异步通信思想;XHR 不是 Fetch 的前身,而是浏览器最早暴露给 JS 的底层网络能力封装;Fetch 不是 Axios 的替代品,它更接近curl的语义,而 Axios 是为业务场景定制的“HTTP 工具箱”。热搜词里反复出现的cors policy、failed to fetch、axios devserver 转发、nextjs 使用原生 fetch 封装拦截器好还是 axios 拦截器,全指向一个现实问题:当请求失败时,你第一反应是查文档,还是查自己对协议、浏览器机制、框架约束的理解盲区?
这篇文章不讲“谁更好”,只讲“在什么场景下,为什么必须用这个、不能用那个”。我会带你从最原始的XMLHttpRequest.open()开始,一层层剥开:为什么GET请求参数要手动拼接 URL 而POST要设置Content-Type;为什么fetch默认不带 cookie 而axios默认带;为什么 Vue 项目里devServer.proxy能解决跨域而生产环境必须后端配合;为什么 Next.js App Router 强制要求fetch的cache和next配置直接影响 SSR 渲染结果。所有内容都来自真实项目现场——比如上周刚上线的 SaaS 后台,因axios拦截器里没处理AbortController导致页面跳转后请求仍在后台执行,拖慢了新页面加载;再比如客户投诉 PDF 预览白屏,排查发现是pdf.jsv2.16.105 内部调用fetch时未处理signal,导致大文件下载超时后无法取消,最终阻塞整个 Worker 线程。
如果你正被Access to fetch at 'xxx' from origin 'yyy' has been blocked by CORS policy折磨,或纠结于“该不该在 Next.js 里弃用 Axios”,或面试官问你“fetch和XHR的核心区别到底是什么”,那么这篇内容就是为你写的。它不教你怎么复制粘贴代码,而是帮你建立一套判断逻辑:看到报错,能立刻定位到是协议层(HTTP)、运行时层(浏览器/Node)、框架层(Vue/Next)还是应用层(业务逻辑)的问题。接下来的内容,每一节都对应一个真实战场。
2. 核心设计思路:为什么前端网络请求工具会不断迭代?
2.1 本质没变,变的是“人”与“机器”的协作方式
很多人误以为 Ajax → XHR → Fetch → Axios 是一条线性进化链,仿佛旧技术被淘汰了。事实恰恰相反:XHR 至今仍是浏览器唯一原生支持的底层网络接口,Fetch 是基于 Promise 对 XHR 的语法糖封装,Axios 是基于 XHR 或 Fetch 的上层业务适配器。它们共存,是因为解决的问题维度不同:
- XHR 解决“能不能发”:提供
open()、send()、onreadystatechange等原始方法,让你能控制连接建立、发送、接收全过程,但需手动处理状态码、超时、重试、错误分类。 - Fetch 解决“好不好写”:用 Promise 替代回调,用
Response对象统一处理响应体,用Headers对象管理头信息,但默认不带 cookie、不自动解析 JSON、不支持请求取消(需AbortController)。 - Axios 解决“方不方便用”:内置请求/响应拦截器、自动 JSON 序列化、超时重试、CSRF token 自动注入、取消重复请求等业务高频需求,但引入额外包体积、隐藏了底层细节,调试时容易迷失在拦截器链条中。
这种分层不是偶然。我参与过三个大型政企系统迁移:第一个用 jQuery.ajax,第二个用原生 Fetch,第三个用 Axios 封装。迁移动力从来不是“新技术更先进”,而是团队能力与项目复杂度的匹配度变化。jQuery 时代,前端工程师常兼做后端接口联调,需要细粒度控制每个请求头;Fetch 初期,团队开始重视代码可读性,async/await让异步逻辑更线性;Axios 普及后,业务模块增多,需要统一处理登录态、错误弹窗、loading 状态,拦截器成了刚需。所以选型逻辑很朴素:当你的团队开始为“减少重复代码”付出比“理解原理”更高的成本时,就该上 Axios;当你发现拦截器里堆了 20 行逻辑却查不出 401 错误原因时,就该回溯到 Fetch 甚至 XHR 查问题。
2.2 浏览器能力演进倒逼 API 设计重构
Fetch 的诞生直接源于浏览器厂商对 Web 平台标准化的推动。2015 年前,XHR 存在严重缺陷:
- 状态管理混乱:
readyState有 0~4 五个值,但4不代表成功(需检查status),200也不代表业务成功(可能返回{ code: 500, msg: '系统繁忙' }); - 二进制处理笨重:读取图片需
responseType = 'arraybuffer',再手动转Blob,而 Fetch 直接支持response.blob()、response.arrayBuffer(); - 流式处理缺失:XHR 无法分块读取大文件,Fetch 提供
response.body.getReader()实现流式解析,这对 PDF.js、音视频播放器至关重要。
而 Axios 的流行,则源于 Node.js 生态的成熟。早期前端发请求只能靠浏览器,Node.js 出现后,axios同时支持浏览器和 Node 环境,让同构渲染(如 Next.js)成为可能。它的create()方法生成实例,配合defaults设置 baseURL、timeout,完美适配微前端架构——主应用统一配置,子应用继承,避免每个模块重复写https://api.xxx.com/v1。这背后是工程化思维的胜利:不是 API 更好,而是它让“配置即代码”落地了。
2.3 框架约束重塑请求范式
Vue 和 Next.js 对网络请求的影响常被低估。Vue 2 的vue-resource已淘汰,Vue 3 官方推荐组合式 API +fetch,因为ref响应式系统天然适配async/await;而 Next.js App Router 更激进:它要求fetch必须在 Server Component 中调用,且强制cache: 'no-store'或revalidate配置,否则会触发静态生成(SSG)而非服务端渲染(SSR)。这意味着你在useEffect里用axios获取用户数据,在 Next.js 中是反模式——数据应在服务端获取并传入组件,避免客户端水合(hydration)时的闪烁。
我曾帮一家电商客户优化首页加载:他们用 Vue 3 + Pinia,在setup()中await axios.get('/api/home'),首屏白屏 2s。改成async setup()+await fetch后,Next.js 自动将请求提升到服务端,首屏时间降至 300ms。这不是 Fetch 比 Axios 快,而是框架利用 Fetch 的声明式特性(cache、next)做了编译时优化。所以选型必须看框架文档:Vue CLI 项目用 Axios 无压力;Vite + Vue 3 可用 Fetch;Next.js App Router 必须用 Fetch;而 Taro 多端框架则推荐taro.request——它底层根据平台自动选择 XHR 或 Fetch,开发者无需关心。
3. 核心细节解析:从 XHR 到 Fetch,每一步都在解决什么痛点?
3.1 XHR:手把手教你“造轮子”的必经之路
尽管现在很少直接写 XHR,但理解它能让你一眼看穿所有高级封装的底层逻辑。下面这段代码,是我当年在银行系统里处理文件上传的真实片段:
const xhr = new XMLHttpRequest(); xhr.open('POST', '/upload', true); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.upload.addEventListener('progress', (e) => { const percent = (e.loaded / e.total) * 100; console.log(`上传进度: ${percent.toFixed(2)}%`); }); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300) { const res = JSON.parse(xhr.responseText); console.log('上传成功:', res); } else { console.error('上传失败:', xhr.status, xhr.statusText); } } }; xhr.send(formData);关键点解析:
open(method, url, async)的async参数必须为true,否则阻塞主线程(已废弃,但老系统仍存在);setRequestHeader手动设置头,Content-Type由send()的参数类型自动决定:send(string)为text/plain,send(FormData)为multipart/form-data,send(JSON.stringify(obj))需手动设application/json;upload对象监听上传进度,download对象监听下载进度(Fetch 无此能力,需用ReadableStream手动实现);onreadystatechange回调中,readyState === 4仅表示请求完成,必须检查status才能确认 HTTP 成功,这是新手最常踩的坑——status为 0 通常意味着跨域被拦截或网络断开。
提示:XHR 的
responseType支持'text'、'json'、'arraybuffer'、'blob'、'document'。设为'json'时,response直接是解析后的对象,但 IE10 不支持,需降级为JSON.parse(xhr.responseText)。
3.2 Fetch:Promise 化带来的范式革命
Fetch 的核心价值是用函数式编程思想重构请求流程。对比 XHR,它把“发起请求”和“处理响应”彻底解耦:
// XHR 风格:请求与响应处理混杂 xhr.send(data); xhr.onreadystatechange = () => { /* 处理逻辑 */ }; // Fetch 风格:请求即返回 Promise,链式处理 fetch('/api/user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 1 }) }) .then(res => { if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`); return res.json(); // 注意:res.json() 本身也返回 Promise! }) .then(data => console.log(data)) .catch(err => console.error('请求失败:', err));这里藏着三个关键设计:
res.ok判断代替status >= 200 && status < 300:ok为true当且仅当status在 200-299,语义更清晰;res.json()、res.text()、res.blob()等方法返回 Promise:避免手动JSON.parse(),且自动处理编码(如 UTF-8 BOM);headers用Headers对象管理:支持append()、set()、get(),比字符串拼接更安全。
但 Fetch 的“简洁”是有代价的:
- 默认不带 cookie:需显式设置
credentials: 'include',否则跨域请求无法携带 session; - 没有超时控制:需配合
AbortController实现,fetch(url, { signal: AbortSignal.timeout(5000) })是 Chrome 107+ 新增的便捷写法; - 错误处理陷阱:
fetch只在网络错误(如 DNS 失败、断网)时 reject,HTTP 状态码 404、500 仍 resolve,必须手动if (!res.ok) throw。
注意:
AbortController是现代前端必备技能。我在一个实时监控大屏项目中,用户切换仪表盘时需取消上一个图表的请求。用AbortController的abort()方法,比在 Axios 拦截器里维护 cancelToken 数组更直观:“创建 controller → 传入 signal → 切换时 controller.abort()”。
3.3 Axios:为业务场景而生的“瑞士军刀”
Axios 的优势在于它把开发者从协议细节中解放出来,专注业务逻辑。以下是一个典型的企业级配置:
const api = axios.create({ baseURL: 'https://api.example.com/v1', timeout: 10000, headers: { 'X-App-Version': '2.3.1' } }); // 请求拦截器:添加 token、日志 api.interceptors.request.use( config => { const token = localStorage.getItem('token'); if (token) config.headers.Authorization = `Bearer ${token}`; console.log(`[REQ] ${config.method} ${config.url}`); return config; }, error => Promise.reject(error) ); // 响应拦截器:统一错误处理、自动刷新 token api.interceptors.response.use( response => response.data, // 直接返回 data,省去 .data async error => { const originalRequest = error.config; if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; const newToken = await refreshToken(); localStorage.setItem('token', newToken); originalRequest.headers.Authorization = `Bearer ${newToken}`; return api(originalRequest); // 重试原请求 } return Promise.reject(error); } );这个配置解决了四大痛点:
- 请求地址统一管理:
baseURL避免硬编码; - 超时与重试:
timeout防止请求挂起,拦截器实现 401 刷新 token; - 响应体标准化:
response.data直接返回业务数据,无需res.data.data嵌套; - 错误分类处理:网络错误、HTTP 错误、业务错误(如
{ code: 1001, msg: '余额不足' })可分层捕获。
但 Axios 的“便利”也带来风险:
- 内存泄漏:拦截器中若引用了组件实例(如
this.$message.error()),组件卸载后请求仍在执行,导致报错; - 过度封装:
axios.get(url, { params: obj })会自动序列化参数,但obj = { a: [1,2], b: null }时,paramsSerializer默认行为是a[]=1&a[]=2&b=,而某些后端要求a=1&a=2&b=,需自定义序列化函数; - Tree-shaking 无效:Axios 是类库而非模块,Webpack 无法摇掉未用方法,gzip 后约 14KB,对性能敏感项目需权衡。
4. 实操过程:从零搭建一个可落地的请求方案
4.1 场景还原:一个需要兼顾 Vue 3 和 Next.js 的中后台系统
假设你正在开发一个 SaaS 管理后台,前端用 Vue 3(SPA 模式),同时需为 SEO 优化提供 Next.js 版本。API 由 Spring Boot 提供,要求:
- 所有请求带
Authorizationtoken; - 登录态失效时自动跳转登录页;
- 文件上传需显示进度;
- Next.js 页面需支持服务端数据获取;
- 错误统一展示 toast 提示。
方案设计原则:不追求“一套代码跑两端”,而是“一套逻辑适配两端”。核心是抽象出request接口,Vue 端用 Axios 实现,Next.js 端用 Fetch 实现,业务代码只调用request.get()。
Vue 3 端实现(基于 Axios)
// utils/request.js import axios from 'axios'; const service = axios.create({ baseURL: import.meta.env.VUE_APP_API_BASE_URL || '/api', timeout: 10000 }); // 请求拦截器 service.interceptors.request.use( config => { const token = localStorage.getItem('token'); if (token) config.headers.Authorization = `Bearer ${token}`; // 添加 loading 状态(配合全局 loading 组件) if (config.showLoading !== false) { window.dispatchEvent(new Event('request-start')); } return config; }, error => Promise.reject(error) ); // 响应拦截器 service.interceptors.response.use( response => { window.dispatchEvent(new Event('request-end')); // 假设后端统一返回 { code: 0, data: {}, msg: '' } if (response.data.code !== 0) { ElMessage.error(response.data.msg || '请求失败'); if (response.data.code === 401) { localStorage.removeItem('token'); router.push('/login'); } return Promise.reject(new Error(response.data.msg)); } return response.data.data; }, error => { window.dispatchEvent(new Event('request-end')); if (error.code === 'ECONNABORTED') { ElMessage.error('请求超时,请重试'); } else if (error.response?.status === 0) { ElMessage.error('网络连接失败,请检查网络'); } else { ElMessage.error(error.response?.data?.msg || '系统异常'); } return Promise.reject(error); } ); export default service;关键实操心得:
window.dispatchEvent触发自定义事件,由全局loading组件监听,避免在每个 API 调用处写loading.value = true;showLoading: false用于导出 Excel 等无需 loading 的场景,通过配置项开关;response.data.code !== 0判断业务错误,比status更贴近实际需求;router.push('/login')跳转前需localStorage.removeItem('token'),否则下次进入仍尝试带无效 token。
Next.js App Router 端实现(基于 Fetch)
// app/lib/request.ts 'use server'; // Next.js Server Action 中的 fetch 需指定 cache 和 next export async function request<T>( url: string, options: RequestInit = {} ): Promise<T> { const token = cookies().get('token')?.value; const headers = new Headers(options.headers || {}); if (token) headers.set('Authorization', `Bearer ${token}`); try { const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}${url}`, { ...options, headers, cache: 'no-store', // 禁用缓存,确保实时数据 next: { revalidate: 0 } // 同上,强制不缓存 }); if (!res.ok) { const errorData = await res.json(); throw new Error(errorData.msg || `HTTP ${res.status}`); } const data = await res.json(); // 同样假设后端返回 { code: 0, data: {}, msg: '' } if (data.code !== 0) { throw new Error(data.msg || '业务错误'); } return data.data as T; } catch (error) { // 服务端错误需转换为客户端可识别格式 if (error instanceof Error) { throw new Error(`[Server] ${error.message}`); } throw new Error('网络请求失败'); } } // 客户端组件中使用(需 use client) 'use client'; import { request } from '@/app/lib/request'; export default function UserList() { const [users, setUsers] = useState([]); useEffect(() => { request('/users').then(setUsers).catch(console.error); }, []); return <div>{/* 渲染列表 */}</div>; }关键实操心得:
'use server'标识 Server Action,cookies()只能在服务端调用;cache: 'no-store'和next: { revalidate: 0 }必须同时设置,否则 Next.js 可能缓存响应;request函数返回Promise<T>,TypeScript 类型推导让const users = await request<User[]>('/users')有完整类型提示;- 客户端组件中调用
request时,需注意useEffect的依赖数组,避免无限请求。
4.2 跨域问题实战:从开发到生产的全链路解决方案
Access to fetch at 'xxx' from origin 'yyy' has been blocked by CORS policy是前端最常见报错,但根源不在前端。我总结出三类场景及解法:
| 场景 | 原因 | Vue 开发环境解法 | Next.js 生产环境解法 | 后端必须配置 |
|---|---|---|---|---|
开发时 API 与前端域名不同(如localhost:3000调localhost:8080) | 浏览器同源策略拦截 | Vue CLI:vue.config.js中devServer.proxy;Vite:vite.config.ts中server.proxy | Next.js:next.config.js中rewrites代理到本地 API | 后端Access-Control-Allow-Origin设为*或具体域名 |
生产环境前后端分离部署(如app.example.com调api.example.com) | 同源策略生效,且credentials: 'include'时Allow-Origin不能为* | 无解,必须后端配置 | 同上 | Access-Control-Allow-Origin设为https://app.example.com;Access-Control-Allow-Credentials: true |
| 第三方 API(如微信支付回调) | 后端无法修改 CORS 头 | 前端无法直连,需走代理 | Next.js Server Action 中调用,绕过浏览器 CORS | 无解,需后端中转 |
Vue 开发代理配置实操:
Vite 项目中vite.config.ts:
export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true, // 修改 Origin 头 rewrite: (path) => path.replace(/^\/api/, '') // 去掉 /api 前缀 } } } });这样fetch('/api/users')实际请求http://localhost:8080/users,浏览器认为是同源。
Next.js 生产代理配置实操:next.config.js:
module.exports = { async rewrites() { return [ { source: '/api/:path*', destination: 'https://api.example.com/:path*' } ]; } };访问/api/users时,Next.js Server 自动转发到https://api.example.com/users,CORS 由 Next.js 服务端发出请求,不受浏览器限制。
提示:
changeOrigin: true在 Vite 中是必需的,否则后端收到的Origin头仍是localhost:3000,导致Allow-Origin匹配失败。很多团队卡在这里一周,只因漏了这一行。
4.3 文件上传与进度监控:XHR 仍是不可替代的方案
尽管 Fetch 支持body: FormData,但上传进度监控必须用 XHR。原因在于 Fetch 的ReadableStream无法获取上传字节数,而 XHR 的upload.onprogress提供e.loaded和e.total。以下是 Vue 3 中的实操封装:
<script setup> import { ref } from 'vue'; const fileInput = ref(null); const uploadProgress = ref(0); const handleUpload = async () => { const file = fileInput.value.files[0]; if (!file) return; const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/upload'); xhr.upload.onprogress = (e) => { if (e.lengthComputable) { uploadProgress.value = Math.round((e.loaded / e.total) * 100); } }; xhr.onload = () => { if (xhr.status === 200) { const res = JSON.parse(xhr.responseText); console.log('上传成功:', res); uploadProgress.value = 0; } }; xhr.onerror = () => { console.error('上传失败'); uploadProgress.value = 0; }; const formData = new FormData(); formData.append('file', file); xhr.send(formData); }; </script> <template> <input type="file" ref="fileInput" @change="handleUpload" /> <div v-if="uploadProgress > 0"> 上传进度: {{ uploadProgress }}% <progress :value="uploadProgress" max="100"></progress> </div> </template>关键点:
xhr.upload.onprogress是唯一能实时获取上传进度的 API;e.lengthComputable为false时(如 chunked 编码),e.total为 0,此时无法计算百分比;xhr.onload在请求完成时触发,xhr.status为 HTTP 状态码,xhr.responseText为响应体。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 “Failed to fetch” 的 7 种真实原因及定位方法
Failed to fetch是 Fetch 的通用错误,但背后原因千差万别。我整理了线上项目中最常遇到的 7 种情况,附带快速定位步骤:
| 错误现象 | 根本原因 | 定位方法 | 解决方案 |
|---|---|---|---|
Failed to fetch且控制台无其他日志 | 网络完全中断(如 WiFi 关闭) | 检查浏览器地址栏是否能打开其他网站;navigator.onLine返回false | 提示用户检查网络,禁用请求按钮 |
Failed to fetch伴随TypeError: Failed to fetch | 跨域请求被拦截(CORS) | 查看 Network 面板,请求状态为(blocked);Preview 为空 | 后端配置Access-Control-Allow-Origin |
Failed to fetch且请求未出现在 Network 面板 | AbortController.abort()被调用 | 在代码中搜索abort();检查路由切换、组件卸载逻辑 | 在useEffect清理函数中调用abort() |
Failed to fetch且fetch调用后立即报错 | URL 协议错误(如http://调用https://API) | 检查fetch的 URL 字符串,确认协议一致 | 统一使用https,或配置反向代理 |
Failed to fetch且response.body为空 | 后端返回空响应体(如 204 No Content) | Network 面板查看 Response Headers,确认Content-Length: 0 | fetch后检查res.status !== 204再调用res.json() |
Failed to fetch且signal超时 | AbortSignal.timeout()时间过短 | 检查timeout参数,对比后端平均响应时间 | 将 timeout 设为后端 P95 响应时间的 1.5 倍 |
Failed to fetch伴随net::ERR_CONNECTION_REFUSED | 后端服务未启动或端口错误 | ping后端域名;telnet host port测试端口连通性 | 检查后端进程、防火墙、Docker 网络配置 |
实操案例:某次上线后大量用户反馈“保存失败”,错误日志全是Failed to fetch。我首先在 Network 面板过滤fetch请求,发现所有失败请求的 URL 都是http://api.xxx.com/save,而生产环境应为https。追查代码发现,环境变量VUE_APP_API_BASE_URL在.env.production中误写为http://,修复后问题消失。教训:环境变量必须用console.log打印验证,不能只信文档。
5.2 Axios 拦截器的三大致命陷阱
Axios 拦截器是双刃剑,用得好提升开发效率,用不好引发线上事故。我踩过的坑:
陷阱一:拦截器中修改config导致后续请求失效
// ❌ 错误:直接修改 config.headers service.interceptors.request.use(config => { config.headers['X-Trace-ID'] = uuid(); // 每次请求覆盖 headers 对象 return config; }); // 后续请求中,config.headers 是同一个引用,可能被其他拦截器污染✅ 正确做法:
service.interceptors.request.use(config => { const headers = { ...config.headers }; // 创建新对象 headers['X-Trace-ID'] = uuid(); return { ...config, headers }; });陷阱二:响应拦截器中未处理undefined响应
// ❌ 错误:假设所有响应都有 data service.interceptors.response.use(res => res.data); // 当后端返回 204 No Content 时,res.data 为 undefined,导致后续 .then() 报错✅ 正确做法:
service.interceptors.response.use( res => { if (res.status === 204) return {}; // 显式返回空对象 return res.data; } );陷阱三:拦截器中引用组件实例造成内存泄漏
// ❌ 错误:在拦截器中使用 this.$message service.interceptors.response.use( res => res, error => { this.$message.error('请求失败'); // this 指向 Vue 实例,组件卸载后 this 仍存在 } );✅ 正确做法:
// 在入口文件中初始化 message 实例 import { ElMessage } from 'element-plus'; const message = ElMessage; service.interceptors.response.use( res => res, error => { message.error('请求失败'); // 使用独立实例,不依赖组件 } );5.3 Next.js 中 Fetch 的cache和revalidate配置详解
Next.js App Router 对fetch的缓存控制极其严格,配置错误会导致数据陈旧或 SSR 失败。以下是真实项目中的配置策略:
| 场景 | cache值 | next.revalidate | 说明 |
|---|---|---|---|
| 用户个人信息(需实时) | 'no-store' | 无 | 禁用所有缓存,每次请求都发往后端 |
| 商品列表(可容忍 30s 陈旧) | 'default' | 30 | 默认缓存,30 秒后重新验证 |
| 静态页面(如关于我们) | 'force-cache' | 无 | 强制使用缓存,即使页面重新加载 |
| 管理后台仪表盘(需实时但允许降级) | 'no-cache' | 无 | 不缓存,但允许 CDN 缓存(需后端配合) |
实操验证:在 Next.js 中,cache: 'no-store'会禁用 App Router 的所有缓存,包括 ISR(增量静态再生)。我曾为一个实时股票行情页配置cache: 'default',结果用户看到的是 1 分钟前的数据。改为cache: 'no-store'后,首屏加载变慢,但数据实时性达标。平衡点在于:对实时性要求高的页面,宁可牺牲性能也要保证正确性;对新闻列表等场景,用revalidate: 60可显著降低后端压力。
5.4 编码问题终极解决方案:ajax请求设置编码格式的真相
热搜词ajax请求设置编码格式暴露了一个长期被误解的问题。实际上:
- GET 请求的编码由浏览器自动处理:URL 中的中文会自动 encodeURIComponent,后端用
URLDecoder.decode()解码; - POST 请求的编码取决于
Content-Type:application/x-www-form-urlencoded:浏览器自动 encode,后端用request.getParameter();application/json:JSON 字符串本身是 UTF-8,无需额外设置;multipart/form-data:文件名等字段需在FormData.append()时手动 encode。
真实案例:某政府项目中,用户上传含中文名的文件,后端收到乱码。排查发现,前端用FormData.append('file', file, '测试文件.xlsx'),而 Chrome 对文件名自动 encode 为 UTF-8,但 IE11 用 GBK。解决方案是:
// 兼容 IE11:手动 encode 文件名 const fileName = encodeURIComponent(file.name); formData.append('file', file, fileName);后端用new String(fileName.getBytes("ISO-8859-1"), "UTF-8")解码。
最后分享一个小技巧:在 Vue 3 中,
<input type="file">的files[0].name在不同浏览器中编码不一致,建议统一用encodeURIComponent处理,再由后端按 UTF-8 解码。这比纠结“怎么设置 Ajax 编码”更治本。