1.0.2 • Published 2 years ago

gl-ajax v1.0.2

Weekly downloads
-
License
UNLICENSED
Repository
-
Last release
2 years ago

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 即可。

  • 以下七种情况会产生异常

    1. 因配置出错、JS脚本出错等原因,请求未能发出(错误码为'error');
    2. 请求已发出,但客户端在指定的超时时间内未接收到服务器响应、或因网络异常、页面被刷新等原因未能接收到响应(错误码为'timeout');
    3. 请求已发出,但接收到服务器返回的非 2xx 的 HTTP 响应码(错误码为 HTTP 响应码);
    4. HTTP 响应码为 2xx,但响应数据中的 codeKey 的值与 succCode 不符,即业务处理未成功(错误码为响应数据中 codeKey 的值);
    5. 因服务器返回的数据格式与期望不符,或在处理响应结果时出现其他异常,导致解析失败(错误码为'error');
    6. 客户端主动取消了请求,且 cancelRaiseError 参数设为了 true(错误码为'abort');
    7. 在 requestFilter 中通过 res.reject() 主动返回了异常,阻止了发送请求(错误码为'error')。
  • 如果要处理超时,需注意区分几种情况:一种是客户端请求超时(上述第 2 种),是指客户端发出请求后没有接收到服务器的响应,此时可能由于网络原因,服务器根本没有收到请求,也可能服务器已经收到了请求,但是处理时间过久没有及时返回,还可能是服务器已输出了响应数据,但由于网络等原因并未抵达客户端;当响应尚未返回时页面被刷新或跳转也可能会触发此异常。另一种情况是服务器处理超时,是因为服务器端也做了超时控制(一般是在网关处),如果服务器接收到请求后,内部处理时间过久,就返回一个前后端接口约定的代表服务器超时的 HTTP 响应码或业务返回码,此时产生的异常其实属于上述第 3 种或第 4 种情况。还有一种服务器登录会话超时,在客户端与服务器长时间未进行交互,导致 session 或 token 失效后发生,也属于上述第 3 种或第 4 种情况,需根据与后台接口约定的规则处理。

  • 无论服务器 format 选项(代表服务器实际返回的数据格式)中配置的 codeKeymsgKey 是什么,在 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"
}
1.0.2

2 years ago

1.0.1

3 years ago

1.0.0

3 years ago