Match-id-55687cb91cabe76fd1dabf70d10b55d43fc5344f

This commit is contained in:
* 2023-09-01 09:10:33 +08:00
commit 4f3ea62175
104 changed files with 6928 additions and 0 deletions

View File

@ -0,0 +1,59 @@
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
root: true,
plugins: ['jest', 'no-for-of-loops', 'no-function-declare-after-return', 'react', '@typescript-eslint'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 8,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
modules: true,
experimentalObjectRestSpread: true,
},
},
env: {
browser: true,
jest: true,
node: true,
es6: true,
},
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-empty-function': 'off',
semi: ['warn', 'always'],
quotes: ['warn', 'single'],
'accessor-pairs': 'off',
'brace-style': ['error', '1tbs'],
'func-style': ['warn', 'declaration', { allowArrowFunctions: true }],
'max-lines-per-function': 'off',
'object-curly-newline': 'off',
// 尾随逗号
'comma-dangle': ['error', 'only-multiline'],
'prefer-const': 'off',
'no-constant-condition': 'off',
'no-for-of-loops/no-for-of-loops': 'error',
'no-function-declare-after-return/no-function-declare-after-return': 'error',
},
globals: {
isDev: true,
isTest: true,
},
overrides: [
{
files: ['scripts/__tests__/**/*.js'],
globals: {
container: true,
},
},
],
};

2
packages/horizon-request/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/node_modules/
/dist/

View File

@ -0,0 +1 @@
node_modules

View File

@ -0,0 +1,17 @@
'use strict';
module.exports = {
printWidth: 120, // 一行120字符数如果超过会进行换行
tabWidth: 2, // tab等2个空格
useTabs: false, // 用空格缩进行
semi: true, // 行尾使用分号
singleQuote: true, // 字符串使用单引号
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
jsxSingleQuote: false, // 在JSX中使用双引号
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
bracketSpacing: true, // 对象的括号间增加空格
bracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
endOfLine: 'lf', // 仅限换行(\n
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
"browsers" : ["> 1%", "last 2 versions", "not ie <= 8"],
"node": "current"
},
useBuiltIns: "usage",
corejs: 3,
}
],
[
'@babel/preset-typescript',
]
]
};

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<coverage generated="1691465395748" clover="3.2.0">
<project timestamp="1691465395748" name="All files">
<metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0" elements="0" coveredelements="0" complexity="0" loc="0" ncloc="0" packages="0" files="0" classes="0"/>
</project>
</coverage>

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,224 @@
body, html {
margin:0; padding: 0;
height: 100%;
}
body {
font-family: Helvetica Neue, Helvetica, Arial;
font-size: 14px;
color:#333;
}
.small { font-size: 12px; }
*, *:after, *:before {
-webkit-box-sizing:border-box;
-moz-box-sizing:border-box;
box-sizing:border-box;
}
h1 { font-size: 20px; margin: 0;}
h2 { font-size: 14px; }
pre {
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
margin: 0;
padding: 0;
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
}
a { color:#0074D9; text-decoration:none; }
a:hover { text-decoration:underline; }
.strong { font-weight: bold; }
.space-top1 { padding: 10px 0 0 0; }
.pad2y { padding: 20px 0; }
.pad1y { padding: 10px 0; }
.pad2x { padding: 0 20px; }
.pad2 { padding: 20px; }
.pad1 { padding: 10px; }
.space-left2 { padding-left:55px; }
.space-right2 { padding-right:20px; }
.center { text-align:center; }
.clearfix { display:block; }
.clearfix:after {
content:'';
display:block;
height:0;
clear:both;
visibility:hidden;
}
.fl { float: left; }
@media only screen and (max-width:640px) {
.col3 { width:100%; max-width:100%; }
.hide-mobile { display:none!important; }
}
.quiet {
color: #7f7f7f;
color: rgba(0,0,0,0.5);
}
.quiet a { opacity: 0.7; }
.fraction {
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 10px;
color: #555;
background: #E8E8E8;
padding: 4px 5px;
border-radius: 3px;
vertical-align: middle;
}
div.path a:link, div.path a:visited { color: #333; }
table.coverage {
border-collapse: collapse;
margin: 10px 0 0 0;
padding: 0;
}
table.coverage td {
margin: 0;
padding: 0;
vertical-align: top;
}
table.coverage td.line-count {
text-align: right;
padding: 0 5px 0 20px;
}
table.coverage td.line-coverage {
text-align: right;
padding-right: 10px;
min-width:20px;
}
table.coverage td span.cline-any {
display: inline-block;
padding: 0 5px;
width: 100%;
}
.missing-if-branch {
display: inline-block;
margin-right: 5px;
border-radius: 3px;
position: relative;
padding: 0 4px;
background: #333;
color: yellow;
}
.skip-if-branch {
display: none;
margin-right: 10px;
position: relative;
padding: 0 4px;
background: #ccc;
color: white;
}
.missing-if-branch .typ, .skip-if-branch .typ {
color: inherit !important;
}
.coverage-summary {
border-collapse: collapse;
width: 100%;
}
.coverage-summary tr { border-bottom: 1px solid #bbb; }
.keyline-all { border: 1px solid #ddd; }
.coverage-summary td, .coverage-summary th { padding: 10px; }
.coverage-summary tbody { border: 1px solid #bbb; }
.coverage-summary td { border-right: 1px solid #bbb; }
.coverage-summary td:last-child { border-right: none; }
.coverage-summary th {
text-align: left;
font-weight: normal;
white-space: nowrap;
}
.coverage-summary th.file { border-right: none !important; }
.coverage-summary th.pct { }
.coverage-summary th.pic,
.coverage-summary th.abs,
.coverage-summary td.pct,
.coverage-summary td.abs { text-align: right; }
.coverage-summary td.file { white-space: nowrap; }
.coverage-summary td.pic { min-width: 120px !important; }
.coverage-summary tfoot td { }
.coverage-summary .sorter {
height: 10px;
width: 7px;
display: inline-block;
margin-left: 0.5em;
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
}
.coverage-summary .sorted .sorter {
background-position: 0 -20px;
}
.coverage-summary .sorted-desc .sorter {
background-position: 0 -10px;
}
.status-line { height: 10px; }
/* yellow */
.cbranch-no { background: yellow !important; color: #111; }
/* dark red */
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
.low .chart { border:1px solid #C21F39 }
.highlighted,
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
background: #C21F39 !important;
}
/* medium red */
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
/* light red */
.low, .cline-no { background:#FCE1E5 }
/* light green */
.high, .cline-yes { background:rgb(230,245,208) }
/* medium green */
.cstat-yes { background:rgb(161,215,106) }
/* dark green */
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
.high .chart { border:1px solid rgb(77,146,33) }
/* dark yellow (gold) */
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
.medium .chart { border:1px solid #f9cd0b; }
/* light yellow */
.medium { background: #fff4c2; }
.cstat-skip { background: #ddd; color: #111; }
.fstat-skip { background: #ddd; color: #111 !important; }
.cbranch-skip { background: #ddd !important; color: #111; }
span.cline-neutral { background: #eaeaea; }
.coverage-summary td.empty {
opacity: .5;
padding-top: 4px;
padding-bottom: 4px;
line-height: 1;
color: #888;
}
.cover-fill, .cover-empty {
display:inline-block;
height: 12px;
}
.chart {
line-height: 0;
}
.cover-empty {
background: white;
}
.cover-full {
border-right: none !important;
}
pre.prettyprint {
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
.com { color: #999 !important; }
.ignore-none { color: #999; font-weight: normal; }
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -48px;
}
.footer, .push {
height: 48px;
}

View File

@ -0,0 +1,87 @@
/* eslint-disable */
var jumpToCode = (function init() {
// Classes of code we would like to highlight in the file view
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
// Elements to highlight in the file listing view
var fileListingElements = ['td.pct.low'];
// We don't want to select elements that are direct descendants of another match
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
// Selecter that finds elements on the page to which we can jump
var selector =
fileListingElements.join(', ') +
', ' +
notSelector +
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
// The NodeList of matching elements
var missingCoverageElements = document.querySelectorAll(selector);
var currentIndex;
function toggleClass(index) {
missingCoverageElements
.item(currentIndex)
.classList.remove('highlighted');
missingCoverageElements.item(index).classList.add('highlighted');
}
function makeCurrent(index) {
toggleClass(index);
currentIndex = index;
missingCoverageElements.item(index).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
}
function goToPrevious() {
var nextIndex = 0;
if (typeof currentIndex !== 'number' || currentIndex === 0) {
nextIndex = missingCoverageElements.length - 1;
} else if (missingCoverageElements.length > 1) {
nextIndex = currentIndex - 1;
}
makeCurrent(nextIndex);
}
function goToNext() {
var nextIndex = 0;
if (
typeof currentIndex === 'number' &&
currentIndex < missingCoverageElements.length - 1
) {
nextIndex = currentIndex + 1;
}
makeCurrent(nextIndex);
}
return function jump(event) {
if (
document.getElementById('fileSearch') === document.activeElement &&
document.activeElement != null
) {
// if we're currently focused on the search input, we don't want to navigate
return;
}
switch (event.which) {
case 78: // n
case 74: // j
goToNext();
break;
case 66: // b
case 75: // k
case 80: // p
goToPrevious();
break;
}
};
})();
window.addEventListener('keydown', jumpToCode);

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

View File

@ -0,0 +1,101 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for All files</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>All files</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Statements</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Lines</span>
<span class='fraction'>0/0</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input oninput="onInput()" type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2023-08-08T03:29:55.735Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1 @@
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

View File

@ -0,0 +1,196 @@
/* eslint-disable */
var addSorting = (function() {
'use strict';
var cols,
currentSort = {
index: 0,
desc: false
};
// returns the summary table element
function getTable() {
return document.querySelector('.coverage-summary');
}
// returns the thead element of the summary table
function getTableHeader() {
return getTable().querySelector('thead tr');
}
// returns the tbody element of the summary table
function getTableBody() {
return getTable().querySelector('tbody');
}
// returns the th element for nth column
function getNthColumn(n) {
return getTableHeader().querySelectorAll('th')[n];
}
function onFilterInput() {
const searchValue = document.getElementById('fileSearch').value;
const rows = document.getElementsByTagName('tbody')[0].children;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
if (
row.textContent
.toLowerCase()
.includes(searchValue.toLowerCase())
) {
row.style.display = '';
} else {
row.style.display = 'none';
}
}
}
// loads the search box
function addSearchBox() {
var template = document.getElementById('filterTemplate');
var templateClone = template.content.cloneNode(true);
templateClone.getElementById('fileSearch').oninput = onFilterInput;
template.parentElement.appendChild(templateClone);
}
// loads all columns
function loadColumns() {
var colNodes = getTableHeader().querySelectorAll('th'),
colNode,
cols = [],
col,
i;
for (i = 0; i < colNodes.length; i += 1) {
colNode = colNodes[i];
col = {
key: colNode.getAttribute('data-col'),
sortable: !colNode.getAttribute('data-nosort'),
type: colNode.getAttribute('data-type') || 'string'
};
cols.push(col);
if (col.sortable) {
col.defaultDescSort = col.type === 'number';
colNode.innerHTML =
colNode.innerHTML + '<span class="sorter"></span>';
}
}
return cols;
}
// attaches a data attribute to every tr element with an object
// of data values keyed by column name
function loadRowData(tableRow) {
var tableCols = tableRow.querySelectorAll('td'),
colNode,
col,
data = {},
i,
val;
for (i = 0; i < tableCols.length; i += 1) {
colNode = tableCols[i];
col = cols[i];
val = colNode.getAttribute('data-value');
if (col.type === 'number') {
val = Number(val);
}
data[col.key] = val;
}
return data;
}
// loads all row data
function loadData() {
var rows = getTableBody().querySelectorAll('tr'),
i;
for (i = 0; i < rows.length; i += 1) {
rows[i].data = loadRowData(rows[i]);
}
}
// sorts the table using the data for the ith column
function sortByIndex(index, desc) {
var key = cols[index].key,
sorter = function(a, b) {
a = a.data[key];
b = b.data[key];
return a < b ? -1 : a > b ? 1 : 0;
},
finalSorter = sorter,
tableBody = document.querySelector('.coverage-summary tbody'),
rowNodes = tableBody.querySelectorAll('tr'),
rows = [],
i;
if (desc) {
finalSorter = function(a, b) {
return -1 * sorter(a, b);
};
}
for (i = 0; i < rowNodes.length; i += 1) {
rows.push(rowNodes[i]);
tableBody.removeChild(rowNodes[i]);
}
rows.sort(finalSorter);
for (i = 0; i < rows.length; i += 1) {
tableBody.appendChild(rows[i]);
}
}
// removes sort indicators for current column being sorted
function removeSortIndicators() {
var col = getNthColumn(currentSort.index),
cls = col.className;
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
col.className = cls;
}
// adds sort indicators for current column being sorted
function addSortIndicators() {
getNthColumn(currentSort.index).className += currentSort.desc
? ' sorted-desc'
: ' sorted';
}
// adds event listeners for all sorter widgets
function enableUI() {
var i,
el,
ithSorter = function ithSorter(i) {
var col = cols[i];
return function() {
var desc = col.defaultDescSort;
if (currentSort.index === i) {
desc = !currentSort.desc;
}
sortByIndex(i, desc);
removeSortIndicators();
currentSort.index = i;
currentSort.desc = desc;
addSortIndicators();
};
};
for (i = 0; i < cols.length; i += 1) {
if (cols[i].sortable) {
// add the click event handler on the th so users
// dont have to click on those tiny arrows
el = getNthColumn(i).querySelector('.sorter').parentElement;
if (el.addEventListener) {
el.addEventListener('click', ithSorter(i));
} else {
el.attachEvent('onclick', ithSorter(i));
}
}
}
}
// adds sorting functionality to the UI
return function() {
if (!getTable()) {
return;
}
cols = loadColumns();
loadData();
addSearchBox();
addSortIndicators();
enableUI();
};
})();
window.addEventListener('load', addSorting);

View File

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Horizon Request Cancel Request Test</title>
<link rel="stylesheet" type="text/css" href="cancelStyles.css">
</head>
<body>
<header>Horizon Request Cancel Request Test</header>
<div class="container">
<div class="button-group">
<button id="sendRequestButton">Send Request</button>
<button id="cancelRequestButton">Cancel Request</button>
</div>
<div class="message" id="message">等待发送请求...</div>
</div>
<script src="../../dist/bundle.js"></script>
<script>
const sendRequestButton = document.getElementById('sendRequestButton');
const cancelRequestButton = document.getElementById('cancelRequestButton');
const message = document.getElementById('message');
let cancelTokenSource;
sendRequestButton.addEventListener('click', function() {
message.innerHTML = '';
cancelTokenSource = horizonRequest.CancelToken.source();
horizonRequest.get('http://localhost:3001/data', {
cancelToken: cancelTokenSource.token
}).then(function(response) {
message.innerHTML = '请求成功: ' + JSON.stringify(response.data, null, 2);
}).catch(function(error) {
message.innerHTML = JSON.stringify(error, null, 2);
});
});
cancelRequestButton.addEventListener('click', function () {
const CancelToken = horizonRequest.CancelToken;
const source = CancelToken.source();
horizonRequest.get('http://localhost:3001/data', {
cancelToken: source.token
}).then(function(response) {
console.log(response.data);
}).catch(function(error) {
if (horizonRequest.isCancel(error)) {
message.innerHTML = '请求已被取消:' + error.message;
} else {
message.innerHTML = '请求出错:' + error.message;
}
});
source.cancel('请求被用户取消。');
});
</script>
</body>
</html>

View File

@ -0,0 +1,58 @@
body {
font-family: Arial, sans-serif;
background-color: #f8f8f8;
padding: 0;
margin: 0;
}
header {
display: flex;
justify-content: center;
align-items: center;
height: 80px;
background-color: #007bff;
color: #fff;
font-size: 24px;
font-weight: bold;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.button-group {
display: flex;
justify-content: center;
align-items: center;
margin-top: 30px;
}
button {
font-family: Arial, sans-serif;
font-size: 16px;
font-weight: bold;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 4px;
padding: 10px 20px;
cursor: pointer;
transition: background-color 0.3s ease-in-out;
margin: 5px;
}
button:hover {
background-color: #0062cc;
}
.message {
text-align: center;
font-size: 18px;
margin-top: 30px;
padding: 20px;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
}

View File

@ -0,0 +1,87 @@
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 2rem;
background-color: #f0f2f5;
}
header {
display: flex;
justify-content: center;
align-items: center;
height: 80px;
background-color: #007bff;
color: #fff;
font-size: 24px;
font-weight: bold;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h2 {
color: #333;
border-bottom: 1px solid #ccc;
padding-bottom: 0.5rem;
text-align: center;
margin-bottom: 1.5rem;
}
h3 {
text-align: center;
font-weight: 600;
margin-bottom: 0.5rem;
}
.button {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
button {
background-color: #007bff;
border: none;
border-radius: 0.25rem;
color: white;
cursor: pointer;
font-size: 1rem;
padding: 0.5rem 1rem;
text-align: center;
text-decoration: none;
display: inline-block;
margin: 0.25rem 1rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
button:hover {
background-color: #0056b3;
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.1);
}
pre {
background-color: #f8f8f8;
border: 1px solid #ccc;
padding: 10px;
white-space: pre-wrap;
word-wrap: break-word;
text-align: center;
font-size: large;
border-radius: 4px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 50vh;
display: inline-block;
}
.response-container {
margin-top: 1rem;
background-color: #fff;
padding: 1.5rem;
border-radius: 4px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.pre-container {
text-align: center;
}

View File

@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Horizon Request Interceptor Test</title>
<link rel="stylesheet" type="text/css" href="interceptorStyles.css">
</head>
<body>
<header>Horizon Request interceptor Test</header>
<h2>使用拦截器:</h2>
<div class="response-container">
<h3>响应状态码:</h3>
<div class="pre-container">
<pre id="responseStatusWithInterceptor">等待发送请求...</pre>
</div>
<h3>请求拦截反馈:</h3>
<div class="pre-container">
<pre id="requestInterceptorFeedback">等待发送请求...</pre>
</div>
<h3>响应拦截反馈:</h3>
<div class="pre-container">
<pre id="responseInterceptorFeedback">等待发送请求...</pre>
</div>
<h3>响应数据:</h3>
<div class="pre-container">
<pre id="responseDataWithInterceptor">等待发送请求...</pre>
</div>
<div class="button">
<button id="sendRequestWithInterceptor">发送请求</button>
</div>
</div>
<h2>不使用拦截器:</h2>
<div class="response-container">
<h3>响应状态码:</h3>
<div class="pre-container">
<pre id="responseStatusWithoutInterceptor">等待发送请求...</pre>
</div>
<h3>拦截反馈:</h3>
<div class="pre-container">
<pre id="noInterceptorFeedback">等待发送请求...</pre>
</div>
<h3>响应数据:</h3>
<div class="pre-container">
<pre id="responseDataWithoutInterceptor">等待发送请求...</pre>
</div>
<div class="button">
<button id="sendRequestWithoutInterceptor">发送请求</button>
</div>
</div>
<script src="../../dist/bundle.js"></script>
<script>
// 创建使用拦截器的 HR 实例
const hrInstance = horizonRequest.create();
// 添加请求拦截器
hrInstance.interceptors.request.use(function(config) {
// 为请求添加自定义请求头
config.headers['HR-Custom-Header'] = 'CustomHeaderValue';
document.getElementById('requestInterceptorFeedback').textContent = '请求已拦截并添加请求头HR-Custom-Header';
return config;
}, function(error) {
return Promise.reject(error);
});
// 添加响应拦截器
hrInstance.interceptors.response.use(function(response) {
// 更新响应状态码
response.status = 404;
document.getElementById('responseStatusWithInterceptor').textContent = String(response.status);
document.getElementById('responseInterceptorFeedback').textContent = '响应已被拦截状态响应码被强制修改成404'
return response;
}, function(error) {
return Promise.reject(error);
});
// 使用拦截器的请求
document.getElementById('sendRequestWithInterceptor').addEventListener('click', function () {
hrInstance.get('http://localhost:3001/')
.then(function (response) {
document.getElementById('responseDataWithInterceptor').textContent = JSON.stringify(response.data, null, 2);
}).catch(function (error) {
document.getElementById('responseDataWithInterceptor').textContent = 'Request failed:' + error;
})
});
// 不使用拦截器的请求
document.getElementById('sendRequestWithoutInterceptor').addEventListener('click', function () {
horizonRequest.get('http://localhost:3001/')
.then(function (response) {
document.getElementById('responseStatusWithoutInterceptor').textContent = response.status;
document.getElementById('responseDataWithoutInterceptor').textContent = JSON.stringify(response.data, null, 2);
document.getElementById('noInterceptorFeedback').textContent = '请求未被拦截';
}).catch(function (error) {
document.getElementById('responseDataWithoutInterceptor').textContent = 'Request failed:' + error;
})
});
</script>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>downloadTest</title>
</head>
<body>
<h3>Hello Horizon Request Master!</h3>
</body>
</html>

View File

@ -0,0 +1,86 @@
body {
font-family: Arial, sans-serif;
background-color: #f8f8f8;
margin: 0;
padding: 0;
}
header {
display: flex;
justify-content: center;
align-items: center;
height: 80px;
background-color: #007bff;
color: #fff;
font-size: 24px;
font-weight: bold;
}
h1 {
text-align: center;
margin-top: 50px;
color: #2c3e50;
font-size: 36px;
}
.container {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin-bottom: 50px;
}
.card {
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 5px #aaa;
width: 320px;
padding: 20px;
margin: 20px;
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 0 15px #aaa;
transform: translateY(-5px);
}
.card h2 {
margin-bottom: 10px;
font-size: 24px;
color: #2c3e50;
}
.card pre {
background-color: #f0f0f0;
padding: 10px;
font-size: 14px;
border-radius: 5px;
white-space: pre-wrap;
word-wrap: break-word;
}
.button {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
button {
font-family: Arial, sans-serif;
font-size: 16px;
font-weight: bold;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 4px;
padding: 10px 20px;
cursor: pointer;
transition: background-color 0.3s ease-in-out;
}
button:hover {
background-color: #0056b3;
}

View File

@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Horizon Request API Test</title>
<link rel="stylesheet" type="text/css" href="requestStyles.css">
</head>
<body>
<header>Horizon Request API Test</header>
<div class="container">
<div class="card">
<h2>Request</h2>
<pre id="request-result">等待发送请求...</pre>
</div>
</div>
<div class="container">
<div class="card">
<h2>GET Request</h2>
<pre id="get-result">等待发送请求...</pre>
</div>
<div class="card">
<h2>POST Request</h2>
<pre id="post-result">等待发送请求...</pre>
</div>
<div class="card">
<h2>PUT Request</h2>
<pre id="put-result">等待发送请求...</pre>
</div>
<div class="card">
<h2>DELETE Request</h2>
<pre id="delete-result">等待发送请求...</pre>
</div>
</div>
<div class="container">
<div class="card">
<h2>HEAD Request</h2>
<pre id="head-result">等待发送请求...</pre>
</div>
<div class="card">
<h2>OPTIONS Request</h2>
<pre id="options-result">等待发送请求...</pre>
</div>
<div class="card">
<h2>PATCH Request</h2>
<pre id="patch-result">等待发送请求...</pre>
</div>
</div>
<div class="container">
<div class="card" style="height: 250px">
<h2>UPLOAD Request</h2>
<input type="file" id="fileInput">
<pre id="upload-progress">UploadProgress: 0%</pre>
<pre id="upload-result">等待发送请求...</pre>
</div>
<div class="card" style="height: 250px">
<h2>DOWNLOAD Request</h2>
<div style="height: 23px"></div>
<pre id="download-progress">DownloadProgress: 0%</pre>
<pre id="download-result">等待发送请求...</pre>
</div>
</div>
<div class="button">
<button id="queryButton">点击发送请求</button>
</div>
<br>
<div class="button">
<button id="resetButton">点击重置</button>
</div>
<script src="../../dist/bundle.js"></script>
<script>
const requestResult = document.getElementById('request-result');
const getResult = document.getElementById('get-result');
const postResult = document.getElementById('post-result');
const putResult = document.getElementById('put-result');
const deleteResult = document.getElementById('delete-result');
const headResult = document.getElementById('head-result');
const optionsResult = document.getElementById('options-result');
const patchResult = document.getElementById('patch-result');
const fileInput = document.getElementById('fileInput');
const uploadProgress = document.getElementById('upload-progress');
const uploadResult = document.getElementById('upload-result');
const downloadProgress = document.getElementById('download-progress');
const downloadResult = document.getElementById('download-result');
const queryButton = document.getElementById('queryButton');
const resetButton = document.getElementById('resetButton');
queryButton.addEventListener('click', function () {
const hrInstance = horizonRequest.create();
hrInstance.request('http://localhost:3001/', {method: 'GET', data: {}})
.then(function (response) {
requestResult.innerHTML = JSON.stringify(response.data, null, 2);
})
.catch(function (error) {
requestResult.innerHTML = JSON.stringify(error, null, 2);
})
horizonRequest.default.get('http://localhost:3001/')
.then(function (response) {
getResult.innerHTML = JSON.stringify(response.data, null, 2);
})
.catch(function (error) {
getResult.innerHTML = JSON.stringify(error, null, 2);
});
horizonRequest('http://localhost:3001/', {method:'POST', name: 'Alice'})
.then(function (response) {
postResult.innerHTML = JSON.stringify(response.data, null, 2);
})
.catch(function (error) {
postResult.innerHTML = JSON.stringify(error, null, 2);
});
horizonRequest.put('http://localhost:3001/users', {id: 1, name: 'Bob'})
.then(function (response) {
putResult.innerHTML = JSON.stringify(response.data, null, 2);
})
.catch(function (error) {
putResult.innerHTML = JSON.stringify(error, null, 2);
});
horizonRequest.delete('http://localhost:3001/users', {params: {id: 1}})
.then(function (response) {
deleteResult.innerHTML = JSON.stringify(response.data, null, 2);
})
.catch(function (error) {
deleteResult.innerHTML = JSON.stringify(error, null, 2);
});
horizonRequest.head('http://localhost:3001/')
.then(function (response) {
headResult.innerHTML = 'Header: ' + JSON.stringify(response.headers['x-powered-by'], null, 2); // IE 浏览器不支持 HEAD 方式访问响应头
})
.catch(function (error) {
headResult.innerHTML = JSON.stringify(error, null, 2);
});
horizonRequest.options('http://localhost:3001/', {
headers: {
'Access-Control-Request-Method': 'POST'
}
})
.then(function (response) {
optionsResult.innerHTML = 'status: ' + JSON.stringify(response.status, null, 2);
})
.catch(function (error) {
optionsResult.innerHTML = JSON.stringify(error, null, 2);
});
horizonRequest.patch('http://localhost:3001/', {name: 'HR'})
.then(function (response) {
patchResult.innerHTML = JSON.stringify(response.data, null, 2);
})
.catch(function (error) {
patchResult.innerHTML = JSON.stringify(error, null, 2);
});
horizonRequest.get('http://localhost:3001/download', {
responseType: 'text',
onDownloadProgress: function (progressEvent) {
const loaded = progressEvent.loaded;
const total = progressEvent.total;
const progressPercentage = Math.round((loaded / total) * 100);
downloadProgress.innerHTML = 'Download progress: ' + progressPercentage + '%';
},
}).then(function (response) {
downloadResult.innerHTML = JSON.stringify(response.data, null, 2);
}).catch(function (error) {
downloadResult.innerHTML = JSON.stringify(error, null, 2);
})
});
resetButton.addEventListener('click', function () {
requestResult.innerHTML = '等待发送请求...';
getResult.innerHTML = '等待发送请求...';
postResult.innerHTML = '等待发送请求...';
putResult.innerHTML = '等待发送请求...';
deleteResult.innerHTML = '等待发送请求...';
headResult.innerHTML = '等待发送请求...';
optionsResult.innerHTML = '等待发送请求...';
patchResult.innerHTML = '等待发送请求...';
fileInput.value = '';
uploadProgress.innerHTML = 'UploadProgress: 0%';
uploadResult.innerHTML = '等待发送请求...';
downloadProgress.innerHTML = 'DownloadProgress: 0%';
downloadResult.innerHTML = '等待发送请求...';
});
function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
horizonRequest.post('http://localhost:3001/', formData, {
onUploadProgress: function (progressEvent) {
const loaded = progressEvent.loaded;
const total = progressEvent.total;
const progressPercentage = Math.round((loaded / total) * 100);
uploadProgress.innerHTML = 'Upload progress: ' + progressPercentage + '%';
},
})
.then(function (response) {
uploadResult.innerHTML = JSON.stringify(response.data, null, 2);
})
.catch(function (error) {
uploadResult.innerHTML = JSON.stringify(error, null, 2);
});
}
fileInput.addEventListener('change', function (event) {
const file = event.target.files[0];
uploadFile(file);
});
</script>
</body>
</html>

View File

@ -0,0 +1,93 @@
import express from "express";
import * as fs from "fs";
import bodyParser from "body-parser";
import cors from "cors";
const app = express();
const port = 3001;
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// 自定义 CORS 配置
const corsOptions = {
origin: '*',
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
allowedHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'X-Requested-With,content-type', 'HR-Custom-Header'],
exposedHeaders: ['X-Powered-By'],
optionsSuccessStatus: 200, // 设置 OPTIONS 请求成功时的状态码为 200
credentials: true
};
app.use(cors(corsOptions));
// 处理 GET 请求
app.get('/', (req, res) => {
res.send('Hello Horizon Request!');
})
app.get('/data', (req, res) => {
const data = {
message: 'Hello Horizon Request!',
};
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(data));
});
app.get('/download', (req, res) => {
const filePath = 'D:\\code\\MRcode\\Horizon-Request\\examples\\request\\downloadTest.html';
const fileName = 'downloadTest.html';
const fileSize = fs.statSync(filePath).size;
if (fs.existsSync(filePath)) {
res.setHeader('Content-Disposition', `attachment; filename=${fileName}`);
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Length', fileSize);
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
} else {
res.status(404).send('File not found');
}
});
// 处理 POST 请求
app.post('/', (req, res) => {
res.send('Got a POST request!');
});
// 处理 PUT 请求
app.put('/users', (req, res) => {
res.send('Got a PUT request at /users');
});
// 处理 DELETE 请求
app.delete('/users', (req, res) => {
res.send('Got a DELETE request at /users');
});
// 处理 HEAD 请求
app.head('/', (req, res) => {
res.setHeader('x-powered-by', 'Express');
res.json({ 'x-powered-by': 'Express' });
res.sendStatus(200);
});
// 处理 OPTIONS 请求
app.options('/', (req, res) => {
res.sendStatus(200);
});
// 处理 PATCH 请求
app.patch('/', (req, res) => {
const name = req.body.name;
const message = `Hello, ${name}! Your name has been updated.`;
res.setHeader('Content-Type', 'text/plain');
res.send(message);
});
// 启动服务器
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});

View File

@ -0,0 +1,36 @@
import Horizon, { useState } from '@cloudsop/horizon';
import { useHR } from '../../index';
const App = () => {
const [message, setMessage] = useState('等待发送请求...');
const [isSent, setSend] = useState(false);
const options = {
pollingInterval: 3000,
enablePollingOptimization: true,
limitation: {minInterval: 500, maxInterval: 4000}
};
const {data} = useHR('http://localhost:3001/', null, options);
const handleClick = () => {
setSend(true);
}
return (
<>
<header>useHR Test</header>
<div className="container">
<div className="card">
<h2 style={{whiteSpace: "pre-wrap"}}>{options ? `实时数据流已激活\n更新间隔${options?.pollingInterval} ms`
: '实时数据流未激活'}</h2>
<pre>{isSent ? data : message}</pre>
</div>
</div>
<div className="button">
<button onClick={handleClick}>点击发送请求</button>
</div>
</>
);
}
export default App;

View File

@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>useHR Test</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f8f8f8;
margin: 0;
padding: 0;
}
header {
display: flex;
justify-content: center;
align-items: center;
height: 80px;
background-color: #007bff;
color: #fff;
font-size: 24px;
font-weight: bold;
}
h1 {
text-align: center;
margin-top: 50px;
color: #2c3e50;
font-size: 36px;
}
.container {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin-bottom: 50px;
}
.card {
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 5px #aaa;
width: 320px;
padding: 20px;
margin: 20px;
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 0 15px #aaa;
transform: translateY(-5px);
}
.card h2 {
margin-bottom: 10px;
font-size: 24px;
color: #2c3e50;
}
.card pre {
background-color: #f0f0f0;
padding: 10px;
font-size: 14px;
border-radius: 5px;
white-space: pre-wrap;
word-wrap: break-word;
}
.button {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
button {
font-family: Arial, sans-serif;
font-size: 16px;
font-weight: bold;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 4px;
padding: 10px 20px;
cursor: pointer;
transition: background-color 0.3s ease-in-out;
}
button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,9 @@
import Horizon from '@cloudsop/horizon';
import App from "./App ";
Horizon.render(
<Horizon.Fragment>
<App/>
</Horizon.Fragment>,
document.querySelector('#root')
)

View File

@ -0,0 +1,57 @@
import horizonRequest from './src/horizonRequest';
import useHR from './src/core/useHR/useHR';
const {
create,
request,
get,
post,
put,
['delete']: propToDelete,
head,
options,
HorizonRequest,
HrError,
CanceledError,
isCancel,
CancelToken,
all,
Cancel,
isHrError,
spread,
HrHeaders,
// 兼容axios
Axios,
AxiosError,
AxiosHeaders,
isAxiosError,
} = horizonRequest;
export {
create,
request,
get,
post,
put,
propToDelete as delete,
head,
options,
HorizonRequest,
HrError,
CanceledError,
isCancel,
CancelToken,
all,
Cancel,
isHrError,
spread,
HrHeaders,
useHR,
// 兼容axios
Axios,
AxiosError,
AxiosHeaders,
isAxiosError,
};
export default horizonRequest;

View File

@ -0,0 +1,25 @@
module.exports = {
clearMocks: true,
collectCoverage: true,
coverageDirectory: "coverage",
coverageProvider: "v8",
moduleFileExtensions: [
"js",
"mjs",
"cjs",
"jsx",
"ts",
"tsx",
"json",
"node"
],
testEnvironment: "jest-environment-jsdom",
testMatch: ["**/tests/**/*.test.[jt]s?(x)"],
testPathIgnorePatterns: [
"\\\\node_modules\\\\"
],
transform: {
"^.+\\.(js|jsx)$": "babel-jest",
"^.+\\.(ts|tsx)$": "babel-jest"
},
};

View File

@ -0,0 +1,11 @@
{
"restartable": "rs",
"ignore": [
".git",
"node_modules/**/node_modules"
],
"env": {
"NODE_ENV": "development"
},
"ext": "js,json"
}

View File

@ -0,0 +1,69 @@
{
"name": "@cloudsop/horizon-request",
"version": "1.0.20",
"description": "Horizon-request brings you a convenient request experience!",
"main": "index.ts",
"scripts": {
"test": "jest --config jest.config.cjs",
"buildExample": "rollup -c rollup.config.example.js --bundleConfigAsCjs",
"build": "rollup -c rollup.config.js --bundleConfigAsCjs",
"useHRExample": "webpack serve --config webpack.useHR.config.js --mode development",
"server": "nodemon .\\examples\\server\\serverTest.mjs"
},
"files": [
"dist",
"README.md"
],
"repository": {
"type": "git",
"url": "ssh://git@szv-open.codehub.huawei.com:2222/innersource/fenghuang/horizon/horizon-ecosystem.git"
},
"keywords": [
"horizon-request"
],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.21.8",
"@babel/preset-env": "^7.21.5",
"@babel/preset-react": "^7.9.4",
"@babel/preset-typescript": "^7.21.4",
"@rollup/plugin-commonjs": "^19.0.0",
"@types/jest": "^29.2.5",
"@types/react": "^17.0.34",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"babel-jest": "^20.0.3",
"babel-loader": "^9.1.0",
"body-parser": "^1.20.2",
"core-js": "3.32.1",
"cors": "^2.8.5",
"eslint": "^8.31.0",
"express": "^4.18.2",
"html-webpack-plugin": "^5.5.3",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.4.1",
"jsdom": "^22.0.0",
"nodemon": "^2.0.22",
"prettier": "^2.6.2",
"rollup": "^3.20.2",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.34.1",
"ts-jest": "^29.0.4",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"tslib": "^2.5.0",
"typescript": "^4.9.4",
"webpack": "^5.81.0",
"webpack-cli": "^5.0.2",
"webpack-dev-server": "^4.13.3"
},
"dependencies": {
"@cloudsop/horizon": "0.0.50"
},
"exclude": [
"node_modules"
]
}

View File

@ -0,0 +1,20 @@
import typescript from 'rollup-plugin-typescript2';
import resolve from 'rollup-plugin-node-resolve'; // 解析第三方模块,并将它们包含在最终的打包文件中
import commonjs from 'rollup-plugin-commonjs'; // 将 CommonJS 模块转换为 ES6 模块
export default {
input: './src/horizonRequest.ts',
output: {
file: 'dist/bundle.js',
format: 'umd',
name: 'horizonRequest',
},
plugins: [
typescript({
tsconfig: 'tsconfig.json',
include: ['src/**/*.ts'],
}),
resolve(),
commonjs(),
],
};

View File

@ -0,0 +1,29 @@
import typescript from 'rollup-plugin-typescript2';
import resolve from 'rollup-plugin-node-resolve'; // 解析第三方模块,并将它们包含在最终的打包文件中
import commonjs from 'rollup-plugin-commonjs'; // 将 CommonJS 模块转换为 ES6 模块
import { terser } from 'rollup-plugin-terser';
import { babel } from '@rollup/plugin-babel';
export default {
input: './index.ts',
output: {
file: 'dist/horizonRequest.js',
format: 'umd',
exports: 'named',
name: 'horizonRequest',
sourcemap: false,
},
plugins: [
resolve(),
commonjs(),
typescript({
tsconfig: 'tsconfig.json',
include: ['./**/*.ts'],
}),
terser(),
babel({
babelHelpers: 'bundled',
presets: ['@babel/preset-env']
})
],
};

View File

@ -0,0 +1,11 @@
class Cancel {
message?: string;
cancelFlag?: boolean; // 用于标志是否为用户主动取消
constructor(message?: string, cancelFlag?: boolean) {
this.message = message;
this.cancelFlag = cancelFlag;
}
}
export default Cancel;

View File

@ -0,0 +1,12 @@
import HrError from '../core/HrError';
import { HrRequestConfig } from '../types/interfaces';
class CancelError extends HrError {
constructor(message: string | undefined, config: HrRequestConfig, request?: any) {
const errorMessage = message || 'canceled';
super(errorMessage, (HrError as any).ERR_CANCELED, config, request);
this.name = 'CanceledError';
}
}
export default CancelError;

View File

@ -0,0 +1,48 @@
import Cancel from './Cancel';
import { CancelFunction, CancelExecutor } from '../types/types';
import { CancelToken as CancelTokenInstance } from '../types/interfaces';
class CancelToken implements CancelTokenInstance {
// 表示取消操作的状态当取消操作被触发时Promise将被解析为一个Cancel对象
promise: Promise<Cancel>;
reason?: Cancel;
// 取消函数,用于触发取消操作
_cancel?: CancelFunction;
constructor(executor: CancelExecutor) {
// promise对象在取消操作触发时将被解析
this.promise = new Promise<Cancel>(resolve => {
this._cancel = (message?: string) => {
if (this.reason) {
return;
}
this.reason = new Cancel(message, true);
resolve(this.reason);
};
});
executor(this._cancel!);
}
throwIfRequested() {
if (this.reason) {
throw this.reason;
}
}
// 创建一个 CancelToken 实例和关联的取消函数
static source() {
let cancel!: CancelFunction;
const token = new CancelToken(cancelFunc => {
cancel = cancelFunc;
});
return {
token,
cancel,
};
}
}
export default CancelToken;

View File

@ -0,0 +1,6 @@
// 检查是否为用户主动请求取消场景
function checkCancel(input: any): boolean {
return input.cancelFlag || false;
}
export default checkCancel;

View File

@ -0,0 +1,28 @@
const defaultConfig = {
method: 'GET',
transitional: {
// 解析 JSON 时静默处理错误。在启用时,如果发生 JSON 解析错误,将不会抛出异常,而是返回解析前的原始数据
silentJSONParsing: true,
// 控制是否强制解析 JSON 数据。在启用时,无论数据的 MIME 类型如text/plain是什么都会尝试将其解析为 JSON
forcedJSONParsing: true,
// 控制是否在超时错误中提供更明确的错误信息。在启用时,将提供更具体的错误消息,指示发生的超时错误类型
clarifyTimeoutError: false,
},
timeout: 0,
maxBodyLength: -1,
validateStatus: (status: number) => {
return status >= 200 && status < 300;
},
headers: {
common: {
Accept: 'application/json, text/plain, */*',
},
},
};
export default defaultConfig;

View File

@ -0,0 +1,211 @@
import getMergedConfig from '../utils/configUtils/getMergedConfig';
import HrHeaders from './HrHeaders';
import InterceptorManager from '../interceptor/InterceptorManager';
import processRequest from '../request/processRequest';
import getRequestInterceptorsInfo from '../interceptor/getRequestInterceptorsInfo';
import getResponseInterceptorChain from '../interceptor/getResponseInterceptorChain';
import handleAsyncInterceptor from '../interceptor/handleAsyncInterceptor';
import handleSyncInterceptor from '../interceptor/handleSyncInterceptor';
import defaultConfig from '../config/defaultConfig';
import { Method } from '../types/types';
import {
HrRequestConfig,
HrResponse,
HrInterface,
HrInstance,
Interceptors,
} from '../types/interfaces';
class HorizonRequest implements HrInterface {
defaultConfig: HrRequestConfig;
interceptors: Interceptors;
processRequest: (config: HrRequestConfig) => Promise<any>;
constructor(config: HrRequestConfig) {
this.defaultConfig = config;
// 初始化拦截器
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager(),
};
this.processRequest = processRequest;
}
request<T = unknown>(requestParam: string | Record<string, any>, config?: HrRequestConfig): Promise<HrResponse<T>> {
// 1. 解析参数
const mergedConfig = this.preprocessing(requestParam, config);
// 2. 生成请求拦截器
let isSync: boolean | undefined = true;
// 生成请求和响应的拦截器链
const requestInterceptorsInfo = getRequestInterceptorsInfo(this.interceptors, config, isSync);
const requestInterceptorChain = requestInterceptorsInfo.requestInterceptorChain;
isSync = requestInterceptorsInfo.isSync;
const responseInterceptorChain = getResponseInterceptorChain.call(this);
// 存在异步拦截器
if (!isSync) {
return handleAsyncInterceptor(
this.processRequest,
requestInterceptorChain,
responseInterceptorChain,
mergedConfig
);
}
// 全都是同步拦截器处理
return handleSyncInterceptor(
this.processRequest,
mergedConfig,
requestInterceptorChain,
responseInterceptorChain
);
}
private preprocessing(requestParam: string | Record<string, any>, config?: HrRequestConfig) {
let configOperation: Record<string, any> = {};
if (typeof requestParam === 'object') {
configOperation = { ...requestParam };
} else {
configOperation.url = requestParam;
configOperation = { ...configOperation, ...config };
}
const mergedConfig: HrRequestConfig = getMergedConfig(this.defaultConfig, configOperation);
mergedConfig.method = (mergedConfig.method || this.defaultConfig.method || 'GET').toUpperCase() as Method;
const { headers } = mergedConfig;
if (headers) {
const contextHeaders = headers[mergedConfig.method]
? { ...headers.common, ...headers[mergedConfig.method] }
: headers.common;
// 删除 headers 中预定义的 common 请求头,确保 headers 对象只包含自定义的请求头
if (contextHeaders) {
Object.keys(headers).forEach(key => {
if (key === 'common') {
delete headers[key];
}
});
}
mergedConfig.headers = HrHeaders.concat(contextHeaders, headers);
}
return mergedConfig;
}
get<T = unknown>(url: string, config: HrRequestConfig) {
return this.request<T>(
getMergedConfig(config || {}, {
method: 'get',
url,
data: (config || {}).data,
})
);
}
delete<T = unknown>(url: string, config: HrRequestConfig) {
return this.request<T>(
getMergedConfig(config || {}, {
method: 'delete',
url,
data: (config || {}).data,
})
);
}
head<T = unknown>(url: string, config: HrRequestConfig) {
return this.request<T>(
getMergedConfig(config || {}, {
method: 'head',
url,
data: (config || {}).data,
})
);
}
options<T = unknown>(url: string, config: HrRequestConfig) {
return this.request<T>(
getMergedConfig(config || {}, {
method: 'options',
url,
data: (config || {}).data,
})
);
}
post<T = unknown>(url: string, data: any, config: HrRequestConfig) {
return this.request<T>(
getMergedConfig(config || {}, {
method: 'post',
url,
data,
})
);
}
postForm<T = unknown>(url: string, data: any, config: HrRequestConfig) {
return this.request<T>(
getMergedConfig(config || {}, {
method: 'post',
headers: { 'Content-Type': 'multipart/form-data' },
url,
data,
})
);
}
put<T = unknown>(url: string, data: any, config: HrRequestConfig) {
return this.request<T>(
getMergedConfig(config || {}, {
method: 'put',
url,
data,
})
);
}
putForm<T = unknown>(url: string, data: any, config: HrRequestConfig) {
return this.request<T>(
getMergedConfig(config || {}, {
method: 'put',
headers: { 'Content-Type': 'multipart/form-data' },
url,
data,
})
);
}
patch<T = unknown>(url: string, data: any, config: HrRequestConfig) {
return this.request<T>(
getMergedConfig(config || {}, {
method: 'patch',
url,
data,
})
);
}
patchForm<T = unknown>(url: string, data: any, config: HrRequestConfig) {
return this.request<T>(
getMergedConfig(config || {}, {
method: 'patch',
headers: { 'Content-Type': 'multipart/form-data' },
url,
data,
})
);
}
// 创建 Hr 实例
static create(instanceConfig?: HrRequestConfig): HrInstance {
const config = getMergedConfig(defaultConfig, instanceConfig || {});
return new HorizonRequest(config) as unknown as HrInstance;
}
}
export default HorizonRequest;

View File

@ -0,0 +1,102 @@
import utils from '../utils/commonUtils/utils';
import { HrErrorInterface, HrInstance, HrRequestConfig, HrResponse } from '../types/interfaces';
class HrError extends Error implements HrErrorInterface {
code?: string;
config?: HrRequestConfig;
request?: HrInstance;
response?: HrResponse;
constructor(message: string, code?: string, config?: HrRequestConfig, request?: any, response?: HrResponse) {
super(message);
this.message = message;
this.name = 'HrError';
this.code = code;
this.config = config;
this.request = request;
this.response = response;
}
toJSON() {
return {
message: this.message,
name: this.name,
config: utils.toJSONSafe(this.config as Record<string, any>),
code: this.code,
status: this.response && this.response.status ? this.response.status : null,
};
}
// 从现有的 Error 对象创建一个新的 HrError 对象
static from(
error: Error,
code: string,
config: HrRequestConfig,
request: any,
response: HrResponse,
customProps?: Record<string, any>
): HrError {
// 由传入的 Error 对象属性初始化 HrError 实例化对象
const hrError = new HrError(error.message, code, config, request, response);
// 将现有的 Error 对象的属性复制到新创建的对象中
utils.flattenObject(
error,
hrError,
obj => obj !== Error.prototype,
prop => prop !== 'isHrError'
);
// 设置基本错误类型属性
hrError.name = error.name;
if (customProps) {
Object.assign(hrError, customProps);
}
return hrError;
}
}
// 在 HrError 类的原型链中添加 Error 类的原型,使 HrError 成为 Error 的子类
Object.setPrototypeOf(HrError.prototype, Error.prototype);
Object.defineProperties(HrError.prototype, {
toJSON: {
value: HrError.prototype.toJSON,
},
isHrError: {
value: true,
},
});
const errorTypes = [
'ERR_BAD_OPTION_VALUE',
'ERR_BAD_OPTION',
'ECONNABORTED',
'ETIMEDOUT',
'ERR_NETWORK',
'ERR_FR_TOO_MANY_REDIRECTS',
'ERR_DEPRECATED',
'ERR_BAD_RESPONSE',
'ERR_BAD_REQUEST',
'ERR_CANCELED',
'ERR_NOT_SUPPORT',
'ERR_INVALID_URL',
];
const descriptors: PropertyDescriptorMap = errorTypes.reduce((acc, code) => {
acc[code] = { value: code };
return acc;
}, {});
// 将 descriptors 对象中定义的属性添加到 HrError 类上
Object.defineProperties(HrError, descriptors);
// 在 HrError 类的原型上定义了一个名为 isHrError 的属性,用于判断错误对象是否为 HrError
Object.defineProperty(HrError.prototype, 'isHrError', { value: true });
// 判断输入值是否为 HrError
export const isHrError = (value: any) => !!value.isHrError;
export default HrError;

View File

@ -0,0 +1,227 @@
import utils from '../utils/commonUtils/utils';
import convertRawHeaders from '../utils/headerUtils/convertRawHeaders';
import { HeaderMatcher } from '../types/types';
import checkHeaderName from '../utils/headerUtils/checkHeaderName';
import processValueByParser from '../utils/headerUtils/processValueByParser';
import deleteHeader from '../utils/headerUtils/deleteHeader';
class HrHeaders {
// 定义 HrHeaders 类索引签名
[key: string]: any;
constructor(headers?: Record<string, string | string[]> | HrHeaders) {
// 将默认响应头加入 HrHeaders
this.defineAccessor();
if (headers) {
this.set(headers);
}
}
private _setHeader(
header: Record<string, string | string[]> | HrHeaders | string,
_value: string | string[],
_header: string
) {
const normalizedHeader = String(header).trim().toLowerCase();
const key = utils.getObjectKey(this, normalizedHeader);
// this[key] 可能为 false
if (!key || this[key] === undefined) {
this[key || _header] = utils.getNormalizedValue(_value);
}
};
private _setHeaders(headers: Record<string, string | string[]> | HrHeaders | string) {
return utils.forEach(headers, (_value: string | string[], _header: string) => {
return this._setHeader(headers, _value, _header);
});
}
set(header: Record<string, string | string[]> | HrHeaders | string): this {
// 通过传入的 headers 创建 HrHeaders 对象
if (utils.checkPlainObject(header) || header instanceof this.constructor) {
this._setHeaders(header);
} else if (utils.checkString(header) && (header = header.trim()) && !checkHeaderName(header as string)) {
this._setHeaders(convertRawHeaders(header as string));
} else {
if (header) {
this._setHeader(header, header as string, header as string);
}
}
return this;
}
// 从对象中获取指定 header 的值并根据可选的parser参数来处理和返回这个值
get(header: string, parser?: HeaderMatcher): string | string[] | null | undefined {
const normalizedHeader = String(header).trim().toLowerCase();
if (!normalizedHeader) {
return;
}
const key = utils.getObjectKey(this, normalizedHeader);
if (!key) {
return;
}
const value = (this as any)[key];
return processValueByParser(key, value, parser);
}
has(header: string): boolean {
const normalizedHeader = String(header).trim().toLowerCase();
if (normalizedHeader) {
const key = utils.getObjectKey(this, normalizedHeader);
return !!(key && this[key] !== undefined);
}
return false;
}
delete(header: string | string[]): boolean {
if (Array.isArray(header)) {
return header.some(deleteHeader, this);
} else {
return deleteHeader.call(this, header);
}
}
clear(): boolean {
const keys = Object.keys(this);
let deleted = false;
for (const key of keys) {
delete this[key];
deleted = true;
}
return deleted;
}
concat(...items: (Record<string, string | string[]> | HrHeaders)[]): HrHeaders {
return HrHeaders.concat(this, ...items);
}
toJSON(arrayToStr?: boolean): Record<string, string | string[]> {
// 过滤无意义的转换
const entries = Object.entries(this).filter(([_, value]) => {
return value != null && value !== false;
});
const mappedEntries = entries.map(([header, value]) => {
// 配置 arrayToStr 将 value 对应的数组值转换成逗号分隔,如 "hobbies": ["reading", "swimming", "hiking"] -> "hobbies": "reading, swimming, hiking"
return [header, arrayToStr && Array.isArray(value) ? value.join(', ') : value];
});
return Object.fromEntries(mappedEntries);
}
toString(): string {
const entries = this.toJSON();
return Object.keys(entries).reduce((acc, header) => {
return acc + header + ': ' + entries[header] + '\n';
}, '');
}
normalize(): this {
// 存储已处理过的 header
const headers: Record<string, boolean> = {};
for (const header in this) {
if (Object.prototype.hasOwnProperty.call(this, header)) {
const value = this[header];
const key = utils.getObjectKey(headers, header);
// 若 key 存在,说明当前遍历到的 header 已经被处理过
if (key) {
this[key] = utils.getNormalizedValue(value);
// header 和 key 不相等key 是忽略大小写的,所以需要删除处理前的 header
delete this[header];
continue;
}
const normalizedHeader = header.trim();
if (normalizedHeader !== header) {
delete this[header];
}
this[normalizedHeader] = utils.getNormalizedValue(value);
headers[normalizedHeader] = true;
}
}
return this;
}
defineAccessor() {
// 用于标记当前响应头访问器是否已添加
const accessors = {};
// 定义默认头部
const defaultHeaders = ['Content-Type', 'Content-Length', 'Accept', 'Accept-Encoding', 'User-Agent'];
// 将默认响应头加入 HrHeaders
defaultHeaders.forEach(header => {
if (!accessors[header]) {
Object.defineProperty(this, header, {
writable: true,
enumerable: true,
configurable: true,
});
accessors[header] = true;
}
});
}
static from(thing: Record<string, string | string[]> | HrHeaders): HrHeaders {
if (thing instanceof HrHeaders) {
return thing;
} else {
const newInstance = new HrHeaders(thing);
// 删除值为 undefined 请求头, fetch 进行自动配置
for (const key in newInstance) {
if (newInstance[key] === undefined) {
delete newInstance[key];
}
}
return newInstance;
}
}
static concat(
firstItem: Record<string, string | string[]> | HrHeaders,
...otherItems: (Record<string, string | string[]> | HrHeaders)[]
): HrHeaders {
// 初始化一个 HrHeaders 对象实例
const newInstance = new HrHeaders(firstItem);
const mergedObject = Object.assign({}, newInstance, ...otherItems);
for (const key in mergedObject) {
if (Object.prototype.hasOwnProperty.call(mergedObject, key)) {
newInstance[key] = mergedObject[key];
}
}
// 删除值为 undefined 请求头, fetch 进行自动配置
for (const key in newInstance) {
if (newInstance[key] === undefined) {
delete newInstance[key];
}
}
return newInstance;
}
}
export default HrHeaders;

View File

@ -0,0 +1,146 @@
import horizonRequest from '../../horizonRequest';
import { CacheItem, HrRequestConfig, Limitation, QueryOptions } from '../../types/interfaces';
import utils from "../../utils/commonUtils/utils";
// 兼容 IE 上没有 CustomEvent 对象
function createCustomEvent(eventName: string, options?: Record<string, any>) {
options = options || { bubbles: false, cancelable: false, detail: null };
const event = document.createEvent('CustomEvent');
event.initCustomEvent(eventName, options.bubbles, options.cancelable, options.detail);
return event;
}
class HRClient {
private cache: Map<string, CacheItem> = new Map();
private historyData: string[] = [];
public requestEvent = utils.isIE() ? createCustomEvent('request') : new CustomEvent('request');
public async query(url: string, config?: HrRequestConfig, options: QueryOptions = {}): Promise<any> {
const {
pollingInterval,
enablePollingOptimization,
limitation,
capacity = 100,
windowSize = 5,
} = options;
let cacheItem = this.cache.get(url);
if (cacheItem && pollingInterval && Date.now() - cacheItem.lastUpdated < pollingInterval) {
return cacheItem.data; // 返回缓存中的数据
}
const response = await horizonRequest.get(url, config);
const data = response.data;
// 如果轮询已配置,设置一个定时器
if (pollingInterval) {
if (cacheItem && cacheItem.timeoutId) {
clearTimeout(cacheItem.timeoutId); // 清除已存在的定时器
}
let optimizedInterval;
// 如果启用动态缓存策略
if (enablePollingOptimization) {
optimizedInterval = this.getDynamicInterval(pollingInterval, windowSize, limitation);
}
const timeoutId = setInterval(async () => {
const result = await this.query(url, config, options); // 执行轮询查询
document.dispatchEvent(new CustomEvent('request', { detail: result }));
}, optimizedInterval ?? pollingInterval);
cacheItem = {
data,
lastUpdated: Date.now(),
pollingInterval: optimizedInterval ?? pollingInterval,
timeoutId,
};
// 保存历史数据,以便动态缓存策略分析
this.historyData.push(data as string);
// 历史数据超过配置容量便老化最早的数据
if (this.historyData.length > capacity) {
this.historyData.unshift();
}
} else {
cacheItem = {
data,
lastUpdated: Date.now(),
};
}
// 更新缓存
this.cache.set(url, cacheItem);
return data;
}
// 计算滑动窗口内相邻字符串的相同个数
private countAdjacentMatches(data: string[]): number {
let count = 0;
for (let i = 0; i < data.length - 1; i++) {
if (data[i] === data[i + 1]) {
count++;
}
}
return count;
}
// 启用动态缓存策略pollingInterval 作为初始轮询时间并分析历史请求数据计算最优轮询时间减缓服务器压力
private getDynamicInterval(pollingInterval: number, windowSize: number, limitation?: Limitation): number {
// 历史数据量过少时,使用用户配置的初始轮询时间
if (this.historyData.length <= windowSize + 5) {
return pollingInterval;
}
const minInterval = limitation?.minInterval ?? 100; // 最小时间间隔
const maxInterval = limitation?.maxInterval ?? 60000; // 最大时间间隔
// PID 控制器初始化参数
const Kp = 0.2; // 比例常数
const Ki = 0.2; // 积分常数
const Kd = 0.1; // 微分常数
const targetCount = windowSize - 1; // 目标结果数量
let lastError = 0; // 上一个错误变量初始值设为0
// 计算总窗口数量
const numWindows = this.historyData.length - windowSize + 1;
// 根据每个滑动窗口内相邻字符串的相同个数来反馈控制调整请求间隔
for (let i = 0; i < numWindows; i++) {
// 获取当前窗口的数据
const windowData = this.historyData.slice(i, i + windowSize);
const windowCount = this.countAdjacentMatches(windowData);
// 根据PID控制器计算调整量
const error = windowCount - targetCount;
const output = Kp * error + Ki * windowCount + Kd * (error - lastError);
// 根据调整量更新请求间隔
pollingInterval += output * 100;
// 更新上一个错误变量
lastError = error;
}
// 限制时间间隔的范围在最小和最大值之间
pollingInterval = Math.max(pollingInterval, minInterval);
pollingInterval = Math.min(pollingInterval, maxInterval);
return pollingInterval;
}
public invalidateCache(url: string): void {
const cacheItem = this.cache.get(url);
if (cacheItem && cacheItem.timeoutId) {
clearTimeout(cacheItem.timeoutId); // 清除定时器
}
this.cache.delete(url); // 从缓存中删除条目
}
}
export default HRClient;

View File

@ -0,0 +1,43 @@
import Horizon from '@cloudsop/horizon';
import HRClient from './HRClient';
import { HrRequestConfig, QueryOptions } from '../../types/interfaces';
// 全局初始化一个 HRClient 实例
const hrClient = new HRClient();
const useHR = <T = unknown>(url: string, config?: HrRequestConfig, options?: QueryOptions): { data?: T; error?: any } => {
const [data, setData] = Horizon.useState<T>(null as unknown as T);
const [error, setError] = Horizon.useState<any>(null);
function handleRequest(result: any) {
return (event: any) => {
result = event.detail;
setData(result);
};
}
Horizon.useEffect(() => {
const fetchData = async () => {
try {
let result = await hrClient.query(url, config, options);
document.addEventListener('request', handleRequest(result));
setData(result); // 未设置轮询查询时展示一次
} catch (err) {
setError(err);
}
};
fetchData().catch(() => {}); // catch作用是消除提示
// 清除缓存
return () => {
hrClient.invalidateCache(url);
document.removeEventListener('request', handleRequest);
};
}, [url, config]);
return { data, error };
};
export default useHR;

View File

@ -0,0 +1,68 @@
import utils from '../utils/commonUtils/utils';
import HrHeaders from '../core/HrHeaders';
import getJSONByFormData from '../utils/dataUtils/getJSONByFormData';
import getFormData from '../utils/dataUtils/getFormData';
import { Strategy } from '../types/types';
// 策略映射,用于根据数据类型处理和转换请求数据
const strategies: Record<string, Strategy> = {
HTMLForm: data => {
return new FormData(data);
},
FormData: (data, headers, hasJSONContentType: boolean) => {
return hasJSONContentType ? JSON.stringify(getJSONByFormData(data)) : data;
},
StreamOrFileOrBlob: (data, headers) => {
return data;
},
URLSearchParams: (data, headers) => {
headers['Content-Type'] = headers['Content-type'] ?? 'application/x-www-form-urlencoded;charset=utf-8';
return data.toString();
},
MultipartFormData: (data, headers, isFileList: boolean) => {
return getFormData(isFileList ? { 'files[]': data } : data);
},
JSONData: (data, headers) => {
headers['Content-Type'] = headers['Content-Type'] ?? 'application/json';
return utils.stringifySafely(data);
},
};
function transformRequest(data: any, headers: HrHeaders): any {
const contentType = headers['Content-Type'] || '';
const hasJSONContentType = contentType.indexOf('application/json') > -1;
const isObjectPayload = utils.checkObject(data);
if (isObjectPayload && utils.checkHTMLForm(data)) {
return strategies.HTMLForm(data, headers);
}
if (utils.checkFormData(data)) {
return strategies.FormData(data, headers, hasJSONContentType);
}
if (utils.checkStream(data) || utils.checkFile(data) || utils.checkBlob(data)) {
return strategies.StreamOrFileOrBlob(data, headers);
}
if (utils.checkURLSearchParams(data)) {
return strategies.URLSearchParams(data, headers);
}
let isFileList: boolean;
if (isObjectPayload) {
if ((isFileList = utils.checkFileList(data)) || contentType.indexOf('multipart/form-data') > -1) {
return strategies.MultipartFormData(data, headers, isFileList);
}
}
if (isObjectPayload || hasJSONContentType) {
return strategies.JSONData(data, headers);
}
return data;
}
export default transformRequest;

View File

@ -0,0 +1,34 @@
import { HrRequestConfig, HrResponse, TransitionalOptions } from '../types/interfaces';
import HrError from '../core/HrError';
import defaultConfig from '../config/defaultConfig';
// this 需要拿到上下文的configprocessRequest 是动态调用的,直接将 config 当参数传入会拿到错误的 config
function transformResponse<T>(this: HrRequestConfig, data: any): T | string | null {
const transitional: TransitionalOptions = this.transitional || defaultConfig.transitional;
// 判断是否需要强制 JSON 解析
const enableForcedJSONParsing: boolean | undefined = transitional && transitional.forcedJSONParsing;
const isJSON: boolean = this.responseType === 'json';
// 如果数据存在且为字符串类型,并且请求的响应类型为 JSON 则进行强制解析
if (data && typeof data === 'string' && ((enableForcedJSONParsing && !this.responseType) || isJSON)) {
// 解析 JSON 失败是否抛出异常
const enableSilentJSONParsing: boolean | undefined = transitional && transitional.silentJSONParsing;
const enableStrictJSONParsing: boolean = !enableSilentJSONParsing && isJSON;
try {
return JSON.parse(data);
} catch (error) {
if (enableStrictJSONParsing) {
if ((error as Error).name !== 'SyntaxError') {
throw HrError.from(error as Error, (HrError as any).ERR_BAD_RESPONSE, this, null, (this as any).response); // 使用拦截器可能会将 response 写入 config 中
}
throw error;
}
}
}
return data;
}
export default transformResponse;

View File

@ -0,0 +1,61 @@
import HorizonRequest from './core/HorizonRequest';
import utils from './utils/commonUtils/utils';
import { CancelTokenStatic, HrInterface, HrRequestConfig } from './types/interfaces';
import defaultConfig from './config/defaultConfig';
import fetchLike from './request/ieCompatibility/fetchLike';
import CancelToken from './cancel/CancelToken';
import checkCancel from './cancel/checkCancel';
import HrError, { isHrError } from './core/HrError';
import buildInstance from './utils/instanceUtils/buildInstance';
import HrHeaders from './core/HrHeaders';
import CancelError from './cancel/CancelError';
import 'core-js/stable';
// 使用默认配置创建 hr 对象实例
const horizonRequest = buildInstance(defaultConfig as HrRequestConfig);
// 提供 Hr 类继承
horizonRequest.HorizonRequest = HorizonRequest as unknown as HrInterface;
// 创建 hr 实例的工厂函数
horizonRequest.create = HorizonRequest.create;
// 提供取消请求令牌
horizonRequest.CancelToken = CancelToken as CancelTokenStatic;
horizonRequest.isCancel = checkCancel;
horizonRequest.Cancel = CancelError;
horizonRequest.all = utils.all;
horizonRequest.spread = utils.spread;
horizonRequest.default = horizonRequest;
horizonRequest.CanceledError = CancelError;
horizonRequest.HrError = HrError;
horizonRequest.isHrError = isHrError;
horizonRequest.HrHeaders = HrHeaders;
horizonRequest.defaults = defaultConfig as HrRequestConfig;
/*--------------------------------兼容axios-----------------------------------*/
horizonRequest.Axios = HorizonRequest;
horizonRequest.AxiosError = HrError;
horizonRequest.isAxiosError = isHrError;
horizonRequest.AxiosHeaders = HrHeaders;
export default horizonRequest;
// 兼容 IE 浏览器 fetch
if (utils.isIE()) {
(window as any).fetch = fetchLike;
}

View File

@ -0,0 +1,45 @@
import utils from '../utils/commonUtils/utils';
import { InterceptorHandler, HrInterceptorManager } from '../types/interfaces';
import { FulfilledFn, RejectedFn } from '../types/types';
class InterceptorManager<V> implements HrInterceptorManager<V> {
private handlers: (InterceptorHandler<V> | null)[];
constructor() {
this.handlers = [];
}
use(
fulfilled?: FulfilledFn<V>,
rejected?: RejectedFn,
options?: { synchronous?: boolean; runWhen?: (value: V) => boolean }
): number {
this.handlers.push({
fulfilled,
rejected,
synchronous: options ? options.synchronous : false,
runWhen: options ? options.runWhen : undefined,
});
return this.handlers.length - 1;
}
eject(id: number): void {
if (this.handlers[id]) {
this.handlers[id] = null;
}
}
clear(): void {
this.handlers = [];
}
forEach(func: Function) {
utils.forEach(this.handlers, function forEachHandler(h: any) {
if (h !== null) {
func(h);
}
});
}
}
export default InterceptorManager;

View File

@ -0,0 +1,26 @@
import { HrRequestConfig, InterceptorHandler, Interceptors } from '../types/interfaces';
import { FulfilledFn } from '../types/types';
// 获取请求拦截器链以及是否异步信息
function getRequestInterceptorsInfo(
interceptors: Interceptors,
config: HrRequestConfig | undefined,
isSync: boolean | undefined
) {
const requestInterceptorChain: (FulfilledFn<any> | undefined)[] = [];
interceptors.request.forEach((interceptor: InterceptorHandler<any>) => {
if (typeof interceptor.runWhen === 'function' && !interceptor.runWhen(config)) {
return;
}
// 只要有一个异步拦截器则拦截器链为异步
isSync = isSync && interceptor.synchronous;
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
return { requestInterceptorChain, isSync: isSync };
}
export default getRequestInterceptorsInfo;

View File

@ -0,0 +1,13 @@
import { FulfilledFn } from '../types/types';
import { InterceptorHandler } from '../types/interfaces';
function getResponseInterceptorChain(this: any) {
const responseInterceptorChain: (FulfilledFn<any> | undefined)[] = [];
this.interceptors.response.forEach((interceptor: InterceptorHandler<any>) => {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
return responseInterceptorChain;
}
export default getResponseInterceptorChain;

View File

@ -0,0 +1,23 @@
import { FulfilledFn } from '../types/types';
import { HrRequestConfig } from '../types/interfaces';
// 处理含有异步拦截器情况
function handleAsyncInterceptor(
processFunc: (value: any) => any,
requestInterceptorChain: (FulfilledFn<any> | undefined)[],
responseInterceptorChain: (FulfilledFn<any> | undefined)[],
mergedConfig: HrRequestConfig
): Promise<any> {
// undefined 占位 rejected 回调函数
const chain = [...requestInterceptorChain, processFunc, undefined, ...responseInterceptorChain];
let promise = Promise.resolve(mergedConfig);
for (let i = 0; i < chain.length; i += 2) {
// 将拦截器使用Promise链进行链接
promise = promise.then(chain[i], chain[i + 1]);
}
return promise;
}
export default handleAsyncInterceptor;

View File

@ -0,0 +1,41 @@
import { FulfilledFn } from '../types/types';
// 处理同步拦截器
function handleSyncInterceptor(
processFunc: (value: any) => any,
mergedConfig: any,
requestInterceptorChain: (FulfilledFn<any> | undefined)[],
responseInterceptorChain: (FulfilledFn<any> | undefined)[]
): Promise<any> {
let newConfig = mergedConfig;
let promise;
for (let i = 0; i < requestInterceptorChain.length; i += 2) {
const fulfilled = requestInterceptorChain[i];
const rejected = requestInterceptorChain[i + 1];
// 返回拦截器处理后的 newConfig
try {
newConfig = fulfilled ? fulfilled(newConfig) : newConfig;
} catch (error) {
if (rejected) {
rejected(error);
}
break;
}
}
try {
promise = processFunc(newConfig);
} catch (error) {
return Promise.reject(error);
}
for (let i = 0; i < responseInterceptorChain.length; i += 2) {
promise = promise.then(responseInterceptorChain[i], responseInterceptorChain[i + 1]);
}
return promise;
}
export default handleSyncInterceptor;

View File

@ -0,0 +1,166 @@
import utils from '../utils/commonUtils/utils';
import HrError from '../core/HrError';
import { HrRequestConfig, HrResponse, Cancel } from '../types/interfaces';
import { Method, ResponseType } from '../types/types';
import processUploadProgress from './processUploadProgress';
import processDownloadProgress from './processDownloadProgress';
export const fetchRequest = (config: HrRequestConfig): Promise<HrResponse> => {
return new Promise((resolve, reject) => {
let {
method = 'GET',
baseURL,
url,
params = null,
data = null,
headers = {},
responseType,
timeout = 0,
timeoutErrorMessage,
cancelToken = null,
withCredentials = false,
onUploadProgress = null,
onDownloadProgress = null,
} = config;
let controller = new AbortController();
let signal = controller.signal;
// 处理请求取消
if (cancelToken) {
cancelToken.promise.then((reason: Cancel) => {
controller.abort();
reject(reason);
});
}
// 拼接URL
if (baseURL) {
url = `${baseURL}${url}`;
}
// 处理请求参数
if (params) {
const queryString = utils.objectToQueryString(utils.filterUndefinedValues(params));
url = `${url}${url!.includes('?') ? '&' : '?'}${queryString}`; // 支持用户将部分请求参数写在 url 中
}
// GET HEAD 方法不允许设置 body
if (method === 'GET' || method === 'HEAD') {
data = null;
}
const options = {
method,
headers,
body: data || null, // 防止用户在拦截器传入空字符串,引发 fetch 错误
signal,
credentials: withCredentials ? 'include' : 'omit',
};
if (timeout) {
setTimeout(() => {
controller.abort();
const errorMsg = timeoutErrorMessage ?? `timeout of ${timeout}ms exceeded`;
const error = new HrError(errorMsg, '', config, undefined, undefined);
reject(error);
}, timeout);
}
if (!url) {
return Promise.reject('URL is undefined!');
}
if (onUploadProgress) {
processUploadProgress(onUploadProgress, data, reject, resolve, method, url, config);
} else {
fetch(url, options as RequestInit)
.then(response => {
// 将 Headers 对象转换为普通 JavaScript 对象,可以使用 [] 访问具体响应头
const headersObj = {};
response.headers.forEach((value, name) => {
headersObj[name] = value;
});
config.method = config.method!.toLowerCase() as Method;
const responseData: HrResponse = {
data: '',
status: response.status,
statusText: response.statusText,
headers: headersObj,
config,
request: null,
};
const responseBody = onDownloadProgress
? processDownloadProgress(response.body, response, onDownloadProgress)
: response.body;
// 根据 responseType 选择相应的解析方法
let parseMethod;
switch (responseType as ResponseType) {
case 'arraybuffer':
parseMethod = new Response(responseBody).arrayBuffer();
break;
case 'blob':
parseMethod = new Response(responseBody).blob();
break;
// text 和 json 服务端返回的都是字符串 统一处理
case 'text':
parseMethod = new Response(responseBody).text();
break;
case 'json':
parseMethod = new Response(responseBody).text().then((text: string) => {
try {
return JSON.parse(text);
} catch (e) {
// 显式指定返回类型 JSON解析失败报错
reject('parse error');
}
});
break;
default:
parseMethod = new Response(responseBody).text().then((text: string) => {
try {
return JSON.parse(text);
} catch (e) {
// 默认为 JSON 类型若JSON校验失败则直接返回服务端数据
return text;
}
});
}
parseMethod
.then((parsedData: any) => {
responseData.data = parsedData;
if (responseData.config.validateStatus!(responseData.status)) {
resolve(responseData);
} else {
const error = new HrError(responseData.statusText, '', responseData.config, responseData.request, responseData);
reject(error);
}
})
.catch((error: HrError) => {
if (error.name === 'AbortError') {
reject(error.message);
} else {
reject(error);
}
});
})
.catch((error: HrError) => {
if (error.name === 'AbortError') {
reject(error.message);
} else {
reject(error);
}
});
}
});
};

View File

@ -0,0 +1,19 @@
import CustomAbortSignal from './CustomAbortSignal';
class CustomAbortController {
private readonly _signal: CustomAbortSignal;
constructor() {
this._signal = new CustomAbortSignal();
}
get signal(): CustomAbortSignal {
return this._signal;
}
abort(): void {
this._signal.abort();
}
}
export default CustomAbortController;

View File

@ -0,0 +1,30 @@
class CustomAbortSignal {
private _isAborted: boolean;
private _listeners: Set<() => void>;
constructor() {
this._isAborted = false;
this._listeners = new Set();
}
get aborted(): boolean {
return this._isAborted;
}
addEventListener(listener: () => void): void {
this._listeners.add(listener);
}
removeEventListener(listener: () => void): void {
this._listeners.delete(listener);
}
abort(): void {
if (!this._isAborted) {
this._isAborted = true;
this._listeners.forEach(listener => listener());
}
}
}
export default CustomAbortSignal;

View File

@ -0,0 +1,35 @@
class CustomHeaders {
private _headers: Map<string, string>;
constructor(headers?: Record<string, string>) {
this._headers = new Map<string, string>();
if (headers) {
for (const key of Object.keys(headers)) {
this._headers.set(key.toLowerCase(), headers[key]);
}
}
}
has(name: string): boolean {
return this._headers.has(name.toLowerCase());
}
get(name: string): string | null {
const headerValue = this._headers.get(name.toLowerCase());
return headerValue !== undefined ? headerValue : null;
}
set(name: string, value: string): void {
this._headers.set(name.toLowerCase(), value);
}
delete(name: string): void {
this._headers.delete(name.toLowerCase());
}
forEach(callback: (value: string, name: string, parent: Map<string, string>) => void): void {
this._headers.forEach(callback);
}
}
export default CustomHeaders;

View File

@ -0,0 +1,35 @@
import CustomHeaders from './CustomHeaders';
class CustomResponse {
private readonly _body: string;
private readonly _status: number;
private readonly _headers: Headers;
constructor(body: string, init?: { status: number; headers?: Record<string, string> }) {
this._body = body;
this._status = init?.status || 200;
this._headers = new CustomHeaders(init?.headers) as any;
}
get status() {
return this._status;
}
get ok() {
return this.status >= 200 && this.status < 300;
}
get headers() {
return this._headers;
}
text(): Promise<string> {
return Promise.resolve(this._body);
}
json(): Promise<any> {
return Promise.resolve(JSON.parse(this._body));
}
}
export default CustomResponse;

View File

@ -0,0 +1,33 @@
import { FetchOptions } from '../../types/interfaces';
import CustomResponse from './CustomResponse';
function fetchLike(url: string, options: FetchOptions = {}): Promise<Response> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const { method = 'GET', headers = {}, body = null } = options;
xhr.open(method, url, true);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(new CustomResponse(xhr.responseText, { status: xhr.status }) as any);
} else {
reject(new Error(`Request failed with status ${xhr.status}`));
}
}
};
xhr.onerror = () => {
reject(new Error('Network error'));
};
Object.keys(headers).forEach(key => {
xhr.setRequestHeader(key, headers[key]);
});
xhr.send(body);
});
}
export default fetchLike;

View File

@ -0,0 +1,149 @@
import utils from '../utils/commonUtils/utils';
import HrError from '../core/HrError';
import CustomAbortController from './ieCompatibility/CustomAbortController';
import { HrRequestConfig, HrResponse, Cancel } from '../types/interfaces';
import { Method, ResponseType } from '../types/types';
export const ieFetchRequest = (config: HrRequestConfig): Promise<HrResponse> => {
return new Promise((resolve, reject) => {
let {
method = 'get',
baseURL,
url,
params = null,
data = null,
headers = {},
responseType,
timeout = 0,
timeoutErrorMessage,
cancelToken = null,
withCredentials = false,
} = config;
let controller: any;
let signal;
// 兼容处理 IE 浏览器AbortController
if (window.AbortController) {
controller = new AbortController();
signal = controller.signal;
} else {
controller = new CustomAbortController();
signal = controller.signal;
}
// 处理请求取消
if (cancelToken) {
cancelToken.promise.then((reason: Cancel) => {
controller.abort();
reject(reason);
});
}
// 拼接URL
if (baseURL) {
url = `${baseURL}${url}`;
}
// 处理请求参数
if (params) {
const queryString = utils.objectToQueryString(params);
url = `${url}?${queryString}`;
}
// GET HEAD 方法不允许设置body
const options = {
method,
headers,
body: data || null, // 防止用户在拦截器传入空字符串,引发 fetch 错误
signal,
credentials: withCredentials ? 'include' : 'omit',
};
if (timeout) {
setTimeout(() => {
controller.abort();
reject(new Error(timeoutErrorMessage ?? `timeout of ${timeout}ms exceeded`));
}, timeout);
}
if (!url) {
return Promise.reject('URL is undefined!');
}
fetch(url, options as RequestInit)
.then(response => {
config.method = config.method!.toLowerCase() as Method;
const responseData: HrResponse = {
data: '',
status: response.status,
statusText: response.statusText,
headers: response.headers,
config,
request: null,
};
// 根据 responseType 选择相应的解析方法
let parseMethod;
switch (responseType as ResponseType) {
case 'arraybuffer':
parseMethod = response.arrayBuffer();
break;
case 'blob':
parseMethod = response.blob();
break;
// text 和 json 服务端返回的都是字符串 统一处理
case 'text':
parseMethod = response.text();
break;
// 显式指定返回类型
case 'json':
parseMethod = response.text().then((text: string) => {
try {
return JSON.parse(text);
} catch (e) {
// 显式指定返回类型 JSON解析失败报错
reject('parse error');
}
});
break;
default:
parseMethod = response.text().then((text: string) => {
try {
return JSON.parse(text);
} catch (e) {
// 默认为 JSON 类型若JSON校验失败则直接返回服务端数据
return text;
}
});
}
parseMethod
.then((parsedData: any) => {
responseData.data = parsedData;
resolve(responseData);
})
.catch((error: HrError) => {
if (error.name === 'AbortError') {
reject(error.message);
} else {
reject(error);
}
});
})
.catch((error: HrError) => {
if (error.name === 'AbortError') {
reject(error.message);
} else {
reject(error);
}
});
});
};

View File

@ -0,0 +1,31 @@
function processDownloadProgress(stream: ReadableStream | null, response: Response, onProgress: Function | null) {
// 文件下载过程中更新进度
if (onProgress) {
const reader = stream?.getReader();
let totalBytesRead = 0; // 跟踪已读取的字节数
return new ReadableStream({
start(controller) {
function read() {
reader?.read().then(({ done, value }) => {
if (done) {
controller.close();
return;
}
totalBytesRead += value.byteLength;
onProgress!({ loaded: totalBytesRead, total: response.headers.get('Content-Length') });
controller.enqueue(value); // 将读取到的数据块添加到新的 ReadableStream 中
read(); // 递归调用,继续读取 stream 直到结束
});
}
read(); // 调用 read 函数以启动从原始 stream 中读取数据的过程
},
});
} else {
return stream;
}
}
export default processDownloadProgress;

View File

@ -0,0 +1,54 @@
import CancelError from '../cancel/CancelError';
import { HrRequestConfig, HrResponse } from '../types/interfaces';
import HrHeaders from '../core/HrHeaders';
import transformData from '../utils/dataUtils/transformData';
import { fetchRequest } from './fetchRequest';
import transformRequest from '../dataTransformers/transformRequest';
import transformResponse from '../dataTransformers/transformResponse';
import checkCancel from '../cancel/checkCancel';
import { ieFetchRequest } from './ieFetchRequest';
import utils from '../utils/commonUtils/utils';
export default function processRequest(config: HrRequestConfig): Promise<HrResponse> {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
if (config.signal && config.signal.aborted) {
throw new CancelError(undefined, config);
}
// 拦截可能会传入普通对象
config.headers = HrHeaders.from(config.headers as Record<string, any>);
// 转换请求数据
if (config.data) {
config.data = transformData(config, transformRequest);
}
return (utils.isIE() ? ieFetchRequest : fetchRequest)(config)
.then(response => {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
// 转换响应数据
response.data = transformData(config, transformResponse, response);
return response;
})
.catch(error => {
if (!checkCancel(error)) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
// 转换响应数据
if (error && error.response) {
error.response.data = transformData(config, transformResponse, error.response);
}
}
return Promise.reject(error);
});
}

View File

@ -0,0 +1,94 @@
import { HrRequestConfig, HrResponse } from '../types/interfaces';
import HrError from "../core/HrError";
function processUploadProgress(
onUploadProgress: Function | null,
data: FormData,
reject: (reason?: any) => void,
resolve: (value: PromiseLike<HrResponse<any>> | HrResponse<any>) => void,
method: string,
url: string | undefined,
config: HrRequestConfig,
) {
if (onUploadProgress && data instanceof FormData) {
let totalBytesToUpload = 0; // 上传的总字节数
data.forEach(value => {
if (value instanceof Blob) {
totalBytesToUpload += value.size;
}
});
const handleUploadProgress = () => {
const xhr = new XMLHttpRequest();
// 添加 progress 事件监听器
xhr.upload.addEventListener('progress', event => {
if (event.lengthComputable) {
// 可以计算上传进度
onUploadProgress!({ loaded: event.loaded, total: event.total });
} else {
onUploadProgress!({ loaded: event.loaded, total: totalBytesToUpload });
}
});
// 添加 readystatechange 事件监听器,当 xhr.readyState 变更时执行回调函数
xhr.addEventListener('readystatechange', () => {
if (xhr.readyState === 4) {
let parsedText;
try {
switch (xhr.responseType) {
case 'json':
parsedText = JSON.parse(xhr.responseText);
break;
case 'text':
default:
parsedText = xhr.responseText;
}
} catch (e) {
reject('parse error');
}
const response: HrResponse = {
data: parsedText,
status: xhr.status,
statusText: xhr.statusText,
headers: xhr.getAllResponseHeaders(),
config: config,
}
if (config.validateStatus!(xhr.status)) {
// 如果 fetch 请求已经成功或者拒绝,则此处不生效
resolve(response);
} else {
const error = new HrError(xhr.statusText, '', config, xhr, response);
reject(error);
}
}
});
xhr.open(method, url as string);
if (config.timeout) {
xhr.timeout = config.timeout;
xhr.ontimeout = function () {
xhr.abort();
const errorMsg = config.timeoutErrorMessage ?? `timeout of ${config.timeout}ms exceeded`;
throw new HrError(errorMsg, '', config, xhr, undefined);
}
}
for (const header in config.headers) {
if (
!['Content-Length', 'Accept-Encoding', 'User-Agent', 'Content-Type'].includes(header) // 过滤不安全的请求头设置
&& Object.prototype.hasOwnProperty.call(config.headers, header) // 不遍历请求头原型上的方法
) {
xhr.setRequestHeader(header, config.headers[header]);
}
}
xhr.send(data);
};
handleUploadProgress(); // 启动文件上传过程
}
}
export default processUploadProgress;

View File

@ -0,0 +1,282 @@
import HrError from '../core/HrError';
import HrHeaders from '../core/HrHeaders';
import { Method, ResponseType, HrTransformer, FulfilledFn, RejectedFn, Callback } from './types';
import CancelError from "../cancel/CancelError";
import { CanceledError } from "../../index";
// 请求配置
export interface HrRequestConfig {
url?: string;
method?: Method;
// 公共URL前缀
baseURL?: string;
headers?: Record<string, any>;
params?: Record<string, any> | null;
data?: any;
timeout?: number;
// 超时错误消息
timeoutErrorMessage?: string;
// 是否发送凭据
withCredentials?: boolean;
// 响应类型
responseType?: ResponseType;
// 上传进度事件回调
onUploadProgress?: (progressEvent: any) => void;
// 下载进度事件回调
onDownloadProgress?: (progressEvent: any) => void;
// 请求取消令牌
cancelToken?: CancelToken;
signal?: AbortSignal;
// 过渡选项
transitional?: TransitionalOptions;
validateStatus?: (status: number) => boolean;
}
export interface TransitionalOptions {
// 是否忽略 JSON parse 的错误配置
silentJSONParsing?: boolean;
// 强制解析为 JSON 格式
forcedJSONParsing?: boolean;
// 请求超时异常错误配置
clarifyTimeoutError?: boolean;
}
// 请求响应
export type HrResponse<T = any> = {
// 响应数据
data: T;
// 响应状态码
status: number;
// 响应状态消息
statusText: string;
// 响应头
headers: any;
// 请求配置
config: HrRequestConfig;
// 请求对象
request?: any;
// 响应事件消息
event?: string;
};
// Hr 类接口类型
export interface HrInterface {
request<T = unknown>(url: string | Record<string, any>, config?: HrRequestConfig): Promise<HrResponse<T>>;
get<T = unknown>(url: string, config?: HrRequestConfig): Promise<HrResponse<T>>;
post<T = unknown>(url: string, data?: any, config?: HrRequestConfig): Promise<HrResponse<T>>;
put<T = unknown>(url: string, data?: any, config?: HrRequestConfig): Promise<HrResponse<T>>;
delete<T = unknown>(url: string, config?: HrRequestConfig): Promise<HrResponse<T>>;
head<T = unknown>(url: string, config?: HrRequestConfig): Promise<HrResponse<T>>;
options<T = unknown>(url: string, config?: HrRequestConfig): Promise<HrResponse<T>>;
postForm<T = unknown>(url: string, data: any, config: HrRequestConfig): Promise<HrResponse<T>>;
putForm<T = unknown>(url: string, data: any, config: HrRequestConfig): Promise<HrResponse<T>>;
patchForm<T = unknown>(url: string, data: any, config: HrRequestConfig): Promise<HrResponse<T>>;
}
// Hr 实例接口类型
export interface HrInstance extends HrInterface {
// Hr 类
HorizonRequest: HrInterface;
// 创建 Hr 实例
create: (config?: HrRequestConfig) => HrInstance;
// 使用内置的配置初始化实例属性
defaults: HrRequestConfig;
// 取消当前正在进行的请求
CancelToken: CancelTokenStatic;
// 判断是否请求取消
isCancel: (value: any) => boolean;
// CanceledError的别名用于向后兼容
Cancel: typeof CanceledError;
// 实例拦截请求
interceptors: Interceptors;
// 并发发送多个 HTTP 请求
all<T>(promises: Array<Promise<T>>): Promise<Array<T>>;
// 封装多个 Promise 至数组,便于作为 all 传入参数
spread: <T>(callback: Callback<T>) => (arr: any[]) => T;
// horizonRequest 对象的默认实例
default: HrInstance;
CanceledError: typeof CancelError;
// HrError 错误
HrError: typeof HrError;
// 判断输入值是否为 HrError
isHrError: (avl: any) => boolean;
// HrHeaders 响应头
HrHeaders: typeof HrHeaders;
useHR: <T = any>(url: string, config?: HrRequestConfig, options?: QueryOptions) => { data?: T; error?: any };
/*----------------兼容axios--------------------*/
Axios: any;
AxiosError: any;
isAxiosError: (val: any) => boolean;
AxiosHeaders: any;
}
export interface Interceptors {
request: HrInterceptorManager<HrRequestConfig>;
response: HrInterceptorManager<HrResponse>;
}
// 拦截器接口类型
export interface InterceptorHandler<T> {
// Promise 成功时,拦截器处理响应函数
fulfilled?: FulfilledFn<T>;
// Promise 拒绝时,拦截器处理响应值函数
rejected?: RejectedFn;
// 截器是否在单线程中执行
synchronous?: boolean;
// 拦截器何时被执行
runWhen?: (value: T) => boolean;
}
// 拦截器管理器接口类型
export interface HrInterceptorManager<T> {
// 添加拦截器
use(
fulfilled?: FulfilledFn<T>,
rejected?: RejectedFn,
options?: {
synchronous?: boolean;
runWhen?: (value: T) => boolean;
}
): number;
// 移除拦截器
eject(id: number): void;
// 清除拦截器
clear(): void;
// 过滤跳过迭代器
forEach(func: Function): void;
}
export interface HrErrorInterface {
// 产生错误的请求配置对象
config?: HrRequestConfig;
// 表示请求错误的字符串代码。例如,"ECONNABORTED"表示连接被中止。
code?: string;
// 产生错误的原始请求实例。
request?: HrInstance;
// 包含错误响应的响应实例。如果请求成功完成但服务器返回错误状态码例如404或500则此属性存在。
response?: HrResponse;
}
// 请求取消令牌
export interface CancelToken {
// 可取消的 Promise在超时时间 (或其他原因) 结束时被解决,并返回一个字符串值,该字符串值将作为取消请求的标识符
promise: Promise<Cancel>;
// 取消请求的标识符
reason?: Cancel;
// 如果请求被取消,则会抛出错误
throwIfRequested(): void;
}
export interface Cancel {
message?: string;
cancelFlag?: boolean;
}
interface CancelExecutor {
(cancel: Cancel): void;
}
interface CancelTokenSource {
token: CancelToken;
cancel: Cancel;
}
export interface CancelTokenStatic {
new (executor: CancelExecutor): CancelToken;
source(): CancelTokenSource;
}
// 兼容 IE fetchLike 接口类型
export interface FetchOptions {
method?: Method;
headers?: Record<string, string>;
body?: any;
}
// 轮询查询配置 轮询间隔(毫秒)
export interface QueryOptions {
pollingInterval?: number;
// 是否启用动态轮询策略
enablePollingOptimization?: boolean;
// 配置动态轮询策略后生效
limitation?: Limitation;
// 动态轮询策略分析历史数据容量默认100
capacity?: number;
// 动态轮询策略窗口大小默认5
windowSize?: number;
}
export interface Limitation {
minInterval: number,
maxInterval: number,
}
// useHR 缓存
export interface CacheItem {
data: any;
lastUpdated: number;
pollingInterval?: number;
timeoutId?: any;
}

View File

@ -0,0 +1,47 @@
import HrHeaders from '../core/HrHeaders';
import { HrRequestConfig, HrResponse } from './interfaces';
export type Method =
| 'get'
| 'GET'
| 'delete'
| 'DELETE'
| 'head'
| 'HEAD'
| 'options'
| 'OPTIONS'
| 'post'
| 'POST'
| 'put'
| 'PUT'
| 'patch'
| 'PATCH';
export type ResponseType = 'text' | 'json' | 'blob' | 'arraybuffer';
// 请求和响应数据转换器
export type HrTransformer = (data: any, headers?: HrHeaders) => any;
// Headers
export type HeaderMap = Record<string, string | string[]>;
export type HeaderMatcher = boolean | RegExp | Function;
// Promise 成功和拒绝类型
export type FulfilledFn<T> = (value: T) => T | Promise<T>; // 泛型确保了拦截器链中各个环节之间的一致性,避免数据类型不匹配引发的错误
export type RejectedFn = (error: any) => any;
// 过滤器
export type FilterFunc = (obj: Record<string, any>, destObj: Record<string, any>) => boolean;
export type PropFilterFunc = (prop: string | symbol, obj: Record<string, any>, destObj: Record<string, any>) => boolean;
export type ObjectDescriptor = PropertyDescriptorMap & ThisType<any>;
// Cancel
export type CancelFunction = (message?: string) => void;
export type CancelExecutor = (cancel: CancelFunction) => void;
export type Callback<T> = (...args: any[]) => T;
export type Strategy = {
(data: any, headers: HrHeaders, ...args: any[]): any;
};

View File

@ -0,0 +1,454 @@
import { Callback, FilterFunc, PropFilterFunc } from '../../types/types';
/**
* func this thisArg
*
* @param func
* @param thisArg
*
* @returns {any} func 使 apply() thisArg func
*/
function bind(func: Function, thisArg: any): (...args: any[]) => any {
return (...args: any[]) => func.apply(thisArg, args);
}
/**
*
*
* @param input
*
* @returns {string}
*/
function getType(input: any): string {
const str: string = Object.prototype.toString.call(input);
return str.slice(8, -1).toLowerCase();
}
/**
*
*
* @param type
*
* @returns {Function}
*/
const createTypeChecker = (type: string) => (input: any) => getType(input) === type.toLowerCase();
const checkString = createTypeChecker('string');
const checkFunction = createTypeChecker('function');
const checkNumber = createTypeChecker('number');
const checkObject = (input: any) => input !== null && typeof input === 'object';
const checkBoolean = (input: any) => input === true || input === false;
const checkUndefined = createTypeChecker('undefined');
/**
*
*
* @param input
*
* @returns {boolean} true false
*/
const checkPlainObject = (input: any) => {
if (Object.prototype.toString.call(input) !== '[object Object]') {
return false;
}
const prototype = Object.getPrototypeOf(input);
return prototype === null || prototype === Object.prototype;
};
const checkDate = createTypeChecker('Date');
const checkFile = createTypeChecker('File');
const checkBlob = createTypeChecker('Blob');
const checkStream = (input: any) => checkObject(input) && checkFunction(input.pipe);
const checkFileList = createTypeChecker('FileList');
const checkFormData = (input: any) => input instanceof FormData;
const checkURLSearchParams = createTypeChecker('URLSearchParams');
const checkRegExp = createTypeChecker('RegExp');
const checkHTMLForm = createTypeChecker('HTMLFormElement');
/**
*
*
* @param input
* @param func
* @param options
*
* @returns {void}
*
* @template T
*/
function forEach<T>(
input: T | T[] | Record<string, T> | null | undefined,
func: Function,
options: { includeAll?: boolean } = {}
): void {
if (input === null || input === undefined) {
return;
}
const { includeAll = false } = options;
if (typeof input !== 'object') {
input = [input] as T[];
}
if (Array.isArray(input)) {
(input as T[]).forEach((value, index, array) => {
func.call(null, value, index, array);
});
} else {
const keys = includeAll
? getAllPropertyNames(input as Record<string, T>)
: Object.keys(input as Record<string, T>);
keys.forEach(key => {
func.call(null, (input as Record<string, T>)[key], key, input!);
});
}
}
/**
* , null
*
* @param obj
* @param key
*
* @returns {string | null} null
*/
function getObjectKey<T>(obj: Record<string, T>, key: string): string | null {
const _key = key.toLowerCase();
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
if (k.toLowerCase() === _key) {
return k;
}
}
return null;
}
/**
*
*
* @param target
* @param source
* @param thisArg 便 `this`
* @param options
*
* @returns {Record<string, any>}
*/
function extendObject(
target: Record<string, any>,
source: Record<string, any>,
thisArg?: any,
options?: { includeAll?: boolean }
) {
const { includeAll = false } = options || {};
forEach(source, (val: any, key: any) => {
if (thisArg && checkFunction(val)) {
target[key as number] = bind(val, thisArg);
} else {
target[key as number] = val;
}
// @ts-ignore
}, { includeAll: includeAll });
return target;
}
/**
*
*
* @param obj
*
* @returns {string[]}
*/
function getAllPropertyNames(obj: Record<string, any>): string[] {
let result: string[] = [];
let currentObj = obj;
while (currentObj) {
const propNames = Object.getOwnPropertyNames(currentObj);
propNames.forEach(propName => {
if (result.indexOf(propName) === -1) {
result.push(propName);
}
});
currentObj = Object.getPrototypeOf(currentObj);
}
return result;
}
/**
*
* 使
* 使
*
* @param sourceObj
* @param destObj
* @param filter
* @param propFilter
*
* @returns {Record<any, any>}
*/
function flattenObject(
sourceObj: Record<any, any> | null | undefined,
destObj: Record<any, any> = {},
filter?: FilterFunc,
propFilter?: PropFilterFunc
): Record<any, any> {
let props: (string | symbol)[];
let i: number;
let prop: string | symbol;
const merged: Record<string, boolean> = {};
if (sourceObj === null || sourceObj === undefined) {
return destObj;
}
do {
props = getAllPropertyNames(sourceObj);
i = props.length;
while (i-- > 0) {
prop = props[i];
if ((!propFilter || propFilter(prop, sourceObj, destObj)) && !merged[prop as any]) {
destObj[prop as any] = sourceObj[prop as any];
merged[prop as any] = true;
}
}
sourceObj = filter && Object.getPrototypeOf(sourceObj);
} while (sourceObj && (!filter || filter(sourceObj, destObj)) && sourceObj !== Object.prototype);
return destObj;
}
/**
*
*
* @param obj
* @param func
*
* @returns {void}
*/
function forEachEntry(obj: Record<any, any>, func: (key: any, val: any) => void) {
if (obj instanceof Map) {
obj.forEach((value, key) => func(key, value));
} else {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
func(key, obj[key]);
}
}
}
}
/**
* `true`
*
* @param input
* @param delimiter `arrayOrString`
*
* @returns {Record<any, boolean>}
*/
const toBooleanObject = (input: string | string[], delimiter?: string): Record<any, boolean> => {
const obj: Record<string, boolean> = {};
const define = (arr: string[]) => {
arr.forEach(value => {
obj[value.trim()] = true;
});
};
if (Array.isArray(input)) {
define(input);
} else {
const stringArray = input.split(delimiter || '');
define(stringArray);
}
return obj;
};
/**
* JSON
*
* @param obj
*
* @returns {Record<string, any> |null} JSON
*/
const toJSONSafe = (obj: Record<string, any>): Record<string, any> | null => {
const visited = new WeakSet<any>();
const visit = (source: Record<string, any>): Record<string, any> | null => {
if (checkObject(source)) {
if (visited.has(source)) {
return null;
}
visited.add(source);
if ('toJSON' in source) {
return (source as any).toJSON();
}
const target: any = Array.isArray(source) ? [] : {};
for (const [key, value] of Object.entries(source)) {
const reducedValue = visit(value);
if (!checkUndefined(reducedValue)) {
target[key] = reducedValue;
}
}
return target;
}
return source;
};
return visit(obj);
};
/**
* JSON JSON SyntaxError
*
* @template T
*
* @param rawValue JSON
* @param parser JSON `JSON.parse`
* @param encoder JSON `JSON.stringify`
*
* @returns {string} JSON
*
* @throws {Error} SyntaxError
*/
function stringifySafely<T>(
rawValue: any,
parser?: (jsonString: string) => T,
encoder: (value: T) => string = JSON.stringify
): string {
if (typeof rawValue === 'string') {
try {
(parser ?? JSON.parse)(rawValue);
return rawValue.trim();
} catch (e) {
if ((e as Error).name !== 'SyntaxError') {
throw e;
}
}
}
return encoder(rawValue);
}
/**
*
*
* @param str
*
* @returns {string}
*/
const convertToCamelCase = (str: string) => {
return str
.split(/[-_\s]/)
.map((word, index) => (index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)))
.join('');
};
function objectToQueryString(obj: Record<string, any>) {
return Object.keys(obj)
.map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]))
.join('&');
}
const all = <T>(promises: Array<Promise<T>>): Promise<T[]> => Promise.all(promises);
function spread<T>(callback: Callback<T>): (arr: any[]) => T {
return (arr: any[]): T => callback.apply(null, arr);
}
function getNormalizedValue(value: string | any[] | boolean | null | number): string | any[] | boolean | null {
if (
value === false
|| value === null
|| value === undefined
) {
return value;
}
return Array.isArray(value) ? value.map(item => getNormalizedValue(item) as string) : String(value);
}
function isIE(): boolean {
return /MSIE|Trident/.test(window.navigator.userAgent);
}
function getObjectByArray(arr: any[]): Record<string, any> {
return arr.reduce((obj, item, index) => {
obj[index] = item;
return obj;
}, {});
}
function filterUndefinedValues(obj: Record<any, any>) {
return Object.keys(obj).reduce((result, key) => {
if (obj[key] !== undefined) {
result[key] = obj[key];
}
return result;
}, {});
}
const utils = {
bind,
checkFormData,
checkString,
checkNumber,
checkBoolean,
checkObject,
checkPlainObject,
checkUndefined,
checkDate,
checkFile,
checkBlob,
checkStream,
checkRegExp,
checkFunction,
checkURLSearchParams,
checkFileList,
checkHTMLForm,
forEach,
extendObject,
flattenObject,
getType,
createTypeChecker,
forEachEntry,
toBooleanObject,
getObjectKey,
toJSONSafe,
stringifySafely,
convertToCamelCase,
objectToQueryString,
spread,
all,
getNormalizedValue,
isIE,
getObjectByArray,
filterUndefinedValues,
};
export default utils;

View File

@ -0,0 +1,34 @@
import utils from '../commonUtils/utils';
// 获取当前上下文对象
function getContextObject(): any {
if (typeof globalThis !== 'undefined') return globalThis;
return typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : global;
}
// 合并多个对象为一个新对象1.如果有多个对象的属性名相同则后面的对象的属性值会覆盖前面的对象的属性值2.如果一个对象的属性值是另一个对象则会递归合并两个对象的属性值3.如果一个对象的属性值是数组,则会拷贝一个新的数组,防止修改原数组
function deepMerge(...objects: Record<string, any>[]): Record<string, any> {
const context = getContextObject();
const { caseless } = context || {};
const result: any = {};
const assignValue = (val: any, key: any) => {
const targetKey = caseless ? utils.getObjectKey(result, key) || key : key;
if (utils.checkPlainObject(result[targetKey]) && utils.checkPlainObject(val)) {
result[targetKey] = deepMerge(result[targetKey], val);
} else if (utils.checkPlainObject(val)) {
result[targetKey] = deepMerge({}, val);
} else if (Array.isArray(val)) {
result[targetKey] = val.slice();
} else {
result[targetKey] = val;
}
};
for (const obj of objects) {
obj && utils.forEach(obj, assignValue);
}
return result;
}
export default deepMerge;

View File

@ -0,0 +1,40 @@
import utils from '../commonUtils/utils';
import deepMerge from './deepMerge';
function getMergedConfig(config1: Record<string, any>, config2: Record<string, any>): Record<string, any> {
config2 = config2 || {};
// 定义一个默认的合并策略函数,用于返回源对象的属性值,如果源对象的属性值为 undefined则返回目标对象的属性值
const defaultMergeStrategy = (a: any, b: any) => (b !== undefined ? b : a);
// 创建一个对象,用于存储每个属性的合并策略
const mergeStrategies: Record<string, (a: any, b: any) => any> = {
url: defaultMergeStrategy,
method: defaultMergeStrategy,
baseURL: defaultMergeStrategy,
data: defaultMergeStrategy,
params: defaultMergeStrategy,
headers: (a: any, b: any) => deepMerge(a || {}, b || {}),
timeout: defaultMergeStrategy,
responseType: defaultMergeStrategy,
onUploadProgress: defaultMergeStrategy,
onDownloadProgress: defaultMergeStrategy,
cancelToken: defaultMergeStrategy,
};
// 使用 deepMerge 函数将 config1 的属性合并到一个新的空对象中,创建一个名为 mergedConfig 的新对象,用于存储合并后的配置
const mergedConfig = deepMerge({}, config1);
for (const key in config2) {
// 从 mergeStrategies 中获取适当的合并策略函数,如果没有特定的合并策略,使用默认的 defaultMergeStrategy 函数
const mergeStrategy = mergeStrategies[key] || defaultMergeStrategy;
// 使用合并策略函数计算合并后的属性值,并将其添加到结果配置对象 mergedConfig 中
mergedConfig[key] = mergeStrategy(config1[key], config2[key]);
}
// 返回合并后的配置对象 mergedConfig
return mergedConfig;
}
export default getMergedConfig;

View File

@ -0,0 +1,19 @@
function getFormData(obj: Record<string, any>, formData: FormData = new FormData()): FormData {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = obj[key];
if (Array.isArray(value)) {
for (const item of value) {
formData.append(key, item.toString());
}
} else {
formData.append(key, value.toString());
}
}
}
return formData;
}
export default getFormData;

View File

@ -0,0 +1,28 @@
import utils from '../commonUtils/utils';
export function parsePath(name: string): string[] {
const matches = Array.from(name.matchAll(/\w+|\[(\w*)]/g));
const arr = [];
for (const match of matches) {
const matchValue = match[0] === '[]' ? '' : match[1] || match[0];
arr.push(matchValue);
}
return arr;
}
function getJSONByFormData(formData: FormData): Record<string, any> | null {
if (utils.checkFormData(formData) && utils.checkFunction((formData as any).entries)) {
const obj: Record<string, any> = {};
for (const [key, value] of (formData as any).entries()) {
obj[key] = value;
}
return obj;
}
return null;
}
export default getJSONByFormData;

View File

@ -0,0 +1,16 @@
import HrHeaders from '../../core/HrHeaders';
import defaultConfig from '../../config/defaultConfig';
import { HrRequestConfig, HrResponse } from '../../types/interfaces';
function transformData(inputConfig: HrRequestConfig, func: Function, response?: HrResponse) {
const config = inputConfig || defaultConfig;
const context = response || config;
const headers = HrHeaders.from(context.headers);
const transformedData = func.call(config, context.data, headers.normalize(), response ? response.status : undefined);
headers.normalize();
return transformedData;
}
export default transformData;

View File

@ -0,0 +1,5 @@
function checkHeaderName(str: string) {
return /^[-_a-zA-Z]+$/.test(str.trim());
}
export default checkHeaderName;

View File

@ -0,0 +1,32 @@
import { HeaderMap } from '../../types/types';
function convertRawHeaders(rawHeaders: string): HeaderMap {
const convertedHeaders: HeaderMap = {};
if (rawHeaders) {
rawHeaders.split('\n').forEach((item: string) => {
let i = item.indexOf(':');
let key = item.substring(0, i).trim().toLowerCase();
let val = item.substring(i + 1).trim();
if (!key || (convertedHeaders[key] && key !== 'set-cookie')) {
return;
}
// Set-Cookie 在 HTTP 响应中可能会出现多次,为了保留每个独立的 Set-Cookie 报头,需要将它们的值存储在一个数组中
if (key === 'set-cookie') {
if (convertedHeaders[key]) {
(convertedHeaders[key] as any).push(val);
} else {
convertedHeaders[key] = [val];
}
} else {
convertedHeaders[key] = convertedHeaders[key] ? `${convertedHeaders[key]}, ${val}` : val;
}
});
}
return convertedHeaders;
}
export default convertRawHeaders;

View File

@ -0,0 +1,18 @@
import utils from '../commonUtils/utils';
function deleteHeader(this: any, header: string) {
const normalizedHeader = String(header).trim().toLowerCase();
if (normalizedHeader) {
const key = utils.getObjectKey(this, normalizedHeader);
if (key) {
delete this[key];
return true;
}
}
return false;
}
export default deleteHeader;

View File

@ -0,0 +1,35 @@
import utils from '../commonUtils/utils';
import { HeaderMatcher } from '../../types/types';
// 解析类似“key=value”格式的字符串并将解析结果以对象的形式返回
export function parseKeyValuePairs(str: string): Record<string, any> {
const parsedObj: Record<string, any> = {};
const matcher = /([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;
let match: RegExpExecArray | null;
while ((match = matcher.exec(str))) {
const [, key, value] = match;
parsedObj[key?.trim()] = value?.trim();
}
return parsedObj;
}
function processValueByParser(key: string, value: any, parser?: HeaderMatcher): any {
if (!parser) {
return value;
}
if (parser === true) {
return parseKeyValuePairs(value);
}
if (utils.checkFunction(parser)) {
return (parser as Function)(value, key);
}
if (utils.checkRegExp(parser)) {
return (parser as RegExp).exec(value)?.filter(item => item !== undefined);
}
throw new TypeError('parser is not correct!');
}
export default processValueByParser;

View File

@ -0,0 +1,15 @@
import { HrInstance, HrRequestConfig } from '../../types/interfaces';
import HorizonRequest from '../../core/HorizonRequest';
import extendInstance from './extendInstance';
function buildInstance(config: HrRequestConfig): HrInstance {
// 使用上下文 context 将请求和响应的配置和状态集中在一个地方,使得整个 Hr 实例可以共享这些配置和状态,避免了在多个地方重复定义和管理这些配置和状态
const context = new HorizonRequest(config);
// 将 Hr.prototype.request 方法上下文绑定到 context 上下文,将一个新的函数返回,并将这个新的函数保存到一个 instance 常量中,可以在 instance 实例上使用 Hr 类原型上的方法和属性,同时又可以保证这些方法和属性在当前实例上下文中正确地执行
const instance = extendInstance(context);
return instance as any;
}
export default buildInstance;

View File

@ -0,0 +1,11 @@
import HorizonRequest from '../../core/HorizonRequest';
import utils from '../commonUtils/utils';
function extendInstance(context: HorizonRequest): (...arg: any) => any {
const instance = utils.bind(HorizonRequest.prototype.request, context);
utils.extendObject(instance, HorizonRequest.prototype, context, { includeAll: true });
utils.extendObject(instance, context, null, { includeAll: true });
return instance;
}
export default extendInstance;

View File

@ -0,0 +1,28 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('bind function', () => {
it('should return a new function', () => {
const fn = () => {
};
const boundFn = utils.bind(fn, {});
expect(boundFn).toBeInstanceOf(Function);
expect(boundFn).not.toBe(fn);
});
it('should call original function with correct this value', () => {
const thisArg = { name: 'Alice' };
const fn = function (this: any) {
return this["name"];
};
const boundFn = utils.bind(fn, thisArg);
const result = boundFn();
expect(result).toBe('Alice');
});
it('should pass arguments to the original function', () => {
const fn = (a: number, b: number) => a + b;
const boundFn = utils.bind(fn, {});
const result = boundFn(2, 3);
expect(result).toBe(5);
});
});

View File

@ -0,0 +1,45 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('convertToCamelCase function', () => {
it('should convert kebab case to camel case', () => {
const input = 'my-first-post';
const expectedOutput = 'myFirstPost';
const result = utils.convertToCamelCase(input);
expect(result).toBe(expectedOutput);
});
it('should convert snake case to camel case', () => {
const input = 'my_snake_case_post';
const expectedOutput = 'mySnakeCasePost';
const result = utils.convertToCamelCase(input);
expect(result).toBe(expectedOutput);
});
it('should convert space separated words to camel case', () => {
const input = 'my space separated words';
const expectedOutput = 'mySpaceSeparatedWords';
const result = utils.convertToCamelCase(input);
expect(result).toBe(expectedOutput);
});
it('should handle already camel cased words', () => {
const input = 'myCamelCasedWords';
const expectedOutput = 'myCamelCasedWords';
const result = utils.convertToCamelCase(input);
expect(result).toBe(expectedOutput);
});
it('should handle empty input', () => {
const input = '';
const expectedOutput = '';
const result = utils.convertToCamelCase(input);
expect(result).toBe(expectedOutput);
});
it('should handle input with only one word', () => {
const input = 'hello';
const expectedOutput = 'hello';
const result = utils.convertToCamelCase(input);
expect(result).toBe(expectedOutput);
});
});

View File

@ -0,0 +1,107 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('createTypeChecker function', () => {
it('should return a function', () => {
const result = utils.createTypeChecker('string');
expect(result).toBeInstanceOf(Function);
});
it('should return true for matching type', () => {
const isString = utils.createTypeChecker('string');
const result = isString('hello');
expect(result).toBe(true);
});
it('should return false for non-matching type', () => {
const isString = utils.createTypeChecker('string');
const result = isString(123);
expect(result).toBe(false);
});
it('should return true for matching type', () => {
const result = utils.checkFunction((a: number, b: number) => a + b);
expect(result).toBe(true);
});
it('should return false for non-matching type', () => {
const result = utils.checkFunction(123);
expect(result).toBe(false);
});
it('should return true for matching type', () => {
const result = utils.checkNumber(123);
expect(result).toBe(true);
});
it('should return false for non-matching type', () => {
const result = utils.checkNumber('123');
expect(result).toBe(false);
});
it('should return true for matching type', () => {
const result = utils.checkObject({ name: 'Tom' });
expect(result).toBe(true);
});
it('should return false for non-matching type', () => {
const result = utils.checkObject('123');
expect(result).toBe(false);
});
it('should return true for matching type', () => {
const result = utils.checkBoolean(true);
expect(result).toBe(true);
});
it('should return false for non-matching type', () => {
const result = utils.checkBoolean('123');
expect(result).toBe(false);
});
it('should return true for matching type', () => {
const result = utils.checkUndefined(undefined);
expect(result).toBe(true);
});
it('should return false for non-matching type', () => {
const result = utils.checkUndefined('123');
expect(result).toBe(false);
});
it('should return true for matching type', () => {
const result = utils.checkPlainObject({ name: 'Jack' });
expect(result).toBe(true);
});
it('should return true for matching type', () => {
const result = utils.checkDate(new Date());
expect(result).toBe(true);
});
it('should return false for non-matching type', () => {
const result = utils.checkDate('2023-03-30');
expect(result).toBe(false);
});
it('should return true for matching type', () => {
const result = utils.checkDate(new Date());
expect(result).toBe(true);
});
it('should return false for non-matching type', () => {
const result = utils.checkDate('2023-03-30');
expect(result).toBe(false);
});
it('should return true for matching type', () => {
const data = 'Hello, world!';
const blob = new Blob([data], { type: 'text/plain' });
const result = utils.checkBlob(blob);
expect(result).toBe(true);
});
it('should return false for non-matching type', () => {
const result = utils.checkFile('test.txt');
expect(result).toBe(false);
});
});

View File

@ -0,0 +1,31 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('extendObject function', () => {
it('should extend an object with another object', () => {
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
const result = utils.extendObject(target, source);
expect(result).toEqual({ a: 1, b: 3, c: 4 });
expect(target).toEqual({ a: 1, b: 3, c: 4 }); // Target 也会被修改
});
it('should bind functions to a given context if "thisArg" option is provided', () => {
const target = {};
const source = {
sayHi() {
return `Hi, ${this.name}!`;
},
name: 'John'
};
const thisArg = { name: 'Sarah' };
const result = utils.extendObject(target, source, thisArg);
expect(result.sayHi()).toBe('Hi, Sarah!');
});
it('should include all properties of the source object if "includeAll" option is set to true', () => {
const target = { a: 1 };
const source = { 'b': 2 };
const result = utils.extendObject(target, source, undefined, { includeAll: true });
expect(result).not.toEqual({ a: 1, 'b': 2 });
});
});

View File

@ -0,0 +1,30 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('flattenObject function', () => {
it('should return an empty object if the source object is null or undefined', () => {
expect(utils.flattenObject(null)).toEqual({});
expect(utils.flattenObject(undefined)).toEqual({});
});
it('should flatten a simple object', () => {
const sourceObj = { a: 1, b: 'hello', c: true };
const destObj = utils.flattenObject(sourceObj);
expect(destObj).toEqual({ a: 1, b: 'hello', c: true });
});
it('should flatten an object with a prototype', () => {
const parentObj = { a: 1 };
const sourceObj = Object.create(parentObj);
sourceObj.b = 'hello';
sourceObj.c = true;
const destObj = utils.flattenObject(sourceObj);
expect(destObj).toEqual({ a: 1, b: 'hello', c: true });
});
it('should filter out properties based on a property filter function', () => {
const sourceObj = { a: 1, b: 'hello', c: true };
const propFilter = (prop: string | symbol) => prop !== 'b';
const destObj = utils.flattenObject(sourceObj, undefined, undefined, propFilter);
expect(destObj).toEqual({ a: 1, c: true });
});
});

View File

@ -0,0 +1,41 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('forEach function', () => {
it('should do nothing when input is null or undefined', () => {
const func = jest.fn();
utils.forEach(null, func);
utils.forEach(undefined, func);
expect(func).not.toHaveBeenCalled();
});
it('should iterate over array and call function with value, index and array', () => {
const arr = [1, 2, 3];
const func = jest.fn();
utils.forEach(arr, func);
expect(func).toHaveBeenCalledTimes(3);
expect(func).toHaveBeenCalledWith(1, 0, arr);
expect(func).toHaveBeenCalledWith(2, 1, arr);
expect(func).toHaveBeenCalledWith(3, 2, arr);
});
it('should iterate over object and call function with value, key and object', () => {
const obj = { a: 1, b: 2, c: 3 };
const func = jest.fn();
utils.forEach(obj, func);
expect(func).toHaveBeenCalledTimes(3);
expect(func).toHaveBeenCalledWith(1, 'a', obj);
expect(func).toHaveBeenCalledWith(2, 'b', obj);
expect(func).toHaveBeenCalledWith(3, 'c', obj);
});
it('should include all properties when options.includeAll is true', () => {
const obj = Object.create({ c: 3 });
obj.a = 1;
obj.b = 2;
const func = jest.fn();
utils.forEach(obj, func, { includeAll: true });
expect(func).toHaveBeenCalledWith(1, 'a', obj);
expect(func).toHaveBeenCalledWith(2, 'b', obj);
expect(func).toHaveBeenCalledWith(3, 'c', obj);
});
});

View File

@ -0,0 +1,34 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('forEachEntry function', () => {
const callback = jest.fn();
it('should call the callback function for each entry in a plain object', () => {
const obj = { a: 1, b: 2, c: 3 };
utils.forEachEntry(obj, callback);
expect(callback).toHaveBeenCalledTimes(3);
expect(callback).toHaveBeenCalledWith('a', 1);
expect(callback).toHaveBeenCalledWith('b', 2);
expect(callback).toHaveBeenCalledWith('c', 3);
});
it('should call the callback function for each entry in a Map object', () => {
const obj = new Map([['a', 1], ['b', 2], ['c', 3]]);
utils.forEachEntry(obj, callback);
expect(callback).toHaveBeenCalledTimes(3);
expect(callback).toHaveBeenCalledWith('a', 1);
expect(callback).toHaveBeenCalledWith('b', 2);
expect(callback).toHaveBeenCalledWith('c', 3);
});
it('should not call the callback function for non-enumerable properties', () => {
const obj = Object.create({}, {
a: { value: 1, enumerable: true },
b: { value: 2, enumerable: false },
c: { value: 3, enumerable: true }
});
utils.forEachEntry(obj, callback);
expect(callback).toHaveBeenCalledTimes(2);
expect(callback).toHaveBeenCalledWith('a', 1);
expect(callback).toHaveBeenCalledWith('c', 3);
});
});

View File

@ -0,0 +1,20 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('getNormalizedValue function', () => {
it('should return the same value if it is false or null', () => {
expect(utils.getNormalizedValue(false)).toBe(false);
expect(utils.getNormalizedValue(null)).toBe(null);
});
it('should convert the value to string if it is not false or null', () => {
expect(utils.getNormalizedValue('test')).toBe('test');
expect(utils.getNormalizedValue(123)).toBe('123');
expect(utils.getNormalizedValue(true)).toBe('true');
});
it('should recursively normalize array values', () => {
expect(utils.getNormalizedValue(['foo', 'bar', 123])).toEqual(['foo', 'bar', '123']);
expect(utils.getNormalizedValue(['test', false, null])).toEqual(['test', false, null]);
expect(utils.getNormalizedValue(['one', ['two', 'three']])).toEqual(['one', ['two', 'three']]);
});
});

View File

@ -0,0 +1,32 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('getObjectByArray function', () => {
it('should convert array to object with correct key-value pairs', () => {
const arr = ['a', 'b', 'c'];
const expected = {
'0': 'a',
'1': 'b',
'2': 'c'
};
const result = utils.getObjectByArray(arr);
expect(result).toEqual(expected);
});
it('should return an empty object if the input array is empty', () => {
const arr: any[] = [];
const expected = {};
const result = utils.getObjectByArray(arr);
expect(result).toEqual(expected);
});
it('should handle arrays with non-string elements', () => {
const arr = [1, true, { key: 'value' }];
const expected = {
'0': 1,
'1': true,
'2': { key: 'value' }
};
const result = utils.getObjectByArray(arr);
expect(result).toEqual(expected);
});
});

View File

@ -0,0 +1,23 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('getObjectKey function', () => {
it('should return null if object is empty', () => {
const obj = {};
const result = utils.getObjectKey(obj, 'foo');
expect(result).toBeNull();
});
it('should return null if key is not found', () => {
const obj = { a: 1, b: 2 };
const result = utils.getObjectKey(obj, 'c');
expect(result).toBeNull();
});
it('should return matching key in case-insensitive manner', () => {
const obj = { a: 1, b: 2, c: 3 };
const result1 = utils.getObjectKey(obj, 'B');
const result2 = utils.getObjectKey(obj, 'C');
expect(result1).toBe('b');
expect(result2).toBe('c');
});
});

View File

@ -0,0 +1,38 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('getType function', () => {
it('should return type "undefined" for undefined value', () => {
const result = utils.getType(undefined);
expect(result).toBe('undefined');
});
it('should return type "null" for null value', () => {
const result = utils.getType(null);
expect(result).toBe('null');
});
it('should return type "string" for string value', () => {
const result = utils.getType('hello');
expect(result).toBe('string');
});
it('should return type "number" for number value', () => {
const result = utils.getType(123);
expect(result).toBe('number');
});
it('should return type "boolean" for boolean value', () => {
const result = utils.getType(true);
expect(result).toBe('boolean');
});
it('should return type "array" for array value', () => {
const result = utils.getType([1, 2, 3]);
expect(result).toBe('array');
});
it('should return type "object" for object value', () => {
const result = utils.getType({ name: 'Alice' });
expect(result).toBe('object');
});
});

View File

@ -0,0 +1,45 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('objectToQueryString function', () => {
it('should return empty string if object is empty', () => {
const input = {};
const expectedResult = '';
const result = utils.objectToQueryString(input);
expect(result).toBe(expectedResult);
});
it('should return a query string with one parameter', () => {
const input = { key: 'value' };
const expectedResult = 'key=value';
const result = utils.objectToQueryString(input);
expect(result).toBe(expectedResult);
});
it('should return a query string with multiple parameters', () => {
const input = { key1: 'value1', key2: 'value2' };
const expectedResult = 'key1=value1&key2=value2';
const result = utils.objectToQueryString(input);
expect(result).toBe(expectedResult);
});
it('should encode keys and values', () => {
const input = { 'key with spaces': 'value with spaces' };
const expectedResult = 'key%20with%20spaces=value%20with%20spaces';
const result = utils.objectToQueryString(input);
expect(result).toBe(expectedResult);
});
it('should handle values of different types', () => {
const input = {
key1: 'string',
key2: 42,
key3: true,
key4: [1, 2, 3],
key5: { a: 'b' },
};
const expectedResult =
'key1=string&key2=42&key3=true&key4=1%2C2%2C3&key5=%5Bobject%20Object%5D';
const result = utils.objectToQueryString(input);
expect(result).toBe(expectedResult);
});
});

View File

@ -0,0 +1,33 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('stringifySafely function', () => {
it('should return the input string if it is a valid JSON string', () => {
const input = '{"a": 1, "b": 2}';
const result = utils.stringifySafely(input);
expect(result).toBe(input.trim());
});
it('should call the parser function if it is provided', () => {
const input = '{"a": 1, "b": 2}';
const parser = jest.fn().mockReturnValue({ a: 1, b: 2 });
const result = utils.stringifySafely(input, parser);
expect(result).toBe(input.trim());
expect(parser).toHaveBeenCalledWith(input);
});
it('should throw an error if the parser function throws an error', () => {
const input = '{"a": 1, "b": 2}';
const parser = jest.fn().mockImplementation(() => {
throw new Error('Invalid JSON');
});
expect(() => utils.stringifySafely(input, parser)).toThrow('Invalid JSON');
});
it('should use the encoder function to stringify the input', () => {
const input = { a: 1, b: 2 };
const encoder = jest.fn().mockReturnValue('{"a":1,"b":2}');
const result = utils.stringifySafely(input, undefined, encoder);
expect(result).toBe('{"a":1,"b":2}');
expect(encoder).toHaveBeenCalledWith(input);
});
});

View File

@ -0,0 +1,45 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('toBooleanObject function', () => {
it('should return an object with boolean properties', () => {
const result = utils.toBooleanObject('foo,bar,baz', ',');
expect(result).toEqual({
'foo': true,
'bar': true,
'baz': true,
});
});
it('should handle an array of strings as input', () => {
const result = utils.toBooleanObject(['foo', 'bar', 'baz']);
expect(result).toEqual({
'foo': true,
'bar': true,
'baz': true,
});
});
it('should return an empty object for empty input', () => {
const result = utils.toBooleanObject('');
expect(result).toEqual({});
});
it('should handle custom delimiter', () => {
const result = utils.toBooleanObject('foo|bar|baz', '|');
expect(result).toEqual({
'foo': true,
'bar': true,
'baz': true,
});
});
it('should handle spaces in input', () => {
const result = utils.toBooleanObject('foo, bar, baz', ',');
expect(result).toEqual({
'foo': true,
'bar': true,
'baz': true,
});
});
});

View File

@ -0,0 +1,26 @@
import utils from '../../../../src/utils/commonUtils/utils';
describe('toJSONSafe function', () => {
it('should return a clone of the object', () => {
const input = { a: 1, b: 2, c: [3, 4, 5], d: { e: 6 } };
const result = utils.toJSONSafe(input);
expect(result).toEqual(input);
expect(result).not.toBe(input);
expect(result?.c).not.toBe(input.c);
expect(result?.d).not.toBe(input.d);
});
it('should handle toJSON method', () => {
const input = { a: 1, toJSON: () => ({ b: 2 }) };
const result = utils.toJSONSafe(input);
expect(result).toEqual({ b: 2 });
});
it('should handle arrays', () => {
const input = [1, 2, { a: 3 }, [4, 5]];
const result = utils.toJSONSafe(input);
expect(result).toEqual([1, 2, { a: 3 }, [4, 5]]);
expect(result?.[2]).not.toBe(input[2]);
expect(result?.[3]).not.toBe(input[3]);
});
});

View File

@ -0,0 +1,26 @@
import deepMerge from '../../../../src/utils/configUtils/deepMerge';
describe('deepMerge function', () => {
it('should return an empty object if no arguments are passed', () => {
expect(deepMerge()).toEqual({});
});
it('should merge two objects', () => {
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { b: { d: 3 }, e: 4 };
expect(deepMerge(obj1, obj2)).toEqual({ a: 1, b: { c: 2, d: 3 }, e: 4 });
});
it('should merge three or more objects', () => {
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { b: { d: 3 }, e: 4 };
const obj3 = { f: 5 };
expect(deepMerge(obj1, obj2, obj3)).toEqual({ a: 1, b: { c: 2, d: 3 }, e: 4, f: 5 });
});
it('should merge objects which later overlap', () => {
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { b: { c: 1, d: 3 }, e: 4 };
expect(deepMerge(obj1, obj2)).toEqual({ a: 1, b: { c: 1, d: 3 }, e: 4 });
});
});

View File

@ -0,0 +1,51 @@
import getMergedConfig from '../../../../src/utils/configUtils/getMergedConfig';
describe('getMergedConfig function', () => {
it('should merge two configs correctly', () => {
const config1 = {
baseURL: 'https://example.com/api',
headers: { 'Content-Type': 'application/json' },
timeout: 5000,
};
const config2 = {
method: 'POST',
data: { name: 'John', age: 25 },
headers: { 'Authorization': 'Bearer token' },
responseType: 'json',
};
const mergedConfig = getMergedConfig(config1, config2);
expect(mergedConfig).toEqual({
baseURL: 'https://example.com/api',
method: 'POST',
data: { name: 'John', age: 25 },
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer token' },
timeout: 5000,
responseType: 'json',
});
});
it('should handle missing or undefined config values', () => {
const config1 = {
baseURL: 'https://example.com/api',
headers: { 'Content-Type': 'application/json' },
};
const config2 = {
method: 'POST',
data: { name: 'John', age: 25 },
headers: undefined,
};
const mergedConfig = getMergedConfig(config1, config2);
expect(mergedConfig).toEqual({
baseURL: 'https://example.com/api',
method: 'POST',
data: { name: 'John', age: 25 },
headers: { 'Content-Type': 'application/json' },
});
});
});

View File

@ -0,0 +1,52 @@
import getFormData from '../../../../src/utils/dataUtils/getFormData';
describe('getFormData function', () => {
it('should convert object to FormData', () => {
const obj = {
name: 'John',
age: 30,
hobbies: ['Reading', 'Gaming'],
};
const formData = getFormData(obj);
expect(formData.get('name')).toBe('John');
expect(formData.get('age')).toBe('30');
expect(formData.getAll('hobbies')).toEqual(['Reading', 'Gaming']);
});
it('should append to existing FormData', () => {
const existingFormData = new FormData();
existingFormData.append('existing', 'value');
const obj = {
name: 'John',
age: 30,
};
const formData = getFormData(obj, existingFormData);
expect(formData.get('existing')).toBe('value');
expect(formData.get('name')).toBe('John');
expect(formData.get('age')).toBe('30');
});
it('should handle empty object', () => {
const obj = {};
const formData = getFormData(obj);
expect(formData.get('name')).toBeNull();
expect(formData.get('age')).toBeNull();
});
it('should convert array values to string', () => {
const obj = {
items: [1, 2, 3],
};
const formData = getFormData(obj);
expect(formData.getAll('items')).toEqual(['1', '2', '3']);
});
});

View File

@ -0,0 +1,31 @@
import getJSONByFormData from '../../../../src/utils/dataUtils/getJSONByFormData';
describe('getJSONByFormData function', () => {
it('should convert FormData to JSON object', () => {
const formData = new FormData();
formData.append('name', 'John');
formData.append('age', '30');
const result = getJSONByFormData(formData);
expect(result).toEqual({
name: 'John',
age: '30',
});
});
it('should return null if FormData or entries() is not available', () => {
const invalidFormData = {} as FormData;
const result = getJSONByFormData(invalidFormData);
expect(result).toBeNull();
});
it('should handle empty FormData', () => {
const emptyFormData = new FormData();
const result = getJSONByFormData(emptyFormData);
expect(result).toEqual({});
});
});

View File

@ -0,0 +1,10 @@
import { parsePath } from '../../../../src/utils/dataUtils/getJSONByFormData';
describe('parsePath function', () => {
it('should parse path correctly', () => {
expect(parsePath('users[0].name')).toEqual(['users', '0', 'name']);
expect(parsePath('books[2][title]')).toEqual(['books', '2', 'title']);
expect(parsePath('')).toEqual([]);
expect(parsePath('property')).toEqual(['property']);
});
});

View File

@ -0,0 +1,21 @@
import checkHeaderName from '../../../../src/utils/headerUtils/checkHeaderName';
describe('checkHeaderName', () => {
it('should return true for valid header name', () => {
const validHeaderName = 'Content-Type';
const result = checkHeaderName(validHeaderName);
expect(result).toBe(true);
});
it('should return false for invalid header name', () => {
const invalidHeaderName = 'Content-Type!';
const result = checkHeaderName(invalidHeaderName);
expect(result).toBe(false);
});
it('should return false for empty string', () => {
const emptyString = '';
const result = checkHeaderName(emptyString);
expect(result).toBe(false);
});
});

View File

@ -0,0 +1,46 @@
import convertRawHeaders from '../../../../src/utils/headerUtils/convertRawHeaders';
describe('convertRawHeaders', () => {
it('should convert raw headers to a header map object', () => {
const rawHeaders = 'Content-Type: application/json\nAuthorization: Bearer token';
const expectedHeaders = {
'content-type': 'application/json',
'authorization': 'Bearer token',
};
const result = convertRawHeaders(rawHeaders);
expect(result).toEqual(expectedHeaders);
});
it('should handle multiple occurrences of Set-Cookie header', () => {
const rawHeaders = 'Set-Cookie: cookie1=value1\nSet-Cookie: cookie2=value2';
const expectedHeaders = {
'set-cookie': ['cookie1=value1', 'cookie2=value2'],
};
const result = convertRawHeaders(rawHeaders);
expect(result).toEqual(expectedHeaders);
});
it('should handle empty raw headers', () => {
const rawHeaders = '';
const expectedHeaders = {};
const result = convertRawHeaders(rawHeaders);
expect(result).toEqual(expectedHeaders);
});
it('should handle raw headers with leading/trailing whitespaces', () => {
const rawHeaders = ' Content-Type: application/json\nAuthorization: Bearer token ';
const expectedHeaders = {
'content-type': 'application/json',
'authorization': 'Bearer token',
};
const result = convertRawHeaders(rawHeaders);
expect(result).toEqual(expectedHeaders);
});
it('should handle raw headers with missing colon', () => {
const rawHeaders = 'Content-Type application/json';
const expectedHeaders = {};
const result = convertRawHeaders(rawHeaders);
expect(result).toEqual(expectedHeaders);
});
});

View File

@ -0,0 +1,35 @@
import deleteHeader from '../../../../src/utils/headerUtils/deleteHeader';
describe('deleteHeader function', () => {
it('should delete existing header property from the object', () => {
const obj = { 'content-type': 'application/json', 'authorization': 'Bearer token' };
const headerToDelete = 'content-type';
const result = deleteHeader.call(obj, headerToDelete);
expect(result).toBe(true);
expect(obj).toEqual({ 'authorization': 'Bearer token' });
});
it('should return false if header property does not exist', () => {
const obj = { 'content-type': 'application/json', 'authorization': 'Bearer token' };
const headerToDelete = 'x-custom-header';
const result = deleteHeader.call(obj, headerToDelete);
expect(result).toBe(false);
expect(obj).toEqual({ 'content-type': 'application/json', 'authorization': 'Bearer token' });
});
it('should return false if header parameter is empty', () => {
const obj = { 'content-type': 'application/json', 'authorization': 'Bearer token' };
const headerToDelete = '';
const result = deleteHeader.call(obj, headerToDelete);
expect(result).toBe(false);
expect(obj).toEqual({ 'content-type': 'application/json', 'authorization': 'Bearer token' });
});
it('should delete header property with different case sensitivity', () => {
const obj = { 'Content-Type': 'application/json', 'Authorization': 'Bearer token' };
const headerToDelete = 'content-type';
const result = deleteHeader.call(obj, headerToDelete);
expect(result).toBe(true);
expect(obj).toEqual({ 'Authorization': 'Bearer token' });
});
});

View File

@ -0,0 +1,65 @@
import { parseKeyValuePairs } from '../../../../src/utils/headerUtils/processValueByParser';
describe('parseKeyValuePairs function', () => {
it('should parse key-value pairs separated by commas', () => {
const input = 'key1=value1, key2=value2, key3=value3';
const expectedOutput = {
'key1': 'value1',
'key2': 'value2',
'key3': 'value3',
};
const result = parseKeyValuePairs(input);
expect(result).toEqual(expectedOutput);
});
it('should parse key-value pairs separated by semicolons', () => {
const input = 'key1=value1; key2=value2; key3=value3';
const expectedOutput = {
'key1': 'value1',
'key2': 'value2',
'key3': 'value3',
};
const result = parseKeyValuePairs(input);
expect(result).toEqual(expectedOutput);
});
it('should parse key-value pairs with no spaces', () => {
const input = 'key1=value1,key2=value2,key3=value3';
const expectedOutput = {
'key1': 'value1',
'key2': 'value2',
'key3': 'value3',
};
const result = parseKeyValuePairs(input);
expect(result).toEqual(expectedOutput);
});
it('should parse key-value pairs with leading/trailing spaces', () => {
const input = ' key1 = value1 , key2 = value2 , key3 = value3 ';
const expectedOutput = {
'key1': 'value1',
'key2': 'value2',
'key3': 'value3',
};
const result = parseKeyValuePairs(input);
expect(result).toEqual(expectedOutput);
});
it('should parse key-value pairs with spaces', () => {
const input = 'key1=value1, key2=value with spaces, key3=value3';
const expectedOutput = {
'key1': 'value1',
'key2': 'value with spaces',
'key3': 'value3',
};
const result = parseKeyValuePairs(input);
expect(result).toEqual(expectedOutput);
});
it('should return an empty object for empty input', () => {
const input = '';
const expectedOutput = {};
const result = parseKeyValuePairs(input);
expect(result).toEqual(expectedOutput);
});
});

Some files were not shown because too many files have changed in this diff Show More