@ngify/http
v2.0.2
Published
A reactive HTTP client
Downloads
725
Maintainers
Readme
@ngify/http
一个形如 @angular/common/http
的响应式 HTTP 客户端,提供了以下主要功能:
先决条件
在使用 @ngify/http
之前,您应该对以下内容有基本的了解:
- JavaScript / TypeScript 编程。
- HTTP 协议的用法。
- RxJS Observable 相关技术和操作符。请参阅 Observables 指南。
API
有关完整的 API 定义,请访问 https://ngify.github.io/ngify.
安装
npm install @ngify/http
基本用法
import { HttpClient } from '@ngify/http';
import { timeout, retry } from 'rxjs';
const http = new HttpClient();
http.get('/api').subscribe();
http.post<YourType>('/api', 'body', {
params: { k: 'v' }
}).pipe(
timeout(3000),
retry(3),
).subscribe();
发起请求
HttpClient
具有与用于发出请求的不同 HTTP 动词相对应的方法,这些方法既可以加载数据,也可以在服务器上应用变更。每个方法都返回一个 RxJS Observable,订阅后会发送请求,然后在服务器响应时发出结果。
[!NOTE] 由
HttpClient
创建的Observable
可以被订阅任意多次,并且每次订阅都会发出一个新的后端请求。
通过传递给请求方法的选项对象,可以调整请求的各种属性和返回的响应类型。
获取 JSON 数据
从后端获取数据通常需要使用 HttpClient.get()
方法发出 GET 请求。此方法采用两个参数:要从中获取的字符串端点 URL,以及用于配置请求的可选选项对象。
例如,要使用 HttpClient.get()
方法从假设的 API 获取配置数据:
http.get<Config>('/api/config').subscribe(config => {
// process the configuration.
});
请注意泛型类型参数,它指定服务器返回的数据的类型为 Config
。该参数是可选的,如果省略它,则返回的数据将具有 any
类型。
[!TIP] 如果数据具有未知形状的类型,那么更安全的方法是使用
unknown
作为响应类型。
[!CAUTION] 请求方法的通用类型是关于服务器返回的数据的类型断言。
HttpClient
不会验证实际返回数据是否与该类型匹配。
获取其他类型的数据
默认情况下, HttpClient
假定服务器将返回 JSON 数据。与非 JSON API 交互时,您可以告诉 HttpClient
在发出请求时期望并返回什么响应类型。这是通过 responseType
选项完成的。
| Response type | Returned response type |
| --------------- | -------------------------------------------------------------------------------------------------------------------------- |
| 'json'
(默认) | 给定泛型类型的 JSON 数据 |
| 'text'
| 字符串文本 |
| 'blob'
| Blob
实例 |
| 'arraybuffer'
| 包含原始响应字节的 ArrayBuffer
|
例如,您可以要求 HttpClient
将 .jpeg
图像的原始字节下载到 ArrayBuffer
中:
http.get('/images/dog.jpg', { responseType: 'arraybuffer' }).subscribe(buffer => {
console.log('The image is ' + buffer.byteLength + ' bytes large');
});
改变服务器状态
执行修改的服务器 API 通常需要发出 POST 请求,并在请求正文中指定新状态或要进行的更改。
HttpClient.post()
方法的行为与 get()
类似,只是第二位参数变为 body 参数:
http.post<Config>('/api/config', newConfig).subscribe(config => {
console.log('Updated config:', config);
});
可以提供许多不同类型的值作为请求的 body
,并且 HttpClient
将相应地序列化它们:
| Body type | Serialization as |
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| string
| 纯文本 |
| number
、boolean
、Array
或 object
| JSON 字符串 |
| ArrayBuffer
| 来自 buffer 的原始数据 |
| Blob
| 具有 Blob
内容类型的原始数据 |
| FormData
| multipart/form-data
表单数据 |
| HttpParams
或 URLSearchParams
| application/x-www-form-urlencoded
格式化字符串 |
设置 URL 参数
使用 params
选项指定应包含在请求 URL 中的请求参数。
传递字面量对象是配置 URL 参数的最简单方法:
http.get('/api/config', {
params: { filter: 'all' },
}).subscribe(config => {
// ...
});
http.post('/api/config', body, {
params: { filter: 'all' },
}).subscribe(config => {
// ...
});
或者,如果您需要对参数的构造或序列化进行更多控制,则可以传递 HttpParams
的实例。
[!IMPORTANT]
HttpParams
的实例是不可变的,不能直接更改。相反,诸如append()
之类的可变方法会返回应用了变更的HttpParams
的新实例。
const baseParams = new HttpParams().set('filter', 'all');
http.get('/api/config', {
params: baseParams.set('details', 'enabled')
}).subscribe(config => {
// ...
});
http.post('/api/config', body, {
params: baseParams.set('details', 'enabled')
}).subscribe(config => {
// ...
});
您可以使用自定义 HttpParameterCodec
实例化 HttpParams
(构造方法中的第二个参数),该自定义 HttpParameterCodec
确定 HttpClient
如何将参数编码到 URL 中。
设置请求标头
使用 headers
选项指定应包含在请求中的请求标头。
传递字面量对象是配置请求标头的最简单方法:
http.get('/api/config', {
headers: {
'X-Debug-Level': 'verbose',
}
}).subscribe(config => {
// ...
});
或者,如果您需要对标头的构造进行更多控制,请传递 HttpHeaders
的实例。
[!IMPORTANT]
HttpHeaders
的实例是不可变的,不能直接更改。相反,诸如append()
之类的可变方法会返回应用了变更的HttpHeaders
的新实例。
const baseHeaders = new HttpHeaders().set('X-Debug-Level', 'minimal');
http.get<Config>('/api/config', {
headers: baseHeaders.set('X-Debug-Level', 'verbose'),
}).subscribe(config => {
// ...
});
与服务器响应事件交互
为了方便起见,HttpClient
默认返回服务器返回的数据的 Observable
(body)。有时需要检查实际响应,例如检索特定的响应标头。
要访问整个响应,请将 observe
选项设置为 'response'
:
http.get<Config>('/api/config', { observe: 'response' }).subscribe(res => {
console.log('Response status:', res.status);
console.log('Body:', res.body);
});
接收原始进度事件
除了响应正文或响应对象之外, HttpClient
还可以返回与请求生命周期中的特定时刻相对应的原始事件流。这些事件包括何时发送请求、何时返回响应标头以及何时完成正文。这些事件还可以包括报告大型请求或响应正文的上传和下载状态的进度事件。
默认情况下,进度事件处于禁用状态(因为它们会产生性能成本),但可以使用 reportProgress
选项来启用。
[!NOTE]
HttpClient
的fetch
实现不支持报告上传进度事件。
要观察事件流,请将 observe
选项设置为 'events'
:
http.post('/api/upload', myData, {
reportProgress: true,
observe: 'events',
}).subscribe(event => {
switch (event.type) {
case HttpEventType.UploadProgress:
console.log('Uploaded ' + event.loaded + ' out of ' + event.total + ' bytes');
break;
case HttpEventType.Response:
console.log('Finished uploading!');
break;
}
});
事件流中报告的每个 HttpEvent
都有一个 type
来区分事件所代表的内容:
| Event type | Description |
| -------------------------------- | -------------------------------------------------- |
| HttpEventType.Sent
| 请求已被发送。 |
| HttpEventType.UploadProgress
| 报告上传请求正文的进度 HttpUploadProgressEvent
。 |
| HttpEventType.ResponseHeader
| 已收到响应的头部,包括状态和标头。 |
| HttpEventType.DownloadProgress
| 报告响应体下载进度的 HttpDownloadProgressEvent
|
| HttpEventType.Response
| 已收到整个响应,包括响应正文。 |
| HttpEventType.User
| 来自 Http 拦截器的自定义事件。 |
处理请求失败
HTTP 请求失败有两种情况:
- 网络或连接错误可能会阻止请求到达后端服务器。
- 后端可以收到请求但无法处理,并返回错误响应。
HttpClient
在 HttpErrorResponse
中捕获这两种错误,并通过 Observable
的错误通道返回该错误。网络错误的 status
代码为 0
,error
是 ProgressEvent
的实例。
后端错误具有后端返回的失败 status
代码,以及错误响应 error
。检查响应以确定错误的原因以及处理错误的适当操作。
RxJS 提供了多个可用于错误处理的运算符。
您可以使用 catchError
运算符将错误响应转换为 UI 的值。该值可以告诉 UI 显示错误页面或值,并在必要时捕获错误原因。
有时,诸如网络中断之类的暂时性错误可能会导致请求意外失败,只需重试请求即可使其成功。RxJS 提供了几个重试运算符,它们在某些条件下自动重新订阅失败的 Observable
。例如,retry()
运算符将自动尝试重新订阅指定的次数。
Http Observables
HttpClient
上的每个请求方法都会构造并返回所请求响应类型的 Observable
。使用 HttpClient
时,了解这些 Observable
的工作原理非常重要。
HttpClient
生成 RxJS 所谓的 “冷” Observable
,这意味着在订阅 Observable
之前不会发生任何实际请求。只有这样,请求才真正发送到服务器。多次订阅同一个 Observable
会触发多个后端请求。每个订阅都是独立的。
一旦响应返回,来自 HttpClient
的 Observable
通常会自动完成(尽管拦截器会影响这一点)。
由于自动完成,如果不清理 HttpClient
订阅,通常不存在内存泄漏的风险。
拦截请求和响应
HttpClient
支持一种称为拦截器的中间件形式。
[!NOTE] TL;DR:拦截器是中间件,允许从单个请求中抽象出有关重试、缓存、日志记录和身份验证的常见模式。
HttpClient
支持两种拦截器:函数式拦截器和基于 class
的拦截器。我们的建议是使用函数式拦截器,因为它们具有更可预测的行为,尤其是在复杂的设置中。本指南中的示例使用函数式拦截器,并且我们在最后的相应部分中介绍了基于 class
的拦截器。
拦截器
拦截器通常是可以为每个请求运行的函数,并且具有影响请求和响应的内容和整体流程的广泛功能。
您可以安装多个拦截器,它们形成拦截器链,其中每个拦截器处理请求或响应,然后将其转发到链中的下一个拦截器。
您可以使用拦截器来实现各种常见模式,例如:
- 将身份验证标头添加到发送到特定 API 的传出请求。
- 使用指数退避重试失败的请求。
- 将响应缓存一段时间,或者直到因突变而失效。
- 自定义响应的解析。
- 测量服务器响应时间并记录它们。
- 在网络操作正在进行时驱动 UI 元素,例如加载旋转器。
- 收集并批量处理特定时间范围内发出的请求。
- 在可配置的截止日期或超时后自动失败请求。
- 定期轮询服务器并刷新结果。
定义拦截器
拦截器的基本形式是接收传出 HttpRequest
的函数和代表拦截器链中下一个处理步骤的 next
函数。
例如,此 loggingInterceptor
将在转发请求之前将传出请求 URL 记录到 console.log
:
export function loggingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
console.log(req.url);
return next(req);
}
为了让这个拦截器真正拦截请求,您必须配置 HttpClient
来使用它。
配置拦截器
您可以使用 withInterceptors
函数配置 HttpClient
要使用的拦截器集合:
const http = new HttpClient(
withInterceptors([loggingInterceptor, cachingInterceptor])
);
您配置的拦截器按照您列出的顺序链接在一起。在上面的示例中,loggingInterceptor
将处理请求,然后将其转发到 cachingInterceptor
。
拦截响应事件
拦截器可以转换 next
返回的 HttpEvent
的 Observable
流,以便访问或操作响应。由于此流包含所有响应事件,因此可能需要检查每个事件的 .type
以便识别最终响应对象。
export function loggingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
return next(req).pipe(tap(event => {
if (event.type === HttpEventType.Response) {
console.log(req.url, 'returned a response with status', event.status);
}
}));
}
[!TIP] 拦截器自然地将响应与其传出请求相关联,因为它们在捕获请求对象的闭包中转换响应流。
修改请求
HttpRequest
和 HttpResponse
实例的大多数方面都是不可变的,拦截器无法直接修改它们。相反,拦截器通过使用 .clone()
操作克隆这些对象并指定应在新实例中改变哪些属性来应用变更。这可能涉及对值本身执行不可变的更新(如 HttpHeaders
或 HttpParams
)。
例如,要向请求添加标头:
const reqWithHeader = req.clone({
headers: req.headers.set('X-New-Header', 'new header value'),
});
如果多次将同一个 HttpRequest
提交到拦截器链,这种不变性允许大多数拦截器具有幂等性。发生这种情况的原因有多种,包括请求失败后重试时。
[!CAUTION] 请求或响应的正文未受到深度变更的保护。如果拦截器必须改变主体,请注意处理同一请求的多次运行。
请求和响应元数据
通常,在不发送到后端但专门用于拦截器的请求中包含信息很有用。 HttpRequest
有一个 .context
对象,它将此类元数据存储为 HttpContext
的实例。该对象充当类型映射,其键类型为 HttpContextToken
。
为了说明该系统的工作原理,让我们使用元数据来控制是否为给定的请求启用缓存拦截器。
定义上下文标记
要存储缓存拦截器是否应在该请求的 .context
映射中缓存特定请求,请定义一个新的 HttpContextToken
来充当键:
export const CACHING_ENABLED = new HttpContextToken<boolean>(() => true);
提供的函数为尚未显式设置值的请求创建令牌的默认值。使用函数可确保如果令牌的值是对象或数组,则每个请求都会获得自己的实例。
在拦截器中读取令牌
然后,拦截器可以读取令牌并根据其值选择是否应用缓存逻辑:
export function cachingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
if (req.context.get(CACHING_ENABLED)) {
// apply caching logic
return ...;
} else {
// caching has been disabled for this request
return next(req);
}
}
发出请求时设置上下文令牌
通过 HttpClient
API 发出请求时,您可以为 HttpContextToken
提供值:
const data$ = http.get('/sensitive/data', {
context: new HttpContext().set(CACHING_ENABLED, false),
});
拦截器可以从请求的 HttpContext
中读取这些值。
请求上下文是可变的
与 HttpRequest
的其他属性不同,关联的 HttpContext
是可变的。如果拦截器更改了稍后重试的请求的上下文,则同一拦截器在再次运行时将观察到上下文变化。如果需要,这对于在多次重试之间传递状态很有用。
构造响应
大多数拦截器将在转换请求或响应时简单地调用下 next
函数,但这并不是严格的要求。本节讨论拦截器可以合并更高级行为的几种方法。
拦截器不需要调用 next
。它们可能会选择通过其他一些机制来构造响应,例如从缓存中或通过替代机制发送请求。
可以使用 HttpResponse
构造函数构建响应:
const resp = new HttpResponse({
body: 'response body',
});
基于 class 的拦截器
HttpClient
还支持基于 class 的拦截器,基于 class 的拦截器与函数式拦截器的功能相同,但配置方法不同。
基于 class 的拦截器是一个实现 HttpInterceptor 接口的类:
export class LoggingInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, handler: HttpHandler): Observable<HttpEvent<any>> {
console.log('Request URL: ' + req.url);
return handler.handle(req);
}
}
基于 class 的拦截器通过 withLegacyInterceptors
函数进行配置:
const http = new HttpClient(
withLegacyInterceptors([new LoggingInterceptor()]),
withInterceptors([cachingInterceptor])
);
更换 HTTP 请求实现
@ngify/http
内置了以下 HTTP 请求实现:
| HTTP 请求实现 | 包 | 描述 |
| ------------- | --------------------------------------------------------------------------------- | ------------------------------------ |
| withXhr
| @ngify/http
| 使用 XMLHttpRequest
进行 HTTP 请求 |
| withFetch
| @ngify/http
| 使用 Fetch API
进行 HTTP 请求 |
| withWx
| @ngify/http-wx
| 在 微信小程序
中进行 HTTP 请求 |
| withTaro
| @ngify/http-taro
| 在 Taro
中进行 HTTP 请求 |
| withUni
| @ngify/http-uni
| 在 Uni-app
中进行 HTTP 请求 |
HttpClient
默认使用 withXhr
,您可以自行切换到其他实现:
import { withXhr, withFetch } from '@ngify/http';
import { withWx } from '@ngify/http-wx';
const xhrHttp = new HttpClient(
withXhr()
);
const fetchHttp = new HttpClient(
withFetch()
);
const wxHttp = new HttpClient(
withWx()
);
你还可使用自定义的 HttpBackend
实现:
// 需要实现 HttpBackend 接口
class CustomHttpBackend implements HttpBackend {
handle(request: HttpRequest<any>): Observable<HttpEvent<any>> {
// ...
}
}
const customHttp = new HttpClient(
{
kind: HttpFeatureKind.Backend,
value: new CustomHttpBackend()
}
);
XSRF/CSRF 防护
HttpClient
支持用于防止 XSRF 攻击的通用机制。执行 HTTP 请求时,拦截器从 cookie 中读取令牌(默认为 XSRF-TOKEN
),并将其设置为 HTTP 标头(默认为 X-XSRF-TOKEN
)。
由于只有在您的域上运行的代码才能读取 cookie,因此后端可以确定 HTTP 请求来自您的客户端应用程序而不是攻击者。
要启用 XSRF 防护,请在 HttpClient
实例化时或在 setupHttpClient
中传递 withXsrfProtection()
:
const http = new HttpClient(
withXsrfProtection()
);
如果您的后端服务对 XSRF 令牌 cookie 或标头使用不同的名称,请自定义 cookie 名称与标头名称:
withXsrfProtection({
cookieName: 'CUSTOM_XSRF_TOKEN',
headerName: 'X-Custom-Xsrf-Header',
});
默认情况下,拦截器会在除 GET/HEAD 外的所有请求(例如 POST)上将此标头发送到相对 URL,但不会在具有绝对 URL 的请求上发送此标头。
[!NOTE]
为什么不保护 GET 请求?
仅对于可以更改后端状态的请求才需要 CSRF 保护。就其本质而言,CSRF 攻击跨越域边界,并且 Web 的同源策略将阻止攻击页面检索经过身份验证的 GET 请求的结果。
为了利用这一点,您的服务器需要在页面加载或第一个 GET 请求时在名为 XSRF-TOKEN
的 cookie 中设置一个令牌。
在后续请求中,服务器可以验证 cookie 是否与 X-XSRF-TOKEN
HTTP 标头匹配,因此确保只有在您的域上运行的代码才能发送请求。
令牌对于每个用户必须是唯一的,并且必须可由服务器验证;这可以防止客户端创建自己的令牌。
[!NOTE]
HttpClient 仅支持 XSRF 保护方案的客户端部分
您的后端服务必须配置为为您的页面设置 cookie,并验证标头是否存在于所有符合条件的请求中。如果不这样做,HttpClient 的 XSRF 保护就会失效。
全局配置
您可以使用 setupHttpClient
函数进行全局配置:
setupHttpClient(
withFetch(),
withXsrfProtection(),
);
测试请求
参考 https://angular.dev/guide/http/testing
API 差异
尽管 @ngify/http
与 @angular/common/http
在 API 上保持高度一致,但仍有一些重要的区别:
| 差异 | @angular/common/http | @ngify/http |
| ---------- | -------------------------------------------- | ----------------------------------------------------------- |
| JSONP | http.jsonp()
| 不受支持 |
| fromObject | new HttpParams({ fromObject: { k: 'v' } })
| new HttpParams({ k: 'v' })
|
| fromString | new HttpParams({ fromString: 'k=v' })
| new HttpParams('k=v')
|
| setParams | request.clone({ setParams: { k: 'v' } })
| request.clone({ params: request.params.set('k', 'v') })
|
| setHeaders | request.clone({ setHeaders: { k: 'v' } })
| request.clone({ headers: request.headers.set('k', 'v') })
|
| request() | http.request('GET', 'url')
| http.request(new HttpRequest('GET', 'url'))
|
| XSRF 防护 | 默认启用 | 默认禁用 |
迁移
从 v1 迁移到 v2,您需要进行以下修改:
- 移除
setupConfig
函数,使用setupHttpClient
替代:
// before
setupConfig({
backend: new HttpFetchBackend(),
});
// after
setupHttpClient(
withFetch()
);
HttpClient
的构造方法签名已更改,您现在需要传递HttpFeature
配置:
// before
const http = new HttpClient([
new CustomInterceptor()
]);
// after
const http = new HttpClient(
withLegacyInterceptors([new CustomInterceptor()])
);
// before
const http = new HttpClient({
backend: new HttpFetchBackend(),
interceptors: [new CustomInterceptor()]
});
// after
const http = new HttpClient(
withFetch(),
withLegacyInterceptors([new CustomInterceptor()])
);
- 如果您正在使用
@ngify/http/fetch
,请将导入点更新为@ngify/http
。 - 如果您正在使用
@ngify/http/wx
,请安装@ngify/http-wx
包并更新导入点。 - v2 仅支持 ESM,不再支持 CommonJS,如果您需要使用 CommonJS,请继续使用 v1。
.get()
、.delete()
、.head()
、.options()
的方法签名已更改,您现在需要将params
参数放置到options
参数中:
// before
http.get('/api', params, options);
// after
http.get('/api', { params });