前端萌新别慌:30分钟搞懂AJAX,告别页面刷新式开发(附实战套路)
先聊聊我自己的感受:刚入行那会儿,写页面最烦的就是“跳转”和“刷新”。只要想往服务器拿点数据,就得拼接URL跳转、提交表单刷新整页,用户填了一半的表单白填了,页面唰地一下白光一闪,体验极差。项目上线后被测试姐姐追着提Bug,说“点击按钮页面直接闪一下,数据才出来,太丑了”。我说这就是传统的同步提交嘛。她问我,能不能不刷新页面就把数据拿回来、把部分内容换上?
那时候我才意识到,AJAX不是面试题里的概念,而是“告别页面刷新式开发”的钥匙。前端萌新如果能把AJAX弄明白,等于迈过了初级到进阶之间最实在的一道坎。这篇文章不讲枯燥的理论长文,按我实际带新人总结的套路走:先建立直觉,再上手XHR和fetch,最后跑一个完整的免刷新评论小功能,把跨域、异常处理这些坑一起填了。
适合谁看?自学前端到HTML/CSS/JS基础都摸过,但一遇到“和服务器打交道”就发懵的人;也适合工作里天天写静态页面、想转现代前端开发流程的朋友。看完你不需要记住所有API,但你会形成一套处理“前后端数据交互”的思维框架。
1. 先搞明白:AJAX到底替我们省了哪些事
1.1 “刷新式开发”到底慢在哪
传统页面提交数据的时候,浏览器会把整个页面打包发给服务器,服务器处理好再返回一整张新的HTML页面,浏览器重新渲染。这个过程里有大量工作是重复劳动——同一个页面的导航栏、侧边栏、footer根本没变,却跟着整页重新加载一遍。
打个生活化的比方:你去银行办业务,传统方式等于每次办完一件事都得出大门、重新排队、重新进门坐到窗口,哪怕只是改个密码。AJAX则是你坐在窗口前,柜员内部系统自己帮你把信息流转好了,你眼前始终是同一个柜台,只是柜台上的内容变了。
在代码层面,经典的一顿操作长这样:form提交、location跳转、整页刷新。等到页面一多、交互一多,你会发现这三个动作让产品显得又笨又重。更现实的问题是,如果表单校验失败、服务器报错,用户原来填的内容全丢了,只能重来。这放在今天的产品标准里几乎不可接受。
1.2 AJAX的底层本质:你早就认识的HTTP
很多人觉得AJAX是个很高深的东西,其实拆开看就是两个词:Asynchronous(异步)和JavaScript + XML。它本身不是一项新技术,而是组合使用已有的浏览器能力——核心叫XMLHttpRequest,让JavaScript在后台发HTTP请求,拿到数据后只更新页面的局部节点。
HTTP请求大家其实早见过:浏览器地址栏输入网址,回车,这就是一个GET请求。AJAX做的事情一模一样,只不过不再由地址栏发起,而是由脚本发起,响应也不再是整页而是纯数据(JSON、XML或纯文本),然后由JavaScript决定如何渲染。
这一段逻辑想通以后,AJAX就褪去了神秘感:它只是把“浏览器自己做的网络请求”这个动作,交给你用代码显式控制。
1.3 同步与异步:两者差距不止“感觉上顺滑”
刚接触的时候容易把“异步”理解成“快”。其实异步的价值不是快,而是不阻塞用户的后续操作。同步请求发出后,浏览器主线程会卡住,页面像死了一样不可点、不能滚动,等响应回来了才恢复。异步请求则把发送和维护连接的过程交给浏览器后台,JavaScript继续执行,用户还能正常操作页面,响应返回后通过回调/事件再通知你。
我见过有新人为了“省事”把AJAX的async参数设为false,结果请求一慢,整个页面假死几秒钟,和刷新一样卡。从第一课开始,就要默认异步,不要把同步参数当捷径。真正的开发里,同步XHR在现代浏览器里已经被标记为过时,而且Chrome主线程上一旦使用,控制台会直接给警告。
2. 30分钟上手核心:原生XHR与fetch,二选一还是都学
2.1 原生XHR:理解AJAX底层机制的必修课
不管你现在用axios用得有多爽,我都建议先手写一遍XMLHttpRequest。它是理解浏览器网络请求模型最直观的入口。一个最基础的GET请求,代码框架如下:
const xhr = new XMLHttpRequest(); xhr.open('GET', 'https://api.example.com/posts'); xhr.onreadystatechange = function () { if (xhr.readyState === 4 && xhr.status === 200) { const data = JSON.parse(xhr.responseText); console.log(data); } }; xhr.send();发现没有,只有四个核心节点:new实例、open初始化、绑定事件、send发送。重点看readyState,它有5个值:
| readyState | 含义 | 说明 |
|---|---|---|
| 0 | 未初始化 | 已创建XHR实例,但open未调用 |
| 1 | 已打开 | open已调用,send未调用 |
| 2 | 已发送 | send已调用,响应头已接收 |
| 3 | 接收中 | 响应体下载中 |
| 4 | 完成 | 响应体接收完毕,可安全使用 |
实战中你只需要关心readyState === 4,它表示请求彻底结束。之后再看status,200是成功,404是资源不存在,500是服务器内部错误。只认4和200这两个数字,先记住再说,其他状态码后面遇到一个记一个。
我再补一个写XHR时的细节:onreadystatechange只是兼容性最稳的老写法。更清晰的是用onload事件,它天然对应“请求完成”这个语义:
const xhr = new XMLHttpRequest(); xhr.open('GET', 'https://api.example.com/posts'); xhr.onload = function () { if (xhr.status >= 200 && xhr.status < 300) { const data = JSON.parse(xhr.responseText); console.log(data); } else { console.error('请求失败:' + xhr.status); } }; xhr.onerror = function () { console.error('网络异常'); }; xhr.send();POST请求无非是多设置一个请求头和send时带数据:
const xhr = new XMLHttpRequest(); xhr.open('POST', 'https://api.example.com/comments'); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onload = function () { console.log(xhr.responseText); }; xhr.send(JSON.stringify({ content: '这是一条评论', postId: 101 }));这里有个新手最容易忽略的坑:send()接收的字符串会被当作请求体发出去,但服务器怎么解析取决于你设置的Content-Type。如果你发JSON却只写text/plain,后端大概率解析失败。前后端联调时第一次报错,先检查请求头。
2.2 fetch:更现代的写法,没那么“黑科技”
XHR写多了你会发现它啰嗦——回调嵌套、状态判断全靠手写、错误处理分散。于是浏览器后来推出了fetch API。它是基于Promise的,代码写起来直观很多,同样是GET:
fetch('https://api.example.com/posts') .then((response) => response.json()) .then((data) => { console.log(data); }) .catch((error) => { console.error('请求出错', error); });用上async/await后,更像同步代码了:
async function loadPosts() { try { const response = await fetch('https://api.example.com/posts'); if (!response.ok) { throw new Error('HTTP状态异常:' + response.status); } const data = await response.json(); console.log(data); } catch (error) { console.error('请求出错', error); } }注意一个细节:fetch只有在网络彻底失败(断网、DNS解析失败)时才走catch。HTTP 404、500这些状态码在fetch眼中不算“错误”,response依然会resolve回来,只是response.ok是false。所以别偷懒只写一个catch就完事,必须手动判断response.ok。
POST场景下,fetch要主动指定method和body:
async function postComment(content) { const response = await fetch('https://api.example.com/comments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) }); const data = await response.json(); return data; }2.3 日常开发用axios?先会原生再用封装
网上教程一上来就让装axios,我的意见相反:至少手写过XHR或fetch,再上axios。因为axios本质上也是XHR的封装,只是替你处理了请求拦截、响应拦截、超时、取消请求、全局错误提示这些重复劳动。理解底层之后,你看到axios的interceptors和config才不觉得是个黑盒。
如果你已经会了底层,那axios值得拥有。IAxios配置里最常用的几个字段:baseURL统一接口前缀、timeout超时时间、params自动拼查询字符串、transformResponse做数据预处理。但本文不展开axios细节,入门阶段我更建议把原生fetch写熟,你会发现换任何框架,思维都是通用。
3. 实战环节:做一个免刷新评论小功能(可直接套用的套路)
3.1 需求还原与页面结构
为了让你有直接的“告别刷新式开发”体感,我用一个最常见的评论功能做完整案例:用户在输入框写内容,点提交,评论不刷新页面直接出现在列表最上方。数据库操作我们不关注,后端接口用模拟返回。
先搭一个最简单的HTML结构:
<div id="comment-box"> <h3>评论列表</h3> <ul id="comment-list"> <!-- 评论动态渲染到这里 --> </ul> <textarea id="comment-input" placeholder="写下你的评论..." rows="3"></textarea> <button id="comment-btn">发布评论</button> </div>对应的JS先加载已有评论:
const listEl = document.getElementById('comment-list'); const inputEl = document.getElementById('comment-input'); const btnEl = document.getElementById('comment-btn'); async function loadComments() { const response = await fetch('/api/comments'); const comments = await response.json(); renderComments(comments); } function renderComments(comments) { listEl.innerHTML = ''; for (const item of comments) { const li = document.createElement('li'); li.textContent = item.content; listEl.appendChild(li); } } loadComments();这个loadComments的套路可以反复迁移:页面加载时先请求数据,然后渲染。后面无论做文章列表、用户列表、商品列表,都是这个结构。这就是最朴素的“数据驱动视图”。
3.2 提交评论:POST流程与局部更新
接着给按钮加提交事件,把输入内容POST给服务器,成功后不刷新页面,直接把新评论追加到列表:
btnEl.addEventListener('click', async () => { const content = inputEl.value.trim(); if (!content) { alert('评论内容不能为空'); return; } const response = await fetch('/api/comments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) }); if (!response.ok) { alert('发布失败,请稍后再试'); return; } const newComment = await response.json(); // 方法一:重新拉取全量列表(最稳) // await loadComments(); // 方法二:局部插入新评论(更快) const li = document.createElement('li'); li.textContent = newComment.content; listEl.insertBefore(li, listEl.firstChild); inputEl.value = ''; });这里有个取舍细节值得展开讲:新评论插入有两种方案。方案一是直接重新拉全量列表,简单、不会出现和服务器数据不一致的问题,但多一次请求;方案二是直接用接口返回值插入局部DOM,体验更顺滑,但如果别人同时也在评论,你的列表可能和真实数据产生偏差。入门阶段我建议先用方案一,先保证数据一致,再优化体验。等熟练以后,可以做“插入 + 后端返回完整评论对象”的组合方案。
3.3 再加一个“点赞”交互:体会局部刷新的实感
评论做完,顺手做一个点赞按钮,进一步体会“局部更新”的意义。点赞的逻辑是:点击按钮,给服务器发POST请求,响应返回最新的点赞数,然后只更新按钮上的数字,页面其他部分纹丝不动。
<button class="like-btn">document.querySelectorAll('.like-btn').forEach((button) => { button.addEventListener('click', async () => { const id = button.dataset.id; const response = await fetch('/api/posts/' + id + '/like', { method: 'POST' }); if (!response.ok) { alert('点赞失败'); return; } const result = await response.json(); button.querySelector('.like-count').textContent = result.likeCount; }); });对比一下传统模式:点击点赞 → 刷新整个页面 → 用户又重新滚动到刚才的位置。而AJAX模式下,只是按钮那个数字默默+1,用户的阅读位置丝毫不动。这个体验差别,就是“告别刷新式开发”最直观的体感。
3.4 如果不写后端代码,怎么联调实测
实战到这一步,很多人卡在一个现实问题:没有后端接口,代码跑不起来。解决方法很多,我的建议是本地起一个最简单的Node服务,或者用各类Mock工具。Node方式大致是这样:
const http = require('http'); const server = http.createServer((req, res) => { res.setHeader('Content-Type', 'application/json'); if (req.url === '/api/comments' && req.method === 'GET') { res.end(JSON.stringify([ { id: 1, content: '第一条评论' }, { id: 2, content: '第二条评论' } ])); } else if (req.url === '/api/comments' && req.method === 'POST') { let body = ''; req.on('data', (chunk) => { body += chunk; }); req.on('end', () => { const parsed = JSON.parse(body || '{}'); parsed.id = Date.now(); res.end(JSON.stringify(parsed)); }); } else { res.statusCode = 404; res.end(JSON.stringify({ message: 'Not Found' })); } }); server.listen(3000);把这个js文件用node命令跑起来,前端页面里的fetch('/api/comments')改成本地相对路径即可。这样你就能完完整整走一遍前端到后端再到渲染的链路。如果不想碰Node,也可以找任何能返回JSON的在线测试接口,把URL替换一下即可。
4. 跨域与CORS:前端实战绕不开的拦路虎
4.1 为什么请求发不出去?同源策略在拦你
本地页面打开了,fetch请求也写了,控制台却报错。错误信息里往往有CORS或者Cross-Origin Request Blocked。这就是跨域问题,是所有人碰到AJAX时的第一个大坑。
浏览器有一个安全策略叫同源策略:协议、域名、端口三者都必须一致,脚本才能读取另一个源的数据。如果你页面在http://localhost:5500,接口却在http://localhost:3000,端口不一样,理论上也算跨域。浏览器默认会禁止跨域请求发出去吗?不,请求其实发出去了,但浏览器拦截了响应,不让你的JavaScript读取。
类比一下:你写了一封信给A公司(服务器),A公司回了信放在前台(响应),但前台保安(浏览器)说你不是本公司认识的人,不让拿信。你人根本没走,可结果就是拿不到回信。
报错里还会提示你检查Access-Control-Allow-Origin响应头,那是服务器需要设置的东西:
Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type第一行表示允许任何源访问。生产环境不建议用*,尤其是涉及用户数据的接口,要精确指定域名。开发阶段为了方便可以用。这个配置如果后端不由你控制,你就只能找后端同事配合调整,前端本身没有绕过的正当手段。网上能搜到各种“关闭浏览器安全策略”的临时方案,只适合本地调试,千万别在交付项目时依赖它。
4.2 非简单请求与OPTIONS预检
实际联调时还会碰到一种情况:后端说没看到你的POST请求,但浏览器控制台有一个OPTIONS请求先出现了。这其实不是bug,而是浏览器的自动化“预检”。
当请求的Content-Type是application/json,或者带了自定义头部(比如Authorization),浏览器会认为这不是简单请求,于是先发一个OPTIONS请求问服务器:“我要用一个带JSON的POST请求,你允许吗?”服务器返回允许后,浏览器才正式发POST。
所以如果后端看到OPTIONS就头疼、直接返回405,你的JSON请求就不可能成功。前端能做的只是在看到这种报错时,心里知道这是预检流程,解决办法是后端对OPTIONS请求放行。如果你第一次接触这个概念,不用深挖实现,理解“有个前置确认动作”就够了。
4.3 开发环境代理:前端自己也能解跨域
如果你用的是Webpack Dev Server或者Vite,开发阶段还有一种优雅的方案:代理。前端页面请求同源的本地地址,由开发服务器把请求转发到真正的接口服务器。浏览器看到的请求是同源的,自然不存在CORS拦截。
Vite配置大致长这样:
// vite.config.js export default { server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } } } };前端代码里继续写fetch('/api/comments'),Vite在开发环境中会自动转发到http://localhost:3000/api/comments。这是我日常开发里最常用的方式之一,配合Mock接口特别顺手。但这只是开发阶段的便利手段,生产环境跨域仍然要后端配合CORS或网关统一处理。
5. 进阶细节:请求状态、超时、错误处理与SEO
5.1 HTTP状态码速查与分类处理
写AJAX一定要建立“状态码思维”。不要只区分200和非200,至少掌握这几类:
| 状态码 | 含义 | 前端处理方式 |
|---|---|---|
| 200-299 | 成功 | 解析数据并渲染 |
| 301/302 | 重定向 | 一般自动跟随,需关注接口是否迁移 |
| 400 | 请求参数有误 | 提示用户检查输入 |
| 401 | 未认证 | 跳转登录页或弹登录框 |
| 403 | 无权限 | 提示无权限访问 |
| 404 | 资源不存在 | 提示接口地址有误或已下线 |
| 500 | 服务器内部错误 | 提示稍后再试,排查后端日志 |
| 502/503/504 | 网关或服务器过载 | 提示服务暂不可用,做好容错 |
在实际项目里,我习惯封装一个统一的请求函数,里面集中处理所有状态码和错误分支。不要让每个业务页面都写一遍状态判断,否则代码会越写越散。例如:
async function request(url, options = {}) { const response = await fetch(url, options); if (!response.ok) { const message = handleHttpError(response.status); throw new Error(message); } return response.json(); } function handleHttpError(status) { if (status === 401) { // 跳到登录页 return '登录已过期,请重新登录'; } if (status === 404) return '请求的资源不存在'; if (status >= 500) return '服务器开小差了,请稍后重试'; return '请求失败'; }5.2 超时和请求取消:别让页面干等
fetch本身没有超时功能,但可以借助AbortController实现。Ajax请求如果一直挂着不结束,用户看到转圈图标会焦虑,体验很差。设置超时的思路是:额外启动一个定时器,到时触发取消动作。
async function requestWithTimeout(url, options = {}, timeoutMs = 10000) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { ...options, signal: controller.signal }); return await response.json(); } finally { clearTimeout(timer); } }这里有个容易忽略的心得:如果请求成功返回了,别忘了clearTimeout清掉定时器,否则定时器到点后还是会触发abort,虽然这个abort对已经完成的请求没有影响,但会产生不必要的警告。
5.3 请求竞态:旧请求回来比新请求晚怎么办
入门阶段可能遇不到这个问题,但做搜索框的实时联想功能就会碰到:用户输入“a”,发了一个请求;接着输入“ab”,又发了一个请求。可能出现“ab”的响应先回来,“a”的后回来,导致界面上显示的是旧输入对应的结果。
解决办法之一是给请求加序号,每次新请求递增序号,只有最新序号的响应才被允许渲染:
let requestSeq = 0; async function search(keyword) { const currentSeq = ++requestSeq; const results = await request('/api/search?keyword=' + keyword); if (currentSeq === requestSeq) { renderResults(results); } }这是一个非常实用的小技巧,成本极低,却能避免一类非常隐蔽的体验Bug。没有经验的人第一次碰到这种问题会摸不着头脑,因为代码逻辑上完全没问题,纯粹是响应时序导致的。现在记下来,以后遇到搜索、筛选功能时直接用。
5.4 AJAX会不会影响SEO?怎么补救
这是很多做前端的人没聊过的问题。搜索引擎的爬虫在解析页面内容时,如果内容全靠AJAX动态渲染,早期版本爬虫可能拿不到渲染后的HTML,导致页面收录不全。现代搜索引擎对JavaScript渲染的容忍度已经高了,但关键页面仍然建议做服务端渲染或预渲染。
如果你做一个内容型网站,别把所有内容都塞进AJAX。首屏核心内容尽量在服务端输出,AJAX负责次要内容和交互更新。这不算纯AJAX问题,但却是实际项目中一定会有人问到的点。我的建议是:项目初期做架构决策时,就去确认好哪些页面适合SPA、哪些适合传统服务端渲染混合,不要等上线了再回头打补丁。
6. 常见报错排查速查表与独家避坑心得
6.1 按报错现象定位问题
几年的经验浓缩成一张排查导向表,碰到问题可以照着查:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| fetch不走then,不报错也无响应 | 浏览器阻止了混合内容 | 检查页面是否HTTPS、接口是否HTTP |
| CORS报错 | 后端未设置响应头 | 找后端配Access-Control-Allow-Origin |
| 状态码400 | 请求参数格式不对 | F12看请求Payload和后端期望字段对比 |
| 状态码401 | 未登录或token失效 | 检查请求头是否带了Authorization |
| 状态码404 | 接口路径不对 | 确认baseURL和路径拼接情况 |
| 状态码500 | 后端代码异常 | 看后端日志,前端只能提示稍后重试 |
| 响应返回HTML而不是JSON | 接口地址写错被重定向到页面 | 检查response的Content-Type |
| 页面能显示但刷新后丢失 | 数据存内存未持久化 | 配合localStorage或后端数据库 |
| 请求阻塞、页面假死 | 同步请求或死循环请求 | 检查async参数、检查是否有递归触发 |
6.2 网络面板是第一现场
我平时排查AJAX问题,90%时间都花在浏览器DevTools的Network面板上。那里能看到每个请求的完整生命周期:请求头、响应头、状态码、耗时、返回体。新人最容易犯的错是瞎猜代码逻辑,其实问题就摆在Network列表里。
重点看四列:Name(请求地址)、Status(状态码)、Type(xhr/fetch)、Time(耗时)。如果状态码正常但页面数据不对,点开看Response,基本立见分晓。
6.3 不要在生产环境把API地址写死在代码里
最后分享一个实战项目的教训:早期我把后端接口域名直接用字符串拼在JS里,结果后端换域名,我灰头土脸翻遍全项目替换。正确做法是把API Base URL放在环境变量里,不同环境(开发、测试、生产)配置不同地址,代码里只引用变量。如果项目还没引入构建工具,至少统一放一个config.js文件,所有请求都从这个配置里取基础路径。
6.4 关于“优雅降级”的碎碎念
AJAX依赖浏览器环境,极端情况下(老旧浏览器、断网、脚本出错),页面可能会变成白板。我在项目里见过最稳妥的处理方式,是页面基本内容先用后端模板渲染,AJAX只做增强交互。这样即使脚本全挂,核心信息用户还能读到。前端越来越复杂,反而更要想清楚哪些数据是“核心中的核心”,用最朴素的方式先保证可用性。
写在最后的一点个人经验
把这套东西练完,你会发现“页面刷新”这个动作彻底从代码里消失了。用户停留在一个页面里,聊天、点赞、加载更多、增删改查,全都在原地完成。这种体验的跃升,不亚于从功能机换到智能手机。
我见过很多新人卡在AJAX面前,总觉得网络请求是很神秘的事情。其实把它拆开就是:发请求、等响应、更新局部DOM。多写几次,形成肌肉记忆,后面学axios、学Vue/Axios的封装、学跨域代理,全都是顺水推舟的事。如果练完还有余力,我建议再去了解一下JSONP的历史、FormData的用法、以及上传文件时如何监听进度条。把这条线摸完,你的前端基本功就算真正扎实了。