从零构建一个轻量级axios替代方案:揭秘XHR封装与拦截器设计
1. 为什么需要自建HTTP请求库?
在当今前端开发中,HTTP请求库就像空气一样无处不在。axios作为目前最流行的选择,确实提供了强大的功能和良好的开发体验。但你是否想过,当面试官问你"axios是如何工作的"时,除了说"它很好用",还能给出什么深度回答?
构建自己的HTTP请求库有几个实际价值:
- 深入理解底层机制:通过手写实现,你会真正明白XMLHttpRequest(XHR)的工作原理
- 定制化需求:商业项目可能有特殊需求,如独特的认证流程或日志记录
- 性能优化:去除不需要的功能,打造更轻量的解决方案
- 面试加分:展示对底层原理的理解,而不只是API调用
我曾在一个性能敏感的项目中,发现axios的体积对首屏加载造成了压力。通过自建精简版请求库,最终减少了约40%的包体积。
2. 基础XHR封装:打造核心请求功能
2.1 XMLHttpRequest基础
现代浏览器都内置了XMLHttpRequest对象,它是所有AJAX请求的基石。让我们先看一个最简单的GET请求示例:
const xhr = new XMLHttpRequest(); xhr.open('GET', 'https://api.example.com/data', true); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300) { console.log(JSON.parse(xhr.responseText)); } else { console.error('Request failed'); } } }; xhr.send();这段代码虽然简单,但已经包含了HTTP请求的核心要素:请求方法、URL、异步处理和响应处理。
2.2 Promise封装
现代前端开发中,Promise已经成为异步处理的标准。让我们将上面的代码用Promise包装:
function request(config) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(config.method || 'GET', config.url, true); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300) { resolve({ data: JSON.parse(xhr.responseText), status: xhr.status, statusText: xhr.statusText }); } else { reject(new Error(`Request failed with status ${xhr.status}`)); } } }; xhr.send(config.data ? JSON.stringify(config.data) : null); }); }现在我们可以这样使用:
request({ url: 'https://api.example.com/data', method: 'GET' }).then(response => { console.log(response.data); }).catch(error => { console.error(error); });2.3 支持多种请求方法
我们的库应该支持常见的HTTP方法:GET、POST、PUT、DELETE等。关键区别在于:
- GET/DELETE:参数通过URL传递
- POST/PUT:参数通过请求体传递
function request(config) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); let url = config.url; const method = (config.method || 'GET').toUpperCase(); // 处理GET/DELETE参数 if (method === 'GET' || method === 'DELETE') { if (config.params) { const params = new URLSearchParams(); Object.keys(config.params).forEach(key => { params.append(key, config.params[key]); }); url += `?${params.toString()}`; } } xhr.open(method, url, true); // 设置POST/PUT请求头 if (method === 'POST' || method === 'PUT') { xhr.setRequestHeader('Content-Type', 'application/json'); } xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300) { resolve({ data: JSON.parse(xhr.responseText), status: xhr.status, statusText: xhr.statusText }); } else { reject(new Error(`Request failed with status ${xhr.status}`)); } } }; xhr.send(method === 'POST' || method === 'PUT' ? JSON.stringify(config.data) : null); }); }3. 拦截器系统设计
拦截器是axios最强大的功能之一,它允许我们在请求发出前和响应返回后插入处理逻辑。典型的应用场景包括:
- 统一添加认证头
- 请求/响应数据转换
- 错误统一处理
- 日志记录
3.1 拦截器基本结构
拦截器本质上是一个中间件系统,包含请求拦截器和响应拦截器。我们可以用数组来存储这些拦截器:
class MiniAxios { constructor() { this.interceptors = { request: [], response: [] }; } useRequestInterceptor(fulfilled, rejected) { this.interceptors.request.push({ fulfilled, rejected }); } useResponseInterceptor(fulfilled, rejected) { this.interceptors.response.push({ fulfilled, rejected }); } }3.2 实现拦截器链
拦截器的执行顺序是:请求拦截器 → 发送请求 → 响应拦截器。我们可以用Promise链来实现:
class MiniAxios { // ...之前的代码 request(config) { // 初始配置 let promise = Promise.resolve(config); // 请求拦截器 this.interceptors.request.forEach(interceptor => { promise = promise.then(interceptor.fulfilled, interceptor.rejected); }); // 发送请求 promise = promise.then(this.dispatchRequest); // 响应拦截器 this.interceptors.response.forEach(interceptor => { promise = promise.then(interceptor.fulfilled, interceptor.rejected); }); return promise; } dispatchRequest(config) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); // ...XHR实现代码 }); } }3.3 拦截器实战应用
让我们看几个实际应用场景:
添加认证头:
const api = new MiniAxios(); api.useRequestInterceptor(config => { config.headers = config.headers || {}; config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`; return config; });统一错误处理:
api.useResponseInterceptor( response => response, error => { if (error.response && error.response.status === 401) { // 跳转到登录页 window.location.href = '/login'; } return Promise.reject(error); } );4. 取消请求实现
取消请求是一个常被忽视但非常重要的功能。想象用户快速切换标签页时,取消前一个未完成的请求可以显著提升性能。
4.1 基于CancelToken的实现
axios的取消机制基于CancelToken。我们可以实现类似的逻辑:
class CancelToken { constructor(executor) { let cancel; this.promise = new Promise(resolve => { cancel = resolve; }); executor(message => { cancel(message); }); } } class MiniAxios { // ...之前的代码 request(config) { if (config.cancelToken) { config.cancelToken.promise.then(message => { // 这里应该取消XHR请求 if (this.xhr) { this.xhr.abort(); reject(new Cancel(message)); } }); } // ...其他代码 } }使用示例:
const source = new CancelToken(c => cancel = c); api.get('/data', { cancelToken: source }).catch(err => { if (err instanceof Cancel) { console.log('Request canceled', err.message); } }); // 取消请求 cancel('Operation canceled by user');4.2 实际应用场景
避免重复请求:
let currentRequest = null; function fetchData() { if (currentRequest) { currentRequest.cancel('New request started'); } currentRequest = new CancelToken(c => cancel = c); api.get('/data', { cancelToken: currentRequest }).then(response => { currentRequest = null; // 处理响应 }); }组件卸载时取消请求:
useEffect(() => { const source = new CancelToken(c => cancel = c); api.get('/data', { cancelToken: source }) .then(response => { // 更新状态 }); return () => { cancel('Component unmounted'); }; }, []);5. 与json-server集成测试
为了测试我们的库,可以使用json-server快速搭建一个模拟API服务。
5.1 设置json-server
首先安装并创建测试数据:
npm install -g json-server创建db.json:
{ "posts": [ { "id": 1, "title": "json-server", "author": "typicode" } ], "comments": [ { "id": 1, "body": "some comment", "postId": 1 } ] }启动服务:
json-server --watch db.json5.2 测试我们的请求库
const api = new MiniAxios(); // 测试GET api.request({ url: 'http://localhost:3000/posts' }).then(response => { console.log('GET response:', response.data); }); // 测试POST api.request({ url: 'http://localhost:3000/posts', method: 'POST', data: { title: 'New Post', author: 'Me' } }).then(response => { console.log('POST response:', response.data); }); // 测试拦截器 api.useRequestInterceptor(config => { console.log('Request sent to:', config.url); return config; }); api.useResponseInterceptor(response => { console.log('Response received:', response.status); return response; });6. 性能优化与扩展
6.1 请求缓存
对于不常变化的数据,可以添加缓存功能:
class MiniAxios { constructor() { this.cache = new Map(); } request(config) { const cacheKey = JSON.stringify(config); if (config.cache && this.cache.has(cacheKey)) { return Promise.resolve(this.cache.get(cacheKey)); } return this.dispatchRequest(config).then(response => { if (config.cache) { this.cache.set(cacheKey, response); } return response; }); } }6.2 并发控制
限制同时进行的请求数量:
class MiniAxios { constructor(maxRequests = 5) { this.maxRequests = maxRequests; this.queue = []; this.activeRequests = 0; } request(config) { return new Promise((resolve, reject) => { this.queue.push({ config, resolve, reject }); this.processQueue(); }); } processQueue() { if (this.activeRequests < this.maxRequests && this.queue.length) { const { config, resolve, reject } = this.queue.shift(); this.activeRequests++; this.dispatchRequest(config) .then(resolve) .catch(reject) .finally(() => { this.activeRequests--; this.processQueue(); }); } } }6.3 超时处理
添加请求超时功能:
dispatchRequest(config) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); let isTimeout = false; const timeoutTimer = setTimeout(() => { isTimeout = true; xhr.abort(); reject(new Error(`Timeout of ${config.timeout}ms exceeded`)); }, config.timeout || 10000); xhr.onreadystatechange = () => { if (isTimeout) return; // ...正常处理 }; // ...其他XHR设置 }); }7. 完整实现与对比
现在我们已经实现了axios的核心功能。让我们对比一下自建方案与axios:
| 功能 | 自建方案 | axios |
|---|---|---|
| 基本请求 | ||
| 拦截器 | ||
| 取消请求 | ||
| 自动转换JSON | ||
| 浏览器/Node | 仅浏览器 | 都支持 |
| 体积 | ~3KB | ~10KB |
| 测试覆盖率 | 需自测 | 完善 |
| 文档和社区支持 | 无 | 丰富 |
在实际项目中,如果需求简单且对体积敏感,自建方案是个不错的选择。但对于复杂项目,axios仍然是更稳妥的选择。