新闻详情

新闻详情

首页 / 资讯中心 / 详情

从原生到 Promise:手写一个实用的 Ajax 封装指南

发布时间:2026/9/30 1:17:05来源:尧图网络
从原生到 Promise:手写一个实用的 Ajax 封装指南
做前端这些年和 Ajax 打交道的时间几乎占了日常开发的三分之一。你问我为什么因为页面上所有和服务器打交道的动作背后都站着这尊大神。原生 Ajax 写起来不难难的是每个项目、每个页面都复制粘贴那一套又臭又长的 XMLHttpRequest 代码今天这个接口要设置超时明天那个接口要处理特殊编码后天又冒出来一个统一错误弹窗的需求。如果不做代码封装光是这些重复劳动就能把人的热情磨光。我早期带项目时就见过一个页面里七八个 axios 风格的请求函数每个函数都在重复处理 loading、错误提示、状态码判断看到那场面才意识到Ajax 请求的 JavaScript 代码封装不是一个“有没有必要”的问题而是一个“早晚要做”的问题。这篇文章就从我实际改造一个后台管理系统的经历出发把 Ajax 请求封装这件事从设计思路到落地实现完整梳理一遍。目标读者是写过原生 Ajax、但还没系统整理过请求层的同学以及已经用 axios 但想自己搞一个小工具库的进阶选手。我会直接给出可用的代码也会把里面那些容易踩的坑一并说清楚。1. 为什么 Ajax 代码一定要封装1.1 原生 Ajax 的“重复体力活”如果你在项目中直接写 XMLHttpRequest大概率会遇到这样的场景每次请求都要重新声明一个XMLHttpRequest对象然后绑定onreadystatechange再判断readyState 4 status 200最后手动用JSON.parse解析返回字符串。一次两次还行次数多了你会发现代码里到处都是重复的结构而需要改动的地方又分散在各个页面。举个最简单的原生例子function getUser(id) { var xhr new XMLHttpRequest(); xhr.open(GET, /api/user?id id); xhr.send(); xhr.onreadystatechange function() { if (xhr.readyState 4) { if (xhr.status 200) { var data JSON.parse(xhr.responseText); renderUser(data); } else { alert(请求失败 xhr.status); } } }; }这段代码本身没有问题但如果你有十个这样的接口就要写十份几乎一样的逻辑。更麻烦的是当产品经理提出“所有接口请求失败时统一弹一个错误提示”时你要去每个地方改当要加“请求超时 10 秒自动中断”时你又要去每个地方改。原生 Ajax 不是不能写而是不适合在业务代码里大量裸写。1.2 一次封装能解决哪些问题封装的核心不是把代码变短而是把变化集中到一处。我个人的体会是一个好的 Ajax 封装至少能解决四类问题统一入口所有请求都走同一个函数调用方只需要关心url、type、data这几个业务参数。统一错误处理状态码异常、网络异常、超时都能在一个地方处理接口失败时不会出现各种风格的弹窗。统一请求配置全局的 baseURL、超时时间、请求头、loading 开关都可以通过配置控制而不是在业务代码里到处塞。方便扩展后续要加请求拦截、响应拦截、取消请求、重复提交校验改动只发生在封装层不影响业务代码。说白了封装就是把“造轮子”的过程集中一次把“用轮子”的成本降到最低。这也是为什么现在前端项目几乎离不开 axios 这种库的原因——它已经把上面这些事都替我们做完了。2. 封装前的设计思路与选型2.1 先想清楚 API 长什么样动手写代码之前我习惯先把“调用体验”定下来。只有先明确最终调用方式是怎样的才能倒推封装函数需要往外暴露什么接口。我最常用的是对象参数形式ajax({ url: /api/user/list, type: GET, params: { page: 1, size: 20 }, data: {}, timeout: 10000 }).then(function(res) { // 成功处理 }).catch(function(err) { // 失败处理 });之所以用对象参数而不是多个位置参数是因为对象参数可读性好、可扩展性强。以后要加headers、responseType、cancelToken这些配置时不用改函数签名直接在对象里加一个字段就行。当然也可暴露一些快捷方法比如get(url, params)、post(url, data)内部再转成统一的ajax调用这样可以满足不同人的习惯。另外我还会为一些高频场景做二次封装比如function getUsers(params) { return ajax.get(/api/user/list, params); }这样业务层面对的就是“语义化”的函数而不是一堆冷冰冰的 URL 字符串。2.2 参数设计约定优于配置参数越多维护成本越高所以封装时一定要有取舍。我把常用的配置项分为三类基础参数url、method、paramsURL 查询参数、data请求体数据、headers。控制参数timeout超时时间、responseType返回类型、withCredentials是否携带 Cookie。扩展参数onUploadProgress上传进度回调、cancelToken取消请求标识、beforeSend发送前拦截。这些参数并不是全部都要暴露我觉得最核心的就是url、method、params、data、timeout、headers这六个。其他参数按需添加没必要一开始就把功能堆满否则封装层本身就变成了一个复杂系统反而失去了轻量封装的意义。默认值也很关键。比如method默认GETtimeout默认0不超时headers默认空对象。这些默认值放在一个config对象里方便后续修改。2.3 用 Promise 还是回调早期 Ajax 封装常用回调风格ajax({ url: /api, success: function(data) {}, error: function(err) {} });这种写法在简单场景下没问题但一旦需要串行请求、并行请求回调嵌套就会变得很难看。所以我强推 Promise 风格。用 Promise 包裹XMLHttpRequest其实非常简单只需要把异步逻辑放进new Promise(function(resolve, reject) { ... })里成功走resolve失败走reject。好处是可以用then/catch链式处理也可以用async / await写出同步风格的代码async function loadData() { try { const data await ajax.get(/api/data); render(data); } catch (err) { console.error(err); } }如果你的项目需要兼容老浏览器Promise 本身可能不存在那时候引入一个es6-promisepolyfill 就够了。这个成本相比带来的代码收益非常划算。3. 手写一个可用的 Ajax 封装3.1 基础版基于 XMLHttpRequest 的封装先写一个最基础、但能直接用的版本。下面这段代码我习惯放在utils/request.js里项目所有请求都走它。// utils/request.js (function (global) { var DEFAULT_CONFIG { baseURL: , method: GET, timeout: 0, headers: {} }; function formatUrl(url, params) { if (!params) return url; var queryStr Object.keys(params) .filter(function (key) { return params[key] ! undefined params[key] ! null; }) .map(function (key) { return encodeURIComponent(key) encodeURIComponent(params[key]); }) .join(); return url (queryStr ? (url.indexOf(?) -1 ? : ?) queryStr : ); } function parseResponse(xhr) { var contentType xhr.getResponseHeader(Content-Type) || ; if (contentType.indexOf(application/json) -1) { try { return JSON.parse(xhr.responseText); } catch (e) { return xhr.responseText; } } return xhr.responseText; } function ajax(options) { var config {}; for (var key in DEFAULT_CONFIG) { config[key] DEFAULT_CONFIG[key]; } if (options) { for (var key in options) { config[key] options[key]; } } return new Promise(function (resolve, reject) { var xhr new XMLHttpRequest(); var method (config.method || GET).toUpperCase(); var url config.baseURL formatUrl(config.url, config.params); xhr.open(method, url, true); xhr.timeout config.timeout; // 设置请求头 var headers config.headers || {}; Object.keys(headers).forEach(function (key) { xhr.setRequestHeader(key, headers[key]); }); xhr.onreadystatechange function () { if (xhr.readyState ! 4) return; if (xhr.status 200 xhr.status 300 || xhr.status 304) { resolve(parseResponse(xhr)); } else { reject({ status: xhr.status, statusText: xhr.statusText, response: parseResponse(xhr) }); } }; xhr.onerror function (e) { reject({ type: error, message: 网络请求异常, event: e }); }; xhr.ontimeout function (e) { reject({ type: timeout, message: 请求超时, event: e }); }; var body null; if (method POST || method PUT || method PATCH) { var contentType headers[Content-Type] || ; if (contentType application/json) { body JSON.stringify(config.data || {}); } else if (contentType application/x-www-form-urlencoded) { body formatUrl(, config.data || {}).slice(1); } else if (config.data instanceof FormData) { body config.data; } else { body config.data || null; } } xhr.send(body); }); } // 暴露 get/post 方法 ajax.get function (url, params, options) { options options || {}; options.url url; options.method GET; options.params params; return ajax(options); }; ajax.post function (url, data, options) { options options || {}; options.url url; options.method POST; options.data data; return ajax(options); }; if (typeof module ! undefined module.exports) { module.exports ajax; } else { global.$http ajax; } })(window);这一段代码的核心逻辑很简单formatUrl负责拼接 URL 查询参数parseResponse负责根据响应头决定是否 JSON 解析ajax内部用 Promise 包裹 XHR 并统一处理成功和失败。有一个容易被忽略的点xhr.timeout属性设为0表示不超时设成大于0的毫秒数才生效。而且ontimeout事件必须在onreadystatechange之外单独绑定否则超时后还会触发 readyState 变化导致结果混乱。3.2 进阶版超时、取消与请求拦截基础版解决的是“能用”进阶版解决的是“好用”。实际项目里我最常遇到的需求有三个请求超时可控、重复提交可取消、请求前后可做统一拦截。先看超时直接给options加一个timeout字段并在 XHR 上设置即可。上面基础版已经包含了这里不再重复。重点说取消请求。取消请求对 XHR 来说最简单的方式就是调用xhr.abort()。但问题是同一个页面可能有多个请求同时进行怎么知道取消哪一个我常用的做法是给每个请求生成一个id用一个对象保存当前活跃的 XHR 引用需要取消时按id找到并调用abort()。var activeXhr {}; var requestSeq 0; function ajax(options) { var seq requestSeq; // ... 前面的配置、xhr 创建 ... activeXhr[seq] xhr; xhr.onreadystatechange function () { if (xhr.readyState ! 4) return; delete activeXhr[seq]; // 成功/失败处理 }; // 提供 cancel 方法 ajax.cancel function (seq) { if (seq activeXhr[seq]) { activeXhr[seq].abort(); delete activeXhr[seq]; } }; return promise; }这里seq也可以换成/api/xxx这种语义化标识方便在控制台排查。再来看请求拦截。拦截器在 axios 里很常见它的作用是在请求正式发出去之前统一修改配置在响应回来之后统一处理数据。我们自己的封装也可以加一个简单的中间件机制ajax.before function (fn) { ajax._before fn; }; ajax.after function (fn) { ajax._after fn; }; // 在 send 之前 if (typeof ajax._before function) { config ajax._before(config) || config; } // 在 resolve/reject 之前 if (typeof ajax._after function) { response ajax._after(response) || response; }这样做的好处是比如你想在每次请求前给 header 加一个 token只需要全局设置一次ajax.before(function (config) { config.headers config.headers || {}; config.headers[Authorization] localStorage.getItem(token); return config; });不需要改动任何业务代码。3.3 基于 fetch 的封装备选方案如果你负责的是一个纯现代浏览器项目没有 IE 兼容压力那么基于fetch封装会更轻量。fetch 本身就是 Promise 的代码结构可以更简洁function request(url, options) { return fetch(url, options).then(function (res) { if (!res.ok) { throw new Error(HTTP res.status); } return res.json(); }); }但 fetch 有两个坑需要注意第一它默认不会携带跨域 Cookie需要手动设置credentials: include第二它对 HTTP 状态码不会自动 reject即使请求返回 404、500 也只会 resolve必须自己在then里判断res.ok。所以我本人的习惯是项目统一用 XHR 封装除非是纯前端 demo 或者明确不需要兼容老环境的项目。4. 关键细节编码格式、参数处理与常见坑4.1 请求编码格式怎么设置很多人问 Ajax 请求怎么设置编码格式其实编码格式主要由请求体Content-Type和 URL 参数编码方式决定。先说 GET 请求。GET 参数是拼在 URL 后面的浏览器会自动对 URL 做百分号编码但中文、特殊字符在不同浏览器上可能表现不一致所以建议在拼接前用encodeURIComponent手动编码。我上面formatUrl里就是这么做的这样不管参数是中文还是空格都能保证后端拿到的是正确值。再说 POST 请求。常用三种编码格式application/x-www-form-urlencoded表单提交格式参数格式是key1value1key2value2后端通常用request.form接收。application/jsonJSON 字符串格式最常用后端用request.get_json()或RequestBody接收。multipart/form-data文件上传格式通常用FormData对象作为请求体。我的封装里根据headers[Content-Type]来决定body的序列化方式。这块新手最容易踩坑调用ajax.post时什么都不设置默认Content-Type为空于是config.data直接以 plain text 格式发送后端解析不了参数。我的建议是在服务器统一接收 JSON 的情况下直接在ajax.post里默认设置Content-Type: application/json除非业务明确要传表单。4.2 参数赋值与序列化技巧“给 ajax 请求参数赋值”这件事听起来很简单但实际开发中经常要处理嵌套对象、数组、空值过滤。比如一个筛选条件var formData { keyword: hello, category: 0, tags: [js, ajax], sort: , page: 1, size: 10 };如果直接把sort: 传给后端后端可能当成一个无意义的空字符串参数如果直接把category: 0漏掉后端又可能拿不到 0 这个值。所以我封装时加了一个过滤逻辑只保留undefined和null不丢弃0和空字符串具体过滤规则要看业务需要。序列化嵌套对象时我习惯用递归处理function serialize(data, prefix) { if (!data || typeof data ! object) return data; var result {}; Object.keys(data).forEach(function (key) { var fullKey prefix ? prefix [ key ] : key; if (Array.isArray(data[key])) { data[key].forEach(function (item, index) { result[fullKey []] item; }); } else if (typeof data[key] object) { Object.assign(result, serialize(data[key], fullKey)); } else { result[fullKey] data[key]; } }); return result; }这个函数在表单提交、URL query 拼接时都能用。需要注意的是序列化结果里 key 可能带[]或[child]后端框架一般都能正确解析为数组或对象。顺带说一句热词里那个“js 判断字符串是否包含”我经常在参数校验和 URL 判断中用到。比如判断一个地址是不是外链function isExternal(url) { return url.indexOf(http://) 0 || url.indexOf(https://) 0; }如果你的环境支持 ES6用String.prototype.includes更简洁function isExternal(url) { return url.includes(http://) || url.includes(https://); }这种工具函数虽然小但能和 Ajax 封装配合得很好比如在请求拦截里校验 URL 合法性、在参数序列化时过滤非法字段。4.3 状态码判断与返回数据类型后端接口的状态码判断有两种层次HTTP 的状态码以及业务层自己定义的 code。我的封装里会先把 HTTP 状态码判断处理好然后预留一个地方给业务层继续判断业务 code。XHR 的status在 2xx 和 304 时都可以视为成功这一点常被忽略。有些文件接口会返回 304 Not Modified 表示缓存有效如果你只判断 200 300就会把 304 当作失败。我的基础版里特意加上了|| xhr.status 304。返回数据类型的处理也很关键。封装里我通过响应的Content-Type来决定是否用JSON.parse。如果后端返回的是字符串、二进制流就不能强行 JSON.parse。即使判断为application/json也建议用try/catch包一层防止后端给了空字符串。顺带把“js json 转换成数组”一点就透。很多时候后端返回的是{ code: 0, data: { list: [1, 2, 3] } }要在前端拿到数组直接var list response.data.list;如果是 JSON 字符串形式先JSON.parse(str)再取字段。本质上不是“json 转换成数组”而是“从 json 结构中取出数组字段”。5. 实际项目中的组合应用5.1 配合 ECharts 做动态图表“原生 js、jquery、ajax、echarts 结合制作网页”是一个很典型的场景。我在很早的项目里就用过把封装好的ajax.get与 ECharts 联合起来做数据可视化。操作步骤一般是这样页面加载时调用ajax.get(/api/trend, { start: 2025-01-01, end: 2025-01-31 })。拿到响应后提取dates和values数组。初始化 ECharts 实例并setOption渲染图表。切换筛选条件时重新调用同一个函数更新图表数据。关键点在于请求函数返回 Promise可以在then里直接写渲染逻辑不用回调嵌套。如果请求失败配合封装的统一错误处理页面不会出现一堆红叉。5.2 与 jQuery 混用时的注意点项目里如果已经引入了 jQuery很多人会直接用$.ajax。但有些场景可能不想引入 jQuery或者团队想逐步迁移这时候自己的轻量封装就是一个过渡方案。混用时有两点需要注意返回值类型jQuery 的$.ajax返回的是 jqXHR 对象它本身是 promise-like 的但不是标准 Promise。如果和async/await混用语法没问题但行为上可能有些差异。自己封装的返回标准 Promise兼容性更好。全局配置如果项目里同时存在$.ajax和自定义$http两套配置、两套错误处理并存容易混乱。建议定一个约定新代码统一走$http老代码慢慢迁移。5.3 开源代码封装可以借鉴什么说到“开源代码封装”前端最值得参考的就是 axios。它并不是什么高深莫测的库核心也还是对 XHR 或 fetch 的封装但它把“拦截器”、“取消请求”、“默认配置”、“适配器”这些设计拆得非常清楚。我自己在实现轻量封装时就借鉴了 axios 的拦截器模式但只在内部保留了before和after两个钩子并没有做成完整链路。因为对于中小型项目过多的抽象反而增加维护成本。开源库的思路可以参考但要结合自己的项目体量做减法。如果你要写一个自己的开源封装建议最起码把配套文档写好README 里给出快速开始和完整配置项说明。代码本身的可用性是一方面别人能看懂、能改才是开源协作的核心价值。6. 常见问题与排查技巧实录6.1 中文乱码问题中文乱码是最常见的问题之一。它通常不是前端 CSS 或页面编码导致的而是请求或响应过程中的字符集不一致。排查步骤我一般这样走先确认前端页面本身是 UTF-8 编码meta charsetUTF-8要写在 head 最前面。再确认请求头中的Content-Type是否带charsetUTF-8。如果是 JSON 格式通常设置Content-Type: application/json; charsetUTF-8。最后看后端是否按 UTF-8 解析请求体。如果后端默认 ISO-8859-1那前端怎么改都白搭只能在服务端过滤器和连接字符串层面统一编码。我的封装里POST JSON 时会默认设置Content-Type: application/json; charsetUTF-8这样能省掉不少麻烦。6.2 请求重复发送与重复提交用户连续点击按钮同一个请求被触发多次这是用 Ajax 最典型的问题。我常用的解决办法是“请求锁”var isLoading false; function submitData(data) { if (isLoading) return; isLoading true; ajax.post(/api/save, data) .then(function () { // 成功逻辑 }) .finally(function () { isLoading false; }); }这是最直观的防抖方式。如果希望做到全局级别可以在封装层加一个 key 维度的“进行中”标记比如指定一个requestKey在请求发出时记录请求完成时清除重复请求直接忽略或取消上一个。6.3 跨域问题跨域是 Ajax 绕不开的话题。如果你在控制台看到No Access-Control-Allow-Origin header is present说明后端没有开启 CORS。常见解决方案有三种后端开启 CORS属于最标准的方式在响应头加Access-Control-Allow-Origin。本地开发用代理前端框架的 devServer 可以配置 proxy把/api转发到后端地址。JSONP 兜底老项目里用过但它只支持 GET而且需要后端配合返回callback包裹的脚本。我的建议是优先 CORS开发环境用代理。不要在业务层硬拼跨域那样只会越搞越复杂。6.4 IE 兼容与缓存问题老浏览器项目里Ajax 有一个非常经典的坑GET 请求结果被浏览器缓存导致接口更新后页面数据不变。解决办法是在 URL 上追加一个时间戳function bustCache(url) { var separator url.indexOf(?) -1 ? : ?; return url separator _ts new Date().getTime(); }我的封装里可以在formatUrl后自动判断是否追加这个参数但默认建议关闭避免影响 CDN 缓存。另外 IE9 及以下不支持XMLHttpRequest要用ActiveXObject(Microsoft.XMLHTTP)做兼容现代项目基本不用考虑这个但如果写通用工具库还是要留意。还有一个 IE 的坑默认最多只能对同一个域名发起 6 个并发请求超出部分会排队。如果你在页面里同时触发多个图片上传和接口请求有可能出现某个请求长时间不返回的假象。这时候要主动控制并发数量或者用队列方式逐个发送。7. 写到最后一点个人体会项目里的 Ajax 封装不是越大越好而是越顺手越好。我从最开始复制粘贴原生代码到写一个又长又复杂的请求库再到现在收敛成一个只有几十行、但刚好满足项目需求的小工具整个过程最大的体会就是封装的边界应该根据团队习惯和业务场景来定。你可以在封装里预留拦截器、取消请求、统一错误处理这些能力但不要在业务代码还没复杂到那个程度时就强行引入一整套重量级抽象。最后再贡献一个非常实用的小技巧调试 Ajax 封装时可以在before拦截器里把当前请求的 method、url、params、data 全部console.log出来接口联调阶段这个习惯能帮你省下一大半排查时间。所有请求都打同一个日志前缀比如[request]控制台一搜就全出来了。这个不涉及任何黑魔法但确实是我每次在项目中落地封装后觉得最值回票价的一个操作。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

开源智慧能源管理平台:架构、采集、存储与预测全攻略 2026/9/30 3:01:02

开源智慧能源管理平台:架构、采集、存储与预测全攻略

看到"开源赋能智慧能源管理"这个标题,我得先说一句:能源行业在很多人印象里是西门子、施耐德这些闭源大厂的地盘,动辄几十万的授权费,中小型园区和制造企业根本吃不消。但实际上,这几年开源生态在能源领域已…

阅读更多 →
linux ps -A|grep gate命令详解:进程排查与端口冲突实战 2026/9/30 3:01:02

linux ps -A|grep gate命令详解:进程排查与端口冲突实战

“gate”这个字符串,在 Linux 服务器上出现的频率比想象中高得多。可能是一个叫 gateway 的服务进程,可能是某个 Java 服务的 gate 线程,也可能是你根本想不起来从哪冒出来的中间件残留。我最早敲下ps -A|grep gate这个组合,是因为…

阅读更多 →
从 Date 到 Temporal:JavaScript 日期时间处理的全新实践指南 2026/9/30 3:01:02

从 Date 到 Temporal:JavaScript 日期时间处理的全新实践指南

如果你写前端写过任何涉及时间的功能,应该会同意一个判断:JavaScript 的 Date 对象,是整个语言里最让人纠结的部分之一。它名义上是“日期时间对象”,实际却把时间戳、字符串解析、时区换算、日历计算这些本该独立的问题全搅在同一…

阅读更多 →
七夕情人节HTML代码开发教程:从docx到可发布页面的完整指南 2026/9/30 3:01:02

七夕情人节HTML代码开发教程:从docx到可发布页面的完整指南

简介:这是一份面向前端初学者与网页爱好者的七夕情人节主题HTML代码开发教程文档,围绕如何用HTML与CSS制作浪漫表白网页展开,适合零基础或刚接触网页布局的读者快速上手节日互动页面。压缩包内共1个docx文件,约18KB,以…

阅读更多 →
质因数分解与差分数组:蓝桥杯选素数题全解析 2026/9/30 3:01:02

质因数分解与差分数组:蓝桥杯选素数题全解析

聊一道蓝桥杯国赛A组的题,P8795 选素数。这题标签写的是“普及”,看上去难度不高,但真做起来,会发现它把数论和基础数据结构结合得非常紧密。题目绕不开两个点:一个是质因数分解,另一个是差分数组。前者负责…

阅读更多 →
BitLocker全盘加密实战:从原理、TPM恢复到移动硬盘加密完整指南 2026/9/30 3:00:55

BitLocker全盘加密实战:从原理、TPM恢复到移动硬盘加密完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉