Skip to content

源码分析与自定义 axios(Day20 2/3) ​

本页对应 Day20-Axios,共 6 段视频,约 39 分钟。

学习目标 ​

  1. 能够实现 baseURL 与请求 URL 的合并逻辑,明确仅在配置了 baseURL 且 URL 非 http/https 绝对地址时拼接,并通过实际请求验证结果。
  2. 能够为 responseType 设置默认值并支持用户配置覆盖,验证 XMLHttpRequest.responseType 按预期生效。
  3. 能够将 params 对象序列化为查询字符串并正确拼接到 URL,通过多参数请求验证拼接与请求结果。
  4. 能够在 open 之后、send 之前完成 headers 与 data 请求体处理:逐项设置请求头,并针对字符串与纯对象设置对应 Content-Type 和请求体,验证请求载荷。
  5. 能够暴露并实现 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

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

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 基础,转写稿里专门敲了黑板。
  • 全局配置只对“没在单次请求里写”的项生效,单次请求里写了同名字段会覆盖全局值。

常见错误 ​

  1. 完整 URL 仍被拼接 baseURL
    现象:传入 http://... 或 https://... 后,请求地址变成 baseURL + 完整URL。
    原因:只判断了是否以 http 开头,漏掉 https,或判断顺序/取反写错。
    修法:仅在“存在 baseURL 且 URL 不以 http://、https:// 开头”时拼接。

  2. params 被拼成 [object Object]
    现象:请求 URL 后出现 ?[object Object] 或参数丢失。
    原因:直接把 params 对象与 URL 相加,未遍历键值对,也未做编码。
    修法:先判断 params 存在,再用 Object.entries(params).map(...).join('&') 拼接,并对键值做 encodeURIComponent。

  3. headers 设置了但请求里没有
    现象:config.headers 有值,Network 面板看不到自定义请求头。
    原因:在 open 之前设置,或忘记遍历对象调用 xhr.setRequestHeader。
    修法:在 xhr.open 之后、xhr.send 之前遍历 config.headers 并逐个 setRequestHeader。

  4. data 传对象却发送成 [object Object]
    现象:请求体内容为 [object Object],后端解析失败。
    原因:未区分字符串与纯对象,或 typeof null/数组 也被当成普通对象处理。
    修法:字符串按表单格式处理;纯对象先 JSON.stringify 再设置 application/json;其他类型不擅自处理。

  5. 全局 defaults 直接重新赋值后失效
    现象:axios.defaults = {...} 后,原有全局配置不生效或请求未带上配置。
    原因:重新赋值替换了引用,原实例仍指向旧对象。
    修法:不要替换 defaults 对象,直接修改其属性,如 axios.defaults.baseURL = '...'。

  6. responseType 只写了默认配置,没有落到 XHR
    现象:配置里是 json,但实际响应类型未按预期生效。
    原因:只在合并配置中设置默认值,发送前未执行 xhr.responseType = config.responseType。
    修法:在发送请求前把最终合并后的 responseType 真正赋给 XHR 实例。

练习 ​

  1. 实现 buildURL(config):满足“有 baseURL、URL 非完整地址才拼接”,并覆盖 http://、https://、相对路径三种情况。
  2. 实现 params 序列化:支持 { id: 101, name: '张三' },输出 ?id=101&name=%E5%BC%A0%E4%B8%89,并处理空对象。
  3. 在自定义 axios 中补全 headers 设置:要求必须在 open 后、send 前完成,并验证两个自定义请求头。
  4. 实现请求体处理分支:字符串设置 application/x-www-form-urlencoded;纯对象设置 application/json 并序列化;其他类型不处理。
  5. 给自定义 axios 暴露全局配置能力:设置 axios.defaults.baseURL 后发起请求,并验证单次请求配置能覆盖全局配置。

原视频对照 ​

#视频时长
1请求配置项设置-baseURL09:08
2请求配置项设置-responseType01:54
3请求配置项设置-params05:40
4请求配置项设置-headers06:33
5请求配置项设置-data请求体10:35
6请求配置项设置-axios.defaults设置全局请求配置选项05:45

小结 ​

  1. 配置项处理要放在“最终配置确定后、请求真正发出前”,否则配置存在但不会生效。
  2. baseURL 拼接必须先做边界判断,完整 URL 不应再拼 baseURL。
  3. params 不是简单字符串相加,必须遍历、编码、拼接,并处理空值。
  4. headers 的关键不是“有没有配置”,而是“有没有在正确阶段逐个写入 XHR”。
  5. data 请求体要按类型分支处理,对象序列化和 Content-Type 必须成对出现。
  6. 全局 defaults 应通过修改属性实现,不能整体替换引用。
  7. 单次请求配置的优先级应高于全局默认配置。
  8. 封装代码要优先保证正常路径正确,异常输入可让系统报错,不必为所有错误输入兜底。