@czf0613/http_client 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/http_client.d.ts +29 -0
- package/dist/http_client.js +178 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kevin Chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# http_client_ts
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 拼接URL和查询参数,会自动处理转义
|
|
3
|
+
* @param url 基础URL,不要带任何查询参数
|
|
4
|
+
* @param queryParams 查询参数对象,键值对形式,值可以是字符串或数字,但是目前不建议出现同key的对象
|
|
5
|
+
* @returns
|
|
6
|
+
*/
|
|
7
|
+
export declare function joinUrlWithParams(url: string, queryParams: Record<string, string | number | boolean>): string;
|
|
8
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD';
|
|
9
|
+
/**
|
|
10
|
+
* 发起一个带有默认配置的 HTTP 请求
|
|
11
|
+
* 默认不处理异常,需要try catch
|
|
12
|
+
* @param url 请求的URL,不要拼接查询参数,但是要提供path params
|
|
13
|
+
* @param method HTTP方法,默认为GET
|
|
14
|
+
* @param queryParams 查询参数,会被自动拼接到url中(会自动进行转义)
|
|
15
|
+
* @param customHeaders 自定义请求头,不需要写content-type这种会被自动处理的头
|
|
16
|
+
* @param body 请求体,适用于POST/PUT请求,传入字符串、数字会被处理成text/plain,传入对象会被处理成application/json,传入FormData会被处理成multipart/form-data,目前不建议直接发送二进制对象
|
|
17
|
+
* @returns 返回Fetch API的Response对象
|
|
18
|
+
*/
|
|
19
|
+
export declare function makeHttpRequest(url: string, method?: HttpMethod, queryParams?: Record<string, string | number> | null, customHeaders?: Record<string, string> | null, body?: any | null, timeoutMs?: number): Promise<Response>;
|
|
20
|
+
/**
|
|
21
|
+
* 发起一个SSE请求,返回一个异步生成器,每次迭代返回一个字符串(流式响应)
|
|
22
|
+
* 这个方法是拿来补充浏览器里面的EventSource的,扩展了很多没有的功能,跟原版的SSE协议不完全兼容。
|
|
23
|
+
* 目前这个接口只支持处理后端不停发送data: xxx\n\n这种格式的响应
|
|
24
|
+
* 默认不抛出异常,会在生成器的返回值里面指明成功还是失败
|
|
25
|
+
* @see makeHttpRequest 这里的参数说明
|
|
26
|
+
* @returns 返回一个异步生成器,每次迭代返回一个字符串(流式响应)
|
|
27
|
+
*/
|
|
28
|
+
export declare function makeSSERequest(url: string, method?: HttpMethod, queryParams?: Record<string, string | number> | null, customHeaders?: Record<string, string> | null, body?: any | null): AsyncGenerator<string, boolean, undefined>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 拼接URL和查询参数,会自动处理转义
|
|
3
|
+
* @param url 基础URL,不要带任何查询参数
|
|
4
|
+
* @param queryParams 查询参数对象,键值对形式,值可以是字符串或数字,但是目前不建议出现同key的对象
|
|
5
|
+
* @returns
|
|
6
|
+
*/
|
|
7
|
+
export function joinUrlWithParams(url, queryParams) {
|
|
8
|
+
if (Object.keys(queryParams).length === 0) {
|
|
9
|
+
return url;
|
|
10
|
+
}
|
|
11
|
+
const params = new URLSearchParams();
|
|
12
|
+
Object.entries(queryParams).forEach(([key, value]) => {
|
|
13
|
+
if (typeof value === 'string') {
|
|
14
|
+
params.append(key, value);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
params.append(key, value.toString());
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return `${url}?${params.toString()}`;
|
|
21
|
+
}
|
|
22
|
+
// 默认超时时间,单位毫秒
|
|
23
|
+
const DEFAULT_TIMEOUT = 5000;
|
|
24
|
+
/**
|
|
25
|
+
* 发起一个带有默认配置的 HTTP 请求
|
|
26
|
+
* 默认不处理异常,需要try catch
|
|
27
|
+
* @param url 请求的URL,不要拼接查询参数,但是要提供path params
|
|
28
|
+
* @param method HTTP方法,默认为GET
|
|
29
|
+
* @param queryParams 查询参数,会被自动拼接到url中(会自动进行转义)
|
|
30
|
+
* @param customHeaders 自定义请求头,不需要写content-type这种会被自动处理的头
|
|
31
|
+
* @param body 请求体,适用于POST/PUT请求,传入字符串、数字会被处理成text/plain,传入对象会被处理成application/json,传入FormData会被处理成multipart/form-data,目前不建议直接发送二进制对象
|
|
32
|
+
* @returns 返回Fetch API的Response对象
|
|
33
|
+
*/
|
|
34
|
+
export async function makeHttpRequest(url, method = 'GET', queryParams = null, customHeaders = null, body = null, timeoutMs = DEFAULT_TIMEOUT) {
|
|
35
|
+
// 处理查询参数
|
|
36
|
+
if (queryParams != null) {
|
|
37
|
+
url = joinUrlWithParams(url, queryParams);
|
|
38
|
+
}
|
|
39
|
+
// 处理header设定,主要是处理Content-Type
|
|
40
|
+
if (customHeaders == null) {
|
|
41
|
+
customHeaders = {};
|
|
42
|
+
}
|
|
43
|
+
if (body == null) {
|
|
44
|
+
// 无body时,不设置Content-Type
|
|
45
|
+
}
|
|
46
|
+
else if (body instanceof FormData) {
|
|
47
|
+
// 使用FormData时,浏览器会自动设置Content-Type和boundary,不要多手
|
|
48
|
+
}
|
|
49
|
+
else if (typeof body === 'string') {
|
|
50
|
+
customHeaders['Content-Type'] = 'text/plain';
|
|
51
|
+
}
|
|
52
|
+
else if (typeof body === 'number') {
|
|
53
|
+
customHeaders['Content-Type'] = 'text/plain';
|
|
54
|
+
body = body.toString();
|
|
55
|
+
}
|
|
56
|
+
else if (typeof body === 'object') {
|
|
57
|
+
customHeaders['Content-Type'] = 'application/json';
|
|
58
|
+
body = JSON.stringify(body);
|
|
59
|
+
}
|
|
60
|
+
// 配置超时
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
63
|
+
const resp = await fetch(url, {
|
|
64
|
+
method: method,
|
|
65
|
+
headers: customHeaders,
|
|
66
|
+
body: body,
|
|
67
|
+
signal: controller.signal,
|
|
68
|
+
});
|
|
69
|
+
clearTimeout(timeoutId);
|
|
70
|
+
return resp;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 发起一个SSE请求,返回一个异步生成器,每次迭代返回一个字符串(流式响应)
|
|
74
|
+
* 这个方法是拿来补充浏览器里面的EventSource的,扩展了很多没有的功能,跟原版的SSE协议不完全兼容。
|
|
75
|
+
* 目前这个接口只支持处理后端不停发送data: xxx\n\n这种格式的响应
|
|
76
|
+
* 默认不抛出异常,会在生成器的返回值里面指明成功还是失败
|
|
77
|
+
* @see makeHttpRequest 这里的参数说明
|
|
78
|
+
* @returns 返回一个异步生成器,每次迭代返回一个字符串(流式响应)
|
|
79
|
+
*/
|
|
80
|
+
export async function* makeSSERequest(url, method = 'GET', queryParams = null, customHeaders = null, body = null) {
|
|
81
|
+
var _a;
|
|
82
|
+
const resp = await makeHttpRequest(url, method, queryParams, customHeaders, body, 30000);
|
|
83
|
+
if (!resp.ok) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const reader = (_a = resp.body) === null || _a === void 0 ? void 0 : _a.getReader();
|
|
87
|
+
if (reader == null) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
let buffer = new Uint8Array(0);
|
|
91
|
+
try {
|
|
92
|
+
while (true) {
|
|
93
|
+
const { done, value } = await reader.read();
|
|
94
|
+
if (done || !value) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
// 拼接之前的残留数据
|
|
98
|
+
let tempBuffer = new Uint8Array(buffer.length + value.length);
|
|
99
|
+
tempBuffer.set(buffer, 0);
|
|
100
|
+
tempBuffer.set(value, buffer.length);
|
|
101
|
+
buffer = tempBuffer;
|
|
102
|
+
if (buffer.length < 8) {
|
|
103
|
+
// 每行数据至少都有'data: \n\n',不够8个字节一定是不完整的
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// 拿出每一行数据出来
|
|
107
|
+
let lineEndIndex = extractSSELine(buffer);
|
|
108
|
+
while (lineEndIndex != null) {
|
|
109
|
+
// 截取出来然后解析
|
|
110
|
+
let line = buffer.slice(0, lineEndIndex + 1);
|
|
111
|
+
let lineStr = new TextDecoder('utf-8').decode(line);
|
|
112
|
+
// 切掉开头和结尾的东西
|
|
113
|
+
yield lineStr.slice(5, lineStr.length - 2);
|
|
114
|
+
if (lineEndIndex == buffer.length - 1) {
|
|
115
|
+
// 这个时候不能去切了,直接返回空数组可能效率更高
|
|
116
|
+
buffer = new Uint8Array(0);
|
|
117
|
+
lineEndIndex = null;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
buffer = buffer.subarray(lineEndIndex + 1);
|
|
121
|
+
lineEndIndex = extractSSELine(buffer);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// 处理掉剩余部份
|
|
126
|
+
if (buffer.length > 0) {
|
|
127
|
+
let lastLineEndIndex = extractSSELine(buffer);
|
|
128
|
+
while (lastLineEndIndex != null) {
|
|
129
|
+
let line = buffer.slice(0, lastLineEndIndex + 1);
|
|
130
|
+
let lineStr = new TextDecoder('utf-8').decode(line);
|
|
131
|
+
yield lineStr.slice(5, lineStr.length - 2);
|
|
132
|
+
buffer = buffer.subarray(lastLineEndIndex + 1);
|
|
133
|
+
lastLineEndIndex = extractSSELine(buffer);
|
|
134
|
+
}
|
|
135
|
+
if (buffer.length > 0) {
|
|
136
|
+
// 有问题,数据不完整
|
|
137
|
+
throw new Error('SSE data is not complete');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
console.error('SSE error:', error);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
reader.releaseLock();
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 解析SSE响应中的一行数据
|
|
152
|
+
* @param buffer 需要处理的数组(只读,该函数不会修改它)
|
|
153
|
+
* @returns 返回解析到的一行数据的结束索引,如果没有完整的一行数据,返回null。这个值指向最后一个\n的位置,slice的时候注意坐标运算
|
|
154
|
+
*/
|
|
155
|
+
function extractSSELine(buffer) {
|
|
156
|
+
if (buffer.length < 8) {
|
|
157
|
+
// 每行数据至少都有'data: \n\n',不够8个字节一定是不完整的
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
// 判断开头的6个字节是否是'data: '
|
|
161
|
+
if (buffer[0] !== 0x64 ||
|
|
162
|
+
buffer[1] !== 0x61 ||
|
|
163
|
+
buffer[2] !== 0x74 ||
|
|
164
|
+
buffer[3] !== 0x61 ||
|
|
165
|
+
buffer[4] !== 0x3A ||
|
|
166
|
+
buffer[5] !== 0x20) {
|
|
167
|
+
// 不是的话就有问题了
|
|
168
|
+
throw new Error('SSE data is not valid');
|
|
169
|
+
}
|
|
170
|
+
// 查找\n\n的位置
|
|
171
|
+
for (let i = 0; i < buffer.length - 1; ++i) {
|
|
172
|
+
if (buffer[i] === 0x0A && buffer[i + 1] === 0x0A) {
|
|
173
|
+
return i + 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// 没有找到\n\n,返回null
|
|
177
|
+
return null;
|
|
178
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { joinUrlWithParams, makeHttpRequest, makeSSERequest } from './http_client';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { joinUrlWithParams, makeHttpRequest, makeSSERequest } from './http_client';
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@czf0613/http_client",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Simple http client with fetch, supporting modified SSE protocol",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"http client",
|
|
8
|
+
"Typescript",
|
|
9
|
+
"browswer",
|
|
10
|
+
"SSE"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"prepack": "rm -rf dist || true && npm run build",
|
|
24
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/czf0613/http_client_ts.git"
|
|
29
|
+
},
|
|
30
|
+
"author": "czf0613",
|
|
31
|
+
"email": "1354016594@qq.com",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"homepage": "https://github.com/czf0613/http_client_ts#readme",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"typescript": "^5.9.3"
|
|
36
|
+
}
|
|
37
|
+
}
|