@mdui/jq
Version:
拥有和 jQuery 相似 API 的轻量级 JavaScript 工具库
221 lines (220 loc) • 8.81 kB
JavaScript
import { getDocument, getWindow } from 'ssr-window';
import { $ } from '../$.js';
import '../methods/trigger.js';
import { globalOptions, ajaxStart, ajaxSuccess, ajaxError, ajaxComplete, isQueryStringData, isCrossDomain, isHttpStatusSuccess, appendQuery, mergeOptions, } from '../shared/ajax.js';
import { isString, isUndefined, eachObject, eachArray, } from '../shared/helper.js';
import { param } from './param.js';
/**
* 发送 ajax 请求
* @param options
* @example
```js
ajax({
method: "POST",
url: "some.php",
data: { name: "John", location: "Boston" }
}).then(function( msg ) {
alert( "Data Saved: " + msg );
});
```
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const ajax = (options) => {
const document = getDocument();
const window = getWindow();
// 是否已取消请求
let isCanceled = false;
// ajaxStart、ajaxError、ajaxComplete 事件参数
// @ts-ignore
const eventParams = {};
// ajaxSuccess 事件参数
// @ts-ignore
const successEventParams = {};
// 参数合并
const mergedOptions = mergeOptions(options);
const method = mergedOptions.method.toUpperCase();
let { data, url } = mergedOptions;
url = url || window.location.toString();
const { processData, async, cache, username, password, headers, xhrFields, statusCode, dataType, contentType, timeout, global, } = mergedOptions;
// 需要发送的数据
// GET/HEAD 请求和 processData 为 true 时,转换为查询字符串格式,特殊格式不转换
const isMethodQueryString = isQueryStringData(method);
if (data &&
(isMethodQueryString || processData) &&
!isString(data) &&
!(data instanceof ArrayBuffer) &&
!(data instanceof Blob) &&
!(data instanceof Document) &&
!(data instanceof FormData)) {
data = param(data);
}
// 对于 GET、HEAD 类型的请求,把 data 数据添加到 URL 中
if (data && isMethodQueryString) {
// 查询字符串拼接到 URL 中
url = appendQuery(url, data);
data = null;
}
/**
* 触发事件和回调函数
* @param event
* @param callback
* @param args
*/
const trigger = (event, callback, ...args) => {
// 触发全局事件
if (global) {
$(document).trigger(event, callback === 'success' ? successEventParams : eventParams);
}
// 触发 ajax 回调和事件
let resultGlobal;
let resultCustom;
// 全局回调
if (callback in globalOptions) {
resultGlobal = globalOptions[callback](...args);
}
// 自定义回调
if (mergedOptions[callback]) {
resultCustom = mergedOptions[callback](...args);
}
// beforeSend 回调返回 false 时取消 ajax 请求
if (callback === 'beforeSend' &&
[resultGlobal, resultCustom].includes(false)) {
isCanceled = true;
}
};
// XMLHttpRequest 请求
const XHR = () => {
let textStatus;
return new Promise((resolve, reject) => {
const doReject = (reason) => {
return reject(new Error(reason));
};
// GET/HEAD 请求的缓存处理
if (isMethodQueryString && !cache) {
url = appendQuery(url, `_=${Date.now()}`);
}
// 创建 XHR
const xhr = new XMLHttpRequest();
xhr.open(method, url, async, username, password);
if (contentType ||
(data && !isMethodQueryString && contentType !== false)) {
xhr.setRequestHeader('Content-Type', contentType);
}
// 设置 Accept
if (dataType === 'json') {
xhr.setRequestHeader('Accept', 'application/json, text/javascript');
}
// 添加 headers
eachObject(headers, (key, value) => {
// undefined 值不发送,string 和 null 需要发送
if (!isUndefined(value)) {
xhr.setRequestHeader(key, value + ''); // 把 null 转换成字符串
}
});
// 检查是否是跨域请求,跨域请求时不添加 X-Requested-With
if (!isCrossDomain(url)) {
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
}
// 设置 xhr 选项
eachObject(xhrFields, (key, value) => {
xhr[key] = value;
});
eventParams.xhr = successEventParams.xhr = xhr;
eventParams.options = successEventParams.options = mergedOptions;
let xhrTimeout;
xhr.onload = () => {
if (xhrTimeout) {
clearTimeout(xhrTimeout);
}
// AJAX 返回的 HTTP 响应码是否表示成功
const isSuccess = isHttpStatusSuccess(xhr.status);
// @ts-ignore
let responseData = undefined;
if (isSuccess) {
textStatus =
xhr.status === 204 || method === 'HEAD'
? 'nocontent'
: xhr.status === 304
? 'notmodified'
: 'success';
if (dataType === 'json' ||
(!dataType &&
(xhr.getResponseHeader('content-type') || '').includes('json'))) {
try {
responseData =
method === 'HEAD' ? undefined : JSON.parse(xhr.responseText);
successEventParams.response = responseData;
}
catch (_err) {
textStatus = 'parsererror';
trigger(ajaxError, 'error', xhr, textStatus);
doReject(textStatus);
}
if (textStatus !== 'parsererror') {
trigger(ajaxSuccess, 'success', responseData, textStatus, xhr);
resolve(responseData);
}
}
else {
responseData =
method === 'HEAD'
? undefined
: xhr.responseType === 'text' || xhr.responseType === ''
? xhr.responseText
: xhr.response;
successEventParams.response = responseData;
trigger(ajaxSuccess, 'success', responseData, textStatus, xhr);
resolve(responseData);
}
}
else {
textStatus = 'error';
trigger(ajaxError, 'error', xhr, textStatus);
doReject(textStatus);
}
// statusCode
eachArray([globalOptions.statusCode ?? {}, statusCode], (func) => {
if (func[xhr.status]) {
if (isSuccess) {
func[xhr.status](responseData, textStatus, xhr);
}
else {
func[xhr.status](xhr, textStatus);
}
}
});
trigger(ajaxComplete, 'complete', xhr, textStatus);
};
xhr.onerror = () => {
if (xhrTimeout) {
clearTimeout(xhrTimeout);
}
trigger(ajaxError, 'error', xhr, xhr.statusText);
trigger(ajaxComplete, 'complete', xhr, 'error');
doReject(xhr.statusText);
};
xhr.onabort = () => {
let statusText = 'abort';
if (xhrTimeout) {
statusText = 'timeout';
clearTimeout(xhrTimeout);
}
trigger(ajaxError, 'error', xhr, statusText);
trigger(ajaxComplete, 'complete', xhr, statusText);
doReject(statusText);
};
// ajax start 回调
trigger(ajaxStart, 'beforeSend', xhr, mergedOptions);
if (isCanceled) {
return doReject('cancel');
}
// Timeout
if (timeout > 0) {
xhrTimeout = window.setTimeout(() => xhr.abort(), timeout);
}
// 发送 XHR
xhr.send(data);
});
};
return XHR();
};