一次请求至少经历开始、等待、成功或失败;成功还可能是空结果。刷新已有列表时,可以保留旧内容并显示刷新状态,首次加载则需要明确的等待反馈。状态设计应让用户知道当前看到的是旧数据、空数据还是请求失败。
Fetch 不把所有 HTTP 错误变成拒绝
fetch 在网络层失败或取消时可能拒绝;服务器返回 404、500 时通常仍会得到 Response,所以要检查 ok。解析 JSON 也可能失败,JSON 结构还要独立验证。
export async function loadLessons(url, { signal, fetcher = fetch } = {}) {
const response = await fetcher(url, { signal });
if (!response.ok) throw new Error("HTTP " + response.status);
const value = await response.json();
if (!Array.isArray(value) || !value.every(item =>
typeof item === "object" && item !== null &&
typeof item.id === "string" && typeof item.title === "string")) {
throw new Error("记录格式不正确");
}
return value.map(item => ({ id: item.id, title: item.title }));
}
请求函数 与可运行测试 使用注入的 fetcher 检查成功、HTTP 错误和结构错误。这个接口边界不会把服务器任意字段直接带进界面状态。实际项目还要处理重复 ID、分页、版本和权限。
页面关闭或查询变更时,用 AbortController 发出取消信号,并区分“主动取消”和真正失败。取消不能保证服务器没处理写操作。POST 重试要考虑幂等键或服务端去重;GET 也不能无视限流而无限重试。
跨域、凭据与缓存
同源由协议、主机和端口共同决定。CORS 是浏览器读取跨源响应的协商机制,设置 mode:“no-cors” 不会让脚本自由读取响应。带凭据请求还涉及服务端明确允许、Cookie 的 SameSite/Secure 属性和浏览器政策。前端配置 credentials 并不会授予服务端权限。
HTTP 缓存用响应头描述可存多久、何时验证。Cache-Control:no-cache 允许存储但要求使用前验证;no-store 要求不存储。ETag 与条件请求可以验证资源版本。应用查询缓存解决组件间数据复用与失效,两者作用层次不同,不能只加一个本地 Map 就宣称解决了 HTTP 缓存。
localStorage 是当前浏览器里的字符串存储,不能自动跨设备同步;读写还可能失败。IndexedDB 更适合较多结构化离线数据,但仍需设计迁移、清理和冲突处理。不要默认把敏感凭据或私人接口响应永久缓存。
练习:给列表增加刷新按钮,让旧数据保留;刷新失败时展示错误与重试,但不把旧列表清空。快速切换查询时,验证旧响应与旧错误都不能覆盖最新结果。
面试自查:Fetch 为什么需要检查 response.ok?CORS 为什么不是访问控制?no-cache 和 no-store 有何差别?取消请求为什么不等于撤销数据库写入?