据说每个大牛、小牛都应该有自己的库——Ajax

作为前端开发者,你一定每天都在和HTTP请求打交道:从页面初始化的数据拉取,到用户交互后的异步更新,再到文件上传、分页加载等复杂业务场景——Ajax 是支撑现代Web应用的核心技术之一。

但你是否满足于直接使用第三方库(如Axios、jQuery.ajax)?还是曾因为原生API的繁琐、业务需求的定制化而头疼?其实,无论你是刚入门的"小牛"还是经验丰富的"大牛",亲手实现一个属于自己的Ajax库,都是深入理解前端网络通信、提升技术深度的关键一步。

本文将从Ajax的核心原理出发,一步步带你构建一个功能完备的自定义Ajax库,并分享实际业务中的最佳实践,帮你彻底吃透Ajax。

目录#

  1. 什么是Ajax?它为什么重要?
  2. Ajax的核心原理与底层API解析 2.1 原生XMLHttpRequest(XHR)的工作流程 2.2 Fetch API:现代浏览器的替代方案 2.3 两者的对比与适用场景
  3. 从零构建自己的Ajax库:一步步实现 3.1 基础版本:封装XHR核心逻辑 3.2 进阶功能1:参数序列化与Content-Type适配 3.3 进阶功能2:请求/响应拦截器 3.4 进阶功能3:错误统一处理与超时控制 3.5 适配Fetch API:兼容现代浏览器 3.6 最终版库的使用示例
  4. 常见业务场景中的Ajax实践 4.1 表单提交与文件上传 4.2 分页加载与无限滚动 4.3 并发请求控制
  5. Ajax开发的最佳实践 5.1 兼容性与降级方案 5.2 性能优化:缓存、请求合并 5.3 安全性:CSRF、XSS防护 5.4 可维护性:日志、监控与调试
  6. 总结:为什么要有自己的Ajax库?
  7. 参考资料

1. 什么是Ajax?它为什么重要?#

Ajax(Asynchronous JavaScript and XML)是一种无需刷新整个页面就能与服务器交换数据并更新部分页面内容的技术。尽管名字里带有XML,但如今JSON已经成为主流的数据交换格式。

为什么Ajax是现代Web的基石?#

  • 提升用户体验:异步更新页面,避免全页刷新的等待感
  • 降低服务器负载:仅传输必要的数据,而非整个HTML页面
  • 支持复杂交互:实现表单实时验证、即时搜索、无限滚动等功能
  • 前后端分离:为前端独立开发提供技术基础,推动RESTful API的普及

2. Ajax的核心原理与底层API解析#

要封装自己的Ajax库,必须先吃透底层API的工作逻辑。前端实现Ajax的核心方式有两种:XMLHttpRequest(XHR)Fetch API

2.1 原生XMLHttpRequest(XHR)的工作流程#

XHR是最早的Ajax底层API,兼容所有现代浏览器及部分旧浏览器(如IE6+),其核心工作流程如下:

  1. 创建XMLHttpRequest实例
  2. 调用open()方法配置请求(方法、URL、是否异步)
  3. 设置请求头(如Content-Type
  4. 监听onreadystatechange事件,处理响应状态变化
  5. 调用send()方法发送请求(可携带请求体)
// 原生XHR示例
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.setRequestHeader('Content-Type', 'application/json');
 
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) { // 请求完成
    if (xhr.status >= 200 && xhr.status < 300) { // HTTP状态码正常
      const response = JSON.parse(xhr.responseText);
      console.log('响应数据:', response);
    } else {
      console.error('请求失败:', xhr.statusText);
    }
  }
};
 
xhr.onerror = function() {
  console.error('网络请求错误');
};
 
xhr.send();

2.2 Fetch API:现代浏览器的替代方案#

Fetch API是ES6之后推出的现代Ajax API,基于Promise实现,语法更简洁、语义化更强,其核心特点:

  • 默认不携带Cookie,需手动配置credentials: 'include'
  • 仅在网络错误时拒绝Promise,HTTP错误码(如404、500)不会触发reject
  • 支持流式响应,适合大文件处理
// 原生Fetch示例
fetch('/api/data', {
  method: 'GET',
  headers: { 'Content-Type': 'application/json' },
  credentials: 'include' // 携带Cookie
})
.then(response => {
  if (!response.ok) { // 手动处理HTTP错误
    throw new Error(`HTTP错误:${response.status}`);
  }
  return response.json();
})
.then(data => console.log('响应数据:', data))
.catch(error => console.error('请求失败:', error));

2.3 两者的对比与适用场景#

特性XMLHttpRequestFetch API
兼容性支持IE6+支持Chrome 42+、Edge 14+
语法回调式,代码冗余Promise式,简洁优雅
Cookie携带默认携带需手动配置credentials
HTTP错误处理状态码判断在回调内需手动检查response.ok
流式响应支持但实现复杂原生支持
请求中断支持abort()支持AbortController

适用场景

  • 需兼容旧浏览器(如IE):选择XHR
  • 现代Web应用、PWA:优先使用Fetch API
  • 复杂请求控制(如进度监听、超时):两者均可,但XHR的进度监听更便捷

3. 从零构建自己的Ajax库:一步步实现#

接下来,我们将从基础到进阶,逐步实现一个功能完备、可定制的Ajax库。

3.1 基础版本:封装XHR核心逻辑#

先实现一个最基础的Promise版本,解决原生XHR回调嵌套的问题:

/**
 * 基础版Ajax库:封装XHR核心逻辑
 * @param {Object} options - 请求配置
 * @returns {Promise} 请求结果Promise
 */
function myAjax(options) {
  const {
    url,
    method = 'GET',
    data = {},
    headers = {}
  } = options;
 
  return new Promise((resolve, reject) => {
    // 1. 创建XHR实例
    const xhr = new XMLHttpRequest();
 
    // 2. 处理GET请求的参数拼接
    let requestUrl = url;
    if (method.toUpperCase() === 'GET' && Object.keys(data).length > 0) {
      const queryStr = new URLSearchParams(data).toString();
      requestUrl = `${url}?${queryStr}`;
    }
 
    // 3. 配置请求
    xhr.open(method.toUpperCase(), requestUrl, true);
 
    // 4. 设置默认请求头
    xhr.setRequestHeader('Content-Type', 'application/json');
    // 合并自定义请求头
    Object.keys(headers).forEach(key => {
      xhr.setRequestHeader(key, headers[key]);
    });
 
    // 5. 监听响应状态
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        // 处理响应数据
        try {
          const responseData = JSON.parse(xhr.responseText);
          if (xhr.status >= 200 && xhr.status < 300) {
            resolve({
              data: responseData,
              status: xhr.status,
              statusText: xhr.statusText
            });
          } else {
            reject(new Error(`请求失败:${xhr.status} ${xhr.statusText}`));
          }
        } catch (e) {
          reject(new Error('响应JSON解析失败'));
        }
      }
    };
 
    // 6. 监听网络错误
    xhr.onerror = function() {
      reject(new Error('网络请求异常'));
    };
 
    // 7. 发送请求
    if (method.toUpperCase() !== 'GET') {
      xhr.send(JSON.stringify(data));
    } else {
      xhr.send();
    }
  });
}

3.2 进阶功能1:参数序列化与Content-Type适配#

基础版本仅支持JSON格式的POST请求,但实际业务中还需要application/x-www-form-urlencodedmultipart/form-data等格式,我们需要添加参数序列化逻辑:

/**
 * 参数序列化工具函数
 * @param {Object} data - 待序列化的数据
 * @param {string} contentType - 请求Content-Type
 * @returns {string|FormData} 序列化后的参数
 */
function serializeData(data, contentType) {
  if (!data) return null;
 
  switch (contentType) {
    case 'application/json':
      return JSON.stringify(data);
    case 'application/x-www-form-urlencoded':
      return new URLSearchParams(data).toString();
    case 'multipart/form-data':
      const formData = new FormData();
      Object.keys(data).forEach(key => {
        formData.append(key, data[key]);
      });
      return formData;
    default:
      return data;
  }
}
 
// 升级myAjax函数中的请求发送逻辑
function myAjax(options) {
  // ... 省略原有代码
 
  // 动态设置Content-Type
  const defaultContentType = data instanceof FormData 
    ? 'multipart/form-data' 
    : 'application/json';
  const contentType = headers['Content-Type'] || defaultContentType;
 
  // ... 省略请求头设置
 
  // 发送请求时使用序列化后的参数
  if (method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
    const serializedData = serializeData(data, contentType);
    // multipart/form-data无需手动设置Content-Type,浏览器会自动添加boundary
    if (contentType !== 'multipart/form-data') {
      xhr.setRequestHeader('Content-Type', contentType);
    }
    xhr.send(serializedData);
  } else {
    xhr.send();
  }
}

3.3 进阶功能2:请求/响应拦截器#

拦截器是Ajax库的核心功能之一,允许在请求发送前、响应返回后统一处理逻辑(如添加Token、修改响应格式):

// 全局拦截器容器
const interceptors = {
  request: [],
  response: []
};
 
/**
 * 添加请求拦截器
 * @param {Function} interceptor - 拦截器函数,接收options,返回修改后的options
 */
myAjax.useRequestInterceptor = function(interceptor) {
  interceptors.request.push(interceptor);
};
 
/**
 * 添加响应拦截器
 * @param {Function} interceptor - 拦截器函数,接收response,返回修改后的response
 */
myAjax.useResponseInterceptor = function(interceptor) {
  interceptors.response.push(interceptor);
};
 
// 升级myAjax函数,添加拦截器执行逻辑
async function myAjax(options) {
  let modifiedOptions = { ...options };
 
  // 执行所有请求拦截器
  for (const interceptor of interceptors.request) {
    modifiedOptions = await interceptor(modifiedOptions);
  }
 
  return new Promise((resolve, reject) => {
    // ... 原有XHR逻辑
 
    // 处理响应时执行响应拦截器
    xhr.onreadystatechange = async function() {
      if (xhr.readyState === 4) {
        try {
          const responseData = JSON.parse(xhr.responseText);
          const rawResponse = {
            data: responseData,
            status: xhr.status,
            statusText: xhr.statusText
          };
 
          if (xhr.status >= 200 && xhr.status < 300) {
            // 执行响应拦截器
            let processedResponse = rawResponse;
            for (const interceptor of interceptors.response) {
              processedResponse = await interceptor(processedResponse);
            }
            resolve(processedResponse);
          } else {
            reject(new Error(`请求失败:${xhr.status} ${xhr.statusText}`));
          }
        } catch (e) {
          reject(new Error('响应JSON解析失败'));
        }
      }
    };
  });
}

3.4 进阶功能3:错误统一处理与超时控制#

添加超时控制和统一错误处理,让库的健壮性更强:

async function myAjax(options) {
  // ... 省略拦截器执行逻辑
 
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    const timeout = options.timeout || 10000; // 默认超时10秒
 
    // 设置超时
    xhr.timeout = timeout;
    xhr.ontimeout = function() {
      reject(new Error(`请求超时(已超过${timeout}ms)`));
    };
 
    // ... 省略原有响应处理逻辑
 
    // 统一错误处理
    xhr.onerror = function() {
      reject(new Error('网络请求异常,请检查网络连接'));
    };
  });
}

3.5 适配Fetch API:兼容现代浏览器#

为了支持现代浏览器,我们可以添加Fetch API的适配逻辑,让库自动选择底层实现:

async function myAjax(options) {
  // ... 省略拦截器执行逻辑
 
  // 优先使用Fetch API(如果浏览器支持)
  if (window.fetch && !options.forceXHR) {
    try {
      const { url, method = 'GET', data = {}, headers = {}, timeout = 10000 } = modifiedOptions;
      const fetchOptions = {
        method: method.toUpperCase(),
        headers: new Headers(headers),
        credentials: 'include'
      };
 
      // 处理请求体
      if (method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
        const contentType = headers['Content-Type'] || 'application/json';
        fetchOptions.body = serializeData(data, contentType);
        if (contentType !== 'multipart/form-data') {
          fetchOptions.headers.set('Content-Type', contentType);
        }
      }
 
      // 超时控制
      const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error(`请求超时(已超过${timeout}ms)`)), timeout);
      });
 
      const requestPromise = fetch(url, fetchOptions)
        .then(response => {
          if (!response.ok) {
            throw new Error(`请求失败:${response.status} ${response.statusText}`);
          }
          return response.json();
        })
        .then(data => ({ data, status: response.status, statusText: response.statusText }));
 
      // 执行响应拦截器
      const rawResponse = await Promise.race([requestPromise, timeoutPromise]);
      let processedResponse = rawResponse;
      for (const interceptor of interceptors.response) {
        processedResponse = await interceptor(processedResponse);
      }
      return resolve(processedResponse);
    } catch (e) {
      return reject(e);
    }
  } else {
    // 回退到XHR实现
    // ... 省略原有XHR逻辑
  }
}

3.6 最终版库的使用示例#

经过以上优化,我们的自定义Ajax库已经具备了第三方库的核心功能,使用方式如下:

// 1. 添加全局拦截器(比如添加Token)
myAjax.useRequestInterceptor(options => {
  const token = localStorage.getItem('token');
  if (token) {
    options.headers = {
      ...options.headers,
      'Authorization': `Bearer ${token}`
    };
  }
  return options;
});
 
// 2. 发起GET请求
myAjax({
  url: '/api/users',
  method: 'GET',
  data: { page: 1, size: 10 },
  timeout: 8000
}).then(res => {
  console.log('用户列表:', res.data);
}).catch(err => {
  console.error('请求错误:', err.message);
});
 
// 3. 发起文件上传请求
const fileInput = document.getElementById('avatar-input');
myAjax({
  url: '/api/upload/avatar',
  method: 'POST',
  data: {
    avatar: fileInput.files[0],
    userId: 123
  },
  headers: {
    'Content-Type': 'multipart/form-data'
  },
  onProgress: (progressEvent) => {
    const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
    console.log(`上传进度:${percent}%`);
  }
}).then(res => {
  console.log('上传成功:', res.data);
}).catch(err => {
  console.error('上传失败:', err.message);
});

4. 常见业务场景中的Ajax实践#

4.1 表单提交与文件上传#

  • 表单提交时,优先使用application/x-www-form-urlencodedmultipart/form-data
  • 文件上传必须使用multipart/form-data,并监听上传进度
  • 提交前添加表单验证,避免无效请求

4.2 分页加载与无限滚动#

  • 分页请求时,携带pagesize参数,避免一次性加载大量数据
  • 无限滚动时,使用节流函数控制请求频率,避免频繁触发请求
  • 加载过程中禁用滚动监听,防止重复请求

4.3 并发请求控制#

当需要同时发起多个请求(如页面初始化时拉取多个接口数据),可以使用Promise.all,但如果请求数量过多,需要限制并发数:

/**
 * 并发请求控制函数
 * @param {Array<Function>} requests - 请求函数数组(每个函数返回Promise)
 * @param {number} limit - 最大并发数
 * @returns {Promise<Array>} 所有请求结果
 */
async function limitConcurrentRequests(requests, limit = 3) {
  const results = [];
  let currentIndex = 0;
 
  async function executeRequest() {
    if (currentIndex >= requests.length) return;
    const index = currentIndex++;
    try {
      results[index] = await requests[index]();
    } catch (err) {
      results[index] = { error: err.message };
    }
    await executeRequest();
  }
 
  // 初始化并发请求
  const initialBatch = Array(Math.min(limit, requests.length)).fill().map(executeRequest);
  await Promise.all(initialBatch);
  return results;
}
 
// 使用示例
const requestList = [
  () => myAjax({ url: '/api/user' }),
  () => myAjax({ url: '/api/menu' }),
  () => myAjax({ url: '/api/notice' }),
  () => myAjax({ url: '/api/setting' })
];
 
limitConcurrentRequests(requestList, 2).then(results => {
  console.log('所有请求结果:', results);
});

5. Ajax开发的最佳实践#

5.1 兼容性与降级方案#

  • 对IE等旧浏览器,强制使用XHR实现
  • 提供Fetch API的polyfill(如whatwg-fetch
  • 测试不同浏览器下的请求行为,避免兼容性问题

5.2 性能优化:缓存、请求合并#

  • HTTP缓存:利用Cache-ControlETag等HTTP头实现浏览器缓存
  • 内存缓存:对GET请求的结果进行内存缓存,设置合理的过期时间
  • 请求合并:将多个小请求合并为一个大请求,减少HTTP连接数

5.3 安全性:CSRF、XSS防护#

  • CSRF防护:在请求头中添加CSRF Token,或使用SameSite Cookie
  • XSS防护:对响应数据进行HTML转义,避免注入攻击
  • HTTPS:生产环境强制使用HTTPS,防止数据被窃听

5.4 可维护性:日志、监控与调试#

  • 在库中添加日志功能,打印请求/响应的关键信息
  • 集成错误监控系统(如Sentry),实时捕获请求错误
  • 使用浏览器开发者工具的Network面板,排查请求问题

6. 总结:为什么要有自己的Ajax库?#

无论是刚入门的开发者还是资深工程师,亲手实现一个Ajax库都有不可替代的价值:

  1. 深入理解底层原理:彻底掌握XHR和Fetch API的工作流程,不再停留在API调用层面
  2. 定制化业务需求:根据团队业务场景添加专属功能,无需依赖第三方库的冗余代码
  3. 提升问题排查能力:遇到网络请求问题时,能快速定位到库的底层逻辑,而非盲目调试
  4. 建立技术自信:从0到1构建工具库,是前端工程师技术成长的重要标志

7. 参考资料#

  1. MDN Web Docs - XMLHttpRequest
  2. MDN Web Docs - Fetch API
  3. Axios 官方文档
  4. W3C XMLHttpRequest 规范
  5. 前端安全:CSRF 防护指南