这一次,放下 axios,使用基于 rxjs 的响应式 HTTP 客户端

16,295 阅读5分钟

众所周知,在浏览器端和 Node.js 端使用最广泛的 HTTP 客户端为 axios 。想必大家都对它很熟悉,它是一个用于浏览器和 Node.js 的、基于 Promise 的 HTTP 客户端,但这次的主角不是它。

起源

axios 的前身其实是 AngularJS$http 服务。

为了避免混淆,这里需要澄清一下:AngularJS 并不等于 AngularAngularJS 是特指 angular.js v1.x 版本,而 Angular 特指 angular v2+ (没有 .js)和其包含的一系列工具链。

这样说可能不太严谨,但 axios 深受 AngularJS 中提供的$http 服务的启发。归根结底,axios 是为了提供一个类似独立的服务,以便在 AngularJS 之外使用。

发展

但在 Angular 中,却没有继续沿用之前的 $http 服务,而是与 rxjs 深度结合,提供了一个比 $http 服务更先进的、现代化的,响应式的 HTTP 客户端。 在这个响应式的 HTTP Client 中,发送请求后接收到的不再是一个 Promise ,而是来自 rxjsObservable,我们可以订阅它,从而侦听到请求的响应:

const observable = http.get('url');
observable.subscribe(o => console.log(o));

有关它的基本形态及详细用法,请参考官方文档

正文

@ngify/http 是一个形如 Angular HttpClient 的响应式 HTTP 客户端。@ngify/http的目标与 axios 相似:提供一个类似独立的服务,以便在 Angular 之外使用。

@ngify/http 提供了以下主要功能:

先决条件

在使用 @ngify/http 之前,您应该对以下内容有基本的了解:

  • JavaScript / TypeScript 编程。
  • HTTP 协议的用法。
  • RxJS Observable 相关技术和操作符。请参阅 Observables 指南。

API

有关完整的 API 定义,请访问 ngify.github.io/ngify.

可靠性

@ngify/http 使用且通过了 Angular HttpClient 的单元测试(测试代码根据 API 的细微差异做出了相应的更改)。

安装

npm i @ngify/http

基本用法

import { HttpClient, HttpContext, HttpContextToken, HttpHeaders, HttpParams } from '@ngify/http';
import { filter } from 'rxjs';

const http = new HttpClient();

http.get<{ code: number, data: any, msg: string }>('url', 'k=v').pipe(
  filter(({ code }) => code === 0)
).subscribe(res => console.log(res));

http.post('url', { k: 'v' }).subscribe(res => console.log(res));

const HTTP_CACHE_TOKEN = new HttpContextToken(() => 1800000);

http.put('url', null, {
  context: new HttpContext().set(HTTP_CACHE_TOKEN)
}).subscribe(res => console.log(res));

http.patch('url', null, {
  params: { k: 'v' }
}).subscribe(res => console.log(res));

http.delete('url', new HttpParams('k=v'), {
  headers: new HttpHeaders({ Authorization: 'token' })
}).subscribe(res => console.log(res));

拦截请求和响应

借助拦截机制,你可以声明一些拦截器,它们可以检查并转换从应用中发给服务器的 HTTP 请求。这些拦截器还可以在返回应用的途中检查和转换来自服务器的响应。多个拦截器构成了请求/响应处理器的双向链表。

@ngify/http 会按照您提供拦截器的顺序应用它们。

import { HttpClient, HttpHandler, HttpRequest, HttpEvent, HttpInterceptor, HttpEventType } from '@ngify/http';
import { Observable, tap } from 'rxjs';

const http = new HttpClient([
  new class implements HttpInterceptor {
    intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
      // 克隆请求以修改请求参数
      request = request.clone({
        headers: request.headers.set('Authorization', 'token')
      });

      return next.handle(request);
    }
  },
  {
    intercept(request: HttpRequest<unknown>, next: HttpHandler) {
      request = request.clone({
        params: request.params.set('k', 'v')
      });

      console.log('拦截后的请求', request);

      return next.handle(request).pipe(
        tap(response => {
          if (response.type === HttpEventType.Response) {
            console.log('拦截后的响应', response);
          }
        })
      );
    }
  }
]);

虽然拦截器有能力改变请求和响应,但 HttpRequestHttpResponse 实例的属性是只读的,因此让它们基本上是不可变的。

有充足的理由把它们做成不可变对象:应用可能会重试发送很多次请求之后才能成功,这就意味着这个拦截器链表可能会多次重复处理同一个请求。 如果拦截器可以修改原始的请求对象,那么重试阶段的操作就会从修改过的请求开始,而不是原始请求。 而这种不可变性,可以确保这些拦截器在每次重试时看到的都是同样的原始请求。

如果你需要修改一个请求,请先将它克隆一份,修改这个克隆体后再把它传递给 next.handle()

替换 HTTP 请求类

@ngify/http 内置了以下 HTTP 请求类:

HTTP 请求类描述
HttpXhrBackend使用 XMLHttpRequest 进行 HTTP 请求
HttpFetchBackend使用 Fetch API 进行 HTTP 请求
HttpWxBackend微信小程序 中进行 HTTP 请求

默认使用 HttpXhrBackend,可以通过修改配置切换到其他的 HTTP 请求类:

import { HttpFetchBackend, HttpWxBackend, setupConfig } from '@ngify/http';

setupConfig({
  backend: new HttpFetchBackend()
});

你还可使用自定义的 HttpBackend 实现类:

import { HttpBackend, HttpClient, HttpRequest, HttpEvent, setupConfig } from '@ngify/http';
import { Observable } from 'rxjs';

// 需要实现 HttpBackend 接口
class CustomHttpBackend implements HttpBackend {
  handle(request: HttpRequest<any>): Observable<HttpEvent<any>> {
    // ...
  }
}

setupConfig({
  backend: new CustomHttpBackend()
});

如果需要为某个 HttpClient 单独配置 HttpBackend,可以在 HttpClient 构造方法中传入:

const http = new HttpClient(new CustomHttpBackend());

// 或者

const http = new HttpClient({
  interceptors: [/* 一些拦截器 */],
  backend: new CustomHttpBackend()
});

在 Node.js 中使用

@ngify/http 默认使用浏览器实现的 XMLHttpRequestFetch API。要在 Node.js 中使用,您需要进行以下步骤:

XMLHttpRequest

如果需要在 Node.js 环境下使用 XMLHttpRequest,可以使用 xhr2,它在 Node.js API 上实现了 W3C XMLHttpRequest 规范。
要使用 xhr2 ,您需要创建一个返回 XMLHttpRequest 实例的工厂函数,并将其作为参数传递给 HttpXhrBackend 构造函数:

import { HttpXhrBackend, setupConfig } from '@ngify/http';
import * as xhr2 from 'xhr2';

setupConfig({
  backend: new HttpXhrBackend(() => new xhr2.XMLHttpRequest())
});

Fetch API

如果需要在 Node.js 环境下使用 Fetch API,可以使用 node-fetchabort-controller
要应用它们,您需要分别将它们添加到 Node.jsglobal

import fetch from 'node-fetch';
import AbortController from 'abort-controller';
import { HttpFetchBackend, HttpWxBackend, setupConfig } from '@ngify/http';

global.fetch = fetch;
global.AbortController = AbortController;

setupConfig({
  backend: new HttpFetchBackend()
});

传递额外参数

为保持 API 的统一,需要借助 HttpContext 来传递一些额外参数。

Fetch API 额外参数

import { HttpContext, FETCH_TOKEN } from '@ngify/http';

// ...

// Fetch API 允许跨域请求
http.get('url', null, {
  context: new HttpContext().set(FETCH_TOKEN, {
    mode: 'cors',
    // ...
  })
});

微信小程序额外参数

import { HttpContext, WX_UPLOAD_FILE_TOKEN, WX_DOWNLOAD_FILE_TOKEN, WX_REQUSET_TOKEN } from '@ngify/http';

// ...

// 微信小程序开启 HTTP2
http.get('url', null, {
  context: new HttpContext().set(WX_REQUSET_TOKEN, {
    enableHttp2: true,
  })
});

// 微信小程序文件上传
http.post('url', null, {
  context: new HttpContext().set(WX_UPLOAD_FILE_TOKEN, {
    filePath: 'filePath',
    fileName: 'fileName'
  })
});

// 微信小程序文件下载
http.get('url', null, {
  context: new HttpContext().set(WX_DOWNLOAD_FILE_TOKEN, {
    filePath: 'filePath'
  })
});

更多

有关更多用法,请访问 angular.cn

下一章《使用基于 rxjs 的响应式 HTTP 客户端处理常见的请求场景》