3.1 基础 Ajax 请求示例


3.1 基础 Ajax 请求示例

本节摘要:把前两章的零散知识拼成第一个能跑的完整程序。本节以"取一个用户列表并渲染到页面"为任务,分别用 XMLHttpRequest 与 Fetch 两种 API 实现完整链路——发起请求、判断结果、解析数据、渲染 DOM、处理失败,并把两种写法逐段对照,看清同一任务在两代 API 下的代码形状差异。

任务定义:从接口到页面

不搞抽象演示,做一件具体的事。页面上有一个空的列表容器,页面加载后向接口发起请求,把返回的用户数组渲染成列表;失败时在原位置给出提示。接口约定返回如下 JSON:

{ "code": 0, "data": [ { "id": 1, "name": "李雷", "email": "lilei@example.com" }, { "id": 2, "name": "韩梅梅", "email": "hanmm@example.com" } ] }

页面骨架只有一段:

<ul id="user-list"> <li class="loading">正在加载用户…</li> </ul>

注意初始状态就放了一条"正在加载"——加载态不是锦上添花,是请求代码的一部分。数据没到之前用户看到的不应该是空白。

一、Fetch 版本:现代默认

先写现代写法,二十几行内完成完整链路:

async function loadUsers() { const list = document.getElementById('user-list'); try { const res = await fetch('/api/users'); // 第一步:HTTP 层分流 Fetch 不拒绝 404 500 必须手动查 if (!res.ok) { throw new Error('服务器返回 ' + res.status); } // 第二步:解析 JSON 解析失败会抛错 落进同一个 catch const body = await res.json(); // 第三步:业务层分流 约定的 code 见 2.4 节 if (body.code !== 0) { throw new Error(body.message || '业务处理失败'); } // 第四步:渲染 list.innerHTML = ''; for (const u of body.data) { const li = document.createElement('li'); li.textContent = u.name + '(' + u.email + ')'; list.appendChild(li); } } catch (err) { // 统一收口:网络错误 HTTP 错误 业务错误 解析错误 都到这 list.innerHTML = ''; const li = document.createElement('li'); li.className = 'error'; li.textContent = '加载失败:' + err.message; list.appendChild(li); } } loadUsers();

逐段看这段代码里的设计决策。

为什么用 async 函数await 让"发请求、查状态、解析体、查业务码"四个异步步骤按同步的形状排列,一行一行读下来就是执行顺序。等价的 Promise 链版本在 3.6 节对照,那里你会看到回调嵌套如何被这个语法消解。

为什么要三步分流。这是实战代码与教程示例的核心差别:教程示例只写"成功路径",实战代码里 HTTP 层(res.ok)、解析层(res.json() 抛错)、业务层(code)三道关卡各有各的失败方式,必须层层设防。三个关卡全部收口到同一个 catch,失败处理只写一处。

为什么用 createElement 而不是拼 HTMLli.textContent = ... 把数据当纯文本插入,天然免疫 XSS——就算接口数据里藏着 <script> 也只会显示成文字。用 innerHTML 拼接字符串当然也能跑,但只要有一处数据没转义,就是注入漏洞(4.2 节展开)。默认用 textContent,确要渲染富文本时再上有白名单的净化方案,这是本教程贯彻的纪律。

为什么先清空再填充list.innerHTML = '' 先移除加载态占位,避免"加载中"和真实数据同屏。批量重建列表时这个顺序别反。

二、XHR 版本:同样的任务,另一代 API

同样的功能换 XHR 实现,对照着读最能看清两代 API 的形状差异:

function loadUsersXhr() { const list = document.getElementById('user-list'); const xhr = new XMLHttpRequest(); xhr.open('GET', '/api/users', true); xhr.responseType = 'json'; // 让浏览器替我们解析 xhr.onload = function () { if (xhr.status >= 200 && xhr.status < 300) { const body = xhr.response; // 已是对象 if (body.code !== 0) { renderError(list, body.message); return; } renderList(list, body.data); } else { renderError(list, '服务器返回 ' + xhr.status); } }; xhr.onerror = function () { // 网络层失败 renderError(list, '网络错误,请检查连接'); }; xhr.send(); } function renderList(list, users) { list.innerHTML = ''; for (const u of users) { const li = document.createElement('li'); li.textContent = u.name + '(' + u.email + ')'; list.appendChild(li); } } function renderError(list, msg) { list.innerHTML = ''; const li = document.createElement('li'); li.className = 'error'; li.textContent = '加载失败:' + msg; list.appendChild(li); }

两个版本逐项对照:

对照点 Fetch 版 XHR 版
代码组织 顺序直线(async/await) 事件回调分块
状态码检查 !res.ok 手动 onload 里判范围
网络错误 走 catch 独立的 onerror 回调
JSON 解析 res.json() 返回 Promise responseType = 'json' 预设
错误收口 一个 catch 全收 每个回调各自处理
学习重点 理解"HTTP 错误不是 reject" 理解事件挂载时机与状态语义

形状差异背后是设计哲学差异:XHR 是事件驱动("响应到了会通知你"),Fetch 是值驱动("调用返回一个将来会有值的 Promise")。后者与函数组合、错误传播的现代 JS 风格更契合,这也是它成为默认推荐的原因。但注意 XHR 版的两个细节仍有价值:onerror 把网络错误从 HTTP 错误里独立出来的语义,以及 responseType 预设让解析失败的路径也走错误事件——这两点在你用 Fetch 时需要自己动手区分,Fetch 不会替你做。

三、加上请求参数:GET 与 POST 各写一遍

真实接口很少有零参数的。GET 带查询参数(记得编码,2.1 节的坑):

const params = new URLSearchParams({ page: 2, keyword: searchInput.value }); const res = await fetch('/api/users?' + params.toString());

URLSearchParams 自动处理编码,比手工拼串再 encodeURIComponent 更不容易漏。POST 提交 JSON 则是四件套——方法、头、体、序列化,一样不能少:

const res = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: '新人', email: 'new@example.com' }) });

漏掉 Content-Type 头,服务端按默认格式解析请求体,轻则 400,重则数据进错字段。"发什么声明什么"(2.2 节)在每次写 POST 时都要默念一遍。

四、从可运行到可维护:提取一个 request 函数

上面的代码能跑,但把"分流三关卡"的逻辑散在每个调用点,第五个接口出现时就会有第五份复制粘贴。实战代码的下一步是提取统一请求函数——这也是理解 axios 这类库在做什么的最好方式:

async function request(url, options = {}) { const res = await fetch(url, { headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }, ...options }); if (!res.ok) { throw new Error('HTTP_' + res.status); // 错误码化 便于上层分流 } const body = await res.json(); if (body.code !== 0) { throw new Error(body.message || '业务失败'); } return body.data; // 调用方直接拿业务数据 } // 业务代码从此只有三行 const users = await request('/api/users?page=1'); renderList(list, users);

提取之后,业务代码不再关心 HTTP 状态码、业务码、解析顺序这些协议细节,只表达"我要什么数据、拿到之后干什么"。错误用带结构的异常抛出(这里用前缀演示,工程里通常用自定义 Error 子类携带 status 字段),上层按类型分流提示。

这个函数就是项目里请求层的雏形。后面几节会持续往它身上加东西:3.2 节加表单序列化、3.5 节加错误分类与超时、4.4 节加取消与重试、5.1 节你会看到 axios 把它做成了有拦截器与实例配置的完整产品。自手写一遍再去看库,库的每个配置项都会变得显然。

五、自测清单:你的第一个请求合格吗

把本节代码当模板用时,按这张清单自查:

  • 加载态在请求发起前就位,不是响应回来后才想起来补。
  • HTTP 状态码用范围判断,!res.ok2xx 区间,没有只写 === 200
  • JSON 解析在 try 保护内,解析失败有兜底 UI 而不是白屏。
  • 业务码(如果约定了)有检查,不是拿到响应体直接用。
  • 网络错误与 HTTP 错误有不同提示文案("网络异常"与"服务暂时不可用"该区分)。
  • 渲染用户数据用 textContent 或转义方案,没有裸 innerHTML 拼接。
  • GET 参数经过 URLSearchParams 或 encodeURIComponent,没有裸拼。
  • POST 带 Content-Type 且与体格式匹配。

八条全过,这个"简单示例"就已经领先相当一部分线上代码了。

常见疑问解答

为什么我的 fetch 代码 404 了却不进 catch

因为 Fetch 的设计只把"请求没能完成 HTTP 交换"(断网、DNS 失败、跨域拦截)当作 rejection;404、500 都是"成功完成的 HTTP 响应",由你检查 res.okres.status 自行处理。这是 Fetch 被抱怨最多的设计,也是必背的一条。

await 可以用在非 async 函数里吗

标准语境不行(顶层 await 仅限模块顶层)。事件回调里用 await,就把回调声明成 async——btn.addEventListener('click', async () => {...}) 是完全合法的写法。

请求要放在页面加载的什么时机

列表类数据放 DOM 就绪后立即发(不必等图片样式全加载完);首屏关键数据甚至可以在入口脚本最前面就发。原则是让网络往返与渲染并行,别让数据请求排队等一个它不依赖的事件。

本节要点回顾

  • 完整链路五步:加载态就位 → 发请求 → 三关卡分流(HTTP、解析、业务)→ 渲染 → 失败收口,一步不能省。
  • Fetch 与 XHR 的哲学差异:值驱动对事件驱动;Fetch 不拒绝 HTTP 错误,XHR 的 onload/onerror 语义更天然分区。
  • 安全渲染是默认项:textContent 优先,innerHTML 必须配合净化。
  • GET 参数走 URLSearchParams,POST 四件套齐全,头与体永远匹配。
  • 尽早提取统一 request 函数,让业务代码回归"要什么、干什么",这也是读懂 axios 类库的路径。
  • 用八条自测清单检验第一段请求代码,从可运行升级到可维护。

练习项目:做一个"接口健康检查"小工具

学完本节最好的巩固方式,是做一个真实有用的小东西。需求:页面上有一个输入框和一个按钮,输入任意接口地址,点击后请求它并把结果以结构化卡片展示——状态码、耗时、响应头摘要、响应体(格式化后)。这个不到百行的小项目,会把本节的每个知识点都逼着用一遍。

拆解一下实现要点。耗时测量用两次时间戳相减,包在 try finally 里保证失败也有记录;更精细的做法用性能接口的条目数据,但 Date 差值对工具场景足够。状态码分流用本节的三关卡:网络层失败显示"无法连接"、非 2xx 显示状态码与状态文本、2xx 再尝试解析。响应体处理分两路:Content-Type 含 json 的格式化展示(解析后缩进两个空格再显示),其余按文本展示——顺带处理解析失败(可能是个长得像 JSON 的错误文本)的兜底。安全展示响应体一律 textContent 放进代码块元素,杜绝把响应当 HTML 渲染的 XSS 面(本工具会被人拿去请求各种地址,这条尤其重要)。按钮防重沿用 3.2 节的状态机。

做完基础版,可以按兴趣加料:加一个历史记录列表(把查过的地址存本地,每次点击复用统一请求函数,练封装);加一个对比模式(同一地址连发三次,展示三次耗时,直观感受网络波动);加一个断网模拟(配合开发者工具的离线模式,观察你的错误分支是否都接住了)。每一项加料都在复用本节的骨架——骨架搭对了,加功能就是抄自己写过的代码

这个小工具的另一个价值是"排障伙伴":以后怀疑某个接口有问题,先拿它查一下,状态码、头部、耗时、响应体一目了然——比直接看代码猜快得多。给自己造工具,是工程师和操作员的分水岭之一。

补充:渲染函数的细节打磨

基础示例里那段十行的渲染函数,在真实项目里通常还要再打磨一轮,这里把打磨点列出来供对照。第一条,行数据的空值处理:姓名或邮箱缺失时直接拼接会显示"undefined",要么给缺省值要么给占位样式,3.5 节的 normalize 思想提前用上。第二条,长文本截断:邮箱超长会撑破布局,样式层做省略号截断,但更稳的是渲染层控制结构(外套容器限制宽度)。第三条,构建方式的封装:把"一行数据变一个元素"的逻辑抽成独立函数后,列表渲染就变成 map 加 append 的两行主流程——这个抽取出来的行构建函数,正是后续加排序、过滤、虚拟滚动时的复用单元。第四条,XSS 的最后自查:行构建函数里所有来自接口的字符串都经过 textContent 而不是字符串拼接进 HTML,一眼扫过去没有拼角括号的写法,就可以放心提交。

再补一个很多人忽略的行为细节:渲染之前先确认容器存在且可见。轮播里不可见的面板、被条件隐藏的区块,对它们渲染数据虽然不报错,但用户感知不到更新,容易形成"数据到了界面没变"的误报。渲染函数开头加一个存在性检查与必要的显隐切换,是把"能跑"写成"可信"的小动作。这些打磨单看都不难,合起来就是教程代码与生产代码的差距所在——生产代码的本质,是把所有"一般情况下不会出问题"的地方也照顾到。把这段打磨过的渲染函数与最初的十行版本放在一起对比读一遍,差异处就是你与"生产级前端"之间剩余的距离清单;照着清单逐项补齐,本节的目标就全部达成了。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U