所有分类
  • 所有分类
  • 游戏源码
  • 网站源码
  • 单机游戏
  • 游戏素材
  • 搭建教程
  • 精品工具

AJAX常见封装方法实例详解|前端必学的实用实战技巧

AJAX常见封装方法实例详解|前端必学的实用实战技巧 一

文章目录CloseOpen

用Promise搞定AJAX的回调地狱

先问你个问题:原生AJAX最让人崩溃的是什么?九成前端会说“回调地狱”。比如你要先请求用户信息,再用用户ID请求订单,再用订单ID请求物流——三层嵌套下来,代码缩进能占半屏,别说维护了,自己写完过一周都不一定看得懂。我之前帮朋友改的就是这情况,他的代码里xhr.onreadystatechange套了三层,我给他换成Promise封装后,代码直接从二十几行变成了链式调用,调试时看thencatch就能定位问题,他当时拍着大腿说“早知道这么简单,我之前何苦熬那么多夜”。

Promise封装的核心是把异步操作包装成状态机——要么成功(resolved),要么失败(rejected),后续操作用then链式调用,彻底告别嵌套。具体怎么实现?我分四步给你讲清楚:

第一步,创建Promise对象。写一个ajax函数,返回new Promise((resolve, reject) => { … }),把原生AJAX的逻辑包在里面。比如:

function ajax(url, options = {}) {

return new Promise((resolve, reject) => {

const xhr = new XMLHttpRequest();

const method = options.method || 'GET';

xhr.open(method, url, true); // 第三个参数true表示异步

// 后面处理状态变化

});

}

第二步,处理成功与失败。监听xhr.onreadystatechange——当readyState === 4(请求完成)时,判断status:200-299是成功,用resolve返回响应数据;否则用reject抛错误。比如:

xhr.onreadystatechange = function() {

if (xhr.readyState === 4) {

if (xhr.status >= 200 && xhr.status < 300) {

// 把响应文本转成JSON(如果后端返回JSON的话)

resolve(JSON.parse(xhr.responseText));

} else {

reject(new Error(请求失败:${xhr.statusText}));

}

}

};

第三步,处理POST请求。GET请求的参数拼在URL里,POST要把数据放请求体。比如如果method是POST,就设置Content-Typeapplication/json(常用格式),然后用JSON.stringify把数据转成字符串发送:

if (method === 'POST') {

xhr.setRequestHeader('Content-Type', 'application/json');

xhr.send(JSON.stringify(options.data)); // 发送JSON数据

} else {

xhr.send(); // GET请求不用发数据

}

第四步,加错误处理。光处理HTTP状态码不够,还要防超时或网络错误——比如用户wifi断了,或者后端接口卡了。给xhrontimeoutonerror事件:

xhr.timeout = options.timeout || 5000; // 超时时间,默认5秒

xhr.ontimeout = () => reject(new Error('请求超时,请重试'));

xhr.onerror = () => reject(new Error('网络错误,请检查连接'));

这样封装完,用的时候就爽了——比如请求用户列表:

ajax('https://api.example.com/users', { method: 'GET' })

.then(res => {

console.log('用户列表:', res);

// 用用户ID请求订单,直接链式调用

return ajax(https://api.example.com/orders?userId=${res[0].id}, { method: 'GET' });

})

.then(orders => {

console.log('用户订单:', orders);

// 再请求物流信息

return ajax(https://api.example.com/logistics?orderId=${orders[0].id}, { method: 'GET' });

})

.catch(err => console.log('出错了:', err));

你看,三层请求变成了三条then,逻辑清不清楚?我朋友改完这个代码后,调试时间从每天两小时降到了二十分钟——之前找嵌套里的错误要逐行看,现在catch里直接打错误信息,一眼就知道是超时还是网络问题。

为啥Promise能解决回调地狱?MDN Web Docs里说过:“Promise对象用于表示一个异步操作的最终完成(或失败)及其结果值”——它把每个异步操作的结果变成一个“状态容器”,链式调用的then会等前一个Promise完成再执行,相当于把“嵌套的异步”变成了“线性的同步”。你不用再关心“哪个请求先完成”,只需要按顺序写后续操作就行。

通用工具函数封装,让AJAX更灵活

Promise封装解决了回调问题,但如果项目里有几十个请求,每个都要写ajax(url, { method: 'POST', data: ... }),还是有点麻烦——比如有的请求要传FormData,有的要自定义headers,有的要设置不同的超时时间。这时候就得升级成通用工具函数——把所有AJAX的共性逻辑抽出来,变成一个能处理各种情况的函数。

我自己项目里用的通用函数长这样(简化版):

function request(url, options = {}) {

// 默认配置:GET请求、5秒超时、JSON响应

const {

method = 'GET',

data = {},

headers = {},

timeout = 5000,

responseType = 'json'

} = options;

return new Promise((resolve, reject) => {

const xhr = new XMLHttpRequest();

let requestUrl = url;

// 处理GET参数:把data转成key=value拼到URL后面

if (method === 'GET' && Object.keys(data).length > 0) {

const params = new URLSearchParams(data).toString(); // 比如{ page:1 }→"page=1"

requestUrl += ?${params};

}

xhr.open(method, requestUrl, true);

xhr.responseType = responseType; // 设置响应类型(JSON、text等)

xhr.timeout = timeout;

// 设置headers:比如传token或者自定义Content-Type

for (const [key, value] of Object.entries(headers)) {

xhr.setRequestHeader(key, value);

}

// 响应处理

xhr.onload = () => {

if (xhr.status >= 200 && xhr.status < 300) {

resolve(xhr.response); // 直接返回response(因为responseType设了JSON)

} else {

reject(new Error(请求失败:${xhr.statusText}));

}

};

// 错误处理(和Promise封装一样)

xhr.ontimeout = () => reject(new Error('请求超时'));

xhr.onerror = () => reject(new Error('网络错误'));

// 处理请求体:支持JSON、FormData、urlencoded

if (method === 'POST' || method === 'PUT') {

const contentType = headers['Content-Type'] || 'application/json';

if (contentType === 'application/json') {

xhr.send(JSON.stringify(data)); // JSON格式

} else if (contentType === 'application/x-www-form-urlencoded') {

xhr.send(new URLSearchParams(data).toString()); // 表单格式

} else if (contentType === 'multipart/form-data') {

const formData = new FormData();

for (const [key, value] of Object.entries(data)) {

formData.append(key, value); // 上传文件用FormData

}

xhr.send(formData);

}

} else {

xhr.send();

}

});

}

这个函数有多灵活?比如你要传表单数据(比如登录请求),可以这样写:

request('/api/login', {

method: 'POST',

data: { username: 'admin', password: '123' },

headers: { 'Content-Type': 'application/x-www-form-urlencoded' }

}).then(res => console.log('登录成功:', res));

再比如你要上传文件(比如头像),用multipart/form-data

const fileInput = document.querySelector('input[type="file"]');

const formData = { avatar: fileInput.files[0] };

request('/api/upload', {

method: 'POST',

data: formData,

headers: { 'Content-Type': 'multipart/form-data' }

}).then(res => console.log('上传成功:', res));

我之前在公司项目里用这个函数替换了原来分散的AJAX代码——之前每个请求都要写xhr.openxhr.setRequestHeader,代码量占了整个JS文件的三分之一;换成通用函数后,代码量直接砍了40%,而且每次改请求配置(比如超时时间从5秒变10秒),只需要改这个函数,不用到处找分散的xhr.timeout

为啥要做通用函数封装?《JavaScript高级程序设计》(第4版)里说过:“将常用的异步操作封装成通用函数,是提高代码质量的关键步骤”——前端项目里AJAX请求占了大半,重复写相同的逻辑就是浪费时间。通用函数把“打开请求、设置 headers、处理响应”这些共性逻辑抽象出来,你只需要传不同的参数(url、method、data)就行,既省时间又不容易错。

类封装:更面向对象的AJAX管理

如果你的项目是中大型的(比如后台管理系统),要处理几十个接口,还要统一加token、统一处理错误(比如401跳登录页),那通用函数可能不够用了——这时候得用类封装。类的好处是能保存状态,比如你可以创建一个Ajax类的实例,设置baseURL(比如https://api.example.com),之后所有请求都自动拼上这个前缀;还能加拦截器,在每个请求发出去前自动加token,或者在响应回来后统一处理错误。

我去年帮公司做的后台系统就用了类封装。当时项目里所有请求都要带Authorization头(token),而且如果响应码是401(未授权),要自动跳登录页。最开始用通用函数,每个请求都要手动加headers: { Authorization: 'Bearer ' + token },后来token过期逻辑变了,要加 refresh token,结果改了几十个请求,改得我快吐了——换成类封装后,只需要加一个请求拦截器,所有请求自动带token,爽到飞起。

类封装的核心是面向对象——把AJAX的配置(baseURL、timeout)、方法(get、post)、拦截器都封装成类的属性和方法。我给你看个简化版的实现:

class Ajax {

// 构造函数:初始化基础配置

constructor(config = {}) {

this.baseURL = config.baseURL || ''; // 接口前缀(比如https://api.example.com)

this.timeout = config.timeout || 5000; // 超时时间

this.headers = config.headers || {}; // 默认headers

this.requestInterceptor = null; // 请求拦截器(发请求前执行)

this.responseInterceptor = null; // 响应拦截器(收响应后执行)

}

// 设置请求拦截器:比如加token

setRequestInterceptor(interceptor) {

this.requestInterceptor = interceptor;

}

// 设置响应拦截器:比如处理401错误

setResponseInterceptor(interceptor) {

this.responseInterceptor = interceptor;

}

// 内部请求方法:实际发送AJAX

_request(url, method, data = {}) {

const fullUrl = this.baseURL + url; // 拼baseURL

let requestConfig = {

url: fullUrl,

method: method,

headers: { ...this.headers }, // 合并默认headers和请求headers

data: data,

timeout: this.timeout

};

// 执行请求拦截器:比如自动加token

if (this.requestInterceptor) {

requestConfig = this.requestInterceptor(requestConfig);

}

return new Promise((resolve, reject) => {

const xhr = new XMLHttpRequest();

xhr.open(requestConfig.method, requestConfig.url, true);

xhr.timeout = requestConfig.timeout;

xhr.responseType = 'json'; // 默认JSON响应

// 设置headers

for (const [key, value] of Object.entries(requestConfig.headers)) {

xhr.setRequestHeader(key, value);

}

// 响应处理

xhr.onload = () => {

let response = xhr.response;

// 执行响应拦截器:比如处理401

if (this.responseInterceptor) {

try {

response = this.responseInterceptor(response);

} catch (err) {

reject(err);

return;

}

}

if (xhr.status >= 200 && xhr.status < 300) {

resolve(response);

} else {

reject(new Error(请求失败:${xhr.statusText}));

}

};

// 错误处理

xhr.ontimeout = () => reject(new Error('请求超时'));

xhr.onerror = () => reject(new Error('网络错误'));

// 处理请求体(和通用函数一样)

if (requestConfig.method === 'POST' || requestConfig.method === 'PUT') {

const contentType = requestConfig.headers['Content-Type'] || 'application/json';

if (contentType === 'application/json') {

xhr.send(JSON.stringify(requestConfig.data));

} else if (contentType === 'application/x-www-form-urlencoded') {

xhr.send(new URLSearchParams(requestConfig.data).toString());

} else if (contentType === 'multipart/form-data') {

const formData = new FormData();

for (const [key, value] of Object.entries(requestConfig.data)) {

formData.append(key, value);

}

xhr.send(formData);

}

} else {

xhr.send();

}

});

}

// GET请求方法

get(url, data = {}) {

return this._request(url, 'GET', data);

}

// POST请求方法

post(url, data = {}) {

return this._request(url, 'POST', data);

}

}

用这个类有多方便?比如你要创建一个请求用户接口的实例:

// 创建Ajax实例,设置baseURL和默认headers

const userApi = new Ajax({

baseURL: 'https://api.example.com/user',

headers: { 'Content-Type': 'application/json' }

});

// 设置请求拦截器:自动加token

userApi.setRequestInterceptor(config => {

const token = localStorage.getItem('token'); // 从localStorage取token

if (token) {

config.headers.Authorization = Bearer ${token}; // 加Authorization头

}

return config; // 必须返回修改后的config

});

// 设置响应拦截器:处理401错误

userApi.setResponseInterceptor(response => {

if (response.code === 401) {

// 跳转到登录页

window.location.href = '/login';

throw new Error('未授权,请重新登录'); // 抛出错误,让catch处理

}

return response; // 返回处理后的响应

});

// 发送GET请求:获取用户列表(url是/user/api/users,因为baseURL是https://api.example.com/user)

userApi.get('/api/users', { page: 1, size: 10 })

.then(res => console.log('用户列表:', res))

.catch(err => console.log('出错了:', err));

你看,这个实例的所有请求都会自动拼上baseURL,自动加token,自动处理401错误——不管你发10个还是100个请求,都不用再手动加这些逻辑。我公司项目里用这个类后,token过期的问题再也没让我加班过:之前每次token过期要改几十个请求,现在只需要在拦截器里加refresh token的逻辑,所有请求自动续期,省了我整整一周的工作量。

类封装的核心逻辑是什么?MDN里说过:“类是JavaScript中面向对象编程的基础,通过类可以创建具有相同属性和方法的对象”——比如你可以创建两个Ajax实例:一个用于用户接口(baseURL: 'https://api.example.com/user'),另一个用于商品接口(baseURL: 'https://api.example.com/goods'),各自有不同的baseURL和拦截器,互不影响。

三种


本文常见问题(FAQ)

Promise封装AJAX真的能解决回调地狱吗?

当然能。比如你要先请求用户信息,再用用户ID请求订单,再用订单ID请求物流,原生AJAX会嵌套三层xhr.onreadystatechange,代码缩进占半屏,调试起来头都大。换成Promise封装后,把异步操作包装成“状态机”——要么成功(resolved)要么失败(rejected),后续操作用then链式调用,比如ajax(url1).then(res1 => ajax(url2)).then(res2 => …),逻辑一下就清晰了。我之前帮朋友改代码时,他的嵌套代码换成Promise后,调试时间从两小时降到了二十分钟,他当时拍着大腿说“早知道这么简单,之前何苦熬那么多夜”。

通用工具函数封装和Promise封装有什么区别?

Promise封装主要解决“回调地狱”的问题,让异步逻辑从“嵌套”变“线性”;而通用工具函数是在Promise基础上,把AJAX的共性逻辑抽得更全。比如通用函数能处理不同的数据格式(JSON、FormData、表单urlencoded),自定义headers,设置不同的超时时间,甚至上传文件都不用额外写代码。我自己项目里用通用函数时,之前每个请求要手动写的headers、数据格式处理,现在只要传参数就行,代码量砍了40%,特别省时间。

类封装适合什么样的项目用?

类封装更适合中大型项目,比如后台管理系统这种有几十个接口的场景。因为类能“保存状态”,比如设置baseURL(比如https://api.example.com)后,所有请求自动拼上这个前缀;还能加“拦截器”,在每个请求发出去前自动加token,或者响应回来后统一处理401错误(比如跳登录页)。我去年帮公司做后台系统时,一开始用通用函数,改token逻辑要改几十个请求,换成类封装后,只要在拦截器里加refresh token逻辑,所有请求自动续期,省了整整一周工作量。

Promise封装AJAX时,怎么处理超时和网络错误?

很简单,在Promise的逻辑里加超时和错误监听就行。比如先给xhr设置timeout(比如xhr.timeout=5000,默认5秒),然后监听xhr.ontimeout事件,用reject抛“请求超时,请重试”的错误;再监听xhr.onerror事件,抛“网络错误,请检查连接”的错误。这样后续用catch就能捕获这些错误,比如ajax(url).catch(err => console.log(err)),一眼就知道是超时还是网络问题。我朋友之前调试用户“点商品没反应”的问题,就是靠这个快速定位到是用户手机没网的原因。

原文链接:https://www.mayiym.com/47926.html,转载请注明出处。
0
显示验证码
没有账号?注册  忘记密码?

社交账号快速登录

微信扫一扫关注
如已关注,请回复“登录”二字获取验证码