学习前提

理解Promise、async/await、JSON及DOM更新。

本课完成后你能做什么

区分网络失败与HTTP失败,设计加载、成功、空结果和错误状态。

先理解机制

fetch返回响应时不保证状态码成功;404和500通常仍会兑现Promise。检查response.ok之后再解析正文,json解析也可能失败。成功返回的数据仍需验证结构,不能把服务端响应当作可靠的DOM片段。

请求状态应在开始时清除旧错误,成功时显示数据或空结果,失败时提供明确反馈,结束时解除忙碌。重复查询可能后发先至,用AbortController取消旧请求,并结合请求编号防止旧结果覆盖新结果。

浏览器跨源读取受CORS约束,no-cors不是获取可读JSON的绕过方法。下面使用本页生成的Blob地址,方便不依赖第三方接口练习;真实项目换成同源API地址,保留状态码与数据检查。

可运行示例

async function readCourses(url, signal) {
  const response = await fetch(url, { signal });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const data = await response.json();
  if (!Array.isArray(data) || !data.every(x => x && typeof x.title === "string")) {
    throw new Error("Unexpected course data");
  }
  return data;
}
const url = URL.createObjectURL(new Blob([
  JSON.stringify([{ title: "HTML" }, { title: "CSS" }])
], { type: "application/json" }));
const controller = new AbortController();
readCourses(url, controller.signal)
  .then(items => console.table(items))
  .catch(error => console.error(error.name, error.message))
  .finally(() => URL.revokeObjectURL(url));

推演与验证

运行后看到HTML与CSS两条记录。将Blob内容改为{},应进入结构错误;改为非JSON应进入解析错误。在读取前controller.abort(),应得到AbortError。生产API还要用浏览器Network模拟离线与HTTP错误。

常见错误与边界

取消请求不代表服务端业务回滚。提交订单等写操作不能仅靠按钮禁用防止重复。错误消息应面向用户解释,详细诊断放在日志中,不把内部堆栈直接显示。

动手练习

给课程加载增加requestId,保证旧请求的finally不会解除新请求的忙碌状态。

参考解法与理由

每次开始保存递增编号current,只有current等于最新编号时才渲染或清除busy。取消用于节省资源,编号用于保证界面状态正确,两者各有作用。

深入实验:网络成功、HTTP成功与数据正确是三件事

加载列表时,不能把await fetch完成等同于“数据可用”。网络返回404、返回200但不是JSON、返回JSON但字段错误,会在不同阶段失败。下面用内存Response建立确定的测试,不请求外部接口,也不依赖它此刻是否在线。

在现代浏览器Console中独立运行这个实验;它不替换上方主示例。

async function readCourses(response) {
  if (!response.ok) throw new Error('HTTP ' + response.status);
  const rows = await response.json();
  if (!Array.isArray(rows) || !rows.every(row =>
    row !== null && typeof row === 'object' && typeof row.title === 'string')) {
    throw new Error('Invalid course data');
  }
  return rows.map(row => row.title);
}
async function runCases() {
  const cases = [
    new Response('missing', {status: 404}),
    new Response('<html>error</html>'),
    new Response('{"title":"HTML"}'),
    new Response('[{"title":"HTML"}]')
  ];
  for (const response of cases) {
    try { console.log(JSON.stringify(await readCourses(response))); }
    catch (error) { console.log(error.name, error.message); }
  }
}
runCases();

预期观察

依次得到HTTP 404错误、SyntaxError、Invalid course data错误、["HTML"]。SyntaxError的具体文字因浏览器不同而变化,不应写成固定断言。

原因、取舍与适用边界

不要在同一个response上先text再json,响应体通常只能消费一次;确实需要两个消费者时先clone并考虑内存成本。UI还应区分loading、empty、success和error:空数组是合法的空结果,不是异常。重试只应针对可恢复错误;字段结构错误反复重试不会修复接口契约。取消旧请求之外,仍用请求序号防止已完成的旧结果覆盖新选择。

进阶练习

允许title为空字符串吗?为null、[]、[{"title":3}]分别说明结果。

解法与推理

当前契约允许空字符串,但可增加row.title.trim().length>0限制;null和数字title失败,[]成功返回[]。先决定业务约束,再修改验证器和测试;不要让UI随意猜测缺失字段并掩盖服务端问题。

进一步阅读