手写 AJAX 与封装 MyAxios:深入理解前端网络请求

前言

在现代前端开发中,我们经常使用 axiosfetch 等库进行网络请求,但理解其底层原理对于成为优秀的前端工程师至关重要。本文将带你从头实现一个简单的 AJAX 函数,并逐步封装成类似 axios 的 Promise 风格库。

什么是 AJAX?

AJAX(Asynchronous JavaScript and XML)是一种创建快速动态网页的技术,通过在后台与服务器进行少量数据交换,实现网页的异步更新,这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。

原生 XMLHttpRequest 基础

在开始封装之前,我们先了解浏览器原生的 XMLHttpRequest 对象:

javascript 复制代码
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
  }
};
xhr.send();

XMLHttpRequest 的核心方法详解

1. xhr.open(method, url, async) 参数说明

open() 方法初始化一个请求,它有五个参数,其中前三个最常用:

  • method:HTTP 请求方法,如 'GET'、'POST'、'PUT'、'DELETE' 等
  • url:请求的 URL 地址,可以是相对路径或绝对路径
  • async (可选):是否异步执行操作,默认为 true
    • true:异步模式,请求在后台执行,不阻塞页面
    • false:同步模式,请求会阻塞页面直到完成(不推荐使用)
  • user(可选):用于认证的用户名
  • password(可选):用于认证的密码
javascript 复制代码
// 异步 GET 请求
xhr.open('GET', '/api/users', true);

// 同步 POST 请求(不推荐)
xhr.open('POST', '/api/users', false);

2. xhr.send(data) 参数说明

send() 方法发送请求到服务器,参数根据请求类型有所不同:

  • GET/HEAD 请求 :通常为 nullundefined
  • POST/PUT 请求 :包含请求体的数据,可以是多种格式:
    • String:普通文本,如 "name=John&age=30"
    • FormData:用于文件上传或表单数据
    • Blob:二进制大对象
    • ArrayBuffer:原始二进制数据
    • Document:XML 文档
    • null:无请求体
javascript 复制代码
// GET 请求,不需要请求体
xhr.send();

// POST 请求,发送表单数据
xhr.send('name=John&age=30');

// POST 请求,发送 JSON 数据
xhr.send(JSON.stringify({ name: 'John', age: 30 }));

// POST 请求,发送 FormData(文件上传)
const formData = new FormData();
formData.append('file', fileInput.files[0]);
xhr.send(formData);

3. readyState 状态说明

readyState 属性表示请求的状态变化,共有 5 个可能的值:

状态 描述
0 UNSENT XMLHttpRequest 对象已创建,但 open() 方法尚未调用
1 OPENED open() 方法已调用,建立了与服务器的连接
2 HEADERS_RECEIVED send() 方法已调用,已接收到响应头
3 LOADING 正在接收响应体,responseText 属性包含部分数据
4 DONE 请求完成,响应数据接收完毕
javascript 复制代码
xhr.onreadystatechange = function() {
  switch(xhr.readyState) {
    case 0:
      console.log('请求未初始化');
      break;
    case 1:
      console.log('服务器连接已建立');
      break;
    case 2:
      console.log('请求已接收,响应头信息可用');
      console.log('状态码:', xhr.status);
      break;
    case 3:
      console.log('请求处理中,响应体部分数据可用');
      console.log('已接收数据:', xhr.responseText.length, '字符');
      break;
    case 4:
      console.log('请求完成,数据接收完毕');
      if (xhr.status === 200) {
        console.log('成功响应:', xhr.responseText);
      }
      break;
  }
};

XMLHttpRequest 的其他关键属性

  • status: HTTP 状态码(200、404、500 等)
  • statusText: HTTP 状态文本("OK"、"Not Found" 等)
  • responseText: 响应文本内容
  • responseXML: 响应 XML 文档(如果内容是 XML)
  • response: 根据响应类型返回相应格式的数据
  • onreadystatechange: 状态改变事件处理器

手写基础 AJAX 函数

让我们从最简单的 AJAX 函数开始:

javascript 复制代码
function ajax(method = 'GET', url = '', data = '') {
  const xhr = new XMLHttpRequest();
  xhr.open(method, url, true);
  
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
      if (xhr.status === 200) {
        console.log(xhr.responseText);
      } else {
        console.error('请求失败:', xhr.status);
      }
    }
  };
  
  xhr.send(data);
}

这个基础版本虽然能用,但存在几个明显问题:

  1. 无法处理 GET 请求的查询参数
  2. 缺乏错误处理机制
  3. 回调方式不够现代化
  4. 不支持请求头设置

改进版 AJAX 函数

让我们修复这些问题:

javascript 复制代码
function ajax(method = 'GET', url = '', data = null) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    
    // 处理 GET 请求的查询参数
    let requestUrl = url;
    if (method.toUpperCase() === 'GET' && data) {
      const params = new URLSearchParams(data).toString();
      requestUrl = `${url}${url.includes('?') ? '&' : '?'}${params}`;
    }
    
    xhr.open(method, requestUrl, true);
    
    // 设置请求头
    if (method.toUpperCase() === 'POST') {
      xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    }
    
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(xhr.responseText);
        } else {
          reject(new Error(`请求失败: ${xhr.status}`));
        }
      }
    };
    
    xhr.onerror = function() {
      reject(new Error('网络错误'));
    };
    
    // 发送请求体数据
    const sendData = method.toUpperCase() === 'GET' ? null : data;
    xhr.send(sendData);
  });
}

这个版本已经相当实用,支持 Promise,正确处理了 GET 和 POST 请求。

封装 MyAxios 库

现在让我们封装一个更加强大、类似 axios 的库:

javascript 复制代码
function myAxios(config) {
  // 处理配置参数
  const {
    method = 'GET',
    url,
    data = null,
    headers = {},
    timeout = 0
  } = typeof config === 'string' ? { url: config } : config;
  
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    
    // 处理查询参数
    let requestUrl = url;
    if (method.toUpperCase() === 'GET' && data && typeof data === 'object') {
      const params = new URLSearchParams(data).toString();
      requestUrl = `${url}${url.includes('?') ? '&' : '?'}${params}`;
    }
    
    xhr.open(method, requestUrl, true);
    
    // 设置请求头
    Object.keys(headers).forEach(key => {
      xhr.setRequestHeader(key, headers[key]);
    });
    
    // 设置超时
    if (timeout > 0) {
      xhr.timeout = timeout;
      xhr.ontimeout = function() {
        reject(new Error(`请求超时: ${timeout}ms`));
      };
    }
    
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
          // 尝试解析 JSON 响应
          let response = xhr.responseText;
          if (xhr.getResponseHeader('Content-Type')?.includes('application/json')) {
            try {
              response = JSON.parse(response);
            } catch (e) {
              // 保持原始响应
            }
          }
          
          resolve({
            data: response,
            status: xhr.status,
            statusText: xhr.statusText,
            headers: xhr.getAllResponseHeaders(),
            config
          });
        } else {
          reject(new Error(`请求失败: ${xhr.status} ${xhr.statusText}`));
        }
      }
    };
    
    xhr.onerror = function() {
      reject(new Error('网络错误'));
    };
    
    // 发送请求体
    let sendData = null;
    if (method.toUpperCase() !== 'GET' && data) {
      if (headers['Content-Type'] === 'application/json') {
        sendData = JSON.stringify(data);
      } else {
        sendData = data;
      }
    }
    
    xhr.send(sendData);
  });
}

// 添加便捷方法
myAxios.get = function(url, config = {}) {
  return myAxios({ ...config, method: 'GET', url });
};

myAxios.post = function(url, data = {}, config = {}) {
  return myAxios({ ...config, method: 'POST', url, data });
};

使用示例

javascript 复制代码
// GET 请求
myAxios.get('https://api.example.com/users', {
  params: { page: 1, limit: 10 },
  timeout: 5000
})
.then(response => {
  console.log('获取用户列表:', response.data);
})
.catch(error => {
  console.error('请求失败:', error.message);
});

// POST 请求
myAxios.post('https://api.example.com/users', {
  name: 'John Doe',
  email: 'john@example.com'
}, {
  headers: {
    'Content-Type': 'application/json'
  }
})
.then(response => {
  console.log('创建用户成功:', response.data);
});

// 使用完整配置
myAxios({
  method: 'PUT',
  url: 'https://api.example.com/users/1',
  data: { name: 'Jane Doe' },
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  timeout: 10000
});

功能对比

功能 基础 AJAX 改进版 AJAX MyAxios
Promise 支持
请求方法处理 基础 基础 完整
请求头设置 基础 完整
超时处理
响应拦截
便捷方法
错误处理 基础 完整 完整

总结

通过手动实现 AJAX 和封装 MyAxios,我们深入理解了网络请求的底层原理。这不仅帮助我们更好地使用现有的请求库,还能在遇到问题时快速定位和解决。

理解 xhr.open()xhr.send()readyState 这些核心概念对于掌握 AJAX 至关重要。它们分别负责初始化请求、发送数据和监控请求状态,是 XMLHttpRequest 对象的三大支柱。

实际开发中,我们通常使用成熟的库如 axios,但理解其背后的原理对于提升技术水平至关重要。希望本文能帮助你更深入地理解前端网络请求的工作原理。

相关推荐
顾青3 小时前
微信小程序实现身份证识别与裁剪(基于 VisionKit)
前端·微信小程序
星链引擎3 小时前
技术深度聚焦版(侧重技术原理与代码细节)
前端
呵阿咯咯3 小时前
ueditor富文本编辑器相关问题
前端
月弦笙音3 小时前
【Vue3】Keep-Alive 深度解析
前端·vue.js·源码阅读
该用户已不存在3 小时前
7个没听过但绝对好用的工具
前端·后端
渣哥4 小时前
代理选错,性能和功能全翻车!Spring AOP 的默认技术别再搞混
javascript·后端·面试
遇见火星4 小时前
Docker入门:快速部署你的第一个Web应用
前端·docker·容器
WeilinerL4 小时前
泛前端代码覆盖率探索之路
前端·javascript·测试
浮游本尊4 小时前
React 18.x 学习计划 - 第五天:React状态管理
前端·学习·react.js