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

XHR / Promise / async-await 之后,用原生 FetchAxios 完成 REST 风格 CRUD;配合 json-servertranscript 接口联调。
参考:MDN Fetch | Axios 中文文档

目录


零、导读与学习价值

0.1 案例覆盖清单

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

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

0.2 核心名词速查

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

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

选型 XHR/Fetch/Axios

Fetch REST

Axios 工程化

json-server

拦截器与生产实践

【代码注释】

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

0.4 建议练习路线

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

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

<!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 客户端技术的发展:

早期阶段 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 技术对比分析

特性XMLHttpRequestFetch APIAxios
API 设计回调函数Promise 链Promise 链
浏览器支持所有浏览器现代浏览器需要引入库
请求拦截复杂实现需要手动封装内置支持
响应转换手动解析手动解析自动转换
取消请求abort() 方法AbortControllerCancelToken
进度监控原生支持有限支持原生支持
超时控制手动实现需要配合 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 配套可运行示例(环境能力检测)

<!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)

<!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 核心概念:

// 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 对象属性和方法:

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 配置选项:

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 高级功能

请求取消:

// 使用 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)。

流式数据处理:

// 处理流式响应
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 笔记 笔记一致):

<!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 页面按钮 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 判断)

<!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 引入)

<!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 特性详解:

// 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 基础使用

安装和引入:

# 使用 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 版本。

基础请求方法:

// 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 实例创建

自定义实例:

// 创建自定义实例
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 拦截器机制

请求拦截器:

// 添加请求拦截器
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 请求取消

取消请求实现:

// 使用 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):

<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):

// ① 配置对象
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):

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):

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 配套可运行示例(拦截器最小版)

<!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 配套可运行示例(方法对照表)

<!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。

核心原则:

// 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 设计规范:

// 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 响应格式规范

标准响应格式:

// 成功响应
{
    "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 联调。

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 一致)。
操作方法示例
列表GEThttp://127.0.0.1:8088/transcript
单条GEThttp://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 四方法)

<!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 客户端封装:

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 响应数据转换

数据转换处理:

// 响应数据转换器
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)

<!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 并行)

<!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。

请求队列管理:

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 请求重试机制

智能重试策略:

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 缓存封装:

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)

<!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 错误类型)

<!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 错误监控和日志

全面的错误监控系统:

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 安全最佳实践:

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 配套可运行示例(统一错误日志)

<!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 取消)

<!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 客户端性能优化:

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 监控和告警

实时监控系统:

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 配套可运行示例(请求耗时统计)

<!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

本篇 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-取消请求


相关资源:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值