@alova/adapter-xhr
Version:
XMLHttpRequest adapter for alova.js
263 lines (253 loc) • 11.5 kB
JavaScript
/**
* @alova/adapter-xhr 2.3.1 (https://alova.js.org)
* Document https://alova.js.org
* Copyright 2025 JOU-amjs. All Rights Reserved
* Licensed under MIT (git://github.com/alovajs/alova/blob/main/LICENSE)
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.AlovaAdapterXhr = {}));
})(this, (function (exports) { 'use strict';
const mockResponseHandler = ({ status, statusText, body, responseHeaders }) => ({
response: {
data: body,
status,
statusText,
headers: responseHeaders
},
headers: responseHeaders
});
/**
* @alova/shared 1.3.2 (https://alova.js.org)
* Document https://alova.js.org
* Copyright 2025 Scott Hu. All Rights Reserved
* Licensed under MIT (https://github.com/alovajs/alova/blob/main/LICENSE)
*/
const undefStr = 'undefined';
const ObjectCls = Object;
const undefinedValue = undefined;
const nullValue = null;
const trueValue = true;
const falseValue = false;
const JSONStringify = (value, replacer, space) => JSON.stringify(value, replacer, space);
const typeOf = (arg) => typeof arg;
const includes = (ary, target) => ary.includes(target);
// Whether it is running on the server side, node and bun are judged by process, and deno is judged by Deno.
// Some frameworks (such as Alipay and uniapp) will inject the process object as a global variable which `browser` is true
typeof window === undefStr && (typeof process !== undefStr ? !process.browser : typeof Deno !== undefStr);
/**
* Empty function for compatibility processing
*/
const noop = () => { };
/**
* Determine whether the parameter is a string any parameter
* @returns Whether the parameter is a string
*/
const isString = (arg) => typeOf(arg) === 'string';
/**
* Global toString any parameter stringified parameters
*/
const globalToString = (arg) => ObjectCls.prototype.toString.call(arg);
/**
* Determine whether it is a normal object any parameter
* @returns Judgment result
*/
const isPlainObject = (arg) => globalToString(arg) === '[object Object]';
/**
* Determine whether it is an instance of a certain class any parameter
* @returns Judgment result
*/
const instanceOf = (arg, cls) => arg instanceof cls;
/**
* Is it special data
* @param data Submit data
* @returns Judgment result
*/
const isSpecialRequestBody = (data) => {
const dataTypeString = globalToString(data);
return (/^\[object (Blob|FormData|ReadableStream|URLSearchParams)\]$/i.test(dataTypeString) || instanceOf(data, ArrayBuffer));
};
/**
* Create class instance
* @param Cls Constructor
* @param args Constructor parameters class instance
*/
const newInstance = (Cls, ...args) => new Cls(...args);
/**
* Convert object to queryString string
* Supports arrays or objects at any level
* @param data Converted data instance
*/
const data2QueryString = (data) => {
const ary = [];
let paths = [];
let index = 0;
let refValueAttrCount = 0;
// Use json.stringify to deeply traverse data
JSONStringify(data, (key, value) => {
if (key !== '') {
// If it is a reference type (array or object), enter the record path
if (typeof value === 'object' && value !== null) {
paths.push(key);
// Record the number of times the next path is used
// It is necessary to use the accumulation method for the following reasons:
/**
* { a: [1, { b: 2 }] }
*/
// If the array contains an array or object, then refValueAttrCount needs to be used once for { b: 2 }, so it is an accumulation method.
refValueAttrCount += Object.keys(value).length;
}
else if (value !== undefinedValue) {
// values of undefined are not added to the query string.
const pathsTransformed = [...paths, key].map((val, i) => (i > 0 ? `[${val}]` : val)).join('');
ary.push(`${pathsTransformed}=${value}`);
// The number of paths has been used up. Reset the mark information.
// Otherwise, index++ is used to record the current number of uses.
if (index >= refValueAttrCount - 1) {
paths = [];
index = 0;
refValueAttrCount = 0;
}
else {
index += 1;
}
}
}
return value;
});
return ary.join('&');
};
/**
* Parse response headers
* @param headerString Response header string
* @returns Response header object
*/
const parseResponseHeaders = (headerString) => {
const headersAry = headerString.trim().split(/[\r\n]+/);
const headersMap = {};
headersAry.forEach(line => {
const [headerName, value] = line.split(/:\s*/);
headersMap[headerName] = value;
});
return headersMap;
};
const err = (msg) => newInstance(Error, msg);
const isBodyData = (data) => isString(data) || isSpecialRequestBody(data);
/**
* XMLHttpRequest request adapter
*/
function requestAdapter({ onCreate = noop } = {}) {
const adapter = ({ type, url, data = null, headers }, method) => {
const { config } = method;
const { auth, withCredentials, mimeType, responseType } = config;
let downloadHandler = noop;
let uploadHandler = noop;
let xhr;
const responsePromise = new Promise((resolve, reject) => {
try {
xhr = new XMLHttpRequest();
xhr.open(type, url, trueValue, auth === null || auth === void 0 ? void 0 : auth.username, auth === null || auth === void 0 ? void 0 : auth.password);
// fix #501
if (responseType && responseType !== 'json') {
xhr.responseType = responseType;
}
xhr.timeout = config.timeout || 0;
if (withCredentials === trueValue) {
xhr.withCredentials = withCredentials;
}
// Set mime type
if (mimeType) {
xhr.overrideMimeType(mimeType);
}
// Set request header
let isContentTypeFormUrlEncoded = falseValue;
const isContentTypeSet = /content-type/i.test(ObjectCls.keys(headers).join());
const isFormData = data && data.toString() === '[object FormData]';
// Content-Type defaults to application/json when not specified; charset=UTF-8
if (!isContentTypeSet && !isFormData) {
headers['Content-Type'] = 'application/json; charset=UTF-8';
}
const ignoringHeaderValues = ['', undefinedValue, nullValue, falseValue];
Object.keys(headers).forEach(headerName => {
if (/content-type/i.test(headerName)) {
isContentTypeFormUrlEncoded = /application\/x-www-form-urlencoded/i.test(headers[headerName]);
}
if (!includes(ignoringHeaderValues, headers[headerName])) {
xhr.setRequestHeader(headerName, headers[headerName]);
}
});
// Listen for download events
xhr.addEventListener('progress', event => {
downloadHandler(event.loaded, event.total);
});
// Listen for upload events
xhr.upload.addEventListener('progress', event => {
uploadHandler(event.loaded, event.total);
});
// Request success event
xhr.addEventListener('load', () => {
let responseData = !responseType || responseType === 'text' || responseType === 'json' ? xhr.responseText : xhr.response;
// try to parse data as json
// if fails, use raw response
if (!responseType || responseType === 'json') {
try {
responseData = JSON.parse(responseData);
}
catch (_a) { }
}
resolve({
status: xhr.status,
statusText: xhr.statusText,
data: responseData,
headers: parseResponseHeaders(xhr.getAllResponseHeaders())
});
});
// request error event
xhr.addEventListener('error', () => {
reject(err('Network Error'));
});
// Request timeout event
xhr.addEventListener('timeout', () => {
reject(err('Network Timeout'));
});
// interrupt event
xhr.addEventListener('abort', () => {
reject(err('The user aborted a request.'));
});
// If the content type in the request header is application/x www form urlencoded, convert the body data into query string
let dataSend = data;
if (isContentTypeFormUrlEncoded && isPlainObject(dataSend)) {
dataSend = data2QueryString(dataSend);
}
// It is null when making a Get request, and there is no need to enter processing at this time.
if (dataSend !== nullValue) {
dataSend = isBodyData(dataSend) ? dataSend : JSON.stringify(dataSend);
}
// export xhr in `onCreate`
onCreate(xhr);
xhr.send(dataSend);
}
catch (error) {
reject(error);
}
});
return {
response: () => responsePromise,
headers: () => responsePromise.then(res => res.headers),
abort: () => {
xhr.abort();
},
onDownload: handler => {
downloadHandler = handler;
},
onUpload: handler => {
uploadHandler = handler;
}
};
};
return adapter;
}
exports.xhrMockResponse = mockResponseHandler;
exports.xhrRequestAdapter = requestAdapter;
}));