@alova/adapter-xhr
Version:
XMLHttpRequest adapter for alova.js
200 lines (193 loc) • 8.3 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)
*/
;
var shared = require('@alova/shared');
const mockResponseHandler = ({ status, statusText, body, responseHeaders }) => ({
response: {
data: body,
status,
statusText,
headers: responseHeaders
},
headers: responseHeaders
});
/**
* 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
shared.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 !== shared.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) => shared.newInstance(Error, msg);
const isBodyData = (data) => shared.isString(data) || shared.isSpecialRequestBody(data);
/**
* XMLHttpRequest request adapter
*/
function requestAdapter({ onCreate = shared.noop } = {}) {
const adapter = ({ type, url, data = null, headers }, method) => {
const { config } = method;
const { auth, withCredentials, mimeType, responseType } = config;
let downloadHandler = shared.noop;
let uploadHandler = shared.noop;
let xhr;
const responsePromise = new Promise((resolve, reject) => {
try {
xhr = new XMLHttpRequest();
xhr.open(type, url, shared.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 === shared.trueValue) {
xhr.withCredentials = withCredentials;
}
// Set mime type
if (mimeType) {
xhr.overrideMimeType(mimeType);
}
// Set request header
let isContentTypeFormUrlEncoded = shared.falseValue;
const isContentTypeSet = /content-type/i.test(shared.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 = ['', shared.undefinedValue, shared.nullValue, shared.falseValue];
Object.keys(headers).forEach(headerName => {
if (/content-type/i.test(headerName)) {
isContentTypeFormUrlEncoded = /application\/x-www-form-urlencoded/i.test(headers[headerName]);
}
if (!shared.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 && shared.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 !== shared.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;