UNPKG

@alova/adapter-xhr

Version:

XMLHttpRequest adapter for alova.js

200 lines (193 loc) 8.3 kB
/** * @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) */ 'use strict'; 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;