主题
axios 封装
本页对应 1-9axios封装案例,共 3 段视频,约 37 分钟。
学习目标
- 能够基于 Promise 和 XMLHttpRequest 封装简易 axios 函数,使其接收配置对象并返回 Promise,支持
.then()与.catch()链式调用。 - 能够实现请求方法与 URL 的配置,当未传
method时默认使用 GET,并通过loadend事件判断状态码 200–299,成功时resolve并解析 JSON 数据,失败时reject返回错误对象。 - 能够为封装函数增加
params支持,使用URLSearchParams将对象转换为查询参数,并以正确格式拼接到 URL 后,调用该函数获取地区列表并渲染到页面。 - 能够为封装函数增加
data支持,在请求体中通过JSON.stringify提交 JSON 数据,并设置Content-Type: application/json请求头。 - 能够调用封装后的函数完成省份列表获取与渲染、地区列表获取与渲染、用户注册请求,并验证成功与失败场景下的页面或控制台输出。
知识点
需求与整体思路
这一节的目标是:基于 Promise 和 XHR 封装一个简易的 myAxios 函数,再用它获取省份列表并展示到页面上,借此理解 axios 内部大致如何运作。

为什么需要:直接写 XHR 也能发请求,但调用方式零散;axios 之所以好用,是因为它把请求封装成函数,并返回 Promise,外部可以链式调用 .then 和 .catch。
怎么想:调用 myAxios({ url, method }) 时,外部希望拿到 Promise,所以函数内部要 return new Promise(...);真正的请求逻辑放在 Promise 的回调里。
怎么做:先定义函数和配置参数,再返回 Promise,在 Promise 内创建 XHR、设置请求方法和地址、监听响应、调用 resolve/reject,最后 send()。
易错点:只定义函数不返回 Promise,外部就不能 .then/.catch;Promise 里忘记调用 resolve 或 reject,外部会一直等待。
第一步:定义 myAxios 并接收配置对象
为什么需要:调用时要能传入 URL 和方法,所以函数必须接收一个配置对象。
怎么想:笔记中调用形式类似 myAxios({ url: '...', method: 'GET' }),因此形参可命名为 config,内部通过 config.url、config.method 取值。
怎么做:
js
function myAxios(config) {
return new Promise((resolve, reject) => {
// 后续发请求逻辑写在这里
});
}易错点:不要漏写形参;不要把 config 当成字符串,它是一个对象。转写稿中先用 console.log(config) 验证,确认传入后能拿到 url 和 method 属性。
第二步:创建 XHR 并设置默认 GET
为什么需要:封装后的函数要像 axios 一样,方法可省略,省略时默认使用 GET。
怎么想:config.method 没传时是 undefined,可以用逻辑或 || 设置默认值:有值用传入值,无值用 'GET'。
怎么做:
js
const xhr = new XMLHttpRequest();
xhr.open(config.method || 'GET', config.url);易错点:不要写死 xhr.open('GET', '固定地址'),否则封装失去意义;默认方法判断要放在 open 的第一个参数位置。小结中的答案就是:config.method 判断有值就用,无值用 'GET'。
第三步:监听响应并处理成功/失败
为什么需要:服务器返回后,要根据状态码决定调用 resolve 还是 reject,这样外部才能走 .then 或 .catch。
怎么想:HTTP 状态码 200–299 视为成功;成功时把响应数据交给 resolve,失败时把异常交给 reject。多数接口返回 JSON 字符串,因此成功时用 JSON.parse 转成对象。
怎么做:
js
xhr.addEventListener('loadend', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response));
} else {
reject(new Error(xhr.response));
}
});
xhr.send();易错点:不要漏掉 xhr.send();状态码上限是 < 300,不是“大于 200 就一直成功”;JSON.parse 只适用于 JSON 格式响应。转写稿中测试错误 URL 时,.catch 能触发,并用 console.dir(err) 查看错误对象,因为 console.log 有时不方便展开。
第四步:调用 myAxios 获取并渲染省份列表
为什么需要:封装完必须调用验证,否则不知道函数是否真的可用。
怎么想:调用后返回 Promise,所以在 .then 中拿结果,在 .catch 中处理失败;页面中准备一个盒子,把 res.list 渲染进去。
怎么做:
js
myAxios({
url: '省份列表接口地址' // 替换为接口文档中的地址
}).then((res) => {
document.querySelector('.box').innerText = res.list;
}).catch((err) => {
console.dir(err);
});易错点:接口地址要按接口文档复制,不能随意编造;渲染前先确认 res 是对象、res.list 是数组。转写稿中先在网络面板确认请求已发出、方法为 GET,再在控制台查看 res,最后渲染到 .box,页面出现省份信息即验证通过。
小结
本节核心是“返回 Promise + XHR 发请求 + 状态码分流 + 调用渲染”。默认请求方法的写法是:config.method 有值就用传入值,无值用 'GET'。调试时建议依次确认:config 是否拿到、请求是否发出、状态码是否成功、res.list 是否存在、页面盒子选择器是否正确。
需求与整体思路
上一节的 myAxios 只能接收 url 和 method,调用时虽然能发请求,但地区列表接口还需要额外携带查询参数。为什么需要这一步:如果每次把参数手写进 URL,既麻烦又容易拼错,也不符合封装函数的目标。怎么想:在调用 myAxios 时额外传入一个 params 对象,例如 { pname: '辽宁省', cname: '大连市' };函数内部判断 config.params 是否存在,存在就转成查询参数字符串,拼到 URL 后面,再交给 XMLHttpRequest 发送。怎么做:顺序必须是“先判断并拼接 URL,再调用 xhr.open”,因为 open 使用的是最终 URL。易错点:params 不是必传项,不能无脑转换;拼接时不要漏掉 ?。
在 myAxios 中加入 params 支持
为什么需要:地区列表接口的参数是查询参数,需要出现在 URL 的 ? 后面。怎么想:转写稿里强调先判断 params 是否存在,再用浏览器内置对象 URLSearchParams 把对象转成查询串。步骤是:实例化 new URLSearchParams(config.params),再调用 toString()。怎么做:把下面逻辑放在 xhr.open 之前。
js
function myAxios(config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// 有 params 才转换并拼接查询参数
if (config.params) {
const params = new URLSearchParams(config.params);
const query = params.toString();
config.url += '?' + query;
}
xhr.open(config.method || 'GET', config.url);
xhr.addEventListener('loadend', () => {
if (xhr.status >= 200 && xhr.status < 300) {
// 绝大多数接口返回 JSON 格式数据
resolve(JSON.parse(xhr.response));
} else {
reject(new Error(xhr.response));
}
});
xhr.send();
});
}易错点:config.params 不存在时不要执行转换;toString() 不能漏;拼接时 ? 不能漏。控制台里看到中文变成 %E8... 是 URL 编码,属于正常现象,网络面板里能正常看到中文参数。
调用函数获取地区列表
为什么需要:封装完函数后必须用真实接口验证 params 是否生效。怎么想:地区列表接口使用 GET,方法可省略,因为函数里用 config.method || 'GET' 设置了默认值;参数按接口文档写成查询参数对象。怎么做:调用 myAxios,在 then 中拿到 res.list 并渲染到页面盒子。
js
myAxios({
url: '地区列表接口地址', // 替换为接口文档中的地址
params: {
pname: '辽宁省',
cname: '大连市'
}
}).then((res) => {
console.log(res);
document.querySelector('.box').innerText = res.list;
}).catch((err) => {
console.dir(err);
});易错点:参数名要按接口文档写,不要自己造;then 里先打印 res 确认结构,再取 res.list;失败时用 catch 和 console.dir 查看错误对象。
调试过程与关键细节
调试时可以先在函数里打印 config.params,确认外面传进来的对象存在;再打印 query,确认已经变成 pname=...&cname=... 的形式;最后打印 config.url,确认问号和参数都拼上了。转写稿中还提到,如果暂时注释掉 send,可以避免请求报错干扰观察拼接结果。真正请求后,到网络面板查看请求参数是否显示“辽宁省、大连市”,再到控制台查看返回对象中的 list,最后渲染到 .box。易错点:拼接代码位置不能放到 open 后面;如果原 URL 本身已带 ?,继续用 ? 会出错,应改用 &,本节练习默认 URL 没有查询串,所以用 ?。
目标:让 myAxios 支持请求体
本节要基于上节课的简易 myAxios 继续调优:调用函数时可以额外传入 data 属性,data 是一个 JS 对象;函数内部把它转成 JSON 字符串,并通过请求体发送给服务器。随后用自己封装的 myAxios 完成注册用户。
为什么需要:GET 请求常把参数拼在 URL 上,而注册、登录这类提交数据更适合放在请求体里。怎么想:先判断有没有 data,有才处理请求体;没有就按原来的方式发送。怎么做:在 send 之前判断 config.data,设置请求头,转换数据,再发送。易错点:不要一上来就 JSON.stringify,也不要在没有 data 时仍然携带请求体。
先判断 data 是否存在
为什么需要:并不是每个接口都需要请求体。转写稿中明确说,只有 data 属性存在时,才通过请求体写数据;不存在就不需要携带。
怎么想:调用 myAxios 时传入的对象最终会到 config 形参里,所以判断 config.data。代码应写在 xhr.send() 之前,因为发送动作一旦执行,再设置请求头或改数据就晚了。
怎么做:保留原来的成功/失败监听,在 send 位置分支处理:
js
if (config.data) {
// 有请求体数据:设置请求头并发送 JSON
} else {
// 请求体不需要携带数据时调用
xhr.send();
}易错点:不要漏掉 else 中的 xhr.send(),否则没有 data 的请求根本发不出去;判断位置也不要放到 send 后面。
设置 Content-Type 请求头
为什么需要:请求体里放的是 JSON,但服务器怎么知道?需要通过请求头告诉服务器数据格式。转写稿中反复强调:在请求体携带数据前,要设置请求头。
怎么想:使用 xhr.setRequestHeader('Content-Type', 'application/json')。值不是凭感觉写的,要去接口文档看。注册账号接口的 body 参数后面写着 application/json,就照文档设置。
怎么做:
js
xhr.setRequestHeader('Content-Type', 'application/json');易错点:Content-Type 不要拼错;setRequestHeader 必须在 xhr.open() 之后、xhr.send() 之前调用。转写稿里的“康ten type”应纠正为 Content-Type,“杰森/节森/精神”应纠正为 JSON。
用 JSON.stringify 转换后发送
为什么需要:config.data 此时还是 JS 对象,直接发送会变成不符合要求的内容。上面已经声明发送 JSON,下面就要让数据格式匹配。
怎么想:转写稿说得很清楚:config.data 是一个对象,需要额外做一步数据转化,用 JSON.stringify 把对象转成 JSON 字符串。
怎么做:在 if (config.data) 中:
js
if (config.data) {
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(config.data));
} else {
xhr.send();
}易错点:不要写成 xhr.send(config.data);也不要设置 application/json 却发送对象。设置的数据格式和提交的数据格式要匹配。
调用 myAxios 完成注册用户
为什么需要:封装完必须用真实接口验证,否则不知道请求头、请求体是否正确。怎么想:先绑定按钮点击事件,在事件里调用 myAxios,按接口文档填 url、method、data。转写稿中注册接口方法是 POST,不能省略。
怎么做:调用结构如下,URL 用接口文档中的注册账号地址:
js
myAxios({
url: '接口文档中的注册账号地址',
method: 'POST',
data: {
username: 'IT黑马666',
password: '123456'
}
}).then(res => {
console.log(res);
}).catch(err => {
console.dir(err);
});易错点:data 里属性名要照接口文档写,例如 username、password;不要漏 .then 和 .catch。转写稿中第一次注册成功,第二次再点同一账号会报“账号被占用”,这是服务器返回的业务错误,会进入失败分支。
调试过程与易错点
转写稿的调试顺序是:先绑定点击事件,写一个 myAxios 调用占位;再回到函数里补 data 判断、请求头、JSON.stringify 和 send;最后结合接口文档填 URL、方法和参数。
测试时打开网络面板:在“标头”里确认 Content-Type: application/json 已设置;在“载荷/请求”里查看提交数据,必要时点“源代码”确认是 JSON 字符串;在“预览”里看到注册成功。若重复注册,控制台会输出错误,用 console.dir 更方便展开 Error 对象查看 message。
易错点再强调:data 判断要早于 send;请求头要在 open 后、send 前;发送前必须 JSON.stringify;无 data 时也要 send();不要把 GET 的查询参数思路和请求体混在一起。
小结
外面传入 data 选项后,myAxios 内部先判断是否传入了这个属性;有则自己转成 JSON 字符串,设置 Content-Type: application/json 请求头,并在 send 方法中携带;没有则正常 send()。这样就用自己封装的简易 axios 完成了注册用户,也理解了请求体传递数据的关键流程。
课件图示








代码示例
1. 简易 myAxios —— 获取省份列表(默认 GET)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>封装 myAxios - 省份列表</title>
</head>
<body>
<div class="box"></div>
<script>
// 第一步:定义 myAxios,接收配置对象,返回 Promise 对象
function myAxios(config) {
return new Promise((resolve, reject) => {
// 第三步:通过 XHR 发请求
const xhr = new XMLHttpRequest()
// 方法不传时默认为 GET(逻辑短路)
xhr.open(config.method || 'GET', config.url)
xhr.addEventListener('loadend', () => {
// 2xx 视为成功
if (xhr.status >= 200 && xhr.status < 300) {
// 绝大多数接口返回 JSON 格式,一步到位解析
resolve(JSON.parse(xhr.response))
} else {
reject(new Error(xhr.response))
}
})
xhr.send() // 别漏了发送
})
}
// 第四步:调用函数实现功能(地址以接口文档为准)
myAxios({
url: 'http://hmajax.itheima.net/api/province'
})
.then((res) => {
console.log(res)
document.querySelector('.box').innerHTML = res.list.join('<br>')
})
.catch((err) => {
console.dir(err) // 用 dir 才方便点开查看
})
</script>
</body>
</html>关键行与易错点
return new Promise((resolve, reject) => {...}):正因为返回的是 Promise 对象,外部才能.then().catch();忘了return就会报“没有 then 方法”。config.method || 'GET':没传method时config.method是undefined(falsy),短路后取'GET';传了小写get也照样能用。- 判断条件
status >= 200 && status < 300:上下限都要写,只写>= 200会把 404、500 也当成功。 resolve(JSON.parse(xhr.response)):解析失败会抛异常,说明接口返回的不是 JSON,需先确认响应内容。reject(new Error(xhr.response)):必须包一层new Error,直接reject(字符串)会让.catch里拿到的不是错误对象。xhr.send()是最容易漏的一行,不调用接口永远不会发出去。
2. 支持 params 查询参数 —— 获取地区列表
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>封装 myAxios - 地区列表</title>
</head>
<body>
<div class="box"></div>
<script>
function myAxios(config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
// 第一步:判断 params 选项;有值才拼查询参数
if (config.params) {
// 第二步:用 URLSearchParams 把对象转成查询参数字符串
const paramsObj = new URLSearchParams(config.params)
const queryString = paramsObj.toString()
// 第三步:拼到 URL 后面,问号不能漏
config.url += `?${queryString}`
}
// 拼接必须在 open 之前,open 之后改 url 不生效
xhr.open(config.method || 'GET', config.url)
xhr.addEventListener('loadend', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response))
} else {
reject(new Error(xhr.response))
}
})
xhr.send()
})
}
// 第四步:获取辽宁省大连市的地区列表
myAxios({
url: 'http://hmajax.itheima.net/api/area',
params: {
pname: '辽宁省',
cname: '大连市'
}
})
.then((res) => {
console.log(res)
document.querySelector('.box').innerHTML = res.list.join('<br>')
})
.catch((err) => console.dir(err))
</script>
</body>
</html>关键行与易错点
if (config.params):查询参数不是每个接口都需要,必须先判断;不判断就会拼出undefined。new URLSearchParams(config.params)→.toString():两步就能生成pname=辽宁省&cname=大连市,它会自动做 URL 编码,控制台看到中文变成%E8%BE%BD...是正常现象,服务器能正确解析。config.url += \?${queryString}`:问号是易错点,漏了就变成api/areapname=...`,接口直接报错。- 拼接代码必须写在
xhr.open()之前:open之后修改config.url不会影响这次请求。 - 查询参数走的是 URL,所以
xhr.send()里不需要传任何东西,与上一节写法完全一致。
3. 支持 data 请求体 —— 注册用户
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>封装 myAxios - 注册用户</title>
</head>
<body>
<button class="btn">注册用户</button>
<script>
function myAxios(config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
if (config.params) {
const paramsObj = new URLSearchParams(config.params)
config.url += `?${paramsObj.toString()}`
}
xhr.open(config.method || 'GET', config.url)
xhr.addEventListener('loadend', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response))
} else {
reject(new Error(xhr.response))
}
})
// 第一步:判断 data 选项是否存在
if (config.data) {
// 第二步:设置请求头,告诉服务器请求体是 JSON
xhr.setRequestHeader('Content-Type', 'application/json')
// 第三步:对象转 JSON 字符串,随请求体发送
xhr.send(JSON.stringify(config.data))
} else {
// 请求体不需要携带数据时正常发送
xhr.send()
}
})
}
// 第四步:调用函数完成注册
document.querySelector('.btn').addEventListener('click', () => {
myAxios({
url: 'http://hmajax.itheima.net/api/register',
method: 'post',
data: {
username: 'itheima666',
password: '123456'
}
})
.then((res) => {
console.log(res)
})
.catch((err) => {
console.dir(err) // 账号被占用等错误信息从这里看
})
})
</script>
</body>
</html>关键行与易错点
xhr.setRequestHeader('Content-Type', 'application/json'):必须在open()之后、send()之前调用,写在open前会直接抛错。- 请求头写的是 JSON,
send里就必须传JSON.stringify(config.data);两者不匹配服务器会解析失败。 JSON.stringify不能漏:直接把对象丢给send,实际发出去的是[object Object]。if / else两条分支都要有xhr.send(),否则无 data 的普通请求就永远发不出去。- 注册接口是 POST,
method不能省略,否则走默认 GET,参数根本不会进请求体。 - 同一个用户名注册第二次会返回非 2xx,此时走
.catch,用console.dir才能展开看到服务器返回的提示信息。
常见错误
调用
myAxios(...).then直接报Cannot read properties of undefined (reading 'then')- 原因:函数体内写了
new Promise(...)但漏了return,函数返回undefined。 - 修法:
return new Promise((resolve, reject) => { ... });写完先console.log(myAxios({url:'...'}))确认打印出 Promise 实例。
- 原因:函数体内写了
Network 面板里请求一直 pending,
.then永不执行,控制台也不报错- 原因:只
xhr.open(...)和绑定了loadend,忘了xhr.send();或send()写在了if (config.data)分支里,无 data 时不发送。 - 修法:把
send()放到if/else两条路径都必经的位置(无 data 时xhr.send(),有 data 时xhr.send(JSON.stringify(config.data))),或先写xhr.send()再补分支。
- 原因:只
带 params 的请求 404 或参数丢失,URL 变成
.../listid=1- 原因:拼接时漏了
?;或没判断config.params是否存在就无条件拼接,把undefined拼进 URL。 - 修法:
if (config.params) { config.url += '?' + new URLSearchParams(config.params).toString() },且该段必须在xhr.open()之前执行。
- 原因:拼接时漏了
注册接口返回 400,后端收到的是
[object Object]- 原因:
send(config.data)直接传了 JS 对象,且没设Content-Type,XHR 按表单默认格式序列化。 - 修法:成对处理——
xhr.setRequestHeader('Content-Type', 'application/json')+xhr.send(JSON.stringify(config.data)),两者缺一不可。
- 原因:
注册成功(201/204)却走进了
.catch- 原因:状态码判断写成
xhr.status === 200。 - 修法:用区间
xhr.status >= 200 && xhr.status < 300判成功,其余走reject。
- 原因:状态码判断写成
页面渲染出
北京市,上海市,天津市一整串,或者显示undefined- 原因:
innerHTML = res.list对数组隐式toString()用逗号连接;或resolve(xhr.response)忘了JSON.parse。 - 修法:
res.list.map(v => - ${v}
).join('')再赋值;resolve前先JSON.parse(xhr.response),并对res.list做|| []兜底。- 原因:
.catch里e.message是undefined,只能看到[object Object]- 原因:
reject传的是xhr.response原始字符串或普通对象。 - 修法:
reject(new Error('请求失败'))或const err = new Error(...); err.response = xhr.response; reject(err),调试用console.dir而非console.log。
- 原因:
练习
加超时能力:给
myAxios支持config.timeout(毫秒),超时后走.catch。- 提示:
xhr.timeout = config.timeout,监听ontimeout事件reject(new Error('请求超时'))。 - 验收:把 timeout 设为 1,能在
.catch里拿到超时错误,Network 中请求状态为(canceled)。
- 提示:
加
baseURL支持:myAxios({ baseURL: 'http://hmajax.itheima.net', url: '/api/province' })能正确请求。- 提示:在 params 拼接之前完成 URL 合并,注意判断 url 是否已是绝对地址(
startsWith('http'))。 - 验收:不传 baseURL 时行为与改造前完全一致(回归测试省份列表)。
- 提示:在 params 拼接之前完成 URL 合并,注意判断 url 是否已是绝对地址(
渲染改造:把省份列表从字符串改成
<ul><li>结构,并在数据为空时显示"暂无数据"。- 提示:
Array.isArray(res.list) && res.list.length双重判断。 - 验收:正常数据出列表;手动把 URL 改成返回空数组的接口,页面显示"暂无数据"而非
undefined。
- 提示:
串行调用:用同一个
myAxios完成"注册 → 用刚注册的账号登录"两步,打印登录结果。- 提示:
async/await+try/catch,第二步依赖第一步的成功。 - 验收:控制台先输出注册成功再输出登录 token;重复注册时只打印一次错误,第二步不执行。
- 提示:
边界加固:让
myAxios在url本身已带?a=1时再传params,拼接结果仍合法。- 提示:判断
config.url.includes('?'),已有查询串时用&连接。 - 验收:
url: '/api/list?a=1'+params: {b:2}得到/api/list?a=1&b=2。
- 提示:判断
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 封装-简易axios-获取省份列表 | 14:47 |
| 2 | 封装-简易axios-获取地区列表 | 12:28 |
| 3 | 封装-简易axios-注册用户 | 10:26 |
小结
- 这套封装的结构本质只有一句话:配置对象进、Promise 出,所有能力(method / params / data)都是在这条主干上做"读配置 → 改请求"的增量改造。
method的默认值用逻辑短路config.method || 'GET'实现,这个模式可以原样复用到timeout、headers等任何"可选配置"上。- params 的拼接时机必须在
open()之前——参数是通过 URL 传给服务器的,URL 定了就改不了了;这是顺序问题,不是写法问题。 - 请求体是三件套:判断 data 存在 → 设
Content-Type→stringify后send,三者必须格式匹配,缺一环后端就收到脏数据。 - 成功判定用状态码区间
200 <= status < 300,不要写等号;reject一律传Error实例(可挂response),否则调用方拿不到可用信息。 - 排查顺序固定为:先看 Network 有没有请求 → 看标头(method / Content-Type)→ 看载荷(查询串 / 请求体)→ 最后才看 JS 逻辑,能省掉大半调试时间。
- 手写
myAxios的价值是看清 axios 的分层(配置解析 / 请求发送 / Promise 包装),生产环境仍应使用 axios,但读它的源码和文档时你会知道每一层在做什么。