背景
最近做了个AI大语言模型的前端页面,类似ChatGPT的那种,其中用到了流式传输的功能,即:
前端发起HTTP请求,服务端响应的HTTP Header
中Conent-Type
的值为text/event-stream
,这种传输方式称之为SSE (Server-Sent Events)
这是一个流式传输的数据,服务端可以一部分一部分的发送到前端,前端也可以一部分一部分的展示到页面中,就像这样:
(自行脑补ChatGPT的聊天回复)
不必像一般的HTTP请求那样等待所有数据下载完成再展示给用户,可以边传输边展示,用户体验较好。
使用浏览器内置的fetch
API可以轻松实现SSE流式传输,具体实现方式可以自行搜索,不是本文的重点内容。
但由于项目中使用了axios
作为请求库,就需要通过axios
来实现,但经过查阅axios
相关的文档、博客、GitHub issue 等,发现axios
对其的支持不是太好,实现起来较为繁琐,因为axios
的底层是基于XHR
的,相对来说已经是一个比较古老的API了。
所以萌生了一个想法,为何不找一个基于fetch
的现代请求库来替代axios
呢?毕竟都2024年这个时候了,fetch
的兼容性已经非常好了。
经过一系列的搜寻和对比,发现了这个十分优秀的请求库------ky
Ky 简介
Ky
是一个基于浏览器Fetch API的,小型优雅的HTTP客户端
,具有以下特点:
- 零依赖,大小仅3.3K
- 跨运行时,支持在浏览器、Node.js、Deno 等不同的运行环境中使用,理论上应该可以运行在任意兼容
fetch
API的环境中 - 支持创建实例 (类似
axios
的axios.create
) - 支持请求前后的
Hooks
钩子函数 (类似axios
的interceptors 拦截器
) - 使用比
fetch
更加的简单便捷 - 支持请求失败后自动重试的功能
- 支持超时设置
- 支持URL前缀
- 更多...... 详见
Ky
的GitHub主页
截至本文撰写的时候,Ky
的GitHub Star
数已经高达11.3K
顺带一提,这个作者还写了一个专门用于Node.js
的请求库叫做Got,其GitHub Star
数更是高达13.9K,大佬恐怖如斯。
Ky 使用
安装
shell
npm install ky --save
基础使用
有种axios
和fetch
结合体的感觉,哈哈哈
ts
import ky from 'ky';
const data = await ky.get('https://example.com/').json();
const text = await ky.get('https://example.com/').text();
const data = await ky.post('https://example.com/', { json: { username: 'xxx' } }).json();
创建实例
这里有两点需要注意:
Ky
默认会在请求出错后自动重试2次,可以通过配置retry: 0
为来关闭自动重试;Ky
实例在配置了prefixUrl (URL前缀)
之后,在发起请求时请求的路径不能再以"/
"开头,否则会报错,详见下方代码
typescript
const httpClient = ky.create({
prefixUrl: 'https://example.com/',
retry: 0, // 请求出错时 不自动重试 默认会自动重试2次
});
/** 正确的写法 最终会请求 https://example.com/api/user */
const data = httpClient.get('api/user').json();
/** 以下是错误的写法! Ky会直接报错,告诉你配置了prefixUrl后请求路径不能以"/"开头 */
const data = httpClient.get('/api/user').json();
Ky Hooks 钩子函数说明 (类似axios的拦截器)
Ky
的Hooks
与axios
的拦截器类似,都可以在请求之前或之后做一些统一的处理。
如果你不了解axios
,Ky
的Hooks
与Node.js
中的中间件(Middleware)
思路上也有些类似。
以下是Hooks
钩子函数列表:
-
beforeRequest
请求发起请求之前调用的Hook,可以在这里为请求统一添加用户的token
,类似axios
的拦截器interceptors.request.use
-
beforeRetry
请求重试之前调用的Hook -
beforeError
请求抛出HTTPError
之前调用的Hook,可以在这里统一处理所有的HTTPError,减少具体业务逻辑中的错误处理代码 -
afterResponse
请求响应之后调用的Hook,可以对响应的数据做一些统一处理 -
更多...... 详见
Ky
的GitHub主页
配置实例的Hooks 进行全局统一的错误处理
在我们的日常开发中,请求的错误一般分为2种:
1
. 业务上的错误,比如用户无权访问此接口、执行此操作之前必须先完成另一个操作等业务错误;
2
. HTTP错误,如请求超时等。
针对第2
种错误,我们可以直接在beforeError
函数中进行处理;
针对第1
种错误,就要分情况讨论了:
(1)
有些项目的业务错误,不会更改响应的HTTP状态码,后端会通过返回的JSON数据中的code
字段来区分,正常情况code
就为200
,业务错误的话code
就为其它值,这样就可以在afterResponse
函数中匹配code
的值进行统一的错误处理。
(2)
还有些项目的业务错误会直接更改响应的HTTP状态码,这种错误就和第2
种错误类似了,都属于HTTP的错误,自然也都是在beforeError
函数中进行统一的错误处理。
我现在所处的项目,就属于第(1)
种情况,以下是一个针对这种情况的Ky
全局统一错误处理小示例,如果你的项目是第(2)
种情况也可以参考其思路:
ts
const httpClient = ky.create({
prefixUrl: 'https://example.com/',
retry: 0, // 请求出错时 不自动重试 默认会自动重试2次
hooks: {
/** 请求发起之前 要执行的额外操作 */
beforeRequest: [
/** 每次发起请求前 获取用户的token 添加到请求header中 */
(request, options) => {
const token = getUserToken();
token && request.headers.set('Authorization', token);
},
],
/** 服务器响应之后 请求调用的函数resolve之前 要执行的额外操作 */
afterResponse: [
/** 每次响应之后 根据响应的code进行错误处理 */
async (request, options, response) => {
// 忽略HTTP状态码不为 200 的响应
if (response.status !== 200) {
return;
}
// 判断 如果响应头的 Content-Type 不是 JSON 则忽略
if (!response.headers.get('Content-Type')?.toLowerCase().includes('application/json')) {
return;
}
// 响应头的 Content-Type 是 JSON 继续进行处理
let data;
try {
data = await response.json();
} catch (error) {
// JSON解析失败
console.error('响应数据解析为JSON时出错:', error);
message.error('响应数据解析为JSON时出错!');
throw error;
}
// code 不存在 或不是 number 的情况 则忽略
if (typeof data.code !== 'number') {
return;
}
// 根据服务端返回的 code 进行不同的处理
switch (data.code) {
case 200: {
// 200 请求成功
// 不执行任何操作
break;
}
case 401: {
// 401 未登录 或者 token 过期
// 退出登录
signOut();
break;
}
default: {
// 其他错误
const errMsg = data.msg || '请求出错';
// 提示错误信息
message.warning(errMsg);
// 抛出错误 请求发送方法调用的位置可以捕获到此错误
throw new Error(errMsg);
}
}
},
],
/** HTTP 请求发生错误时 在抛出错误之前执行的操作
* (afterResponse中抛出的错误不会导致这个钩子执行 只会在HTTPError时执行)
*/
beforeError: [
async error => {
console.error('HTTPError', error);
message.error(error.message);
// 返回要抛出的 HTTPError 对象
return error;
},
],
},
});
结语
一段时间使用下来,感觉Ky
基本上是可以代替掉axios
的,而且Ky
可以直接返回fetch
的Response
对象,进而可以基于Response
对象进行一些高级的操作,比如上文中提到的SSE流式传输
,可以充分利用fetch
API的优势,并且结合了axios
的许多优点。
PS: 而且还支持跨运行时使用,浏览器、Node、Deno使用起来都无缝衔接 ~