gl-ajax v1.0.2
Ajax 客户端库
支持统一管理服务器和接口地址、数据格式的 Ajax 客户端库。
安装
npm install gl-ajax
用法
选择Ajax请求库
此库并不直接处理底层 Ajax 请求,而是在其他 Ajax 请求库之上进行封装,支持与以下常用 Ajax 请求库结合使用:
- axios
- jQuery.ajax(尚未实现)
- fetch(尚未实现)
针对每一种请求库,都提供了两种模块格式,用于不同的引用方式
- esm 格式:使用 ES6 的 import 语句导入
- umd 格式:使用 CommonJS 或 AMD 的 require 语句导入,或通过 script 标签引用(全局对象)
ES6 模块引用示例:
import Ajax from '@yondervision/ajax/dist/ajax-jquery.es';
//或
import Ajax from '@yondervision/ajax/dist/ajax-axios.es';
//或
import Ajax from '@yondervision/ajax/dist/ajax-fetch.es';
- CommonJS 模块引用示例:
const Ajax = require('@yondervision/ajax/dist/ajax-jquery');
//或
const Ajax = require('@yondervision/ajax/dist/ajax-axios');
//或
const Ajax = require('@yondervision/ajax/dist/ajax-fetch');
- script 标签引用示例(非模块化开发):
<!-- 需要将引用的 js 文件放在 http 可访问的目录下,输出全局对象 Ajax -->
<script src="/lib/ajax-jquery.js"></script>
<!-- 或 -->
<script src="/lib/ajax-axios.js"></script>
<!-- 或 -->
<script src="/lib/ajax-fetch.js"></script>
浏览器兼容性
- 支持 Chrome 和 Firefox 的最新版本
- 支持 IE 10 和 IE 11,在 IE 中使用需要先引入 polyfill.min.js(包括 Promise、URL、String.prototype.includes、regenerator-runtime)
初始化
var ajax = new Ajax({
server: {}, //服务器定义
api: {}, //接口定义
}, lib); //底层 Ajax 请求库对象(通过 script 标签引用时可省略)
服务器定义
- 如果只需要访问唯一的一个服务器,可直接在
server
选项中设置服务器参数
var ajax = new Ajax({
server: {
//基础URL,所有接口URL都相对于此地址,缺省为"/"
baseUrl: 'http://10.22.11.10/someApi/',
//默认请求方法,缺省为"POST"
method: 'GET',
//默认请求超时时间,单位毫秒,如果请求超过该时间后未得到响应,则会自动取消,缺省为0(代表不设置超时)
timeout: 0,
//服务器响应数据格式
format: {
//返回码字段名,如果设为false(布尔值),将只使用HTTP响应码判断成功
codeKey: 'returnCode',
//返回信息字段名
msgKey: 'message',
//返回数据字段名,如果直接在首层返回数据可设为null
dataKey: 'returnData',
//代表成功的返回码
succCode: 0,
},
//返回给调用代码的响应数据格式(建议设置 msgKey 为 'message',以便统一处理各种异常)
resFormat: {
//格式同上
},
},
api: {},
});
- 如果需要访问多个服务器,可以在
server
选项中分别设置每个服务器的参数,key 为任意设置的服务器 ID,在接口参数配置中使用
var ajax = new Ajax({
server: {
server1: {
baseUrl: 'http://10.22.11.10/someApi/',
method: 'GET',
format: {
codeKey: 'returnCode',
msgKey: 'message',
dataKey: 'returnData',
succCode: 0,
},
},
server2: {
baseUrl: 'http://10.22.11.20/otherApi/',
method: 'POST',
format: {
codeKey: 'code',
msgKey: 'msg',
dataKey: 'data',
succCode: '000000',
},
}
},
api: {},
});
- 在创建了 Ajax 对象之后,还可以通过
setServer(config, serverId)
方法动态添加或修改服务器配置信息。该方法一次只能添加或修改一个服务器的信息,如果一共只有一个未指定 ID 的服务器,可省略 serverId 参数。
var ajax = new Ajax({
server: {
server1: { baseUrl: 'http://10.22.11.1:9080/api/', method: 'POST' },
server2: { baseUrl: 'http://10.22.11.1:9081/api/', format: { msgKey: 'msg', succCode: '000' }},
}
});
//修改现有服务器,指定的参数会被新的值覆盖,未指定的参数会继承原有的值
ajax.setServer({ baseUrl: 'http://11.22.33.44/new/', format: { succCode: '999' }}, 'server2');
//增加新的服务器
ajax.setServer({ baseUrl: 'http://10.22.11.1:9083/', method: 'GET' }, 'server3');
/* 以上执行完毕后,最终服务器配置信息如下:
{
server1: { baseUrl: 'http://10.22.11.1:9080/api/', method: 'POST' },
server2: { baseUrl: 'http://11.22.33.44/new/', format: { msgKey: 'msg', succCode: '999' }},
server3: { baseUrl: 'http://10.22.11.1:9083/', method: 'GET' },
}
*/
接口定义
- 注意:为了方便易用,在发送请求时只需要提供接口 ID,无需指定服务器。所以,如果定义了多个服务器,需要保证每个接口 ID 在所有服务器中都是唯一的,否则后定义的接口会覆盖先定义的接口。
- 如果只有一个服务器,可以直接在
api
选项中指定接口配置
var ajax = new Ajax({
server: {
//省略……
},
api: {
//查询所有用户列表
listUser: 'GET user/list',
//添加用户
addUser: 'POST user',
//删除用户
delUser: 'DELETE user/:id',
//修改用户信息
editUser: 'PUT user/:id',
//……
},
});
- 如果定义了多个服务器,可通过
setApi()
方法分别给每个服务器添加接口定义,第二个参数是服务器 ID
var ajax = new Ajax({
server: {
//组织机构服务器配置
orgServer: {/* 省略…… */},
//产品信息服务器配置
prdServer: {/* 省略…… */},
}
});
//组织机构服务接口配置
ajax.setApi({
listUser: 'GET user/list',
addUser: 'POST user',
delUser: 'DELETE user/:id',
editUser: 'PUT user/:id',
//……
}, 'orgServer');
//产品信息服务接口配置
ajax.setApi({
listProduct: 'GET product/list',
addProduct: 'POST product',
delProduct: 'DELETE product/:id',
editProduct: 'PUT product/:id',
//……
}, 'prdServer');
- 只有一个服务器的时候也可以使用
setApi()
定义接口,此时不需要传第二个参数
接口定义格式
- 可以使用字符串
"请求方法 接口地址"
来定义,例如:
listUser: 'GET user/list',
- 可以省略
请求方法
部分,将使用服务器定义中method
选项指定的默认请求方法,例如:
listUser: 'user/list',
- 也可以使用对象方式来定义接口,
url
是必需的,method
可以省略, 例如:
listUser: {
method: 'GET',
url: 'user/list',
},
- 使用对象方式定义接口的好处是可以添加静态上传字段,其典型应用场景是,后台接口要求必须固定提交某些字段,但这些字段与业务逻辑无关,写在业务代码中的请求上传参数里不合适,就可以定义在接口的
key
参数里,它们会在每次请求该接口时自动提交,这样业务代码里就无需考虑这些字段了,例如:
listUser: {
method: 'GET',
url: 'user/list',
key: {
channel: 'web',
transCode: '142857',
}
}
- 接口
key
参数的另一个典型应用场景是,多个不同功能的后台接口,可能使用同一个接口地址,只是通过某个标志字段来区分。如果只定义成一个接口,违背了接口设计的单一职责原则,如果将来后台改成了多个接口,还不得不修改业务代码。这种情况下,就可以将每个功能分别定义成不同的接口,通过key
参数来区分功能,业务代码无需考虑传哪个标志字段,将来如果后台改成了多个接口,也无需修改业务代码,例如:
listUser: {
method: 'GET',
url: 'info/list',
key: {
infoType: 'user',
}
},
listProduct: {
method: 'GET',
url: 'info/list',
key: {
infoType: 'product',
}
},
- 接口 url 中可以包含一个或多个冒号开头的变量,在发送请求时,变量将被上传数据中的同名值替换
{
listUser: 'GET user/:userId',
}
baseUrl 一般包含服务器地址,例如
baseUrl: 'http://10.22.11.10/someApi/'
,如果前端页面与后端接口部署于同一个应用,同域调用,也可以省略服务器部分,例如baseUrl: '/someApi/'
。如果未提供 baseUrl 参数,其缺省值为baseUrl: '/'
。如果 url 写成相对路径(即不以 / 开头),是相对于服务器 baseUrl 的路径。可以将所有 url 开头部分的相同路径写在 baseUrl 里(如下例中的 /hello/world 部分),如果以后这部分发生变化,可以在 baseUrl 里统一调整,无需分别调整每一个接口的 url。url 也可以写成绝对路径(以 / 开头),此时将忽略 baseUrl 的设置,适用于个别特殊的接口地址。例如:
{
server: {
baseUrl: 'http://10.22.11.10/hello/world/',
},
api: {
api1: 'GET aaa', //相对路径,请求地址:http://10.22.11.10/hello/world/aaa
api2: 'GET aaa/bbb', //相对路径,请求地址:http://10.22.11.10/hello/world/aaa/bbb
api3: 'GET ../ccc', //相对路径,请求地址:http://10.22.11.10/hello/ccc
api4: 'GET ../../ddd', //相对路径,请求地址:http://10.22.11.10/ddd
api5: 'GET /eee', //绝对路径,请求地址:http://10.22.11.10/eee
api6: 'GET /eee/fff', //绝对路径,请求地址:http://10.22.11.10/eee/fff
}
}
发送请求前后的处理
- 前后处理函数可用于发送请求前后的公共处理,如格式化、加解密、跨域处理、会话管理、日志监控等
- 前后处理函数都是异步函数,建议通过 async/await 语法来简化逻辑
- 前后处理函数中传递的 options 对象和返回对象的格式依赖于底层请求库,并不统一,需要针对特定请求库进行处理
- 同一个请求的前后处理函数中会传递相同且唯一的 requestId,它包括两部分,前半部分是 uuid 以保证唯一,后半部分是递增数字,可用来判定请求顺序
var ajax = new Ajax({
/**
* 前处理函数,在每个请求发送前执行,可以改变请求的数据、参数
* @param {Object} data 上传数据
* @param {Object} options 请求参数对象(格式由请求库决定)
* @param {Object} api 接口配置参数
* @param {Object} formData 上传数据的FormData对象(有上传文件时)
* @param {String} requestId 请求ID,与responseFilter中同一个请求的requestId相同
* @param {Object} res 包含 resove 和 reject 方法,具体用法见下面示例
* @returns {Promise} 上传数据(格式由请求库及请求方式决定)
*/
requestFilter: async function (data, options, api, formData, requestId, res) {
//返回修改后的上传数据,如果有上传文件,且需要在这里统一修改上传数据时,需要直接修改formData对象
return data;
//下面两种用法可以阻止 ajax.request() 的默认请求,通过自定义请求,或构造模拟数据,直接返回结果或抛出异常
//阻止继续发出请求,直接返回替代结果,由 ajax.request(...).then(res => {}) 获取
return res.resolve({ hello: 'world' });
//阻止继续发出请求,直接抛出异常,由 ajax.request(...).catch(err => {}) 捕获
return res.reject(new Error('request error'));
},
/**
* 后处理函数,在服务器返回请求后执行,可以改变返回给回调函数的数据
* @param {Object} res 响应数据(格式由请求库决定)
* @param {Object} xhr 请求xhr对象(仅jQuery库可用)
* @param {Object} api 接口配置参数
* @param {String} requestId 请求ID,与requestFilter中同一个请求的requestId相同
* @returns {Promise} 响应数据(格式由请求库决定)
*/
responseFilter: async function (res, xhr, api, requestId) {
return res;
},
/**
* 异常处理函数,在发生请求或响应异常后执行,可以对返回的异常对象属性值按需修改
* @param {Object} err 异常对象(统一为 {code, msg} 或 resFormat 配置的格式)
* @param {Object} api 接口配置参数
* @param {String} requestId 请求ID,与requestFilter中同一个请求的requestId相同
* @param {Object} requestOptions 请求选项,即 ajax.request() 的第三个参数
* @returns {Object} 异常对象,如果返回 null,将不会继续触发请求方法的 catch 事件
*/
errorFilter: async function (err, api, requestId, requestOptions) {
return err;
},
});
- 使用示例:
//例1:针对jquery库,使用json格式提交上传数据
requestFilter: async function (data, options, api) {
options.contentType = 'application/json; charset=UTF-8';
options.processData = false;
return JSON.stringify(data);
}
//例2:针对axios库,使用json格式提交上传数据
requestFilter: async function (data, options, api) {
options.headers = {
'Content-Type': 'application/json; charset=UTF-8',
};
return data;
}
//例3:针对axios库,在获取响应数据后记录登录token
responseFilter: async function (res) {
if (res.headers.authorization) {
window.sessionStorage.setItem('token', res.headers.authorization);
}
return res;
},
发送请求
ajax.request(apiId, data, options)
- 第一个参数为接口 ID
- 第二个参数为上传数据(若没有可省略),可以是 Plain Object 或表单 DOM 对象(提交的字段需设置 name 属性)
- 第三个参数为其他选项(均可省略,详见以下说明)
- 返回 Promise 对象
//options
{
//请求超时时间,单位毫秒,此处的设置将覆盖服务器设置中的默认值
timeout: 3000,
//取消请求令牌(用法详见后面取消请求部分)
cancelToken: ajax.cancelToken,
//请求被取消后是否抛出异常
//缺省为 false,即请求被取消后不需要做任何处理,直接忽略
//如果设为 true,可以在 catch 中捕获到 err.code === 'abort' 的异常,可以有针对性地处理后续逻辑
cancelRaiseError: true,
//是否上传文件,如果在 data 部分添加了 File 或 FileList 对象,需要开启此参数
//如果 data 直接提交表单 DOM 对象,则不需要设置此参数
uploadFile: true,
//上传进度事件(percent:0~100的百分比数值,loaded:已处理字节数,total:总字节数)
onUploadProgress: (percent, loaded, total) => { console.log(`已上传 ${percent}%`); },
//下载进度事件(percent:0~100的百分比数值,loaded:已处理字节数,total:总字节数)
onDownloadProgress: (percent, loaded, total) => { console.log(`已下载 ${percent}%`); },
}
如果 server 配置的 dataKey 非空,resolve 的数据只包含 dataKey 下的内容;否则是服务器返回的完整数据
示例:
ajax.request('addUser', {
name: '张三',
age: 18,
}).then(res => {
alert(`添加用户成功,新用户的ID为:${res.userId}`);
}).catch(err => {
alert(`添加用户失败:${err.code} ${err.msg}`);
});
异常处理
通过
ajax.request(...).catch(err => {...})
,可以对单次请求进行异常处理。当请求发生异常时,无论是否进行了 catch,无论取消请求时是否设置了 cancelRaiseError 参数,都会始终执行 errorFilter 函数,所以可以在 errorFilter 函数里对所有请求的异常情况进行统一处理。errorFilter 函数应返回原始的异常对象,或返回经过处理的新的异常对象,这个异常对象将继续被发起请求的代码
ajax.request().catch()
处理。如果异常被 errorFilter 中的公共代码处理完以后,不需要再被发起请求的代码捕获并处理,让 errorFilter 返回 null 即可。以下七种情况会产生异常
- 因配置出错、JS脚本出错等原因,请求未能发出(错误码为
'error'
); - 请求已发出,但客户端在指定的超时时间内未接收到服务器响应、或因网络异常、页面被刷新等原因未能接收到响应(错误码为
'timeout'
); - 请求已发出,但接收到服务器返回的非 2xx 的 HTTP 响应码(错误码为 HTTP 响应码);
- HTTP 响应码为 2xx,但响应数据中的 codeKey 的值与 succCode 不符,即业务处理未成功(错误码为响应数据中 codeKey 的值);
- 因服务器返回的数据格式与期望不符,或在处理响应结果时出现其他异常,导致解析失败(错误码为
'error'
); - 客户端主动取消了请求,且 cancelRaiseError 参数设为了 true(错误码为
'abort'
); - 在 requestFilter 中通过 res.reject() 主动返回了异常,阻止了发送请求(错误码为
'error'
)。
- 因配置出错、JS脚本出错等原因,请求未能发出(错误码为
如果要处理超时,需注意区分几种情况:一种是
客户端请求超时
(上述第 2 种),是指客户端发出请求后没有接收到服务器的响应,此时可能由于网络原因,服务器根本没有收到请求,也可能服务器已经收到了请求,但是处理时间过久没有及时返回,还可能是服务器已输出了响应数据,但由于网络等原因并未抵达客户端;当响应尚未返回时页面被刷新或跳转也可能会触发此异常。另一种情况是服务器处理超时
,是因为服务器端也做了超时控制(一般是在网关处),如果服务器接收到请求后,内部处理时间过久,就返回一个前后端接口约定的代表服务器超时的 HTTP 响应码或业务返回码,此时产生的异常其实属于上述第 3 种或第 4 种情况。还有一种服务器登录会话超时
,在客户端与服务器长时间未进行交互,导致 session 或 token 失效后发生,也属于上述第 3 种或第 4 种情况,需根据与后台接口约定的规则处理。无论服务器
format
选项(代表服务器实际返回的数据格式)中配置的codeKey
和msgKey
是什么,在 catch 到的 Error 对象上均被统一为resFormat
选项(代表业务代码中获取到的数据格式)中配置的键名(缺省为'code'
和'msg'
),这样如果将来服务器返回数据格式发生变化,业务代码也无需修改。Error 对象中可能还会包含服务器返回的原始响应数据,其键名由
resFormat.dataKey
配置决定,如果该配置项为空,则键名固定为data
。只有在接收到服务器响应数据之后出现的异常,Error 对象中才会包含这些数据,如果是发送请求之前出现的异常,或请求被取消,或超时未接收到响应,Error 对象中都不会包含响应数据。
上传文件
- 文件需要通过
<input type="file">
元素来选择 - 添加 multiple 属性
<input type="file" multiple>
,可以允许同时选择多个文件 - 添加 accept 属性,可以限制可选择的文件类型,例如:
<input type="file" accept="image/png">
(只接受 png 图片)<input type="file" accept=".png">
(同上)<input type="file" accept="image/png,image/jpeg">
(接受 png 和 jpeg 图片)<input type="file" accept=".png,.jpg,.jpeg">
(同上)<input type="file" accept="image/*">
(接受任意图片类型)<input type="file" accept=".doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document">
(接受 office 文档类型)
- 通过 input.files 可以获取已选择的文件(FileList 对象,通过下标可以选择每一个 File 对象)
- 可以直接将 File 或 FileList 对象放入上传文件,同时也可以传入其他文本字段值,这种用法需要设置
uploadFile: true
选项,例如:
ajax.request('uploadApi', {
file1: form.file1.files[0],
fileDesc1: form.fileDesc1.value,
file2: form.file2.files,
fileDesc2: form.fileDesc2.value,
}, {uploadFile: true}).then(res => {
alert(`上传文件成功:${JSON.stringify(res)}`);
}).catch(err => {
alert(`上传文件失败:${err.code} ${err.msg}`)
});
- 也可以直接传入整个表单的 DOM 对象,表单中带有 name 属性的字段均会被提交,这种用法不需要设置
uploadFile
选项,例如:
document.forms[0].onsubmit = function (event) {
event.preventDefault();
ajax.request('uploadApi', event.target).then(function (res) {
alert(`上传文件成功:${JSON.stringify(res)}`);
}, function (res) {
alert(`上传文件失败:${err.code} ${err.msg}`)
});
};
取消请求
- 先用
ajax.cancelToken
申请取消请求令牌,通过ajax.request()
的第三个参数的cancelToken
选项传入,然后在需要取消的时候执行ajax.cancel(token)
- 如果设置了
cancelRaiseError: true
,则取消请求后将抛出异常,否则将被忽略,不会执行任何回调
//申请取消请求令牌
var cancelToken = ajax.cancelToken;
ajax.request('testApi', {}, {
cancelToken,
cancelRaiseError: true,
}).then(res => {
alert('请求成功返回:' + JSON.stringify(res));
}).catch(err => {
if (err.code === 'abort') {
alert('请求被取消了');
}
else {
alert(`请求发生异常:${err.code} ${err.msg}`);
}
});
//这里模拟一段时间后取消请求
setTimeout(function () {
ajax.cancel(cancelToken);
}, 1000);
取消全部请求
- 无需申请取消令牌,直接调用
ajax.cancelAll();
即可取消未完成的全部请求 - 此方法一般用在销毁组件、关闭对话框、前端路由切换等事件发生时,防止未完成的请求在不恰当的时候触发回调函数
//针对axios库,直接调用即可
ajax.cancelAll();
//针对jQuery库,需要在请求发起后才能取消。如果在发起请求的同一个同步过程中执行取消动作,
//可能会取消失败,通过 setTimeout 0 将取消动作延迟到下一个事件循环可以解决这个问题
setTimeout(function () {
ajax.cancelAll();
}, 0);
获取接口信息
- 根据接口ID查询接口和服务器配置信息
let config = ajax.getApi(apiId);
//输出结果格式:
{
//请求方法
"method": "POST",
//请求地址(已根据baseUrl计算出完整地址,但url变量不会被替换)
"url": "http://127.0.0.1:9666/demoApi/hello/world/:ccc",
//静态上传字段值
"key": {
"xxx": 888,
"yyy": 999
},
//服务器配置
"server": {
"baseUrl": "/demoApi/",
"method": "GET",
"timeout": 0,
"format": {
"codeKey": "returnCode",
"msgKey": "message",
"dataKey": "returnData",
"succCode": 0
}
},
//接口ID
"apiId": "hello",
//服务器ID
"serverId": "demo"
}