主题
源码分析与自定义 axios(Day20 2/3)
本页对应 Day20-Axios,共 6 段视频,约 39 分钟。
学习目标
- 能够实现
baseURL与请求URL的合并逻辑,明确仅在配置了baseURL且URL非http/https绝对地址时拼接,并通过实际请求验证结果。 - 能够为
responseType设置默认值并支持用户配置覆盖,验证XMLHttpRequest.responseType按预期生效。 - 能够将
params对象序列化为查询字符串并正确拼接到URL,通过多参数请求验证拼接与请求结果。 - 能够在
open之后、send之前完成headers与data请求体处理:逐项设置请求头,并针对字符串与纯对象设置对应Content-Type和请求体,验证请求载荷。 - 能够暴露并实现
axios.defaults全局配置接口,理解对象引用传值,验证全局配置影响请求且不能通过重新赋值替换defaults对象。
知识点
源码分析先看“拼接条件”
本节是在封装 XHR 适配器时,把请求配置项真正落到发送请求的流程里。转写里提到源码处理 baseURL 无非是先做判断:baseURL 存在,并且 url 不是完整地址时,才把两者拼起来。也就是说,完整 URL 以 http/https 开头时,baseURL 不参与拼接。为什么需要:用户可能把完整地址和相对地址混用;怎么想:先判断再拼接,不能无脑相加;怎么做:在修改请求配置的阶段处理 config.url;易错点:只要写了完整 URL,baseURL 就应当失效,不能强行拼。
js
if (
config.baseURL &&
!config.url.startsWith('http') &&
!config.url.startsWith('https')
) {
config.url = config.baseURL + config.url
}转写中先用 baseURL + url 发请求失败,因为拼出来的不是 GitHub 地址;后来按条件拼接才成功。这里用 startsWith 判断前缀,注意完整地址不拼。
responseType:默认值要落到 xhr 上
为什么需要:响应类型决定 XHR 如何解析响应,默认应是 json;怎么想:用户传了就用用户的,不传就用全局默认;怎么做:在全局默认配置里设置 responseType: 'json',发送前赋给 xhr.responseType;易错点:只在配置对象里写默认值还不够,必须真的设置到 XHR 实例上。
js
// 全局默认配置中
responseType: 'json'
// 发送前
xhr.responseType = config.responseType转写强调:配置项会合并,用户不传时用全局默认值;但“光设置配置项不行,得真的给人设置上”。
params:对象拼到 URL 后面
为什么需要:GET 参数既可以直接写在 URL 后,也可以通过 params 对象传入;怎么想:把对象转成 key=value&key=value,再拼到 URL 后;怎么做:用 Object.entries 得到二维数组,再 map 成字符串并 join('&');易错点:这里没有做 encodeURIComponent,也没有处理已有 ?、数组和嵌套对象,按转写思路“不正常情况让系统报错”。
js
if (config.params) {
const params = Object.entries(config.params)
.map(([key, value]) => `${key}=${value}`)
.join('&')
config.url += '?' + params
}转写里先用 for in 也能做,但演示了 Object.entries + map + join 的写法。调试时看 Network:URL 后出现 id=100001&age=12,说明拼接成功。注意载荷里的查询字符串参数不等于请求体。
headers:在 open 之后、send 之前设置
为什么需要:请求头要由用户配置传入;怎么想:判断 config.headers 是否存在,存在就逐项设置;怎么做:用 xhr.setRequestHeader(key, value);易错点:必须在 open 之后、send 之前设置,不能在 open 前乱设。
js
if (config.headers) {
for (const key in config.headers) {
xhr.setRequestHeader(key, config.headers[key])
}
}转写中给 GitHub 请求随便加 token1、token2 后出现跨域/拒绝,这是 GitHub 对请求头的限制,不是封装代码没生效;后来换成本地服务验证,Network 中能看到自定义请求头。
data:字符串和纯对象分别处理
为什么需要:请求体要按内容类型发送,并自动补 Content-Type;怎么想:字符串按表单格式,纯对象转 JSON;怎么做:用 typeof 判断字符串,用 Object.prototype.toString.call(config.data) === '[object Object]' 判断纯对象;易错点:toString.call 的调用对象和参数别写反,否则对象会被发成 [object Object]。
js
let body = config.data
if (typeof config.data === 'string') {
body = config.data
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
} else if (Object.prototype.toString.call(config.data) === '[object Object]') {
body = JSON.stringify(config.data)
xhr.setRequestHeader('Content-Type', 'application/json')
}
xhr.send(body)转写调试时对象请求体出现 [object Object],打印类型后发现判断写反,改正后 JSON.stringify 正常。form-data 不在这里处理,XHR 的 send 能识别 FormData 并自动处理。
axios.defaults:暴露全局配置对象
为什么需要:用户希望统一设置 baseURL 等全局配置;怎么想:axios 是函数,不是类实例,不能直接凭空拥有 defaults;怎么做:把内部全局默认配置对象暴露为 axios.defaults,之后改属性;易错点:不要重新给 axios.defaults 赋一个新对象,否则会换掉引用。
js
// 内部已有全局默认配置对象 defaults
axios.defaults = defaults
// 使用
axios.defaults.baseURL = 'http://localhost:8088'转写中直接写 axios.defaults.baseURL 报错,因为小写 axios 是函数;手动添加 axios.defaults = defaults 后,改的是同一个全局对象,请求就能带上全局配置。注意:可以改 axios.defaults.baseURL,但不要写成 axios.defaults = { ... } 去替换引用。
先明确本节目标
这一节要完成“自定义 axios”的请求配置项:baseURL、responseType、params、headers、data,最后暴露 axios.defaults 做全局配置。整体思路不是重写 XHR,而是在真正 send() 之前,把用户传入的 config 合并、加工,再设置到 XMLHttpRequest 实例上。
baseURL:为什么要拼接、什么时候才拼
为什么需要:调用时只写相对路径,例如 /users,由 baseURL 统一补全,避免每个请求都写完整域名。
怎么想:不能无脑拼。只有配置了 baseURL,并且 url 不是完整地址时才拼;如果 url 以 http:// 或 https:// 开头,说明用户要覆盖 baseURL。
怎么做:
js
if (
config.baseURL &&
!config.url.startsWith('http://') &&
!config.url.startsWith('https://')
) {
config.url = config.baseURL + config.url;
}易错点:只判断 baseURL 不够,还要判断 url 是否完整;startsWith 区分大小写;拼接前要保证两边格式,避免出现双斜杠或缺少斜杠。转写稿中先注释掉 baseURL 验证系统写法正常,再打开验证拼接成功,这是有效调试过程。
responseType:默认 json,并在 send 前设置
为什么需要:响应可能是 JSON、文本、二进制等,必须告诉 XHR 期望的响应类型。
怎么想:用户传了就用用户的;没传就默认 json。可以在全局默认配置里给默认值,再在请求前设置到 XHR 上。
怎么做:
js
xhr.responseType = config.responseType;易错点:只在配置对象里写 responseType 不会自动生效,必须在 send() 前设置 xhr.responseType;默认值要依赖配置合并,不能假设用户一定传。
params:把对象拼成查询字符串
为什么需要:GET 请求常带查询参数,直接手拼 URL 容易漏 ?、漏 &、不编码。
怎么想:params 是对象,用 Object.entries() 转成二维数组,再 map 成 key=value,最后 join('&')。
怎么做:
js
if (config.params) {
const query = Object.entries(config.params)
.map(([key, value]) => `${key}=${value}`)
.join('&');
config.url += `?${query}`;
}易错点:拼接前要判断 params 是否存在;注意加 ?;真实项目还要 encodeURIComponent。转写稿中通过 Network 看到 id=101001&age=12 已拼到 URL 后,说明生效。另注意:URL 上的查询参数也会出现在载荷里,但它不是请求体。
headers:在 open 之后、send 之前设置
为什么需要:鉴权、内容类型等常放在请求头里。
怎么想:headers 是对象,要遍历后逐个调用 xhr.setRequestHeader(key, value)。位置必须在 open() 之后、send() 之前。
怎么做:
js
if (config.headers) {
Object.entries(config.headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value);
});
}易错点:在 open() 前设置会报错;不是所有跨域请求都允许随意加请求头。转写稿中给 GitHub 加自定义 token 头被拒绝,换成本地服务后请求头正常出现,说明代码没问题,是服务端跨域限制。
data:区分字符串和纯对象,设置请求体与 Content-Type
为什么需要:POST 请求要发送请求体,并让后端知道格式。
怎么想:字符串按表单格式处理,设置 application/x-www-form-urlencoded;纯对象用 JSON.stringify(),设置 application/json。
怎么做:
js
let body = config.data;
if (typeof config.data === 'string') {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
} else if (Object.prototype.toString.call(config.data) === '[object Object]') {
body = JSON.stringify(config.data);
xhr.setRequestHeader('Content-Type', 'application/json');
}
xhr.send(body);易错点:判断纯对象不要只靠 typeof,数组也会得到 object;转写稿中曾把 toString.call(config.data) 写反,导致请求体变成 [object Object],用 console.log 修正后成功。FormData 可交给 XHR 自动处理,不必强行设置。
axios.defaults:暴露全局配置,注意引用赋值
为什么需要:统一设置 baseURL 等默认项,避免每次请求重复写。
怎么想:让 axios.defaults 指向内部默认配置对象,请求时再合并。
怎么做:
js
axios.defaults = defaults;易错点:不能写成 axios.defaults = { baseURL: '...' } 重新赋一个新对象,否则会切断与内部默认配置的引用关系。转写稿强调“引用传值”:改 axios.defaults.baseURL 才能改到同一个对象。验证时请求成功,说明全局配置已参与合并。
课堂画面






代码示例
1. myAxios.js —— 自定义 axios:请求配置项(baseURL / responseType / params / headers / data)
js
/* myAxios.js —— 自定义 axios,集中处理各种请求配置项 */
/* 1. 默认配置(相当于 axios 内部的那个 defaults 对象) */
const defaults = {
method: 'get',
responseType: 'json' // 用户不传,默认就是 json
};
/* 2. 对外暴露的函数(小 a) */
function axios(config) {
// 合并:默认配置 + 用户传入的配置,后面的覆盖前面的
config = Object.assign({}, defaults, config);
return new Promise((resolve, reject) => {
dispatchRequest(config, resolve, reject);
});
}
/* 3. 真正发请求的逻辑 */
function dispatchRequest(config, resolve, reject) {
/* ---------- 3.1 baseURL 与 url 的合并 ---------- */
// 三个条件必须同时满足才拼接
if (
config.baseURL && // ① 配置了 baseURL
!config.url.startsWith('http') && // ② url 不是完整 http 地址
!config.url.startsWith('https') // ③ url 也不是完整 https 地址
) {
config.url = config.baseURL + config.url;
}
/* ---------- 3.2 params 拼接成查询字符串 ---------- */
if (config.params) {
// Object.entries({id:1001, age:12}) => [['id',1001], ['age',12]]
const query = Object.entries(config.params)
.map(item => item[0] + '=' + item[1])
.join('&');
config.url += '?' + query; // 拼上问号
}
const xhr = new XMLHttpRequest();
/* ---------- 3.3 open ---------- */
xhr.open(config.method.toUpperCase(), config.url, true);
/* ---------- 3.4 responseType(必须在 send 之前设置) ---------- */
xhr.responseType = config.responseType;
/* ---------- 3.5 请求头(必须在 open 之后、send 之前设置) ---------- */
if (config.headers) {
for (let key in config.headers) {
xhr.setRequestHeader(key, config.headers[key]);
}
}
/* ---------- 3.6 data 请求体:只处理两种默认格式 ---------- */
let body = config.data; // 默认原样发送
if (typeof config.data === 'string') {
// 字符串 → 3W 格式
body = config.data;
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
} else if (Object.prototype.toString.call(config.data) === '[object Object]') {
// 纯对象 → JSON 格式
body = JSON.stringify(config.data);
xhr.setRequestHeader('Content-Type', 'application/json');
}
/* ---------- 3.7 监听结果 ---------- */
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
data: xhr.response,
status: xhr.status,
statusText: xhr.statusText,
headers: xhr.getAllResponseHeaders(),
config: config,
request: xhr
});
} else {
reject(new Error('请求失败,状态码:' + xhr.status));
}
}
};
xhr.onerror = () => reject(new Error('网络错误'));
xhr.send(body);
}
/* 4. 手动把全局配置对象暴露出去(引用传值,指向同一个对象) */
axios.defaults = defaults;关键行与易错点
baseURL的拼接必须同时满足三个条件:有baseURL、url不以http开头、url不以https开头。转写稿里反复强调:url一旦是完整地址,baseURL就“废了”,不会给你拼。startsWith不支持正则,所以只能写两个判断。responseType要在xhr.open()之后、xhr.send()之前设置;默认值放在defaults里,用户不传就用json,用户传了就覆盖默认值。params用Object.entries拿到二维数组[['id',1001],['age',12]],map拼成id=1001,再join('&'),最后config.url += '?' + query。这一步没做encodeURIComponent,也没判断url里是否已有?,是这套简化实现的已知短板。xhr.setRequestHeader必须写在open之后,否则直接抛InvalidStateError。data类型判断用的是Object.prototype.toString.call(config.data) === '[object Object]'(安全的类型判断)。转写稿里演示过“写反了”的翻车现场:config.data.toString.call(...)顺序写反,结果拿到[object Object]判不出来,请求体直接变成[object Object]。- 请求头循环先执行、
Content-Type后设置,所以用户在headers里传的Content-Type会被data的处理逻辑覆盖。 formData不用管:xhr.send()检测到是FormData会自己加Content-Type。
2. index.html —— 验证 baseURL / params / headers / data 两种格式
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>请求配置项测试</title>
</head>
<body>
<!-- 先起本地服务:json-server --watch db.json --port 8088 -->
<!-- db.json 内容:{ "posts": [] } -->
<script src="./myAxios.js"></script>
<script>
// ① baseURL + url + params
axios({
baseURL: 'http://localhost:8088',
url: '/posts',
method: 'get',
params: { id: 1001, age: 12 }
}).then(res => console.log('①', res.data));
// ② data 传字符串 → 自动加 application/x-www-form-urlencoded
axios({
baseURL: 'http://localhost:8088',
url: '/posts',
method: 'post',
data: 'name=zs&age=18&gender=男'
}).then(res => console.log('②', res.data));
// ③ data 传纯对象 → JSON.stringify + application/json
axios({
baseURL: 'http://localhost:8088',
url: '/posts',
method: 'post',
data: { name: 'ls', age: 18, gender: '未知' }
}).then(res => console.log('③', res.data));
// ④ 自定义请求头
axios({
baseURL: 'http://localhost:8088',
url: '/posts',
method: 'get',
headers: { token1: 'aaa', token2: 'bbb' }
}).then(res => console.log('④', res.data));
</script>
</body>
</html>关键行与易错点
data的两种格式必须用post验证,get请求的data发不出去,send(body)会被忽略。- 验证
params要看 Network 里的 Request URL(是否拼成?id=1001&age=12);验证data要看 Request Payload / 载荷。转写稿特别提醒:URL 上的参数也会出现在“查询字符串参数”里,不要把它误认为请求体。 - 请求头只在自建服务上验证:GitHub 这类公开接口对请求头有严格限制,随便加
token1/token2会直接被拦(表现为跨域错误),不是自己代码的问题。 - 如果
db.json放在 json-server 监听目录内,改数据会触发页面自动刷新 → 再发请求 → 死循环,所以演示用的数据文件要放到监听目录外面。
3. defaults.html —— axios.defaults 全局请求配置项
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>全局配置项 axios.defaults</title>
</head>
<body>
<script src="./myAxios.js"></script>
<script>
// myAxios.js 里已有一行:axios.defaults = defaults; ← 引用传值
// 这里改属性,改的就是内部那个默认配置对象
axios.defaults.baseURL = 'http://localhost:8088'; // 最常见的全局配置
axios.defaults.responseType = 'json';
axios.defaults.headers = { token1: 'aaa' };
// 之后所有请求都不用再写 baseURL
axios({ url: '/posts', method: 'get' })
.then(res => console.log('全局配置生效', res.config));
// ❌ 错误写法:重新赋值会断开与内部 defaults 的引用
// axios.defaults = { baseURL: 'http://localhost:8088' };
</script>
</body>
</html>关键行与易错点
axios.defaults = defaults这一行是“手动添加”的:小a(函数)本身没有defaults属性,不手动挂上去,axios.defaults.baseURL = ...会直接报“不能设置未定义属性的属性”。- 对象是引用传值:
axios.defaults和内部的defaults指向同一个对象,所以改属性就是改全局配置,request合并时自然能拿到。 - 千万不能写
axios.defaults = { baseURL: '...' }:这是重新赋值,会换掉引用指向,内部defaults根本没变,全局配置就失效了。这是 JS 基础,转写稿里专门敲了黑板。 - 全局配置只对“没在单次请求里写”的项生效,单次请求里写了同名字段会覆盖全局值。
常见错误
完整 URL 仍被拼接 baseURL
现象:传入http://...或https://...后,请求地址变成baseURL + 完整URL。
原因:只判断了是否以http开头,漏掉https,或判断顺序/取反写错。
修法:仅在“存在 baseURL 且 URL 不以http://、https://开头”时拼接。params 被拼成
[object Object]
现象:请求 URL 后出现?[object Object]或参数丢失。
原因:直接把params对象与 URL 相加,未遍历键值对,也未做编码。
修法:先判断params存在,再用Object.entries(params).map(...).join('&')拼接,并对键值做encodeURIComponent。headers 设置了但请求里没有
现象:config.headers有值,Network 面板看不到自定义请求头。
原因:在open之前设置,或忘记遍历对象调用xhr.setRequestHeader。
修法:在xhr.open之后、xhr.send之前遍历config.headers并逐个setRequestHeader。data 传对象却发送成
[object Object]
现象:请求体内容为[object Object],后端解析失败。
原因:未区分字符串与纯对象,或typeof null/数组也被当成普通对象处理。
修法:字符串按表单格式处理;纯对象先JSON.stringify再设置application/json;其他类型不擅自处理。全局 defaults 直接重新赋值后失效
现象:axios.defaults = {...}后,原有全局配置不生效或请求未带上配置。
原因:重新赋值替换了引用,原实例仍指向旧对象。
修法:不要替换defaults对象,直接修改其属性,如axios.defaults.baseURL = '...'。responseType 只写了默认配置,没有落到 XHR
现象:配置里是json,但实际响应类型未按预期生效。
原因:只在合并配置中设置默认值,发送前未执行xhr.responseType = config.responseType。
修法:在发送请求前把最终合并后的responseType真正赋给 XHR 实例。
练习
- 实现
buildURL(config):满足“有 baseURL、URL 非完整地址才拼接”,并覆盖http://、https://、相对路径三种情况。 - 实现
params序列化:支持{ id: 101, name: '张三' },输出?id=101&name=%E5%BC%A0%E4%B8%89,并处理空对象。 - 在自定义 axios 中补全
headers设置:要求必须在open后、send前完成,并验证两个自定义请求头。 - 实现请求体处理分支:字符串设置
application/x-www-form-urlencoded;纯对象设置application/json并序列化;其他类型不处理。 - 给自定义 axios 暴露全局配置能力:设置
axios.defaults.baseURL后发起请求,并验证单次请求配置能覆盖全局配置。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 请求配置项设置-baseURL | 09:08 |
| 2 | 请求配置项设置-responseType | 01:54 |
| 3 | 请求配置项设置-params | 05:40 |
| 4 | 请求配置项设置-headers | 06:33 |
| 5 | 请求配置项设置-data请求体 | 10:35 |
| 6 | 请求配置项设置-axios.defaults设置全局请求配置选项 | 05:45 |
小结
- 配置项处理要放在“最终配置确定后、请求真正发出前”,否则配置存在但不会生效。
- baseURL 拼接必须先做边界判断,完整 URL 不应再拼 baseURL。
- params 不是简单字符串相加,必须遍历、编码、拼接,并处理空值。
- headers 的关键不是“有没有配置”,而是“有没有在正确阶段逐个写入 XHR”。
- data 请求体要按类型分支处理,对象序列化和 Content-Type 必须成对出现。
- 全局 defaults 应通过修改属性实现,不能整体替换引用。
- 单次请求配置的优先级应高于全局默认配置。
- 封装代码要优先保证正常路径正确,异常输入可让系统报错,不必为所有错误输入兜底。