主题
后端 API与使用json-server搭建 restful(Day19 3/3)
本页对应 Day19-Fetch&Axios,共 5 段视频,约 1 小时 27 分。
学习目标
- 能分别使用“单次请求配置对象”和
axios.defaults全局配置两种方式设置baseURL、timeout、headers、params、data等请求配置项,并通过请求结果或网络面板验证配置生效。 - 能使用
axios.create()按不同数据源创建多个 axios 实例,为各实例配置独立的baseURL、超时时间等,并调用实例的get/post/put/delete方法完成请求。 - 能按
CancelToken流程取消请求:在请求配置中创建取消令牌、保存取消函数、在指定时机调用取消函数,并在错误回调中使用axios.isCancel()判断是否为主动取消。 - 能使用
axios.all()并发处理多个请求返回的 Promise,并通过回调参数或数组解构获取各响应结果,说明其与Promise.all()处理批量响应的等价关系。 - 能分别为 axios 全局对象或实例添加请求拦截器和响应拦截器,通过返回值实现放行、通过不返回实现中断,并在拦截器中修改请求配置或响应数据。
- 能使用拦截器返回的标识配合
eject()移除指定请求拦截器或响应拦截器,并通过再次发送请求验证拦截器已失效。
知识点
请求配置项:把“每次写”和“全局写”分开
本节笔记未附图片,以下保留课堂中出现的核心代码写法。
为什么需要:一个页面里可能发很多请求,URL 前缀、超时时间、公共请求头反复写很麻烦,也容易漏。请求配置项就是把这些“请求周边信息”集中管理。
怎么想:配置项是给请求用的。设置方式有两种:一是每次发请求时随请求传一个配置对象;二是写到 axios.defaults 里做全局配置。全局配置适合 baseURL、timeout、公共 headers,单次配置适合 params、本次特有的头信息。
怎么做:
js
axios.defaults.baseURL = 'http://localhost:8088'
axios.defaults.timeout = 5000
axios.defaults.headers.token = 'xxx'
// GET 携带查询参数:用 params
axios.get('/transcript', {
params: { age: 56, gender: '男' }
}).then(res => console.log(res.data))POST 要特别注意参数位置:
js
axios.post('/transcript', { name: '高二十八', age: 25 }, {
// 第三个参数才是请求配置项
})易错点:GET 带参数用 params,不要用 data;POST 的第二个参数是请求体,第三个才是配置项。转写稿里有个真实调试:把 data 写进了请求配置项,结果后端收到的请求体多出一层 date 字段。原因是 axios 把配置对象里的 data 当成了请求体内容。POST/PUT 默认会自动把对象转成 JSON,一般不需要手动设置 Content-Type;只有后端要求非 JSON 格式时才需要改。
创建实例:一个项目多数据源时的小全局
为什么需要:项目可能同时请求两个后端,比如网易云音乐接口和本地 json-server 成绩表接口。两个 baseURL 不同,全局只能设一个,这时全局配置就不合适了。
怎么想:实例可以理解成“带默认配置的 axios 副本”。每个实例有自己的 baseURL、timeout、请求头,互不干扰。
怎么做:
js
const musicInstance = axios.create({
baseURL: 'http://localhost:54255',
timeout: 5000
})
const scoreInstance = axios.create({
baseURL: 'http://localhost:8088',
timeout: 2000
})
musicInstance.get('/playlist/detail', { params: { id: 1 } })
scoreInstance.get('/transcript')易错点:实例拥有 get/post/put/patch/delete 等方法,但没有取消请求和批量请求方法;实例上设置的配置和拦截器只对该实例生效。写路径时注意 baseURL 末尾和请求路径开头的斜杠,避免拼出双斜杠。
取消请求:cancelToken 的“传球”过程
为什么需要:请求还没结束时,用户可能点了取消按钮,或者切换页面,需要主动中断请求。
怎么想:cancelToken 接收一个执行器函数,axios 内部会调用它,并传出一个 cancel 函数。我们要提前定义变量,把这个 cancel 存下来,之后调用它就能取消“这一次”请求。
怎么做:
js
let cancelFn
axios.get('/transcript', {
cancelToken: new axios.CancelToken(cb => {
cancelFn = cb
})
}).then(res => console.log(res.data))
.catch(err => {
console.log(axios.isCancel(err)) // true 表示主动取消
})
// 需要取消时
cancelFn()易错点:cancelFn 只能取消它对应的那次请求,因为它来自那次请求的配置项;不存变量就没法取消。取消后 Promise 进入 reject,不是 resolve。转写稿中用后端延迟 3 秒来观察取消效果,否则请求太快,手速跟不上。
批量处理响应:axios.all 与 spread
为什么需要:同时发多个请求,想等它们都完成后再统一处理结果。
怎么想:axios.all 类似 Promise.all,等数组里的 Promise 全部成功才成功,有一个失败就整体失败。
怎么做:
js
const r1 = axios.get('/transcript?id=1')
const r2 = axios.get('/transcript?id=2')
axios.all([r1, r2]).then(axios.spread((a, b) => {
console.log(a.data, b.data)
}))
// 现在也可以直接解构
axios.all([r1, r2]).then(res => {
const [a, b] = res
})易错点:所谓“批量发请求”其实请求早就发出去了,all 只是批量处理响应。spread 只是把结果数组拆成多个参数,现代写法用解构更直观。一个请求失败,整体会走失败回调。
拦截器:请求前和响应后的统一关卡
为什么需要:几百个接口都要带 token、统一判断后端业务码、统一提示错误,不能每个请求都写一遍。
怎么想:拦截器像中间件。use 注册函数,请求拦截器拿到 config,return config 就是放行,不 return 请求就被拦截;响应拦截器拿到响应对象,也要 return,否则后面的 then 拿不到结果。
怎么做:
js
const reqInterceptor = axios.interceptors.request.use(config => {
config.headers.token = 'token-01'
return config
})
axios.interceptors.response.use(res => {
return res
}, err => {
return Promise.reject(err)
})
// 移除
axios.interceptors.request.eject(reqInterceptor)易错点:请求拦截器不 return config,请求直接失败;响应拦截器不 return res,业务代码拿不到响应。拦截器可以注册多个,按顺序经过;也可以设置在实例上,各实例互不影响。移除拦截器要保存 use 的返回值,再调用 eject。转写稿里还演示了在响应拦截器里统一判断后端返回的业务标记,这是工程化项目中很常见的顶层设计。
本节围绕后端 API 调用,把 axios 的请求配置、实例、取消请求、批量响应和拦截器串起来。转写稿中的“显应体”应写“响应体”,“请求投”应写“请求头”,“接圣/节层”应写“JSON”,“premise”应写“params”,“or”应写“all”。
axios 请求配置项:先分清“全局”和“单次”
为什么需要:请求要带 URL、方式、请求体、请求头、超时等配置,若每次重复写会很乱。怎么想:配置项分两类——每次请求随请求传入,或写到 axios.defaults 做全局默认。怎么做:全局适合 baseURL、timeout、公共请求头;单次适合 params、data 等。示例:
js
axios.defaults.baseURL = 'http://localhost:8088'
axios.defaults.timeout = 2000
axios.defaults.headers.token = 'xxx'
axios.get('/transcript', {
params: { age: 56, gender: '男' }
}).then(res => console.log(res.data))
.catch(err => console.log('请求失败', err))易错点:GET 携带查询参数用 params,axios 会自动拼到 URL 并编码中文;POST/PUT 的请求体用 data。转写稿中把 data 写进配置对象,结果请求体多出一个 data 字段,这是把“配置项”和“请求体”混了。另外全局 timeout 设 2 秒而后端延迟 3 秒会超时,调试时改成 5 秒才成功。
axios 创建实例:不同数据源用不同“小全局”
为什么需要:项目可能同时请求成绩表和网易云音乐两个后端,baseURL 不同,不能用一个全局配置。怎么想:给每个数据源创建一个 axios 实例,实例上的配置只对该实例请求生效。怎么做:
js
const music = axios.create({ baseURL: '音乐服务器地址', timeout: 5000 })
const score = axios.create({ baseURL: '成绩服务器地址:8088', timeout: 2000 })
music.get('/playlist/detail', { params: { id: 1 } })
score.get('/transcript')易错点:实例不是请求函数,创建后仍要调用 get/post/put/delete 才发请求;实例除取消请求、批量请求外,方法与 axios 本身基本一致。
axios 取消请求:保存“这一次”的取消函数
为什么需要:请求未结束时用户可能离开页面或点“取消”,需要中断请求。怎么想:在请求配置里放 cancelToken,new axios.CancelToken(cb => ...) 时把取消函数保存下来,合适时机调用。怎么做:
js
let cancelFn
axios.get('/transcript', {
cancelToken: new axios.CancelToken(cb => { cancelFn = cb })
})
// 需要取消时
cancelFn()
// 失败回调中判断
if (axios.isCancel(err)) console.log('主动取消请求')易错点:cancelFn 只取消它所属的那次请求,不能拿来取消别的请求;不保存回调参数就无法取消。转写稿中请求太快看不出效果,给后端加延迟后才观察到进入失败回调。
批量处理响应:axios.all 本质是 Promise.all
为什么需要:一次发多个请求后要等全部完成再统一处理。怎么想:先让多个请求返回 Promise,再用 axios.all([...]) 汇总;全部成功才成功,一个失败就失败。怎么做:
js
const r1 = axios.get('/transcript/1')
const r2 = axios.get('/transcript/2')
axios.all([r1, r2]).then(axios.spread((s1, s2) => {
console.log(s1.data, s2.data)
}))
// 现在也可:const [s1, s2] = await Promise.all([r1, r2])易错点:axios.spread 只是把结果数组拆成多个参数,现代写法可直接解构;标题说“批量发送请求”不准确,请求其实已各自发出,这里主要是批量处理响应。
axios 拦截器:请求和响应都能“过一手”
为什么需要:统一加 token、做权限判断、统一处理响应约定。怎么想:请求拦截器拿 config,必须 return config 才放行;响应拦截器拿 response,必须 return response 才放行。怎么做:
js
const req1 = axios.interceptors.request.use(config => {
config.headers.token1 = 'xxx'
return config
})
const res1 = axios.interceptors.response.use(response => {
return response
})
// 取消拦截器
axios.interceptors.request.eject(req1)
axios.interceptors.response.eject(res1)易错点:不返回就相当于拦截不放行;响应拦截器若返回自己造的数组,后续 res.data 就取不到原响应。转写稿中请求拦截器加了 token1,网络面板的请求头能看到;取消拦截器时要用创建时保存的返回值。实例也可各自设置拦截器。
请求配置项:两种设置方式
为什么需要:发请求时总有大量重复设置——同一个后端地址、同样的超时时间、同样的请求头。如果每个请求都写一遍,改一次地址就要改几十处。
怎么想:axios 把配置分成两层。一层是“全局配置”,一次设置,所有请求生效;另一层是“单次配置”,写在某一次请求的配置对象里,只对那一次生效。全局配置最适合放 baseURL、timeout 这类所有请求都一样的项。
怎么做:
js
// 全局配置
axios.defaults.baseURL = 'http://localhost:8088'
axios.defaults.timeout = 2000
axios.defaults.headers = { token: 'abc123' }
// 单次配置:URL 后面跟一个对象就是请求配置项
axios.get('/transcript', { timeout: 5000 })易错点:单次配置里如果没写任何东西,这个对象可以整个省略,不用写空对象。另外转写里踩过一个坑——timeout 设成 2 秒,而后端加了 3 秒延迟,请求必然超时,报错却不是代码逻辑问题。调试时先看网络面板的耗时,再怀疑代码。
GET 带参:用 params,不要手拼 URL
为什么需要:查询条件(年龄、性别)要拼成 ?age=56&gender=男 的形式,手拼容易漏掉编码,中文直接拼进 URL 会出问题。
怎么做:
js
axios.get('/transcript', {
params: { age: 56, gender: '男' }
})易错点:params 的值必须是对象,不是字符串;axios 会自动把它拼到 URL 后面,并对中文做编码。转写里还提到 URLSearchParams 这种内置对象也能作为 params 的值,但日常传纯对象就够了。
POST/PUT:第二个参数是请求体,不是配置项
为什么需要:POST 的价值就在于能携带请求体。
怎么想:GET 的第二个参数是配置对象,而 POST/PUT 的第二个参数是请求体,第三个参数才是配置对象。这个差异是本节最大的坑。
怎么做:
js
const user = { name: '高二十八', age: 25, score: 99 }
axios.post('/transcript', user).then(res => {
console.log('添加成功', res.data)
}).catch(err => {
console.log('添加失败', err)
})易错点:转写里最初写成 axios.post(url, { data: user }),结果 data 被当成请求体里的一个字段发了出去,后端收到的对象里多了一层 data。正确做法是直接把对象作为第二个参数。另外,发 JSON 时 axios 会自动设置 Content-Type 并序列化,不用手动配;只有发 form-data 等其它格式才需要自己指定请求头。
axios 实例:多数据源场景
为什么需要:项目里同时请求两个后端(比如网易云音乐接口和本地成绩表接口),baseURL 不同,全局配置就失效了。
怎么想:创建一个“小全局”——实例。实例带着自己的配置,只对从它发出的请求生效。
怎么做:
js
const musicInstance = axios.create({
baseURL: 'http://music.example.com',
timeout: 5000
})
const scoreInstance = axios.create({
baseURL: 'http://localhost:8088',
timeout: 2000
})
musicInstance.get('/playlist', { params: { id: 123 } })
scoreInstance.get('/transcript')易错点:create 只是创建实例,不会发请求;实例上有 get/post/put/patch/delete,但没有取消请求和批量请求这两个方法(这两个挂在 axios 本身)。
取消请求:cancelToken 四步流程
为什么需要:用户点了“取消”,或者切换页面时,旧请求还在跑,回来可能覆盖新数据。
怎么做:
js
let cancelFn // 第一步:提前定义变量存取消函数
axios.get('/transcript', {
cancelToken: new axios.CancelToken(cb => {
// 第二步:new 时回调立即执行,cb 就是取消函数
cancelFn = cb
})
}).then(res => {
console.log('请求成功', res.data)
}).catch(err => {
// 第四步:判断是不是主动取消
if (axios.isCancel(err)) {
console.log('请求失败:主动取消')
} else {
console.log('请求失败', err)
}
})
// 第三步:合适时机调用
cancelFn()易错点:cb 只针对这一次请求,不能拿它去取消别的请求——这正是 axios 这么设计的原因:精准取消,而不是“大水漫灌”。调试时如果请求太快来不及点取消,给后端加延迟再试。
批量处理响应:axios.all 与 spread
为什么需要:一个页面要同时拿四份数据,想让四份都回来后再统一处理。
怎么做:
js
const r1 = axios.get('/transcript?id=1')
const r2 = axios.get('/transcript?id=2')
const r3 = axios.get('/transcript?id=3')
const r4 = axios.get('/transcript?id=4')
axios.all([r1, r2, r3, r4]).then(res => {
console.log(res) // 四个响应对象组成的数组
const [s1, s2, s3, s4] = res // 解构比 spread 清爽
})易错点:所谓“批量发送请求”其实不准确——请求在你调用 get 的那一刻就已经发出去了,all 只是批量处理响应,等价于 Promise.all。另外 axios.spread 只是把数组拆成多个参数,现在用解构赋值或展开运算符更直观。
拦截器:请求拦截与响应拦截
为什么需要:几百个接口都要带 token、都要统一判断后端返回的业务码,不可能每个请求写一遍。
怎么想:拦截器像中间件,请求发出前先过一遍请求拦截器,响应回来后先过一遍响应拦截器。不 return 就不放行,请求会被终止。
怎么做:
js
const i1 = axios.interceptors.request.use(config => {
config.headers.token1 = 'abc100'
return config // 必须返回,否则请求被拦截
})
const s1 = axios.interceptors.response.use(res => {
return res // 不返回就拿不到响应
}, err => Promise.reject(err))
// 取消拦截器
axios.interceptors.request.eject(i1)
axios.interceptors.response.eject(s1)易错点:请求拦截器里拿到的是请求配置对象,响应拦截器里拿到的是响应对象,两者别混。想取消拦截器,创建时必须用变量把返回值存下来。拦截器可以挂在 axios 上,也可以挂在实例上,实例各配各的互不干扰。
本节说明与目标
这节笔记的主题是“配置 hosts 文件”。需要先说明一点:转写稿主要围绕 axios 的请求配置项、实例、取消请求、批量处理响应和拦截器展开,并没有出现 hosts 文件的推导或调试过程。因此本节不能把 axios 的 baseURL、拦截器等内容强行套到 hosts 上,否则就是编造。下面只依据笔记给出的系统路径、hosts 格式和示例,把“hosts 文件是什么、为什么这样写、怎么配、容易错在哪”讲清楚。
hosts 文件为什么需要
为什么需要: 在本地开发或调试时,我们可能希望把某个域名指向本机或指定 IP。hosts 文件提供的就是这种“本地域名到 IP 的映射”能力。笔记中的例子把 example.com 映射到 127.0.0.1,意思是:当本机向 example.com 发请求时,不再按外部 DNS 去解析,而是先按 hosts 里的记录映射到 127.0.0.1。
怎么想: 可以把 hosts 理解成一张很短的“本地对照表”。每一行写一条对照关系:左边是 IP,右边是域名。系统查到时,就按这个 IP 走。
怎么做: 找到对应系统的 hosts 文件,按“IP + 空白 + 域名”的格式追加一行,保存后让系统重新读取映射。
易错点: 不要把顺序写反。笔记示例是 127.0.0.1 example.com,不是 example.com 127.0.0.1。另外,hosts 文件没有扩展名,不要误建为 hosts.txt。
两个系统的默认位置
为什么需要: 不同操作系统存放 hosts 的位置不同,找错文件就会出现“明明改了却不生效”的假象。
怎么想: 先确认当前系统,再按笔记给出的目录定位。Windows 在系统盘的系统目录下,macOS 在 /etc 目录下。
怎么做: Windows 系统的 hosts 文件一般位于:
text
C:\Windows\System32\drivers\etc\macOS 系统的 hosts 文件一般位于:
text
/etc注意笔记里写的是“host 文件”,更准确的叫法是“hosts 文件”。在 macOS 下完整文件名就是 /etc/hosts;在 Windows 下就是上述目录中的 hosts 文件。
易错点: Windows 路径中容易把 System32 写错,或者只找到 etc 目录却没有打开无扩展名的 hosts 文件。macOS 下不要误以为文件叫 host,本节讨论的是 hosts。
hosts 的格式怎么读
为什么需要: 只有看懂格式,才能判断一行配置是否有效。
怎么想: 一行一条映射;IP 在前,域名在后;中间用空白分隔。笔记给出的格式如下:
config
127.0.0.1 localhost
127.0.0.1 example.com第一行把 localhost 映射到 127.0.0.1,第二行把 example.com 映射到 127.0.0.1。
怎么做: 新增映射时,照着已有行的结构写:先写目标 IP,再写空格或制表符,再写域名。行尾多余空格一般不影响这一行的含义,但不要在一行里漏掉 IP 或域名。
易错点: 不要把 IP 和域名粘在一起;不要写成域名在前、IP 在后;不要把多组映射挤在一行里。还要注意 localhost 和 example.com 是两个不同的域名,示例只是分别映射到同一个本机 IP。
示例映射怎么推导
为什么需要: 笔记不仅给了格式,还给了行为说明:向 example.com 发送请求时会映射到 127.0.0.1。要能从这个例子反推出 hosts 的查询逻辑。
怎么想: 请求目标是 example.com,系统查 hosts 表,发现有一行 127.0.0.1 example.com,于是把目标解析为 127.0.0.1。所以请求最终落到本机地址上。
怎么做: 如果希望某个测试域名指向本机,就写:
config
127.0.0.1 example.com保存后,再向 example.com 发请求,预期就是走 127.0.0.1。验证时重点看“域名是否被解析到预期 IP”,而不是只看页面是否打开,因为本机对应端口是否有服务也会影响结果。
易错点: 不要误以为 hosts 会“启动服务”。它只负责域名到 IP 的映射;如果 127.0.0.1 上对应端口没有服务,请求仍然会失败。也不要把 hosts 映射和 axios 的请求配置混为一谈:axios 配的是请求怎么发,hosts 配的是域名怎么解析。
动手配置与排查顺序
为什么需要: 实际配置时最容易出现“改了没生效”,所以要有顺序。
怎么想: 先定位文件,再按格式改,再保存,最后验证映射是否按预期生效。排查时先看文件是否找对,再看格式是否写对,再看是否保存到了正确位置。
怎么做: 第一步,按系统打开对应 hosts 文件;第二步,在文件里按 IP 域名 的格式追加映射;第三步,保存;第四步,重新发起对目标域名的请求,观察是否映射到指定 IP。Windows 下编辑系统目录文件通常需要足够权限;macOS 下编辑 /etc/hosts 也通常需要管理员权限。若没有权限,先解决权限问题,不要反复改内容。
易错点: 一是改错文件,例如改成了同目录下别的文本文件;二是保存成 hosts.txt;三是只改未保存;四是把注释符、空格、换行弄乱;五是忘了本机服务是否真的监听在 127.0.0.1 对应端口。排查时不要一上来怀疑框架,先确认 hosts 行是否真的写对。
小结
本节的核心只有三件事:hosts 文件在 Windows 和 macOS 的默认位置;hosts 的基本格式是“IP 在前、域名在后”;示例 127.0.0.1 example.com 表示向 example.com 发请求时映射到 127.0.0.1。配置时按“找对文件 → 按格式写 → 保存 → 验证映射”的顺序做,重点避开路径写错、顺序写反、扩展名写错和权限不足这几类问题。
本节资料范围核对
本节笔记只给出一句核心定位:Fetch 被设计用来取代 XMLHttpRequest,它提供了许多与 XMLHttpRequest 相同的功能,但被设计成更具可扩展性和高效性。 笔记没有附图片,也没有附代码。转写稿的实际内容却是 axios 的请求配置项、创建实例、取消请求、批量处理响应和拦截器,并没有讲解 Fetch 的 API、参数、返回值或调试过程。因此,本节教程只能围绕笔记中这一句可靠信息展开;不能把转写稿里的 axios 写法改头换面写成 Fetch,否则就是编造。
知识点:Fetch 的定位——为什么需要它
为什么需要: 笔记说 Fetch 被设计用来取代 XMLHttpRequest,说明它要解决的是“浏览器发请求的旧方式不够理想”这一问题。旧 API 能完成请求,但笔记强调新方案要具备更好的可扩展性和高效性。学习时先要建立这个定位:Fetch 不是新的网络协议,也不是 axios 这类第三方库,而是浏览器提供的请求能力演进。
怎么想: 不要一上来记零散方法,而要先判断“它和 XMLHttpRequest 是什么关系”。笔记用“取代”而不是“补充”,说明二者在功能上有重叠;用“更具可扩展性和高效性”,说明选型时关注的是使用方式和扩展能力,而不只是“能不能发请求”。
怎么做: 本节可靠做法是:把 Fetch 当作 XMLHttpRequest 的替代方案来理解;后续学习具体用法时,应以 Fetch 的规范、文档和实际代码为准,不能拿 axios 的配置项直接套。记笔记时可以把“相同功能”“可扩展性”“高效性”作为三个关键词。
易错点: 第一,把 axios 的 baseURL、timeout、params、data、cancelToken、拦截器等概念误当成 Fetch 的知识点。转写稿里大量出现“请求配置项”“创建实例”“取消请求”“批量处理响应”“拦截器”,这些是 axios 的内容,不是本节 Fetch 笔记的内容。第二,把“取代 XMLHttpRequest”理解成“XMLHttpRequest 完全不能用了”,笔记只说被设计用来取代,并没有展开兼容性和迁移细节,不能自行下结论。第三,把“高效性”误写成具体性能数字,笔记没有给出数据,不能编造。
转写稿中同音字与主题纠偏
转写稿里常见的同音或口误包括:“显应体/响应体”“请求投/请求头”“征信数据/响应数据”“excel/axios”“接身 server/json-server”“节圣/JSON”“permise/promise”“凹/all”“SPRED/spread”“蓝截器/拦截器”等。它们可以帮助理解 axios 课堂内容,但不能用来补 Fetch 的推导和调试过程。尤其是转写稿中“请求配置项可全局设置”“创建 axios 实例”“cancelToken 取消请求”“all 批量处理响应”“请求/响应拦截器”等,都属于 axios 体系。若把它们写成 Fetch 教程,会直接违背“不得编造”。
本节学习边界与后续补充建议
为什么需要明确边界: 教程正文必须对读者负责。笔记主题是 Fetch,转写稿主题是 axios,两者不能混写。把 axios 内容硬改成 Fetch,会让读者形成错误知识结构。
怎么想: 可以把本节视为“Fetch 的引入课”,只完成定位认知;等拿到 Fetch 的代码、参数说明和调试记录后,再扩写“怎么发请求、怎么处理响应、怎么处理错误、怎么取消”等细节。
怎么做: 复习时先背下笔记原句,再标注“待补充:Fetch 具体 API、代码示例、错误处理、与 XHR 的对比实验”。后续补充材料到位后,再按“为什么需要→怎么想→怎么做→易错点”逐项展开。
易错点: 不要把“本节没有代码”补写成虚构代码;不要把 axios 的拦截器、实例、取消请求流程迁移到 Fetch;不要用转写稿中的课堂调试过程冒充 Fetch 调试过程。本节最可靠的结论只有:Fetch 的定位是取代 XMLHttpRequest,并在可扩展性和高效性上做改进。
课堂画面





代码示例
示例一:全局请求配置项 + GET/POST/PUT/DELETE 四种请求
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Day19 - 请求配置项</title>
</head>
<body>
<button id="btnGet">GET 请求</button>
<button id="btnPost">POST 请求</button>
<button id="btnPut">PUT 请求</button>
<button id="btnDelete">DELETE 请求</button>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// ===== 1. 全局请求配置项:设置一次,所有 axios 请求都带上 =====
axios.defaults.baseURL = 'http://localhost:8088'; // 按自己 json-server 的地址改
axios.defaults.timeout = 5000; // 响应超时时间
axios.defaults.headers.token = 'shiyi100'; // 公共请求头
// ===== 2. GET:携带查询参数用 params(必须是纯对象) =====
document.getElementById('btnGet').addEventListener('click', function () {
axios.get('/transcript', {
params: { age: 56, sex: '男' } // 会被自动拼到 URL 后面并做中文编码
}).then(res => {
console.log('请求成功', res.data);
}).catch(reason => {
console.log('请求失败', reason);
});
});
// ===== 3. POST:第二个参数就是请求体,不要套 data 字段 =====
const user = { name: '高二十八', age: 25, subject: '数学', score: 99 };
document.getElementById('btnPost').addEventListener('click', function () {
axios.post('/transcript', user).then(res => {
console.log('添加成功', res.data);
}).catch(reason => {
console.log('添加失败', reason);
});
});
// ===== 4. PUT:地址要带 id,第二参数是请求体 =====
document.getElementById('btnPut').addEventListener('click', function () {
axios.put('/transcript/20', user).then(res => {
console.log('修改成功', res.data);
}).catch(reason => {
console.log('修改失败', reason);
});
});
// ===== 5. DELETE:地址带 id 即可 =====
document.getElementById('btnDelete').addEventListener('click', function () {
axios.delete('/transcript/22').then(res => {
console.log('删除成功', res.data);
}).catch(reason => {
console.log('删除失败', reason);
});
});
</script>
</body>
</html>关键行与易错点
axios.defaults.baseURL:全局配置项,之后每次请求只写'/transcript'这样的路径即可。timeout、公共token也最适合放在这里。- GET 携带参数推荐写
params: { ... },值必须是纯对象,axios 会自动拼到 URL 后面并做中文编码;不推荐手动在地址里拼查询字符串。 - 最大易错点:
axios.post(url, { data: user })是错的!POST 的第二个参数就是请求体,写成data: user会把data当成请求体里的一个字段名发给后端(转写稿里现场踩的就是这个坑)。正确写法是axios.post(url, user),第三个参数才是请求配置项。 - 请求体是 JSON 时不用手动设
Content-Type,axios 会自动判断并序列化;只有发非 JSON 格式(如 form-data 之外的格式)时才需要在配置项里指定。 - 后端返回 404/500 时,axios 会把 Promise 状态改成 rejected,会走
catch,而不是then。
示例二:axios 创建实例 + 取消请求
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Day19 - 实例与取消请求</title>
</head>
<body>
<button id="btnMusic">获取歌曲榜单</button>
<button id="btnScore">获取成绩表</button>
<button id="btnSend">发送请求(延迟 3 秒返回)</button>
<button id="btnCancel">取消请求</button>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// ===== 1. 两个数据源,用 axios.create 创建两个实例,各自持有配置 =====
const musicInstance = axios.create({
baseURL: 'http://localhost:3000', // 音乐接口地址
timeout: 5000
});
const scoreInstance = axios.create({
baseURL: 'http://localhost:8088', // 成绩表 json-server 地址
timeout: 2000
});
document.getElementById('btnMusic').addEventListener('click', function () {
musicInstance.get('/playlist/detail', {
params: { id: 3778678 }
}).then(res => {
console.log('获取歌曲成功', res.data);
}).catch(reason => {
console.log('获取歌曲失败', reason);
});
});
document.getElementById('btnScore').addEventListener('click', function () {
scoreInstance.get('/transcript').then(res => {
console.log('获取成绩表成功', res.data);
}).catch(reason => {
console.log('获取成绩表失败', reason);
});
});
// ===== 2. 取消请求:提前定义变量,用来存"取消本次请求"的函数 =====
let cancelFn = null;
document.getElementById('btnSend').addEventListener('click', function () {
axios.get('http://localhost:8088/transcript', {
cancelToken: new axios.CancelToken(function (c) {
// 这个回调 new 的时候立即执行,c 就是取消函数
cancelFn = c;
})
}).then(res => {
console.log('请求成功', res.data);
}).catch(reason => {
console.log('请求失败', reason);
// 判断这次失败是不是因为自己主动取消
console.log('是否主动取消:', axios.isCancel(reason));
});
});
document.getElementById('btnCancel').addEventListener('click', function () {
cancelFn && cancelFn('用户主动取消了请求');
});
</script>
</body>
</html>关键行与易错点
axios.create(config)返回一个实例,这个 config 是"小全局":只对该实例发出的请求生效,所以两个不同baseURL的数据源可以各建一个实例,互不干扰。- 实例上有
get / post / put / patch / delete等方法,用法和axios本身完全一样;但实例没有取消请求和批量请求的方法。 - 取消请求四步走:① 请求配置项里写
cancelToken: new axios.CancelToken(cb);② 用变量把回调拿到的c存下来;③ 需要时调用c();④ 在catch里用axios.isCancel(err)判断失败原因。 - 易错点:
CancelToken的执行器函数是立即执行的,不是请求结束时才执行;c只能取消它自己那一次请求,所以每个请求要各自用一个变量存(否则会张冠李戴)。 - 请求被取消后 Promise 变成 rejected,一定走
catch,then里的逻辑不会执行。
示例三:请求/响应拦截器 + 批量处理响应
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Day19 - 拦截器与批量处理响应</title>
</head>
<body>
<button id="btnGet">GET 请求</button>
<button id="btnPost">POST 请求</button>
<button id="btnAll">批量请求(axios.all)</button>
<button id="btnEject">取消拦截器</button>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
axios.defaults.baseURL = 'http://localhost:8088';
axios.defaults.timeout = 5000;
// ===== 1. 请求拦截器:必须 return config 才放行 =====
const reqId = axios.interceptors.request.use(function (config) {
console.log('经过请求拦截器', config);
config.headers.token1 = 'shiyi100'; // 可以在这里统一加请求头
return config; // 不 return 就相当于把请求拦截掉了
}, function (error) {
return Promise.reject(error);
});
// ===== 2. 响应拦截器:必须 return response 才放行 =====
const resId = axios.interceptors.response.use(function (response) {
console.log('经过响应拦截器', response);
return response; // 不 return 响应就传不到 then 里
}, function (error) {
return Promise.reject(error);
});
document.getElementById('btnGet').addEventListener('click', function () {
axios.get('/transcript', { params: { age: 56, sex: '男' } })
.then(res => console.log('请求成功', res.data))
.catch(reason => console.log('请求失败', reason));
});
document.getElementById('btnPost').addEventListener('click', function () {
const user = { name: '高二十八', age: 25, subject: '数学', score: 99 };
axios.post('/transcript', user)
.then(res => console.log('添加成功', res.data))
.catch(reason => console.log('添加失败', reason));
});
// ===== 3. 批量处理响应:axios.all 等价于 Promise.all =====
document.getElementById('btnAll').addEventListener('click', function () {
const r1 = axios.get('/transcript/1');
const r2 = axios.get('/transcript/2');
const r3 = axios.get('/transcript/3');
const r4 = axios.get('/transcript/4');
axios.all([r1, r2, r3, r4]).then(res => {
console.log('全部成功', res); // res 是一个数组
const [s1, s2, s3, s4] = res; // 解构赋值,比 spread 更直观
console.log(s1.data, s2.data, s3.data, s4.data);
}).catch(reason => {
console.log('有请求失败', reason);
});
});
// ===== 4. 取消拦截器:添加时要用变量存 id,eject 时传回 =====
document.getElementById('btnEject').addEventListener('click', function () {
axios.interceptors.request.eject(reqId);
axios.interceptors.response.eject(resId);
console.log('拦截器已取消');
});
</script>
</body>
</html>关键行与易错点
axios.interceptors.request.use(fn1, fn2)名字像中间件,fn1收请求配置对象,必须return config才是放行,不 return 请求就废了;fn2处理请求阶段的错误。axios.interceptors.response.use(fn1, fn2)的fn1收到的是响应对象(含data、status、headers等),必须return response才能传进.then,不 return 也会被"拦掉"。- 拦截器可以做顶层设计:统一加 token、统一判断后端约定的业务码、统一中断并提示错误,实际工程化项目里很常用。
- 可以添加多个拦截器,按添加顺序依次执行;也可以把拦截器加到实例上(
instance.interceptors...),取消时要用同一个实例去eject。 axios.all([...])本质就是Promise.all,参数必须是数组,全部成功才成功、有一个失败就失败;返回值仍是 Promise,结果是按顺序排列的响应对象数组。axios.spread(function (s1, s2, ...) {...})只是把数组拆成多个参数,用 ES6 解构赋值const [s1, s2] = res更清楚。
常见错误
post 请求提交的数据多包了一层
data- 现象:Network 里请求体是
{"data":{"name":"高二十八"}},json-server 新增的记录里凭空多出data字段。 - 原因:把请求体写进了请求配置项对象(
axios.post(url, { data: user })),axios 把整个对象当成了请求体。 - 修法:
axios.post(url, user);只有需要额外配置时才写第三参数axios.post(url, user, config),无配置就直接省略。
- 现象:Network 里请求体是
配了全局
baseURL后,向第二个数据源发的请求全打到同一台服务器- 现象:请求歌曲榜单的 URL 被拼成了
http://localhost:8088/...,404 或返回错误数据。 - 原因:
axios.defaults对所有请求生效,而项目里存在两套 API 地址。 - 修法:改用
axios.create({ baseURL, timeout })建多个实例,各实例发各自的请求;全局 defaults 只保留真正全局的东西。
- 现象:请求歌曲榜单的 URL 被拼成了
请求拦截器里忘了
return config- 现象:一点按钮就进 catch,Network 里根本没有请求发出。
- 原因:拦截器不放行,请求链在拦截器处终止。
- 修法:
return config放行;需要拦截时明确不返回(或return Promise.reject(...)),并在 catch 里给出可读提示。
响应拦截器返回值写错,页面拿到
undefined- 现象:
res.data为undefined,或res变成了自己拼的数组。 - 原因:响应拦截器的返回值就是后续
then的入参,返回什么就拿到什么。 - 修法:正常放行
return response;要加工就在原对象上改属性后返回它,不要返回裸数据。
- 现象:
取消请求不生效,或把别的请求一起取消了
- 现象:点了取消没反应;或者同时发两个请求,取消一个另一个也挂了。
- 原因:
cancelToken回调里的cancel函数没保存下来,或用了同一个外层变量被后一次请求覆盖。 - 修法:每次发请求前定义局部变量存
cancel,在请求配置项里new axios.CancelToken(cb => { cancel = cb });catch 中用axios.isCancel(err)区分主动取消与真实失败。
改了 hosts 但域名仍然不通
- 现象:访问
example.com仍走公网或直接失败。 - 原因:hosts 只做域名→IP 映射,不改变端口和协议;且系统有 DNS 缓存。
- 修法:确认访问时带上正确端口(
http://example.com:8088),刷新缓存(ipconfig /flushdns),必要时重启浏览器;排查时先用127.0.0.1验证服务本身是否正常。
- 现象:访问
超时报错,误以为是跨域或代码问题
- 现象:请求 pending 一段时间后进 catch,错误信息是 timeout。
- 原因:全局
timeout设得比后端响应延迟还短。 - 修法:
timeout按接口实际耗时设(演示环境后端有 delay 时尤其注意),并把超时和跨域、取消在 catch 里分别处理。
练习
REST 全流程 用 json-server 起一个含
users、posts、comments三个资源的服务,在 hosts 里配一个自定义域名指向127.0.0.1,分别用 fetch 和 axios 各完成一次 GET,对比res.json()与res.data的差别;再用同一套资源完成 POST / PUT / DELETE,并在浏览器 Network 面板确认请求体已被自动序列化为 JSON。请求配置项三处作用域 写一个页面,全局
axios.defaults只配baseURL和timeout;GET 用params传查询条件(含中文,观察自动编码),POST 用第二参数传对象体,PUT/DELETE 在 URL 后拼 id。要求:先只用全局配置跑通,再把timeout改成单次请求覆盖,验证覆盖关系。双实例隔离 创建两个实例:一个指向本地 json-server,一个指向另一个前缀(或另一个端口的 mock 服务),分别设置不同的
timeout和不同的默认请求头。各发两个请求,在 Network 里核对 baseURL、超时行为、请求头互不污染;然后再用axios本体发一次请求作对照。精准取消 准备一个带 3 秒延迟的接口。页面上放「发送 A」「发送 B」「取消 A」三个按钮,要求取消 A 时 B 不受影响;catch 中用
axios.isCancel输出失败原因。完成后改成AbortController实现一遍,比较两者写法差异。拦截器链 + 统一处理 给 axios 加两个请求拦截器和一个响应拦截器:请求拦截器 1 统一注入 token 并做白名单判断(不在白名单直接拦截),请求拦截器 2 只打印日志后放行;响应拦截器按前后端约定的业务码统一处理,码不对时中断并抛出可读错误。再用变量保存
eject引用,做一个「取消拦截器」按钮,点击后验证后续请求不再经过拦截器。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | axios请求配置项 | 25:24 |
| 2 | axios创建实例 | 11:46 |
| 3 | axios取消请求 | 14:30 |
| 4 | 批量处理响应 | 17:37 |
| 5 | axios拦截器 | 17:48 |
小结
- axios 的配置有三个作用域层级——全局 defaults、实例、单次请求,越靠内优先级越高;
headers这类对象属性的合并行为要单独验证,不能想当然。 - 同一个方法名的第二参数含义不同:get / delete 是 config,post / put / patch 是请求体。这是本课最容易踩的坑,写之前先想清楚「这个参数是给谁的」。
- 只要项目里出现「两套以上 API 地址」,就该用
axios.create,全局 defaults 只在单数据源的简单场景下成立。 - 拦截器是中间件模型:放行靠显式 return,返回值决定下游拿到什么;它的价值在于顶层统一治理(鉴权、错误码、loading),而不是零散地做业务。
- 取消请求的最小粒度是单次请求,所以取消函数必须与该次请求的配置项绑定保存,不能共用一个外层变量。
axios.all/spread是Promise.all的历史等价物,现代代码优先用Promise.all加解构赋值,可读性更好。- json-server 和 hosts 都只属于开发期手段:前者是 mock,后者只管域名解析,不解决端口、协议和跨域问题,排查时先回到「服务本身能不能通」。
- 本节所有能力最终都服务于同一个目标——把请求层收敛成一处可配置、可拦截、可中断、可批量处理的统一入口,而不是在每个组件里各写一遍。