Skip to content

后端 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,并在可扩展性和高效性上做改进。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:全局请求配置项 + 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 更清楚。

常见错误 ​

  1. post 请求提交的数据多包了一层 data

    • 现象:Network 里请求体是 {"data":{"name":"高二十八"&#125;&#125;,json-server 新增的记录里凭空多出 data 字段。
    • 原因:把请求体写进了请求配置项对象(axios.post(url, { data: user })),axios 把整个对象当成了请求体。
    • 修法:axios.post(url, user);只有需要额外配置时才写第三参数 axios.post(url, user, config),无配置就直接省略。
  2. 配了全局 baseURL 后,向第二个数据源发的请求全打到同一台服务器

    • 现象:请求歌曲榜单的 URL 被拼成了 http://localhost:8088/...,404 或返回错误数据。
    • 原因:axios.defaults 对所有请求生效,而项目里存在两套 API 地址。
    • 修法:改用 axios.create({ baseURL, timeout }) 建多个实例,各实例发各自的请求;全局 defaults 只保留真正全局的东西。
  3. 请求拦截器里忘了 return config

    • 现象:一点按钮就进 catch,Network 里根本没有请求发出。
    • 原因:拦截器不放行,请求链在拦截器处终止。
    • 修法:return config 放行;需要拦截时明确不返回(或 return Promise.reject(...)),并在 catch 里给出可读提示。
  4. 响应拦截器返回值写错,页面拿到 undefined

    • 现象:res.data 为 undefined,或 res 变成了自己拼的数组。
    • 原因:响应拦截器的返回值就是后续 then 的入参,返回什么就拿到什么。
    • 修法:正常放行 return response;要加工就在原对象上改属性后返回它,不要返回裸数据。
  5. 取消请求不生效,或把别的请求一起取消了

    • 现象:点了取消没反应;或者同时发两个请求,取消一个另一个也挂了。
    • 原因:cancelToken 回调里的 cancel 函数没保存下来,或用了同一个外层变量被后一次请求覆盖。
    • 修法:每次发请求前定义局部变量存 cancel,在请求配置项里 new axios.CancelToken(cb => { cancel = cb });catch 中用 axios.isCancel(err) 区分主动取消与真实失败。
  6. 改了 hosts 但域名仍然不通

    • 现象:访问 example.com 仍走公网或直接失败。
    • 原因:hosts 只做域名→IP 映射,不改变端口和协议;且系统有 DNS 缓存。
    • 修法:确认访问时带上正确端口(http://example.com:8088),刷新缓存(ipconfig /flushdns),必要时重启浏览器;排查时先用 127.0.0.1 验证服务本身是否正常。
  7. 超时报错,误以为是跨域或代码问题

    • 现象:请求 pending 一段时间后进 catch,错误信息是 timeout。
    • 原因:全局 timeout 设得比后端响应延迟还短。
    • 修法:timeout 按接口实际耗时设(演示环境后端有 delay 时尤其注意),并把超时和跨域、取消在 catch 里分别处理。

练习 ​

  1. REST 全流程 用 json-server 起一个含 users、posts、comments 三个资源的服务,在 hosts 里配一个自定义域名指向 127.0.0.1,分别用 fetch 和 axios 各完成一次 GET,对比 res.json() 与 res.data 的差别;再用同一套资源完成 POST / PUT / DELETE,并在浏览器 Network 面板确认请求体已被自动序列化为 JSON。

  2. 请求配置项三处作用域 写一个页面,全局 axios.defaults 只配 baseURL 和 timeout;GET 用 params 传查询条件(含中文,观察自动编码),POST 用第二参数传对象体,PUT/DELETE 在 URL 后拼 id。要求:先只用全局配置跑通,再把 timeout 改成单次请求覆盖,验证覆盖关系。

  3. 双实例隔离 创建两个实例:一个指向本地 json-server,一个指向另一个前缀(或另一个端口的 mock 服务),分别设置不同的 timeout 和不同的默认请求头。各发两个请求,在 Network 里核对 baseURL、超时行为、请求头互不污染;然后再用 axios 本体发一次请求作对照。

  4. 精准取消 准备一个带 3 秒延迟的接口。页面上放「发送 A」「发送 B」「取消 A」三个按钮,要求取消 A 时 B 不受影响;catch 中用 axios.isCancel 输出失败原因。完成后改成 AbortController 实现一遍,比较两者写法差异。

  5. 拦截器链 + 统一处理 给 axios 加两个请求拦截器和一个响应拦截器:请求拦截器 1 统一注入 token 并做白名单判断(不在白名单直接拦截),请求拦截器 2 只打印日志后放行;响应拦截器按前后端约定的业务码统一处理,码不对时中断并抛出可读错误。再用变量保存 eject 引用,做一个「取消拦截器」按钮,点击后验证后续请求不再经过拦截器。

原视频对照 ​

#视频时长
1axios请求配置项25:24
2axios创建实例11:46
3axios取消请求14:30
4批量处理响应17:37
5axios拦截器17:48

小结 ​

  1. axios 的配置有三个作用域层级——全局 defaults、实例、单次请求,越靠内优先级越高;headers 这类对象属性的合并行为要单独验证,不能想当然。
  2. 同一个方法名的第二参数含义不同:get / delete 是 config,post / put / patch 是请求体。这是本课最容易踩的坑,写之前先想清楚「这个参数是给谁的」。
  3. 只要项目里出现「两套以上 API 地址」,就该用 axios.create,全局 defaults 只在单数据源的简单场景下成立。
  4. 拦截器是中间件模型:放行靠显式 return,返回值决定下游拿到什么;它的价值在于顶层统一治理(鉴权、错误码、loading),而不是零散地做业务。
  5. 取消请求的最小粒度是单次请求,所以取消函数必须与该次请求的配置项绑定保存,不能共用一个外层变量。
  6. axios.all / spread 是 Promise.all 的历史等价物,现代代码优先用 Promise.all 加解构赋值,可读性更好。
  7. json-server 和 hosts 都只属于开发期手段:前者是 mock,后者只管域名解析,不解决端口、协议和跨域问题,排查时先回到「服务本身能不能通」。
  8. 本节所有能力最终都服务于同一个目标——把请求层收敛成一处可配置、可拦截、可中断、可批量处理的统一入口,而不是在每个组件里各写一遍。