现代 HTTP 客户端深度解析:Fetch 与 Axios

XHR / Promise / async-await 之后,用原生 FetchAxios 完成 REST 风格 CRUD;配合 json-servertranscript 接口联调。

参考:MDN Fetch | Axios 中文文档

目录

  • 零、导读与学习价值
    • [0.1 案例覆盖清单](#0.1 案例覆盖清单)
    • [0.2 核心名词速查](#0.2 核心名词速查)
    • [0.5 本章在学什么(知识地图)](#0.5 本章在学什么(知识地图))
    • [0.4 建议练习路线](#0.4 建议练习路线)
    • [0.6 配套可运行示例(环境自检)](#0.6 配套可运行示例(环境自检))
    • [0.3 与前几章的衔接](#0.3 与前几章的衔接)
  • [1. HTTP客户端技术演进](#1. HTTP客户端技术演进)
    • [1.1 技术发展历程](#1.1 技术发展历程)
    • [1.2 技术对比分析](#1.2 技术对比分析)
    • [1.3 选择建议](#1.3 选择建议)
    • [1.4 配套可运行示例(环境能力检测)](#1.4 配套可运行示例(环境能力检测))
  • [2. Fetch API 深度解析](#2. Fetch API 深度解析)
    • [2.1 Fetch API 基础](#2.1 Fetch API 基础)
    • [2.0 配套可运行示例(最简 GET)](#2.0 配套可运行示例(最简 GET))
    • [2.2 Response 对象详解](#2.2 Response 对象详解)
    • [2.3 Request 对象配置](#2.3 Request 对象配置)
    • [2.4 Fetch 高级功能](#2.4 Fetch 高级功能)
    • [2.5 配套示例:03-Fetch 与 REST CRUD](#2.5 配套示例:03-Fetch 与 REST CRUD)
    • [2.6 配套可运行示例(response.ok 判断)](#2.6 配套可运行示例(response.ok 判断))
  • [3. Axios 完整指南](#3. Axios 完整指南)
    • [3.1 Axios 核心概念](#3.1 Axios 核心概念)
    • [3.0 配套可运行示例(CDN 引入)](#3.0 配套可运行示例(CDN 引入))
    • [3.2 Axios 基础使用](#3.2 Axios 基础使用)
    • [3.3 Axios 实例创建](#3.3 Axios 实例创建)
    • [3.4 拦截器机制](#3.4 拦截器机制)
    • [3.5 请求取消](#3.5 请求取消)
    • [3.8 配套示例:04-Axios 案例对照](#3.8 配套示例:04-Axios 案例对照)
    • [3.7 配套可运行示例(拦截器最小版)](#3.7 配套可运行示例(拦截器最小版))
  • [4. REST API 设计原则](#4. REST API 设计原则)
    • [4.1 REST 架构风格](#4.1 REST 架构风格)
    • [4.0 配套可运行示例(方法对照表)](#4.0 配套可运行示例(方法对照表))
    • [4.2 REST API 设计规范](#4.2 REST API 设计规范)
    • [4.3 响应格式规范](#4.3 响应格式规范)
    • [4.4 配套:json-server 搭建 REST 练习环境](#4.4 配套:json-server 搭建 REST 练习环境)
    • [4.5 配套可运行示例(REST 四方法)](#4.5 配套可运行示例(REST 四方法))
  • [5. 请求拦截与响应处理](#5. 请求拦截与响应处理)
    • [5.1 统一请求处理](#5.1 统一请求处理)
    • [5.2 响应数据转换](#5.2 响应数据转换)
    • [5.2 配套可运行示例(响应拦截解包 data)](#5.2 配套可运行示例(响应拦截解包 data))
  • [6. 高级功能实现](#6. 高级功能实现)
    • [6.1 并发请求控制](#6.1 并发请求控制)
    • [6.0 配套可运行示例(Fetch 并行)](#6.0 配套可运行示例(Fetch 并行))
    • [6.2 请求重试机制](#6.2 请求重试机制)
    • [6.3 缓存策略实现](#6.3 缓存策略实现)
    • [6.4 配套可运行示例(axios.all)](#6.4 配套可运行示例(axios.all))
  • [7. 生产环境最佳实践](#7. 生产环境最佳实践)
    • [7.0 配套可运行示例(区分 axios 错误类型)](#7.0 配套可运行示例(区分 axios 错误类型))
    • [7.1 错误监控和日志](#7.1 错误监控和日志)
    • [7.2 安全防护措施](#7.2 安全防护措施)
    • [7.3 配套可运行示例(统一错误日志)](#7.3 配套可运行示例(统一错误日志))
  • [8. 性能优化与监控](#8. 性能优化与监控)
    • [8.0 配套可运行示例(AbortController 取消)](#8.0 配套可运行示例(AbortController 取消))
    • [8.1 性能优化策略](#8.1 性能优化策略)
    • [8.2 监控和告警](#8.2 监控和告警)
    • [8.3 配套可运行示例(请求耗时统计)](#8.3 配套可运行示例(请求耗时统计))
  • 总结

零、导读与学习价值

0.1 案例覆盖清单

目录 / 文件 知识点 本文章节
01-自定义Promise-Class版02-function版 手写 Promise 篇 验收(可选复习) §0.3
03-Fetch/index.html GET/POST/PUT/DELETE + await fetch §2.5
04-Axios/01-引入axios.html CDN / 本地 axios.js §3.2
04/02-axios的基本使用.html axios(config)getawait §3.3
04/03-请求配置项.html url、baseURL、params、data、timeout §3.4
04/04-创建axios实例.html axios.create 多后端 §3.5
04/05-取消请求.html CancelToken / AbortController §3.6
04/06-批量发送请求.html axios.all + 解构 §3.7
04/07-拦截器.html 请求/响应拦截、eject §5.1
data/transcript.json json-server REST 数据源 §4.3

配套接口示例:http://shirly.com:8088/transcript(需 hosts 或改用本地 json-server)。

0.2 核心名词速查

术语 一句话解释
fetch(url, options) 返回 Promise;默认 GET
response.ok status 在 200~299;404 时 ok 为 false
response.json() 再返回 Promise,解析响应体
axios(config) 基于 XHR + Promise 的请求库
baseURL 拼在相对 url 前的公共前缀
拦截器 发请求前 / 进 then 前统一改 config 或 response
RESTful 同一路径用 HTTP 方法区分增删改查

0.5 本章在学什么(知识地图)

#mermaid-svg-NnBbRNOpEPnPXe4q{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NnBbRNOpEPnPXe4q .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NnBbRNOpEPnPXe4q .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NnBbRNOpEPnPXe4q .error-icon{fill:#552222;}#mermaid-svg-NnBbRNOpEPnPXe4q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NnBbRNOpEPnPXe4q .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NnBbRNOpEPnPXe4q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NnBbRNOpEPnPXe4q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NnBbRNOpEPnPXe4q .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NnBbRNOpEPnPXe4q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NnBbRNOpEPnPXe4q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NnBbRNOpEPnPXe4q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NnBbRNOpEPnPXe4q .marker.cross{stroke:#333333;}#mermaid-svg-NnBbRNOpEPnPXe4q svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NnBbRNOpEPnPXe4q p{margin:0;}#mermaid-svg-NnBbRNOpEPnPXe4q .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NnBbRNOpEPnPXe4q .cluster-label text{fill:#333;}#mermaid-svg-NnBbRNOpEPnPXe4q .cluster-label span{color:#333;}#mermaid-svg-NnBbRNOpEPnPXe4q .cluster-label span p{background-color:transparent;}#mermaid-svg-NnBbRNOpEPnPXe4q .label text,#mermaid-svg-NnBbRNOpEPnPXe4q span{fill:#333;color:#333;}#mermaid-svg-NnBbRNOpEPnPXe4q .node rect,#mermaid-svg-NnBbRNOpEPnPXe4q .node circle,#mermaid-svg-NnBbRNOpEPnPXe4q .node ellipse,#mermaid-svg-NnBbRNOpEPnPXe4q .node polygon,#mermaid-svg-NnBbRNOpEPnPXe4q .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NnBbRNOpEPnPXe4q .rough-node .label text,#mermaid-svg-NnBbRNOpEPnPXe4q .node .label text,#mermaid-svg-NnBbRNOpEPnPXe4q .image-shape .label,#mermaid-svg-NnBbRNOpEPnPXe4q .icon-shape .label{text-anchor:middle;}#mermaid-svg-NnBbRNOpEPnPXe4q .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NnBbRNOpEPnPXe4q .rough-node .label,#mermaid-svg-NnBbRNOpEPnPXe4q .node .label,#mermaid-svg-NnBbRNOpEPnPXe4q .image-shape .label,#mermaid-svg-NnBbRNOpEPnPXe4q .icon-shape .label{text-align:center;}#mermaid-svg-NnBbRNOpEPnPXe4q .node.clickable{cursor:pointer;}#mermaid-svg-NnBbRNOpEPnPXe4q .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NnBbRNOpEPnPXe4q .arrowheadPath{fill:#333333;}#mermaid-svg-NnBbRNOpEPnPXe4q .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NnBbRNOpEPnPXe4q .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NnBbRNOpEPnPXe4q .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NnBbRNOpEPnPXe4q .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NnBbRNOpEPnPXe4q .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NnBbRNOpEPnPXe4q .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NnBbRNOpEPnPXe4q .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NnBbRNOpEPnPXe4q .cluster text{fill:#333;}#mermaid-svg-NnBbRNOpEPnPXe4q .cluster span{color:#333;}#mermaid-svg-NnBbRNOpEPnPXe4q div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NnBbRNOpEPnPXe4q .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NnBbRNOpEPnPXe4q rect.text{fill:none;stroke-width:0;}#mermaid-svg-NnBbRNOpEPnPXe4q .icon-shape,#mermaid-svg-NnBbRNOpEPnPXe4q .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NnBbRNOpEPnPXe4q .icon-shape p,#mermaid-svg-NnBbRNOpEPnPXe4q .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NnBbRNOpEPnPXe4q .icon-shape .label rect,#mermaid-svg-NnBbRNOpEPnPXe4q .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NnBbRNOpEPnPXe4q .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NnBbRNOpEPnPXe4q .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NnBbRNOpEPnPXe4q :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 选型 XHR/Fetch/Axios
Fetch REST
Axios 工程化
json-server
拦截器与生产实践

【代码注释】

  • 左到右对应 03-Fetch04-Axiosdata/transcript.json 练习顺序。

0.4 建议练习路线

顺序 配套 验证点
启动 json-server GET /transcript 有数据
03-Fetch GET/POST/PUT/DELETE
04/02 基本使用 res.data
04/04 实例 多 baseURL
04/06 批量 axios.all / Promise.all
04/07 拦截器 token、统一错误
04/05 取消 AbortController

0.6 配套可运行示例(环境自检)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>Fetch 自检</title></head>
<body>
<script>
fetch('https://httpbin.org/get').then(r => r.json()).then(d => console.log('Fetch 可用', d.url));
</script>
</body>
</html>

【代码注释】

  • 外网自检无需 json-server;本地 CRUD 再启 json-server03-Fetch

0.3 与前几章的衔接

  • Ajax 基础~进阶篇 :XHR、ajax() 回调。
  • Promise 基础~进阶篇ajaxPromiseasync/await 链。
  • 手写 Promise 篇 :理解 Promise 后,Fetch/Axios 的 .then / await 更易上手。
  • 本篇不再手写 XHR ,用 Fetch 或 Axios 对接 REST API(笔记分 Fetch / Axios / RestAPI 三份)。

本地 mock:json-server --watch data/transcript.json --port 8088(见 §4.3)。


1. HTTP客户端技术演进

1.1 技术发展历程

HTTP 客户端技术的发展:
#mermaid-svg-6nD7chmJXNTK9AMs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6nD7chmJXNTK9AMs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6nD7chmJXNTK9AMs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6nD7chmJXNTK9AMs .error-icon{fill:#552222;}#mermaid-svg-6nD7chmJXNTK9AMs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6nD7chmJXNTK9AMs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6nD7chmJXNTK9AMs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6nD7chmJXNTK9AMs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6nD7chmJXNTK9AMs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6nD7chmJXNTK9AMs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6nD7chmJXNTK9AMs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6nD7chmJXNTK9AMs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6nD7chmJXNTK9AMs .marker.cross{stroke:#333333;}#mermaid-svg-6nD7chmJXNTK9AMs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6nD7chmJXNTK9AMs p{margin:0;}#mermaid-svg-6nD7chmJXNTK9AMs .edge{stroke-width:3;}#mermaid-svg-6nD7chmJXNTK9AMs .section--1 rect,#mermaid-svg-6nD7chmJXNTK9AMs .section--1 path,#mermaid-svg-6nD7chmJXNTK9AMs .section--1 circle,#mermaid-svg-6nD7chmJXNTK9AMs .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .section--1 text{fill:#ffffff;}#mermaid-svg-6nD7chmJXNTK9AMs .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-6nD7chmJXNTK9AMs .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .edge-depth--1{stroke-width:17;}#mermaid-svg-6nD7chmJXNTK9AMs .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-6nD7chmJXNTK9AMs .lineWrapper line{stroke:#ffffff;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled,#mermaid-svg-6nD7chmJXNTK9AMs .disabled circle,#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:lightgray;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:#efefef;}#mermaid-svg-6nD7chmJXNTK9AMs .section-0 rect,#mermaid-svg-6nD7chmJXNTK9AMs .section-0 path,#mermaid-svg-6nD7chmJXNTK9AMs .section-0 circle,#mermaid-svg-6nD7chmJXNTK9AMs .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-6nD7chmJXNTK9AMs .section-0 text{fill:black;}#mermaid-svg-6nD7chmJXNTK9AMs .node-icon-0{font-size:40px;color:black;}#mermaid-svg-6nD7chmJXNTK9AMs .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-6nD7chmJXNTK9AMs .edge-depth-0{stroke-width:14;}#mermaid-svg-6nD7chmJXNTK9AMs .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-6nD7chmJXNTK9AMs .lineWrapper line{stroke:black;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled,#mermaid-svg-6nD7chmJXNTK9AMs .disabled circle,#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:lightgray;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:#efefef;}#mermaid-svg-6nD7chmJXNTK9AMs .section-1 rect,#mermaid-svg-6nD7chmJXNTK9AMs .section-1 path,#mermaid-svg-6nD7chmJXNTK9AMs .section-1 circle,#mermaid-svg-6nD7chmJXNTK9AMs .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .section-1 text{fill:black;}#mermaid-svg-6nD7chmJXNTK9AMs .node-icon-1{font-size:40px;color:black;}#mermaid-svg-6nD7chmJXNTK9AMs .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .edge-depth-1{stroke-width:11;}#mermaid-svg-6nD7chmJXNTK9AMs .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-6nD7chmJXNTK9AMs .lineWrapper line{stroke:black;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled,#mermaid-svg-6nD7chmJXNTK9AMs .disabled circle,#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:lightgray;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:#efefef;}#mermaid-svg-6nD7chmJXNTK9AMs .section-2 rect,#mermaid-svg-6nD7chmJXNTK9AMs .section-2 path,#mermaid-svg-6nD7chmJXNTK9AMs .section-2 circle,#mermaid-svg-6nD7chmJXNTK9AMs .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .section-2 text{fill:#ffffff;}#mermaid-svg-6nD7chmJXNTK9AMs .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-6nD7chmJXNTK9AMs .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .edge-depth-2{stroke-width:8;}#mermaid-svg-6nD7chmJXNTK9AMs .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-6nD7chmJXNTK9AMs .lineWrapper line{stroke:#ffffff;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled,#mermaid-svg-6nD7chmJXNTK9AMs .disabled circle,#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:lightgray;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:#efefef;}#mermaid-svg-6nD7chmJXNTK9AMs .section-3 rect,#mermaid-svg-6nD7chmJXNTK9AMs .section-3 path,#mermaid-svg-6nD7chmJXNTK9AMs .section-3 circle,#mermaid-svg-6nD7chmJXNTK9AMs .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .section-3 text{fill:black;}#mermaid-svg-6nD7chmJXNTK9AMs .node-icon-3{font-size:40px;color:black;}#mermaid-svg-6nD7chmJXNTK9AMs .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .edge-depth-3{stroke-width:5;}#mermaid-svg-6nD7chmJXNTK9AMs .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-6nD7chmJXNTK9AMs .lineWrapper line{stroke:black;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled,#mermaid-svg-6nD7chmJXNTK9AMs .disabled circle,#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:lightgray;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:#efefef;}#mermaid-svg-6nD7chmJXNTK9AMs .section-4 rect,#mermaid-svg-6nD7chmJXNTK9AMs .section-4 path,#mermaid-svg-6nD7chmJXNTK9AMs .section-4 circle,#mermaid-svg-6nD7chmJXNTK9AMs .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .section-4 text{fill:black;}#mermaid-svg-6nD7chmJXNTK9AMs .node-icon-4{font-size:40px;color:black;}#mermaid-svg-6nD7chmJXNTK9AMs .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .edge-depth-4{stroke-width:2;}#mermaid-svg-6nD7chmJXNTK9AMs .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-6nD7chmJXNTK9AMs .lineWrapper line{stroke:black;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled,#mermaid-svg-6nD7chmJXNTK9AMs .disabled circle,#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:lightgray;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:#efefef;}#mermaid-svg-6nD7chmJXNTK9AMs .section-5 rect,#mermaid-svg-6nD7chmJXNTK9AMs .section-5 path,#mermaid-svg-6nD7chmJXNTK9AMs .section-5 circle,#mermaid-svg-6nD7chmJXNTK9AMs .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .section-5 text{fill:black;}#mermaid-svg-6nD7chmJXNTK9AMs .node-icon-5{font-size:40px;color:black;}#mermaid-svg-6nD7chmJXNTK9AMs .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .edge-depth-5{stroke-width:-1;}#mermaid-svg-6nD7chmJXNTK9AMs .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-6nD7chmJXNTK9AMs .lineWrapper line{stroke:black;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled,#mermaid-svg-6nD7chmJXNTK9AMs .disabled circle,#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:lightgray;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:#efefef;}#mermaid-svg-6nD7chmJXNTK9AMs .section-6 rect,#mermaid-svg-6nD7chmJXNTK9AMs .section-6 path,#mermaid-svg-6nD7chmJXNTK9AMs .section-6 circle,#mermaid-svg-6nD7chmJXNTK9AMs .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .section-6 text{fill:black;}#mermaid-svg-6nD7chmJXNTK9AMs .node-icon-6{font-size:40px;color:black;}#mermaid-svg-6nD7chmJXNTK9AMs .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .edge-depth-6{stroke-width:-4;}#mermaid-svg-6nD7chmJXNTK9AMs .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-6nD7chmJXNTK9AMs .lineWrapper line{stroke:black;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled,#mermaid-svg-6nD7chmJXNTK9AMs .disabled circle,#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:lightgray;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:#efefef;}#mermaid-svg-6nD7chmJXNTK9AMs .section-7 rect,#mermaid-svg-6nD7chmJXNTK9AMs .section-7 path,#mermaid-svg-6nD7chmJXNTK9AMs .section-7 circle,#mermaid-svg-6nD7chmJXNTK9AMs .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .section-7 text{fill:black;}#mermaid-svg-6nD7chmJXNTK9AMs .node-icon-7{font-size:40px;color:black;}#mermaid-svg-6nD7chmJXNTK9AMs .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .edge-depth-7{stroke-width:-7;}#mermaid-svg-6nD7chmJXNTK9AMs .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-6nD7chmJXNTK9AMs .lineWrapper line{stroke:black;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled,#mermaid-svg-6nD7chmJXNTK9AMs .disabled circle,#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:lightgray;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:#efefef;}#mermaid-svg-6nD7chmJXNTK9AMs .section-8 rect,#mermaid-svg-6nD7chmJXNTK9AMs .section-8 path,#mermaid-svg-6nD7chmJXNTK9AMs .section-8 circle,#mermaid-svg-6nD7chmJXNTK9AMs .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .section-8 text{fill:black;}#mermaid-svg-6nD7chmJXNTK9AMs .node-icon-8{font-size:40px;color:black;}#mermaid-svg-6nD7chmJXNTK9AMs .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .edge-depth-8{stroke-width:-10;}#mermaid-svg-6nD7chmJXNTK9AMs .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-6nD7chmJXNTK9AMs .lineWrapper line{stroke:black;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled,#mermaid-svg-6nD7chmJXNTK9AMs .disabled circle,#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:lightgray;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:#efefef;}#mermaid-svg-6nD7chmJXNTK9AMs .section-9 rect,#mermaid-svg-6nD7chmJXNTK9AMs .section-9 path,#mermaid-svg-6nD7chmJXNTK9AMs .section-9 circle,#mermaid-svg-6nD7chmJXNTK9AMs .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .section-9 text{fill:black;}#mermaid-svg-6nD7chmJXNTK9AMs .node-icon-9{font-size:40px;color:black;}#mermaid-svg-6nD7chmJXNTK9AMs .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .edge-depth-9{stroke-width:-13;}#mermaid-svg-6nD7chmJXNTK9AMs .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-6nD7chmJXNTK9AMs .lineWrapper line{stroke:black;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled,#mermaid-svg-6nD7chmJXNTK9AMs .disabled circle,#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:lightgray;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:#efefef;}#mermaid-svg-6nD7chmJXNTK9AMs .section-10 rect,#mermaid-svg-6nD7chmJXNTK9AMs .section-10 path,#mermaid-svg-6nD7chmJXNTK9AMs .section-10 circle,#mermaid-svg-6nD7chmJXNTK9AMs .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .section-10 text{fill:black;}#mermaid-svg-6nD7chmJXNTK9AMs .node-icon-10{font-size:40px;color:black;}#mermaid-svg-6nD7chmJXNTK9AMs .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .edge-depth-10{stroke-width:-16;}#mermaid-svg-6nD7chmJXNTK9AMs .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-6nD7chmJXNTK9AMs .lineWrapper line{stroke:black;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled,#mermaid-svg-6nD7chmJXNTK9AMs .disabled circle,#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:lightgray;}#mermaid-svg-6nD7chmJXNTK9AMs .disabled text{fill:#efefef;}#mermaid-svg-6nD7chmJXNTK9AMs .section-root rect,#mermaid-svg-6nD7chmJXNTK9AMs .section-root path,#mermaid-svg-6nD7chmJXNTK9AMs .section-root circle{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-6nD7chmJXNTK9AMs .section-root text{fill:#ffffff;}#mermaid-svg-6nD7chmJXNTK9AMs .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-6nD7chmJXNTK9AMs .edge{fill:none;}#mermaid-svg-6nD7chmJXNTK9AMs .eventWrapper{filter:brightness(120%);}#mermaid-svg-6nD7chmJXNTK9AMs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 早期阶段 1999 XMLHttpRequest 出现 2005 Ajax 概念正式提出 2006 jQuery Ajax 简化开发 标准化阶段 2008 XHR Level 2 规范 2014 Fetch API 规范发布 2015 Axios 库发布 现代阶段 2017 Axios 成为主流选择 2020 原生 Fetch 广泛支持 2025 现代HTTP客户端生态 HTTP 客户端技术演进

【代码注释】

  • XHR → Fetch 标准化 → Axios 生态的时间线概览。
  • 现代项目仍可能维护 legacy XHR 代码。
  • Axios 底层仍是 XHR,不是 Fetch API。
  • 选型表见 §1.2。

1.2 技术对比分析

特性 XMLHttpRequest Fetch API Axios
API 设计 回调函数 Promise 链 Promise 链
浏览器支持 所有浏览器 现代浏览器 需要引入库
请求拦截 复杂实现 需要手动封装 内置支持
响应转换 手动解析 手动解析 自动转换
取消请求 abort() 方法 AbortController CancelToken
进度监控 原生支持 有限支持 原生支持
超时控制 手动实现 需要配合 AbortController 内置支持
Node.js 支持 需要适配器 原生支持 原生支持

1.3 选择建议

根据场景选择HTTP客户端:

使用 Fetch API 的场景:

  • 现代浏览器环境
  • 简单的 HTTP 请求
  • 不需要复杂的请求拦截
  • 希望减少依赖库

使用 Axios 的场景:

  • 需要请求/响应拦截器
  • 需要请求取消功能
  • 需要自动的 JSON 数据转换
  • 需要同时支持浏览器和 Node.js
  • 需要更完善的错误处理

使用 XMLHttpRequest 的场景:

  • 需要支持老版本浏览器
  • 需要上传进度监控
  • 维护现有代码库

【实战要点】

  • 新项目浏览器端优先 Fetch 或封装 Axios;需上传进度、极老 IE 再考虑 XHR。
  • Axios 本质是 XHR,不是 Fetch;拦截器、超时、JSON 转换是其优势。

【面试考点】

Q1:Fetch 与 XHR、Axios 对比?

A:XHR 回调+进度;Fetch 原生 Promise 需判 ok;Axios 封装 XHR+拦截器。

Q2:Fetch 为何说「HTTP 错误不 reject」?

A:只有网络级失败 reject;4xx/5xx 仍 resolve Response,需看 ok

1.4 配套可运行示例(环境能力检测)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>客户端 API</title></head>
<body>
<script>
console.log({ XHR: typeof XMLHttpRequest, fetch: typeof fetch });
</script>
</body>
</html>

【代码注释】

  • 浏览器内置 XHR/fetch;Axios 需单独引入(见 §3.0)。

【本章小结】

技术 特点
XHR 回调、进度、老项目
Fetch 原生 Promise、需判 ok
Axios 拦截器、超时、res.data

2. Fetch API 深度解析

2.1 Fetch API 基础

2.0 配套可运行示例(最简 GET)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>最简 fetch</title></head>
<body>
<script>
fetch('https://httpbin.org/get')
  .then(r => r.json())
  .then(d => console.log(d));
</script>
</body>
</html>

【代码注释】

  • 两步:fetchjson();生产环境应对 r.ok 做判断。

Fetch API 核心概念:

javascript 复制代码
// Fetch 基础语法
fetch(url)
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
    })
    .then(data => {
        console.log('获取数据:', data);
    })
    .catch(error => {
        console.error('请求失败:', error);
    });

// 完整的 Fetch 请求
fetch('https://api.example.com/data', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer token123'
    },
    body: JSON.stringify({
        name: '张三',
        age: 28
    }),
    mode: 'cors',
    credentials: 'include',
    cache: 'no-cache',
    redirect: 'follow'
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));

【代码注释】

  • Fetch 经典陷阱:网络错误才 reject,HTTP 4xx/5xx 仍 resolve。
  • 必须 if (!response.ok) throw 或判断 status。
  • 第二参数配置 method、headers、body、signal 等。
  • 等价 Promise 基础篇 的 Promise 链式请求。

2.2 Response 对象详解

Response 对象属性和方法:

javascript 复制代码
async function analyzeResponse() {
    const response = await fetch('https://api.example.com/data');
    
    // 响应状态信息
    console.log('状态码:', response.status);           // 200
    console.log('状态文本:', response.statusText);     // "OK"
    console.log('是否成功:', response.ok);             // true
    console.log('响应URL:', response.url);             // 实际请求的URL
    console.log('重定向:', response.redirected);       // 是否重定向
    console.log('响应类型:', response.type);           // basic, cors, opaque
    
    // 响应头信息
    console.log('Content-Type:', response.headers.get('Content-Type'));
    console.log('Content-Length:', response.headers.get('Content-Length'));
    
    // 遍历所有响应头
    for (const [key, value] of response.headers.entries()) {
        console.log(`${key}: ${value}`);
    }
    
    // 响应体处理方法
    if (response.headers.get('Content-Type').includes('application/json')) {
        const jsonData = await response.json();  // 解析JSON
        console.log('JSON数据:', jsonData);
    } else if (response.headers.get('Content-Type').includes('text/')) {
        const textData = await response.text();   // 获取文本
        console.log('文本数据:', textData);
    } else if (response.headers.get('Content-Type').includes('image/')) {
        const blobData = await response.blob();   // 获取二进制数据
        const imageUrl = URL.createObjectURL(blobData);
        console.log('图片URL:', imageUrl);
    }
    
    // 原始响应体
    const arrayBuffer = await response.arrayBuffer();
    console.log('ArrayBuffer:', arrayBuffer);
}

【代码注释】

  • Response 体只能消费一次;json() 后再 text() 会报错。
  • headers.get 不区分大小写;遍历用 entries()。
  • blob/arrayBuffer 用于文件、图片下载。
  • response.type 在跨域 no-cors 时为 opaque。

2.3 Request 对象配置

完整的 Request 配置选项:

javascript 复制代码
const request = new Request('https://api.example.com/data', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-Custom-Header': 'custom-value'
    },
    body: JSON.stringify({ key: 'value' }),
    mode: 'cors',              // cors, no-cors, same-origin, navigate
    credentials: 'include',     // include, same-origin, omit
    cache: 'default',          // default, no-store, reload, no-cache, force-cache
    redirect: 'follow',        // manual, follow, error
    referrer: 'no-referrer',   // no-referrer, client
    referrerPolicy: 'no-referrer-when-downgrade',
    integrity: 'sha256-abcdef1234567890', // SRI (Subresource Integrity)
    keepalive: false,          // 保持连接
    signal: abortController.signal  // 取消信号
});

// 使用 Request 对象
fetch(request)
    .then(response => response.json())
    .then(data => console.log(data));

【代码注释】

  • 结合本节标题与 0.1 案例表对照学习。
  • Fetch 注意 ok/status;Axios 注意 data 字段。
  • 本地先 json-server 再跑 HTML。
  • 跨域与 Cookie 参考 Ajax 进阶与跨域篇、会话控制篇 Session。

2.4 Fetch 高级功能

请求取消:

javascript 复制代码
// 使用 AbortController 取消请求
const abortController = new AbortController();

const fetchPromise = fetch('https://api.example.com/slow-endpoint', {
    signal: abortController.signal
});

// 设置超时取消
const timeoutId = setTimeout(() => {
    abortController.abort();
    console.log('请求超时,已取消');
}, 5000);

fetchPromise
    .then(response => {
        clearTimeout(timeoutId);
        return response.json();
    })
    .then(data => {
        console.log('数据:', data);
    })
    .catch(error => {
        if (error.name === 'AbortError') {
            console.log('请求被取消');
        } else {
            console.error('请求失败:', error);
        }
    });

【代码注释】

  • 现代取消标准;Axios v0.22+ 支持 signal
  • abort 后 fetch reject DOMException。
  • 超时 = race 或 setTimeout+abort。
  • 旧 Axios 用 CancelToken(05-取消请求.html)。

流式数据处理:

javascript 复制代码
// 处理流式响应
async function fetchStreamData() {
    const response = await fetch('https://api.example.com/large-data');
    
    if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    
    let result = '';
    let receivedLength = 0;
    
    while (true) {
        const { done, value } = await reader.read();
        
        if (done) {
            console.log('流式数据接收完成');
            break;
        }
        
        const chunk = decoder.decode(value, { stream: true });
        result += chunk;
        receivedLength += value.length;
        
        console.log(`接收到 ${value.length} 字节,总计 ${receivedLength} 字节`);
        console.log('当前数据块:', chunk);
        
        // 实时处理数据
        processChunk(chunk);
    }
    
    return result;
}

// 分块传输数据处理
async function fetchChunkedData() {
    const response = await fetch('https://api.example.com/chunked-data');
    
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    
    let buffer = '';
    
    while (true) {
        const { done, value } = await reader.read();
        
        if (done) break;
        
        buffer += decoder.decode(value, { stream: true });
        
        // 按行分割处理
        const lines = buffer.split('\n');
        buffer = lines.pop(); // 保留不完整的行
        
        for (const line of lines) {
            if (line.trim()) {
                try {
                    const data = JSON.parse(line);
                    console.log('接收到数据对象:', data);
                    // 处理每个数据对象
                } catch (error) {
                    console.error('JSON解析错误:', error);
                }
            }
        }
    }
}

【代码注释】

  • 流式读取大响应;SSE/分块 JSON 场景。
  • Fetch 的 body 是 ReadableStream。
  • Axios 也有 onDownloadProgress(XHR 能力)。
  • 配套未展开,作进阶了解。

2.5 配套示例:03-Fetch 与 REST CRUD

03-Fetch/index.htmltranscript 资源做四种方法(与 REST API 笔记 笔记一致):

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>Fetch REST</title></head>
<body>
<button id="get">GET</button>
<button id="post">POST</button>
<script>
const BASE = 'http://127.0.0.1:8088/transcript'; // json-server 或 hosts 映射的 shirly.com

document.querySelector('#get').onclick = async () => {
  const res = await fetch(BASE);
  const data = await res.json();
  console.log(data);
};

document.querySelector('#post').onclick = async () => {
  const res = await fetch(BASE, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: '测试', age: 20 })
  });
  console.log(await res.json());
};
</script>
</body>
</html>

【代码注释】

  • fetch 返回 Promise,解析后为 Response 对象;必须await res.json() 才得到数据对象。
  • PUT transcript/19DELETE transcript/18 见完整案例;路径中带 id 表资源主键。
  • 网络失败进 catchHTTP 4xx/5xx 默认仍 resolve ,需判断 res.okres.status
  • 可用 async/await.then(res => res.json()) 两种风格(配套三种写法都有)。

REST 服务 fetch 页面按钮 REST 服务 fetch 页面按钮 #mermaid-svg-v9JwPs5tcgEp8O6K{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-v9JwPs5tcgEp8O6K .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-v9JwPs5tcgEp8O6K .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-v9JwPs5tcgEp8O6K .error-icon{fill:#552222;}#mermaid-svg-v9JwPs5tcgEp8O6K .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-v9JwPs5tcgEp8O6K .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-v9JwPs5tcgEp8O6K .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-v9JwPs5tcgEp8O6K .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-v9JwPs5tcgEp8O6K .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-v9JwPs5tcgEp8O6K .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-v9JwPs5tcgEp8O6K .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-v9JwPs5tcgEp8O6K .marker{fill:#333333;stroke:#333333;}#mermaid-svg-v9JwPs5tcgEp8O6K .marker.cross{stroke:#333333;}#mermaid-svg-v9JwPs5tcgEp8O6K svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-v9JwPs5tcgEp8O6K p{margin:0;}#mermaid-svg-v9JwPs5tcgEp8O6K .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-v9JwPs5tcgEp8O6K text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-v9JwPs5tcgEp8O6K .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-v9JwPs5tcgEp8O6K .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-v9JwPs5tcgEp8O6K .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-v9JwPs5tcgEp8O6K .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-v9JwPs5tcgEp8O6K #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-v9JwPs5tcgEp8O6K .sequenceNumber{fill:white;}#mermaid-svg-v9JwPs5tcgEp8O6K #sequencenumber{fill:#333;}#mermaid-svg-v9JwPs5tcgEp8O6K #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-v9JwPs5tcgEp8O6K .messageText{fill:#333;stroke:none;}#mermaid-svg-v9JwPs5tcgEp8O6K .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-v9JwPs5tcgEp8O6K .labelText,#mermaid-svg-v9JwPs5tcgEp8O6K .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-v9JwPs5tcgEp8O6K .loopText,#mermaid-svg-v9JwPs5tcgEp8O6K .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-v9JwPs5tcgEp8O6K .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-v9JwPs5tcgEp8O6K .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-v9JwPs5tcgEp8O6K .noteText,#mermaid-svg-v9JwPs5tcgEp8O6K .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-v9JwPs5tcgEp8O6K .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-v9JwPs5tcgEp8O6K .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-v9JwPs5tcgEp8O6K .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-v9JwPs5tcgEp8O6K .actorPopupMenu{position:absolute;}#mermaid-svg-v9JwPs5tcgEp8O6K .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-v9JwPs5tcgEp8O6K .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-v9JwPs5tcgEp8O6K .actor-man circle,#mermaid-svg-v9JwPs5tcgEp8O6K line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-v9JwPs5tcgEp8O6K :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} fetch(url, {method, headers, body}) HTTP 请求 Response res.json() → 业务数据

【代码注释】

  • POST/PUT 的 bodyContent-Type 必须匹配(JSON 用 application/json)。
  • 跨域时服务端需 CORS(Ajax 进阶与跨域篇);Fetch 支持 credentials: 'include' 带 Cookie。
  • 取消请求用 AbortController + signal(§2.4),与 Axios 的 signal 概念一致。

【实战要点】

  • 统一封装:async function request(url, options) { const res = await fetch(url, options); if (!res.ok) throw new Error(res.status); return res.json(); }
  • 不要对同一 response 多次调用 json(),body 只能读一次。

【面试考点】

Q1:fetch 在什么情况下会进入 catch?

A:网络错误、CORS 失败、手动 throw、Abort 取消等;非 2xx 默认不进 catch。

Q2:response.okresponse.status 的关系?

A:ok 为 status 在 200~299 时为 true。

2.6 配套可运行示例(response.ok 判断)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>Fetch ok</title></head>
<body>
<script>
fetch('https://httpbin.org/status/404')
  .then(res => {
    console.log('status', res.status, 'ok', res.ok);
    if (!res.ok) throw new Error('HTTP ' + res.status);
    return res.json();
  })
  .catch(e => console.error(e.message));
</script>
</body>
</html>

【代码注释】

  • 404 时 fetch 仍 resolve ,必须看 res.okstatus;与 Axios 默认 reject 不同。

【本章小结】

API 要点
fetch 返回 Promise,再 json()
res.json() 再一层 Promise,body 只读一次
AbortController 取消请求

3. Axios 完整指南

3.1 Axios 核心概念

3.0 配套可运行示例(CDN 引入)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>引入 axios</title></head>
<body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
console.log('axios', typeof axios);
axios.get('https://httpbin.org/get').then(r => console.log(r.data.url));
</script>
</body>
</html>

【代码注释】

  • 对应 01-引入axios.html ;本地案例可用 ./axios/axios.js

Axios 特性详解:

javascript 复制代码
// Axios 核心特性
const axiosFeatures = {
    // 1. 支持 Promise API
    promiseAPI: true,
    
    // 2. 拦截器支持
    interceptors: true,
    
    // 3. 请求取消
    requestCancellation: true,
    
    // 4. 自动 JSON 数据转换
    jsonTransform: true,
    
    // 5. 客户端防止 XSRF
    xsrfProtection: true,
    
    // 6. 并发请求支持
    concurrentRequests: true,
    
    // 7. 超时设置
    timeout: true,
    
    // 8. 浏览器和 Node.js 支持
    crossPlatform: true
};

console.log('Axios 特性:', axiosFeatures);

【代码注释】

  • Axios 基于 XHR + Promise;浏览器/Node 均可。
  • GET 查询用 params 对象;POST 体用 data
  • 响应在 response.data,状态在 response.status
  • 01-引入axios.html 引入脚本后即可用全局 axios

3.2 Axios 基础使用

安装和引入:

bash 复制代码
# 使用 npm 安装
npm install axios

# 使用 yarn 安装
yarn add axios

# 使用 CDN
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

【代码注释】

  • npm 安装 axios;浏览器可用 CDN 或本地 axios.js
  • Node 项目 require('axios') 或 ESM import。
  • 版本与课堂 bundled axios 可能略有 API 差异(CancelToken 等)。
  • 生产锁定 package 版本。

基础请求方法:

javascript 复制代码
// ES6 模块方式
import axios from 'axios';

// CommonJS 方式
const axios = require('axios');

// GET 请求
axios.get('/user?id=12345')
    .then(response => {
        console.log('用户数据:', response.data);
    })
    .catch(error => {
        console.error('获取失败:', error);
    });

// 带参数的 GET 请求
axios.get('/user', {
    params: {
        id: 12345,
        name: '张三'
    }
})
.then(response => console.log(response.data));

// POST 请求
axios.post('/user', {
    name: '李四',
    age: 28,
    email: 'lisi@example.com'
})
.then(response => console.log('新用户:', response.data))
.catch(error => console.error('创建失败:', error));

// 多种请求方式
axios.put('/user/12345', { name: '王五' });
axios.patch('/user/12345', { age: 29 });
axios.delete('/user/12345');

【代码注释】

  • Axios 基于 XHR + Promise;浏览器/Node 均可。
  • GET 查询用 params 对象;POST 体用 data
  • 响应在 response.data,状态在 response.status
  • 01-引入axios.html 引入脚本后即可用全局 axios

3.3 Axios 实例创建

自定义实例:

javascript 复制代码
// 创建自定义实例
const instance = axios.create({
    baseURL: 'https://api.example.com',
    timeout: 5000,
    headers: {
        'Content-Type': 'application/json',
        'X-Custom-Header': 'custom-value'
    }
});

// 使用自定义实例
instance.get('/users')
    .then(response => console.log(response.data))
    .catch(error => console.error(error));

// 创建多个实例用于不同用途
const apiInstance = axios.create({
    baseURL: 'https://api.example.com',
    timeout: 10000
});

const authInstance = axios.create({
    baseURL: 'https://auth.example.com',
    timeout: 5000,
    headers: {
        'Authorization': 'Bearer token123'
    }
});

// 为不同实例配置不同的拦截器
apiInstance.interceptors.request.use(config => {
    // API 请求的特定处理
    return config;
});

authInstance.interceptors.request.use(config => {
    // 认证请求的特定处理
    return config;
});

【代码注释】

  • 07-拦截器.html :请求拦截改 config,须 return config 放行。
  • 响应拦截改 res 或统一解包 datareturn res 传给 then。
  • eject(id) 移除拦截器;多个拦截器按注册顺序执行。
  • 流程:请求拦截2→1→发请求→响应拦截→回调(笔记)。

3.4 拦截器机制

请求拦截器:

javascript 复制代码
// 添加请求拦截器
axios.interceptors.request.use(
    config => {
        // 在发送请求之前做些什么
        console.log('发送请求:', config.method.toUpperCase(), config.url);
        
        // 添加认证令牌
        const token = localStorage.getItem('token');
        if (token) {
            config.headers.Authorization = `Bearer ${token}`;
        }
        
        // 添加时间戳防止缓存
        if (config.method === 'get') {
            config.params = {
                ...config.params,
                _t: Date.now()
            };
        }
        
        // 显示加载指示器
        showLoadingIndicator();
        
        return config;
    },
    error => {
        // 对请求错误做些什么
        console.error('请求错误:', error);
        return Promise.reject(error);
    }
);

// 添加响应拦截器
axios.interceptors.response.use(
    response => {
        // 对响应数据做点什么
        console.log('接收响应:', response.status, response.config.url);
        
        // 隐藏加载指示器
        hideLoadingIndicator();
        
        // 统一处理响应数据格式
        if (response.data.code === 0) {
            return response.data.data;
        } else {
            return Promise.reject(new Error(response.data.message));
        }
    },
    error => {
        // 对响应错误做点什么
        console.error('响应错误:', error);
        
        // 隐藏加载指示器
        hideLoadingIndicator();
        
        // 处理特定错误状态码
        if (error.response) {
            switch (error.response.status) {
                case 401:
                    console.error('未授权,请重新登录');
                    redirectToLogin();
                    break;
                case 403:
                    console.error('权限不足');
                    break;
                case 404:
                    console.error('请求的资源不存在');
                    break;
                case 500:
                    console.error('服务器错误');
                    break;
                default:
                    console.error('请求失败:', error.response.status);
            }
        } else if (error.request) {
            console.error('网络错误,请检查网络连接');
        } else {
            console.error('请求配置错误:', error.message);
        }
        
        return Promise.reject(error);
    }
);

【代码注释】

  • 07-拦截器.html :请求拦截改 config,须 return config 放行。
  • 响应拦截改 res 或统一解包 datareturn res 传给 then。
  • eject(id) 移除拦截器;多个拦截器按注册顺序执行。
  • 流程:请求拦截2→1→发请求→响应拦截→回调(笔记)。

3.5 请求取消

取消请求实现:

javascript 复制代码
// 使用 CancelToken
const source = axios.CancelToken.source();

axios.get('/api/data', {
    cancelToken: source.token
})
.then(response => {
    console.log('数据:', response.data);
})
.catch(error => {
    if (axios.isCancel(error)) {
        console.log('请求被取消:', error.message);
    } else {
        console.error('请求失败:', error);
    }
});

// 取消请求
source.cancel('操作被用户取消');

// 使用 AbortController
const controller = new AbortController();

axios.get('/api/data', {
    signal: controller.signal
})
.then(response => console.log(response.data))
.catch(error => {
    if (error.name === 'CanceledError') {
        console.log('请求被取消');
    }
});

// 取消请求
controller.abort();

【代码注释】

  • 05-取消请求.htmlcancel() 触发,catch 里 axios.isCancel(err)
  • 新版推荐 AbortController + signal 统一 fetch/axios。
  • 路由切换时应取消未完成请求,避免 setState 已卸载组件。
  • 重复点击提交前 cancel 上一次。

3.8 配套示例:04-Axios 案例对照

引入(01-引入axios.html):

html 复制代码
<script src="./axios/axios.js"></script>
<!-- 或 CDN: https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js -->

【代码注释】

  • 浏览器直接打开;需后端或 json-server 已启动。
  • 与 0.1 表中同名案例对照。
  • BASE URL 改为你本机 127.0.0.1:8088 即可。
  • 失败时看 Network 面板状态码与 CORS。

基本使用(02-axios的基本使用.html):

javascript 复制代码
// ① 配置对象
axios({ method: 'GET', url: 'http://127.0.0.1:8088/transcript' })
  .then(res => console.log(res));

// ② url + config(baseURL 拼相对路径)
axios('/transcript/2', { method: 'get', baseURL: 'http://127.0.0.1:8088', timeout: 5000 });

// ③ 别名
axios.get('/transcript/4', { baseURL: 'http://127.0.0.1:8088' });

// ④ async/await
const res = await axios.get('/transcript/5', { baseURL: 'http://127.0.0.1:8088' });
console.log(res.data); // 业务数据在 data,不是 xhr.response

【代码注释】

  • resaxios 响应对象datastatusheadersconfig(笔记 §5)。
  • 失败进 catch 或 try/catch;Axios 对 非 2xx 默认 reject(与 Fetch 不同)。
  • 03-请求配置项.html 演示 params(查询串)、data(请求体)、timeout 等。

创建实例(04-创建axios实例.html):

javascript 复制代码
const musicInstance = axios.create({
  baseURL: 'http://api.fuming.site:54255',
  timeout: 5000
});
const transcriptInstance = axios.create({
  baseURL: 'http://127.0.0.1:8088',
  timeout: 2000
});
musicInstance.get('/playlist/detail', { params: { id: 3779629 } })
  .then(res => console.log(res.data));

【代码注释】

  • 不同业务域用不同实例,避免每次写完整 URL;实例 all/CancelToken 静态方法,用 axios.all 或从默认 axios 引入。
  • params 自动序列化为 URL 查询;data 用于 POST/PUT/PATCH 体。

批量请求(06-批量发送请求.html):

javascript 复制代码
axios.defaults.baseURL = 'http://127.0.0.1:8088';
const r1 = axios.get('/transcript/1');
const r2 = axios.get('/transcript/2');
axios.all([r1, r2, r3, r4])
  .then(([s1, s2, s3, s4]) => { console.log(s1.data, s2.data); });
// 旧写法:.then(axios.spread((s1,s2,s3,s4)=>{...}))

【代码注释】

  • 等价 Promise.all;任一失败整体进 catch。
  • 与 Promise 进阶篇 Promise.all 并行拉歌单场景同一思路。

拦截器(07-拦截器.html) 见 §5;05-取消请求.html 见 §3.6 CancelToken。

【实战要点】

  • 项目入口 axios.create({ baseURL }) + 拦截器挂 token、统一错误提示。
  • 读响应用 res.data,不要和 Fetch 的 response.json() 混淆。

【面试考点】

Q1:axios 响应结构有哪些字段?

A:datastatusstatusTextheadersconfig

Q2:axios.createaxios.defaults 区别?

A:create 新实例隔离配置;defaults 改全局默认。

Q3:axios 与 fetch 在错误处理上的差异?

A:axios 非 2xx 默认 reject;fetch 需判 ok

3.7 配套可运行示例(拦截器最小版)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>拦截器</title></head>
<body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
axios.defaults.baseURL = 'http://127.0.0.1:8088';
axios.interceptors.request.use(cfg => {
  cfg.headers['X-Demo'] = '1';
  return cfg;
});
axios.interceptors.response.use(res => res.data);
axios.get('/transcript/1').then(d => console.log('data', d));
</script>
</body>
</html>

【代码注释】

  • 对应 07-拦截器.html ;须先 json-server;响应拦截直接返回 data 简化业务代码。

【本章小结】

能力 API
实例 axios.create
批量 axios.all
取消 CancelToken / signal
拦截 interceptors.request/response

4. REST API 设计原则

4.1 REST 架构风格

4.0 配套可运行示例(方法对照表)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>REST 方法</title></head>
<body>
<pre id="out"></pre>
<script>
const rows = [
  ['GET', '查询列表/单条'],
  ['POST', '新增'],
  ['PUT', '全量更新'],
  ['PATCH', '部分更新'],
  ['DELETE', '删除']
];
out.textContent = rows.map(r => r[0].padEnd(6) + r[1]).join('\n');
</script>
</body>
</html>

【代码注释】

  • 03-Fetchtranscript 演示 GET/POST/PUT/DELETE;PATCH 视后端支持。

REST (Representational State Transfer) 是一种软件架构风格,用于设计网络应用的API。

核心原则:

javascript 复制代码
// REST API 设计原则
const restPrinciples = {
    // 1. 统一接口
    uniformInterface: {
        description: '使用统一的资源标识符和接口',
        example: '/api/users/123'
    },
    
    // 2. 无状态
    stateless: {
        description: '每个请求都包含所有必要信息',
        example: '请求头包含认证信息'
    },
    
    // 3. 可缓存
    cacheable: {
        description: '响应应该明确是否可缓存',
        example: 'Cache-Control: max-age=3600'
    },
    
    // 4. 客户端-服务器分离
    clientServer: {
        description: '客户端和服务器职责分离',
        example: '前端负责UI,后端负责数据处理'
    },
    
    // 5. 分层系统
    layeredSystem: {
        description: '允许中间层(代理、负载均衡)',
        example: 'CDN、API网关'
    },
    
    // 6. 按需代码(可选)
    codeOnDemand: {
        description: '服务器可以临时扩展客户端功能',
        example: 'JavaScript代码传输'
    }
};

【代码注释】

  • REST 是架构风格,非标准协议;实践中有各种变体。
  • URL 用名词复数、层级不宜过深。
  • 与 §4.4 json-server 路由对照记忆。
  • 业务 code 包装见 §4.3 响应格式(扩展)。

4.2 REST API 设计规范

URL 设计规范:

javascript 复制代码
// REST API URL 设计
const apiDesign = {
    // 基础 URL
    baseUrl: 'https://api.example.com/v1',
    
    // 资源命名(使用名词复数)
    resources: {
        users: '/users',           // 用户资源
        articles: '/articles',     // 文章资源
        comments: '/comments'      // 评论资源
    },
    
    // 层级关系
    hierarchy: {
        userArticles: '/users/{userId}/articles',
        articleComments: '/articles/{articleId}/comments'
    },
    
    // 过滤和排序
    filtering: {
        base: '/articles',
        filters: {
            status: '/articles?status=published',
            date: '/articles?created_after=2024-01-01',
            sort: '/articles?sort=-created_at',
            pagination: '/articles?page=1&limit=20'
        }
    },
    
    // HTTP 方法映射
    methods: {
        collection: {
            list: 'GET /articles',        // 获取列表
            create: 'POST /articles'      // 创建资源
        },
        resource: {
            get: 'GET /articles/{id}',         // 获取单个资源
            update: 'PUT /articles/{id}',      // 更新资源
            patch: 'PATCH /articles/{id}',     // 部分更新
            delete: 'DELETE /articles/{id}'    // 删除资源
        }
    }
};

// 实际使用示例
const apiExamples = {
    // 获取用户列表
    getUsers: 'GET /api/v1/users?page=1&limit=20',
    
    // 获取特定用户
    getUser: 'GET /api/v1/users/123',
    
    // 创建用户
    createUser: 'POST /api/v1/users',
    
    // 更新用户
    updateUser: 'PUT /api/v1/users/123',
    
    // 删除用户
    deleteUser: 'DELETE /api/v1/users/123',
    
    // 获取用户的文章
    getUserArticles: 'GET /api/v1/users/123/articles',
    
    // 搜索文章
    searchArticles: 'GET /api/v1/articles?q=keyword&status=published'
};

【代码注释】

  • REST 是架构风格,非标准协议;实践中有各种变体。
  • URL 用名词复数、层级不宜过深。
  • 与 §4.4 json-server 路由对照记忆。
  • 业务 code 包装见 §4.3 响应格式(扩展)。

4.3 响应格式规范

标准响应格式:

javascript 复制代码
// 成功响应
{
    "code": 0,
    "message": "success",
    "data": {
        "id": 123,
        "name": "张三",
        "email": "zhangsan@example.com"
    },
    "timestamp": "2024-01-15T10:30:00Z"
}

// 错误响应
{
    "code": 1001,
    "message": "用户不存在",
    "error": {
        "type": "NotFoundError",
        "details": "找不到ID为123的用户"
    },
    "timestamp": "2024-01-15T10:30:00Z"
}

// 分页响应
{
    "code": 0,
    "message": "success",
    "data": {
        "items": [
            { "id": 1, "name": "项目1" },
            { "id": 2, "name": "项目2" }
        ],
        "pagination": {
            "page": 1,
            "limit": 20,
            "total": 100,
            "totalPages": 5
        }
    },
    "timestamp": "2024-01-15T10:30:00Z"
}

【代码注释】

  • 结合本节标题与 0.1 案例表对照学习。
  • Fetch 注意 ok/status;Axios 注意 data 字段。
  • 本地先 json-server 再跑 HTML。
  • 跨域与 Cookie 参考 Ajax 进阶与跨域篇、会话控制篇 Session。

4.4 配套:json-server 搭建 REST 练习环境

REST API 笔记 笔记:用 json-server 快速提供 REST 接口,供 03-Fetch / 04-Axios 联调。

bash 复制代码
npm install -g json-server
cd 本篇-Fetch&Axios
json-server --watch data/transcript.json --port 8088 --host 127.0.0.1 --delay 500

【代码注释】

  • JSON 根必须是对象 (如 { "transcript": [...] }),不能是数组。
  • --delay 模拟网络延迟;--watch 文件改动后接口数据同步更新。
  • 资源路径:GET/POST /transcriptGET/PUT/PATCH/DELETE /transcript/:id(与 03-Fetch / 04-Axios 中 URL 一致)。
操作 方法 示例
列表 GET http://127.0.0.1:8088/transcript
单条 GET http://127.0.0.1:8088/transcript/2
新增 POST 带 JSON 请求体
全量改 PUT /transcript/2 + body
删除 DELETE /transcript/2

非 RESTful vs RESTful(笔记对比):

复制代码
非 REST:POST /news/create、GET /news/delete?id=1  (URL 动作化)
RESTful:POST /news、DELETE /news/1               (URL 表资源,方法表动作)

【代码注释】

  • 笔记示例用 shirly.com:8088 时可在 hosts 映射到 127.0.0.1,或直接把案例里的域名改为 127.0.0.1:8088
  • 与业务接口 { code, msg, data } 包装不同,json-server 直接返回 JSON 数组/对象。

【实战要点】

  • 前后端分离联调:先 json-server,再换真实后端;Axios baseURL 一处修改即可。
  • 用 Postman / Apipost 先测通接口再写前端。

【面试考点】

Q1:RESTful 的核心是什么?

A:URL 表资源,HTTP 方法表动作,而非 URL 里写 create/delete。

Q2:POST 与 GET 在 Fetch/Axios 配置上的区别?

A:GET 用 params/查询串;POST 用 method+body+Content-Type。

4.5 配套可运行示例(REST 四方法)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>REST 速览</title></head>
<body>
<button id="g">GET</button><button id="d">DELETE id=1</button>
<script>
const B = 'http://127.0.0.1:8088/transcript';
document.getElementById('g').onclick = async () => console.log(await (await fetch(B)).json());
document.getElementById('d').onclick = async () => console.log(await (await fetch(B+'/1',{method:'DELETE'})).json());
</script>
</body>
</html>

【代码注释】

  • 03-Fetch/index.html 同源;PUT/PATCH 见完整案例。

【本章小结】

原则 说明
资源 URL 表名词,动词用 HTTP 方法
json-server 快速 mock REST
响应 列表数组或单条对象

5. 请求拦截与响应处理

5.1 统一请求处理

HTTP 客户端封装:

javascript 复制代码
class HttpClient {
    constructor(config = {}) {
        this.instance = axios.create({
            baseURL: config.baseURL || '/api',
            timeout: config.timeout || 10000,
            headers: {
                'Content-Type': 'application/json'
            }
        });
        
        this.setupInterceptors();
    }
    
    setupInterceptors() {
        // 请求拦截器
        this.instance.interceptors.request.use(
            config => {
                // 添加认证令牌
                const token = this.getAuthToken();
                if (token) {
                    config.headers.Authorization = `Bearer ${token}`;
                }
                
                // 添加请求ID用于追踪
                config.headers['X-Request-ID'] = this.generateRequestId();
                
                // 请求日志
                console.log(`[${config.method.toUpperCase()}] ${config.url}`);
                
                return config;
            },
            error => {
                console.error('请求错误:', error);
                return Promise.reject(error);
            }
        );
        
        // 响应拦截器
        this.instance.interceptors.response.use(
            response => {
                // 统一处理响应数据
                const { data } = response;
                
                // 处理业务状态码
                if (data.code === 0) {
                    return data.data;
                } else {
                    return Promise.reject(new Error(data.message));
                }
            },
            error => {
                // 统一错误处理
                return this.handleError(error);
            }
        );
    }
    
    getAuthToken() {
        return localStorage.getItem('auth_token');
    }
    
    generateRequestId() {
        return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    }
    
    handleError(error) {
        if (error.response) {
            // 服务器响应错误
            const { status, data } = error.response;
            
            switch (status) {
                case 401:
                    this.handleAuthError();
                    break;
                case 403:
                    console.error('权限不足');
                    break;
                case 404:
                    console.error('资源不存在');
                    break;
                case 500:
                    console.error('服务器错误');
                    break;
                default:
                    console.error('请求失败:', data.message);
            }
        } else if (error.request) {
            // 网络错误
            console.error('网络错误,请检查连接');
        } else {
            // 请求配置错误
            console.error('请求配置错误:', error.message);
        }
        
        return Promise.reject(error);
    }
    
    handleAuthError() {
        console.error('认证失败,请重新登录');
        localStorage.removeItem('auth_token');
        // 跳转到登录页
        window.location.href = '/login';
    }
    
    // 快捷方法
    get(url, params, config = {}) {
        return this.instance.get(url, { ...config, params });
    }
    
    post(url, data, config = {}) {
        return this.instance.post(url, data, config);
    }
    
    put(url, data, config = {}) {
        return this.instance.put(url, data, config);
    }
    
    delete(url, config = {}) {
        return this.instance.delete(url, config);
    }
    
    patch(url, data, config = {}) {
        return this.instance.patch(url, data, config);
    }
}

// 使用示例
const httpClient = new HttpClient({
    baseURL: 'https://api.example.com',
    timeout: 15000
});

// 发送请求
async function fetchData() {
    try {
        const users = await httpClient.get('/users', { page: 1, limit: 20 });
        console.log('用户列表:', users);
    } catch (error) {
        console.error('获取用户失败:', error);
    }
}

【代码注释】

  • 07-拦截器.html :请求拦截改 config,须 return config 放行。
  • 响应拦截改 res 或统一解包 datareturn res 传给 then。
  • eject(id) 移除拦截器;多个拦截器按注册顺序执行。
  • 流程:请求拦截2→1→发请求→响应拦截→回调(笔记)。

5.2 响应数据转换

数据转换处理:

javascript 复制代码
// 响应数据转换器
class ResponseTransformer {
    // 转换用户数据
    static transformUser(data) {
        return {
            id: data._id || data.id,
            username: data.username || data.name,
            email: data.email,
            avatar: data.avatar || data.profile?.avatar,
            role: data.role || 'user',
            createdAt: data.created_at || data.createdAt,
            updatedAt: data.updated_at || data.updatedAt
        };
    }
    
    // 批量转换用户数据
    static transformUserList(data) {
        return {
            items: data.items?.map(item => this.transformUser(item)) || [],
            pagination: {
                page: data.page || data.pagination?.page || 1,
                limit: data.limit || data.pagination?.limit || 20,
                total: data.total || data.pagination?.total || 0,
                totalPages: data.total_pages || data.pagination?.totalPages || 0
            }
        };
    }
    
    // 转换文章数据
    static transformArticle(data) {
        return {
            id: data._id || data.id,
            title: data.title,
            content: data.content,
            excerpt: data.excerpt || data.summary,
            author: this.transformUser(data.author),
            tags: data.tags || [],
            category: data.category,
            status: data.status || 'draft',
            views: data.views || 0,
            createdAt: data.created_at || data.createdAt,
            updatedAt: data.updated_at || data.updatedAt
        };
    }
    
    // 通用错误转换
    static transformError(error) {
        return {
            code: error.code || error.response?.data?.code || 500,
            message: error.message || error.response?.data?.message || '未知错误',
            details: error.details || error.response?.data?.details || null,
            timestamp: new Date().toISOString()
        };
    }
}

// 使用转换器
class ApiClient {
    constructor(httpClient) {
        this.http = httpClient;
    }
    
    async getUsers(params) {
        try {
            const rawData = await this.http.get('/users', params);
            return ResponseTransformer.transformUserList(rawData);
        } catch (error) {
            throw ResponseTransformer.transformError(error);
        }
    }
    
    async getUser(id) {
        try {
            const rawData = await this.http.get(`/users/${id}`);
            return ResponseTransformer.transformUser(rawData);
        } catch (error) {
            throw ResponseTransformer.transformError(error);
        }
    }
    
    async getArticles(params) {
        try {
            const rawData = await this.http.get('/articles', params);
            return {
                items: rawData.items?.map(item => 
                    ResponseTransformer.transformArticle(item)
                ) || [],
                pagination: rawData.pagination
            };
        } catch (error) {
            throw ResponseTransformer.transformError(error);
        }
    }
}

【代码注释】

  • 结合本节标题与 0.1 案例表对照学习。
  • Fetch 注意 ok/status;Axios 注意 data 字段。
  • 本地先 json-server 再跑 HTML。
  • 跨域与 Cookie 见 Ajax 进阶篇、会话控制篇。

5.2 配套可运行示例(响应拦截解包 data)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>响应拦截</title></head>
<body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
axios.defaults.baseURL = 'http://127.0.0.1:8088';
axios.interceptors.response.use(res => {
  if (res.data && typeof res.data === 'object') return res.data;
  return res;
});
axios.get('/transcript').then(d => console.log('业务数据', d));
</script>
</body>
</html>

【代码注释】

  • 对应 07-拦截器.html 响应分支;业务层直接拿到列表数组,无需再写 .data

【本章小结】

环节 作用
请求拦截 加 token、改 header
响应拦截 解包 data、统一错误

【实战要点】

  • 请求拦截必须 return config;勿在拦截器里抛错而不 reject。
  • 多个拦截器按注册顺序执行。

【面试考点】

Q1:axios 拦截器执行顺序?

A:请求拦截后注册先执行;响应拦截先注册先执行。


6. 高级功能实现

6.1 并发请求控制

6.0 配套可运行示例(Fetch 并行)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>并行 fetch</title></head>
<body>
<script>
Promise.all([
  fetch('https://httpbin.org/get?n=1').then(r => r.json()),
  fetch('https://httpbin.org/get?n=2').then(r => r.json())
]).then(arr => console.log(arr.map(x => x.args.n)));
</script>
</body>
</html>

【代码注释】

  • 06-批量发送请求.html 思路一致;任一失败整体 reject。

请求队列管理:

javascript 复制代码
class RequestQueue {
    constructor(concurrency = 5) {
        this.concurrency = concurrency;
        this.running = 0;
        this.queue = [];
    }
    
    add(requestFn) {
        return new Promise((resolve, reject) => {
            const task = {
                requestFn,
                resolve,
                reject
            };
            
            this.queue.push(task);
            this.run();
        });
    }
    
    async run() {
        while (this.running < this.concurrency && this.queue.length > 0) {
            const task = this.queue.shift();
            this.running++;
            
            try {
                const result = await task.requestFn();
                task.resolve(result);
            } catch (error) {
                task.reject(error);
            } finally {
                this.running--;
                this.run();
            }
        }
    }
}

// 使用示例
const queue = new RequestQueue(3); // 最多同时3个请求

const urls = [
    'https://api.example.com/data1',
    'https://api.example.com/data2',
    'https://api.example.com/data3',
    'https://api.example.com/data4',
    'https://api.example.com/data5'
];

const promises = urls.map(url => 
    queue.add(() => axios.get(url))
);

Promise.all(promises)
    .then(responses => console.log('所有请求完成:', responses))
    .catch(error => console.error('有请求失败:', error));

【代码注释】

  • 结合本节标题与 0.1 案例表对照学习。
  • Fetch 注意 ok/status;Axios 注意 data 字段。
  • 本地先 json-server 再跑 HTML。
  • 跨域与 Cookie 参考 Ajax 进阶与跨域篇、会话控制篇 Session。

6.2 请求重试机制

智能重试策略:

javascript 复制代码
class RetryableRequest {
    constructor(options = {}) {
        this.maxRetries = options.maxRetries || 3;
        this.retryDelay = options.retryDelay || 1000;
        this.backoffMultiplier = options.backoffMultiplier || 2;
        this.retryCondition = options.retryCondition || this.defaultRetryCondition;
    }
    
    defaultRetryCondition(error) {
        // 默认重试条件
        if (!error.response) {
            // 网络错误重试
            return true;
        }
        
        const status = error.response.status;
        // 5xx 错误重试,4xx 错误不重试
        return status >= 500 && status < 600;
    }
    
    async execute(requestFn) {
        let lastError;
        let delay = this.retryDelay;
        
        for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
            try {
                const result = await requestFn();
                
                if (attempt > 0) {
                    console.log(`重试成功,尝试次数: ${attempt}`);
                }
                
                return result;
                
            } catch (error) {
                lastError = error;
                
                if (attempt < this.maxRetries && this.retryCondition(error)) {
                    console.log(`第 ${attempt + 1} 次尝试失败,${delay}ms 后重试`);
                    await this.sleep(delay);
                    delay *= this.backoffMultiplier;
                } else {
                    break;
                }
            }
        }
        
        throw new Error(`请求失败,已尝试 ${this.maxRetries + 1} 次: ${lastError.message}`);
    }
    
    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

// 使用示例
const retryable = new RetryableRequest({
    maxRetries: 5,
    retryDelay: 1000,
    backoffMultiplier: 1.5
});

async function fetchWithRetry(url) {
    return retryable.execute(async () => {
        const response = await axios.get(url);
        return response.data;
    });
}

// 实际应用
fetchWithRetry('https://api.example.com/unstable-endpoint')
    .then(data => console.log('数据:', data))
    .catch(error => console.error('最终失败:', error));

【代码注释】

  • 仅对幂等请求或安全错误重试;POST 慎重重试。
  • 指数退避减轻服务端压力。
  • 与 axios 拦截器结合可统一重试。
  • 生产配合网关限流。

6.3 缓存策略实现

HTTP 缓存封装:

javascript 复制代码
class CachedHttpClient {
    constructor(options = {}) {
        this.http = options.http || axios;
        this.cache = options.cache || new Map();
        this.defaultTTL = options.defaultTTL || 60000; // 默认1分钟
    }
    
    async get(url, params, options = {}) {
        const cacheKey = this.buildCacheKey('GET', url, params);
        const cached = this.cache.get(cacheKey);
        
        // 检查缓存
        if (cached && !this.isCacheExpired(cached)) {
            console.log('使用缓存数据:', cacheKey);
            return cached.data;
        }
        
        // 发送请求
        try {
            const response = await this.http.get(url, { params });
            const data = response.data;
            
            // 存入缓存
            if (options.cache !== false) {
                const ttl = options.ttl || this.defaultTTL;
                this.cache.set(cacheKey, {
                    data,
                    expiresAt: Date.now() + ttl
                });
            }
            
            return data;
            
        } catch (error) {
            // 如果请求失败且有缓存数据,返回缓存
            if (cached && options.fallbackToCache) {
                console.log('请求失败,使用缓存数据');
                return cached.data;
            }
            
            throw error;
        }
    }
    
    buildCacheKey(method, url, params) {
        const paramsStr = JSON.stringify(params || {});
        return `${method}:${url}:${paramsStr}`;
    }
    
    isCacheExpired(cached) {
        return cached.expiresAt < Date.now();
    }
    
    clear() {
        this.cache.clear();
    }
    
    clearPattern(pattern) {
        for (const key of this.cache.keys()) {
            if (key.includes(pattern)) {
                this.cache.delete(key);
            }
        }
    }
}

// 使用示例
const cachedClient = new CachedHttpClient({
    http: axios,
    defaultTTL: 30000 // 30秒缓存
});

// 第一次请求会发送网络请求
cachedClient.get('/api/users', { page: 1 })
    .then(data => console.log('用户数据:', data));

// 第二次请求会使用缓存
cachedClient.get('/api/users', { page: 1 })
    .then(data => console.log('用户数据(缓存):', data));

// 清除所有缓存
cachedClient.clear();

【代码注释】

  • 07-拦截器.html :请求拦截改 config,须 return config 放行。
  • 响应拦截改 res 或统一解包 datareturn res 传给 then。
  • eject(id) 移除拦截器;多个拦截器按注册顺序执行。
  • 流程:请求拦截2→1→发请求→响应拦截→回调(笔记)。

6.4 配套可运行示例(axios.all)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>批量请求</title></head>
<body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
axios.defaults.baseURL = 'http://127.0.0.1:8088';
Promise.all([1,2].map(id => axios.get('/transcript/'+id)))
  .then(arr => console.log(arr.map(r => r.data)));
</script>
</body>
</html>

【代码注释】

  • 对应 06-批量发送请求.html ;也可用原生 axios.all(旧 API)。

【本章小结】

能力 场景
并发队列 限流防打爆
重试 弱网补偿
缓存 减少重复 GET

【实战要点】

  • 批量用 Promise.all 时注意单点失败;需要部分成功用 allSettled
  • 重试加指数退避,避免雪崩。

【面试考点】

Q1:如何实现请求并发上限?

A:维护队列 + running 计数,完成后再取下一个任务。


7. 生产环境最佳实践

7.0 配套可运行示例(区分 axios 错误类型)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>错误类型</title></head>
<body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
axios.get('https://httpbin.org/status/500').catch(e => {
  console.log('有 response', !!e.response, 'status', e.response?.status);
});
axios.get('https://invalid.invalid').catch(e => {
  console.log('无 response', !e.response, 'message', e.message);
});
</script>
</body>
</html>

【代码注释】

  • error.response 存在表示服务器有响应;仅有 error.request 多为 DNS/网络问题。

7.1 错误监控和日志

全面的错误监控系统:

javascript 复制代码
class ApiMonitor {
    constructor(httpClient) {
        this.http = httpClient;
        this.setupMonitoring();
    }
    
    setupMonitoring() {
        // 监控请求开始
        this.http.interceptors.request.use(config => {
            config.metadata = {
                startTime: Date.now(),
                requestId: this.generateRequestId()
            };
            
            this.logRequest(config);
            return config;
        });
        
        // 监控响应
        this.http.interceptors.response.use(
            response => {
                this.logResponse(response);
                this.trackPerformance(response.config);
                return response;
            },
            error => {
                this.logError(error);
                this.trackError(error);
                return Promise.reject(error);
            }
        );
    }
    
    generateRequestId() {
        return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    }
    
    logRequest(config) {
        const logData = {
            requestId: config.metadata.requestId,
            method: config.method.toUpperCase(),
            url: config.url,
            headers: this.sanitizeHeaders(config.headers),
            params: config.params,
            data: config.data,
            timestamp: new Date().toISOString()
        };
        
        console.log('🚀 API 请求:', logData);
        
        // 发送到日志系统
        this.sendToLogSystem('request', logData);
    }
    
    logResponse(response) {
        const duration = Date.now() - response.config.metadata.startTime;
        
        const logData = {
            requestId: response.config.metadata.requestId,
            status: response.status,
            duration: duration,
            size: JSON.stringify(response.data).length,
            timestamp: new Date().toISOString()
        };
        
        console.log('✅ API 响应:', logData);
        
        // 发送到日志系统
        this.sendToLogSystem('response', logData);
    }
    
    logError(error) {
        const logData = {
            requestId: error.config?.metadata?.requestId,
            message: error.message,
            code: error.code,
            status: error.response?.status,
            url: error.config?.url,
            timestamp: new Date().toISOString()
        };
        
        console.error('❌ API 错误:', logData);
        
        // 发送到错误跟踪系统
        this.sendToErrorTracking(logData);
    }
    
    trackPerformance(config) {
        const duration = Date.now() - config.metadata.startTime;
        
        // 记录性能指标
        if (duration > 3000) {
            console.warn('⚠️ 慢请求告警:', {
                url: config.url,
                duration: duration
            });
        }
        
        // 发送到性能监控系统
        this.sendToPerformanceMonitor({
            url: config.url,
            method: config.method,
            duration: duration,
            timestamp: Date.now()
        });
    }
    
    trackError(error) {
        // 发送到错误监控系统
        this.sendToErrorTracking({
            type: 'api_error',
            message: error.message,
            stack: error.stack,
            url: error.config?.url,
            timestamp: Date.now()
        });
    }
    
    sanitizeHeaders(headers) {
        const sanitized = { ...headers };
        
        // 移除敏感信息
        const sensitiveKeys = ['authorization', 'token', 'cookie'];
        sensitiveKeys.forEach(key => {
            delete sanitized[key];
        });
        
        return sanitized;
    }
    
    sendToLogSystem(type, data) {
        // 实现发送到日志系统的逻辑
        // 例如: sendToSplunk(data), sendToELK(data) 等
    }
    
    sendToErrorTracking(error) {
        // 实现发送到错误跟踪系统的逻辑
        // 例如: sendToSentry(error), sendToBugsnag(error) 等
    }
    
    sendToPerformanceMonitor(metric) {
        // 实现发送到性能监控系统的逻辑
        // 例如: sendToDataDog(metric), sendToNewRelic(metric) 等
    }
}

【代码注释】

  • 07-拦截器.html :请求拦截改 config,须 return config 放行。
  • 响应拦截改 res 或统一解包 datareturn res 传给 then。
  • eject(id) 移除拦截器;多个拦截器按注册顺序执行。
  • 流程:请求拦截2→1→发请求→响应拦截→回调(笔记)。

7.2 安全防护措施

API 安全最佳实践:

javascript 复制代码
class SecureHttpClient {
    constructor(options = {}) {
        this.http = options.http || axios.create();
        this.securityConfig = options.security || {};
        this.setupSecurity();
    }
    
    setupSecurity() {
        // 请求安全拦截器
        this.http.interceptors.request.use(config => {
            // 添加 CSRF 令牌
            const csrfToken = this.getCSRFToken();
            if (csrfToken) {
                config.headers['X-CSRF-Token'] = csrfToken;
            }
            
            // 添加请求签名
            if (this.securityConfig.enableSignature) {
                config.headers['X-Signature'] = this.generateRequestSignature(config);
            }
            
            // 验证请求大小
            const requestSize = this.calculateRequestSize(config);
            if (requestSize > this.securityConfig.maxRequestSize) {
                throw new Error('请求大小超过限制');
            }
            
            // 添加时间戳防止重放攻击
            config.headers['X-Timestamp'] = Date.now();
            
            return config;
        });
        
        // 响应安全验证
        this.http.interceptors.response.use(
            response => {
                // 验证响应签名
                if (this.securityConfig.enableSignature) {
                    this.validateResponseSignature(response);
                }
                
                // 检查安全响应头
                this.checkSecurityHeaders(response);
                
                return response;
            },
            error => {
                // 处理安全相关错误
                this.handleSecurityError(error);
                return Promise.reject(error);
            }
        );
    }
    
    getCSRFToken() {
        return document.querySelector('meta[name="csrf-token"]')?.content;
    }
    
    generateRequestSignature(config) {
        // 实现请求签名逻辑
        const secret = this.securityConfig.signatureSecret;
        const payload = {
            method: config.method,
            url: config.url,
            params: config.params,
            timestamp: Date.now()
        };
        
        return this.signPayload(payload, secret);
    }
    
    signPayload(payload, secret) {
        const crypto = require('crypto');
        const payloadStr = JSON.stringify(payload);
        return crypto.createHmac('sha256', secret).update(payloadStr).digest('hex');
    }
    
    validateResponseSignature(response) {
        // 实现响应签名验证逻辑
        const receivedSignature = response.headers['x-signature'];
        if (!receivedSignature) {
            throw new Error('缺少响应签名');
        }
        
        // 验证签名...
    }
    
    checkSecurityHeaders(response) {
        // 检查安全相关的响应头
        const securityHeaders = [
            'X-Content-Type-Options',
            'X-Frame-Options',
            'X-XSS-Protection',
            'Strict-Transport-Security'
        ];
        
        securityHeaders.forEach(header => {
            if (!response.headers[header]) {
                console.warn(`缺少安全响应头: ${header}`);
            }
        });
    }
    
    calculateRequestSize(config) {
        let size = 0;
        
        if (config.data) {
            size += JSON.stringify(config.data).length;
        }
        
        if (config.params) {
            size += JSON.stringify(config.params).length;
        }
        
        return size;
    }
    
    handleSecurityError(error) {
        // 处理安全相关错误
        if (error.response?.status === 403) {
            console.error('权限被拒绝');
            // 可能的 CSRF 攻击
        } else if (error.response?.status === 419) {
            console.error('CSRF 令牌过期');
            // 刷新 CSRF 令牌
            this.refreshCSRFToken();
        }
    }
    
    refreshCSRFToken() {
        // 实现刷新 CSRF 令牌的逻辑
    }
}

【代码注释】

  • Response 体只能消费一次;json() 后再 text() 会报错。
  • headers.get 不区分大小写;遍历用 entries()。
  • blob/arrayBuffer 用于文件、图片下载。
  • response.type 在跨域 no-cors 时为 opaque。

7.3 配套可运行示例(统一错误日志)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>错误处理</title></head>
<body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
axios.interceptors.response.use(
  r => r,
  err => { console.error('[API]', err.response?.status, err.message); return Promise.reject(err); }
);
axios.get('/not-exist').catch(() => {});
</script>
</body>
</html>

【代码注释】

  • 生产可对接 Sentry;区分 error.response / error.request / 配置错误。

【本章小结】

主题 要点
监控 耗时、状态码、错误率
安全 HTTPS、token、CSRF

【实战要点】

  • 勿在前端日志打印完整 token;敏感字段脱敏。
  • CORS、CSP 与接口鉴权配合。

【面试考点】

Q1:axios 错误对象如何区分网络错与 4xx?

A:有 error.response 为服务端响应;仅有 error.request 多为网络/超时。


8. 性能优化与监控

8.0 配套可运行示例(AbortController 取消)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>取消 fetch</title></head>
<body>
<button id="go">请求</button><button id="cancel">取消</button>
<script>
const c = new AbortController();
document.getElementById('go').onclick = () => {
  fetch('https://httpbin.org/delay/5', { signal: c.signal })
    .catch(e => console.log(e.name));
};
document.getElementById('cancel').onclick = () => c.abort();
</script>
</body>
</html>

【代码注释】

  • 对应 05-取消请求.html 的 Abort 方案;路由离开页面前应 abort 未完成请求。

8.1 性能优化策略

HTTP 客户端性能优化:

javascript 复制代码
class PerformanceOptimizedClient {
    constructor(options = {}) {
        this.http = options.http || axios.create();
        this.performanceConfig = options.performance || {};
        this.setupOptimizations();
    }
    
    setupOptimizations() {
        // 启用请求压缩
        if (this.performanceConfig.enableCompression) {
            this.setupCompression();
        }
        
        // 启用连接复用
        if (this.performanceConfig.enableKeepAlive) {
            this.setupKeepAlive();
        }
        
        // 启用请求批处理
        if (this.performanceConfig.enableBatching) {
            this.setupBatching();
        }
    }
    
    setupCompression() {
        this.http.interceptors.request.use(config => {
            // 启用请求压缩
            config.headers['Accept-Encoding'] = 'gzip, deflate, br';
            return config;
        });
    }
    
    setupKeepAlive() {
        // 配置 HTTP keep-alive
        this.http.defaults.headers.common['Connection'] = 'keep-alive';
    }
    
    setupBatching() {
        this.requestQueue = [];
        this.batchTimeout = null;
        
        this.http.interceptors.request.use(config => {
            return new Promise((resolve) => {
                this.requestQueue.push({ config, resolve });
                
                if (!this.batchTimeout) {
                    this.batchTimeout = setTimeout(() => {
                        this.flushBatch();
                    }, this.performanceConfig.batchDelay || 100);
                }
            });
        });
    }
    
    flushBatch() {
        const batch = this.requestQueue.splice(0, this.performanceConfig.batchSize || 10);
        
        // 批量处理请求
        batch.forEach(({ config, resolve }) => {
            resolve(config);
        });
        
        this.batchTimeout = null;
    }
    
    // 预加载资源
    async preloadResources(urls) {
        const preloadPromises = urls.map(url => 
            this.http.head(url).catch(error => {
                console.warn(`预加载失败: ${url}`, error);
            })
        );
        
        await Promise.all(preloadPromises);
    }
    
    // 资源优先级管理
    prioritizeRequest(config, priority) {
        config.priority = priority;
        config.metadata = {
            ...config.metadata,
            priority
        };
        
        return config;
    }
}

【代码注释】

  • 07-拦截器.html :请求拦截改 config,须 return config 放行。
  • 响应拦截改 res 或统一解包 datareturn res 传给 then。
  • eject(id) 移除拦截器;多个拦截器按注册顺序执行。
  • 流程:请求拦截2→1→发请求→响应拦截→回调(笔记)。

8.2 监控和告警

实时监控系统:

javascript 复制代码
class ApiMonitoringSystem {
    constructor() {
        this.metrics = {
            requests: {
                total: 0,
                success: 0,
                error: 0,
                timeout: 0
            },
            performance: {
                totalTime: 0,
                avgTime: 0,
                maxTime: 0,
                minTime: Infinity
            },
            endpoints: {}
        };
        
        this.alerts = [];
        this.thresholds = {
            errorRate: 0.05,        // 5% 错误率
            avgResponseTime: 2000,  // 2秒平均响应时间
            timeoutRate: 0.01       // 1% 超时率
        };
    }
    
    recordRequest(config, duration, success) {
        // 更新总体指标
        this.metrics.requests.total++;
        
        if (success) {
            this.metrics.requests.success++;
        } else {
            this.metrics.requests.error++;
        }
        
        // 更新性能指标
        this.updatePerformanceMetrics(duration);
        
        // 更新端点指标
        this.updateEndpointMetrics(config.url, duration, success);
        
        // 检查告警条件
        this.checkAlerts();
    }
    
    updatePerformanceMetrics(duration) {
        const perf = this.metrics.performance;
        
        perf.totalTime += duration;
        perf.avgTime = perf.totalTime / this.metrics.requests.total;
        perf.maxTime = Math.max(perf.maxTime, duration);
        perf.minTime = Math.min(perf.minTime, duration);
    }
    
    updateEndpointMetrics(url, duration, success) {
        if (!this.metrics.endpoints[url]) {
            this.metrics.endpoints[url] = {
                requests: 0,
                totalTime: 0,
                errors: 0,
                avgTime: 0
            };
        }
        
        const endpoint = this.metrics.endpoints[url];
        endpoint.requests++;
        endpoint.totalTime += duration;
        endpoint.avgTime = endpoint.totalTime / endpoint.requests;
        
        if (!success) {
            endpoint.errors++;
        }
    }
    
    checkAlerts() {
        const { requests, performance } = this.metrics;
        
        // 检查错误率
        const errorRate = requests.error / requests.total;
        if (errorRate > this.thresholds.errorRate) {
            this.triggerAlert('HIGH_ERROR_RATE', {
                currentRate: errorRate,
                threshold: this.thresholds.errorRate
            });
        }
        
        // 检查平均响应时间
        if (performance.avgTime > this.thresholds.avgResponseTime) {
            this.triggerAlert('SLOW_RESPONSE', {
                currentAvg: performance.avgTime,
                threshold: this.thresholds.avgResponseTime
            });
        }
    }
    
    triggerAlert(type, details) {
        const alert = {
            type,
            details,
            timestamp: new Date().toISOString()
        };
        
        this.alerts.push(alert);
        
        // 发送告警通知
        this.sendAlertNotification(alert);
    }
    
    sendAlertNotification(alert) {
        // 实现告警通知逻辑
        console.warn('🚨 API 告警:', alert);
        
        // 可以集成各种通知渠道
        // - Slack
        // - Email
        // - 短信
        // - PagerDuty
    }
    
    getMetrics() {
        return {
            ...this.metrics,
            calculated: {
                errorRate: this.metrics.requests.error / this.metrics.requests.total,
                successRate: this.metrics.requests.success / this.metrics.requests.total
            }
        };
    }
    
    getEndpointMetrics(url) {
        return this.metrics.endpoints[url] || null;
    }
    
    reset() {
        this.metrics = {
            requests: { total: 0, success: 0, error: 0, timeout: 0 },
            performance: { totalTime: 0, avgTime: 0, maxTime: 0, minTime: Infinity },
            endpoints: {}
        };
        this.alerts = [];
    }
}

// 使用示例
const monitoring = new ApiMonitoringSystem();

// 集成到 HTTP 客户端
const monitoredClient = axios.create();

monitoredClient.interceptors.request.use(config => {
    config.metadata = { startTime: Date.now() };
    return config;
});

monitoredClient.interceptors.response.use(
    response => {
        const duration = Date.now() - response.config.metadata.startTime;
        monitoring.recordRequest(response.config, duration, true);
        return response;
    },
    error => {
        const duration = Date.now() - error.config.metadata.startTime;
        monitoring.recordRequest(error.config, duration, false);
        return Promise.reject(error);
    }
);

// 获取监控指标
setInterval(() => {
    const metrics = monitoring.getMetrics();
    console.log('API 监控指标:', metrics);
}, 60000); // 每分钟输出一次

【代码注释】

  • 07-拦截器.html :请求拦截改 config,须 return config 放行。
  • 响应拦截改 res 或统一解包 datareturn res 传给 then。
  • eject(id) 移除拦截器;多个拦截器按注册顺序执行。
  • 流程:请求拦截2→1→发请求→响应拦截→回调(笔记)。

8.3 配套可运行示例(请求耗时统计)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>耗时</title></head>
<body>
<script>
async function timedFetch(url) {
  const t0 = performance.now();
  const res = await fetch(url);
  await res.json();
  console.log(url, (performance.now() - t0).toFixed(0) + 'ms');
}
timedFetch('https://httpbin.org/delay/1');
</script>
</body>
</html>

【代码注释】

  • 结合 Performance API;Axios 可在拦截器记录 config.metadata.start

【本章小结】

优化 手段
减少请求 合并、缓存、防抖
体验 骨架屏、取消无用请求

【实战要点】

  • 列表页离开路由时 abort 未完成请求。
  • 静态资源走 CDN,接口走独立域名便于监控。

【面试考点】

Q1:如何监控前端 API 性能?

A:拦截器记时 + 上报;PerformanceResourceTiming 看 DNS/TTFB。


总结

高频面试题速查

要点
Fetch vs Axios 错误 Fetch 看 ok;Axios 非 2xx reject
res.json() 再一层 Promise,body 只读一次
axios 拦截器 必须 return config/res
REST 资源+HTTP 动词
axios.create 隔离 baseURL/timeout
取消请求 AbortController / CancelToken

#mermaid-svg-uPwvCN2M5eu5r5iZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uPwvCN2M5eu5r5iZ .error-icon{fill:#552222;}#mermaid-svg-uPwvCN2M5eu5r5iZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uPwvCN2M5eu5r5iZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uPwvCN2M5eu5r5iZ .marker.cross{stroke:#333333;}#mermaid-svg-uPwvCN2M5eu5r5iZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uPwvCN2M5eu5r5iZ p{margin:0;}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge{stroke-width:3;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section--1 rect,#mermaid-svg-uPwvCN2M5eu5r5iZ .section--1 path,#mermaid-svg-uPwvCN2M5eu5r5iZ .section--1 circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .section--1 polygon,#mermaid-svg-uPwvCN2M5eu5r5iZ .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .section--1 text{fill:#ffffff;}#mermaid-svg-uPwvCN2M5eu5r5iZ .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-depth--1{stroke-width:17;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:lightgray;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:#efefef;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-0 rect,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-0 path,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-0 circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-0 polygon,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-0 text{fill:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .node-icon-0{font-size:40px;color:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-depth-0{stroke-width:14;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:lightgray;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:#efefef;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-1 rect,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-1 path,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-1 circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-1 polygon,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-1 text{fill:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .node-icon-1{font-size:40px;color:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-depth-1{stroke-width:11;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:lightgray;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:#efefef;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-2 rect,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-2 path,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-2 circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-2 polygon,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-2 text{fill:#ffffff;}#mermaid-svg-uPwvCN2M5eu5r5iZ .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-depth-2{stroke-width:8;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:lightgray;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:#efefef;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-3 rect,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-3 path,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-3 circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-3 polygon,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-3 text{fill:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .node-icon-3{font-size:40px;color:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-depth-3{stroke-width:5;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:lightgray;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:#efefef;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-4 rect,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-4 path,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-4 circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-4 polygon,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-4 text{fill:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .node-icon-4{font-size:40px;color:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-depth-4{stroke-width:2;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:lightgray;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:#efefef;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-5 rect,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-5 path,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-5 circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-5 polygon,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-5 text{fill:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .node-icon-5{font-size:40px;color:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-depth-5{stroke-width:-1;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:lightgray;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:#efefef;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-6 rect,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-6 path,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-6 circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-6 polygon,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-6 text{fill:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .node-icon-6{font-size:40px;color:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-depth-6{stroke-width:-4;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:lightgray;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:#efefef;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-7 rect,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-7 path,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-7 circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-7 polygon,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-7 text{fill:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .node-icon-7{font-size:40px;color:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-depth-7{stroke-width:-7;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:lightgray;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:#efefef;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-8 rect,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-8 path,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-8 circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-8 polygon,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-8 text{fill:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .node-icon-8{font-size:40px;color:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-depth-8{stroke-width:-10;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:lightgray;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:#efefef;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-9 rect,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-9 path,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-9 circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-9 polygon,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-9 text{fill:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .node-icon-9{font-size:40px;color:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-depth-9{stroke-width:-13;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:lightgray;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:#efefef;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-10 rect,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-10 path,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-10 circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-10 polygon,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-10 text{fill:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .node-icon-10{font-size:40px;color:black;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge-depth-10{stroke-width:-16;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:lightgray;}#mermaid-svg-uPwvCN2M5eu5r5iZ .disabled text{fill:#efefef;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-root rect,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-root path,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-root circle,#mermaid-svg-uPwvCN2M5eu5r5iZ .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-root text{fill:#ffffff;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-root span{color:#ffffff;}#mermaid-svg-uPwvCN2M5eu5r5iZ .section-2 span{color:#ffffff;}#mermaid-svg-uPwvCN2M5eu5r5iZ .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-uPwvCN2M5eu5r5iZ .edge{fill:none;}#mermaid-svg-uPwvCN2M5eu5r5iZ .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-uPwvCN2M5eu5r5iZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 本篇 HTTP 客户端
Fetch
Promise Response
res.ok 判断
REST CRUD
Axios
create 实例
拦截器
all 批量
REST
json-server
方法+资源路径
衔接
Ajax 基础篇 XHR
Promise
手写 Promise 篇 手写

【代码注释】

  • 本篇 主线:Fetch REST + Axios 工程化 + json-server。
  • 练习顺序见总结;与 Promise 系列 Promise 衔接。
  • 生产项目多用 Axios 或自封装 Fetch。
  • 配套路径以 03-Fetch、04-Axios 为准。

知识主线:

  1. 选型:简单场景 Fetch;要拦截器、超时、自动 JSON 用 Axios。
  2. Fetchfetch → json()4xx 不自动 reject ,需 response.ok
  3. Axiosres.datacreate 多后端;all 并行;拦截器改 config/响应。
  4. REST03-Fetch / 04-Axiostranscript 做 GET/POST/PUT/DELETE;本地 json-server 搭环境。
  5. 工程:§5~§8 封装类、重试、监控属生产扩展,与配套 HTML 案例互补。

建议练习顺序json-server 启动03-Fetch 四按钮 → 02-axios基本使用04-创建实例06-批量07-拦截器05-取消请求


相关资源:

相关推荐
ziyitty3 小时前
MiMoCode 配置 “Unrecognized key: mcpServers“ 问题解决方案
前端·chrome
酉鬼女又兒4 小时前
零基础入门计算机网络运输层:端到端通信核心作用、端口号分类规则、复用分用工作机制及UDP与TCP协议全方位对比详解
网络·网络协议·tcp/ip·计算机网络·考研·udp·php
dog2504 小时前
不要再继续优化 TCP
网络协议·tcp/ip·php
程序员mine4 小时前
HTTPS-TLS加密与证书完全指南(上)
网络协议·https
VidDown5 小时前
视频帧率技术详解:从 24fps 到 120fps,帧率如何影响你的观看体验?
网络·网络协议·编辑器·音视频·视频编解码·视频
程序员mine6 小时前
HTTPS-TLS加密与证书完全指南(下)
网络协议·http·https
hbugs0017 小时前
EVE-NG V7常用网络协议流量洞察Filter
网络·网络协议
七夜zippoe7 小时前
DolphinDB WebSocket接入:实时数据流
网络·websocket·网络协议·dolphindb·实时数据流
root_107 小时前
kylin-v10-sp3-x86系统安装vmware-17
大数据·chrome·kylin