From 62afb2124d3f6262abe2b017c4b2a2508a7d7956 Mon Sep 17 00:00:00 2001 From: Zhicheng Wang Date: Wed, 14 Mar 2018 08:48:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BF=BB=E8=AF=91=E3=80=8AHttpClient?= =?UTF-8?q?=E3=80=8B=E4=B8=AD=E9=81=97=E6=BC=8F=E7=9A=84=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aio/content/guide/http.md | 485 +++++++++++++++++++++++++++++++++++++- 1 file changed, 477 insertions(+), 8 deletions(-) diff --git a/aio/content/guide/http.md b/aio/content/guide/http.md index 962c54bf0a..f05ffa97e1 100644 --- a/aio/content/guide/http.md +++ b/aio/content/guide/http.md @@ -1,7 +1,5 @@ # HttpClient -# HttpClient 库 - Most front-end applications communicate with backend services over the HTTP protocol. Modern browsers support two different APIs for making HTTP requests: the `XMLHttpRequest` interface and the `fetch()` API. 大多数前端应用都需要通过 HTTP 协议与后端服务器通讯。现代浏览器支持使用两种不同的 API 发起 HTTP 请求:`XMLHttpRequest` 接口和 `fetch()` API。 @@ -15,6 +13,8 @@ Additional benefits of `HttpClient` include testability features, typed request You can run the that accompanies this guide. +你可以到 中运行本章的代码。 +
The sample app does not require a data server. @@ -23,8 +23,13 @@ It relies on the which replaces the _HttpClient_ module's `HttpBackend`. The replacement service simulates the behavior of a REST-like backend. +该应用代码并不需要数据服务器。 +它基于 [Angular _in-memory-web-api_](https://github.com/angular/in-memory-web-api/blob/master/README.md) 库,该库会替换 `HttpClient` 模块中的 `HttpBackend`。用于替换的这个服务会模拟 REST 风格的后端的行为。 + Look at the `AppModule` _imports_ to see how it is configured. +到 `AppModule` 的 `imports` 中查看这个库是如何配置的。 +
## Setup @@ -34,6 +39,8 @@ Look at the `AppModule` _imports_ to see how it is configured. Before you can use the `HttpClient`, you need to import the Angular `HttpClientModule`. Most apps do so in the root `AppModule`. +要想使用 `HttpClient`,就要先导入 Angular 的 `HttpClientModule`。大多数应用都会在根模块 `AppModule` 中导入它。 + @@ -65,6 +79,8 @@ that specifies resource URLs. The `ConfigService` fetches this file with a `get()` method on `HttpClient`. +`ConfigService` 会通过 `HttpClient` 的 `get()` 方法取得这个文件。 + @@ -115,12 +153,20 @@ The subscribe callback above requires bracket notation to extract the data value You can't write `data.heroesUrl` because TypeScript correctly complains that the `data` object from the service does not have a `heroesUrl` property. +你没法写成 `data.heroesUrl` ,因为 TypeScript 会报告说来自服务器的 `data` 对象中没有名叫 `heroesUrl` 的属性。 + The `HttpClient.get()` method parsed the JSON server response into the anonymous `Object` type. It doesn't know what the shape of that object is. +这是因为 `HttpClient.get()` 方法把 JSON 响应体解析成了匿名的 `Object` 类型。它不知道该对象的具体形态如何。 + You can tell `HttpClient` the type of the response to make consuming the output easier and more obvious. +你可以告诉 `HttpClient` 该响应体的类型,以便让对这种输出的消费更容易、更明确。 + First, define an interface with the correct shape: +首先,定义一个具有正确形态的接口: + @@ -129,6 +175,8 @@ First, define an interface with the correct shape: Then, specify that interface as the `HttpClient.get()` call's type parameter in the service: +然后,在服务器中把该接口指定为 `HttpClient.get()` 调用的类型参数。 + `. +不是所有的 API 都会返回 JSON 数据。在下面这个例子中,`DownloaderService` 中的方法会从服务器读取文本文件, +并把文件的内容记录下来,然后把这些内容使用 `Observable` 的形式返回给调用者。 + `. `HttpClient.get()` returns a string rather than the default JSON because of the `responseType` option. +这里的 `HttpClient.get()` 返回字符串而不是默认的 JSON 对象,因为它的 `responseType` 选项是 `'text'`。 + The RxJS `tap` operator (as in "wiretap") lets the code inspect good and error values passing through the observable without disturbing them. +RxJS 的 `tap` 操作符(可看做 wiretap - 窃听),让这段代码探查由可观察对象传过来的正确值和错误值,而不用打扰它们。 + A `download()` method in the `DownloaderComponent` initiates the request by subscribing to the service method. +在 `DownloaderComponent` 中的 `download()` 方法通过订阅这个服务中的方法来发起一次请求。 + The component isn't expecting a result from the delete operation and subscribes without a callback. The bare `.subscribe()` _seems_ pointless. +该组件不关心删除操作返回的结果,订阅时也没有回调函数。 +单纯的 `.subscribe()` 方法看似毫无意义。 + In fact, it is essential. Merely calling `HeroService.deleteHero()` **does not initiate the DELETE request.** +但实际上,它是必备的。 +否则调用 `HeroService.deleteHero()` 时**不会发起 DELETE 请求**。 + @@ -427,12 +577,18 @@ Merely calling `HeroService.deleteHero()` **does not initiate the DELETE request ### Always _subscribe_! +### 别忘了*订阅*! + An `HttpClient` method does not begin its HTTP request until you call `subscribe()` on the observable returned by that method. This is true for _all_ `HttpClient` _methods_. +在调用方法返回的可观察对象的 `subscribe()` 方法之前,`HttpClient` 方法不会发起 HTTP 请求。这适用于 `HttpClient` 的*所有方法*。 +
The [`AsyncPipe`](api/common/AsyncPipe) subscribes (and unsubscribes) for you automatically. +[`AsyncPipe`](api/common/AsyncPipe) 会自动为你订阅(以及取消订阅)。 +
All observables returned from `HttpClient` methods are _cold_ by design. @@ -440,16 +596,26 @@ Execution of the HTTP request is _deferred_, allowing you to extend the observable with additional operations such as `tap` and `catchError` before anything actually happens. +`HttpClient` 的所有方法返回的可观察对象都设计为*冷的*。 +HTTP 请求的执行都是*延期执行的*,让你可以用 `tap` 和 `catchError` 这样的操作符来在实际执行什么之前,先对这个可观察对象进行扩展。 + Calling `subscribe(...)` triggers execution of the observable and causes `HttpClient` to compose and send the HTTP request to the server. +调用 `subscribe(...)` 会触发这个可观察对象的执行,并导致 `HttpClient` 组合并把 HTTP 请求发给服务器。 + You can think of these observables as _blueprints_ for actual HTTP requests. +你可以把这些可观察对象看做实际 HTTP 请求的*蓝图*。 +
In fact, each `subscribe()` initiates a separate, independent execution of the observable. Subscribing twice results in two HTTP requests. +实际上,每个 `subscribe()` 都会初始化此可观察对象的一次单独的、独立的执行。 +订阅两次就会导致发起两个 HTTP 请求。 + ```javascript const req = http.get('/api/heroes'); @@ -465,9 +631,14 @@ req.subscribe(); ### Making a PUT request +### 发起 PUT 请求 + An app will send a PUT request to completely replace a resource with updated data. The following `HeroService` example is just like the POST example. +应用可以发送 PUT 请求,来使用修改后的数据完全替换掉一个资源。 +下面的 `HeroService` 例子和 POST 的例子很像。 + @@ -517,6 +708,9 @@ before making the next request. Adding URL search parameters works a similar way. Here is a `searchHeroes` method that queries for heroes whose names contain the search term. +添加 URL 搜索参数也与此类似。 +这里的 `searchHeroes` 方法会查询名字中包含搜索词的英雄列表。 + @@ -525,17 +719,29 @@ Here is a `searchHeroes` method that queries for heroes whose names contain the If there is a search term, the code constructs an options object with an HTML URL encoded search parameter. If the term were "foo", the GET request URL would be `api/heroes/?name=foo`. -The `HttpParms` are immutable so you'll have to use the `set()` method to update the options. +如果有搜索词,这段代码就会构造一个包含进行过 URL 编码的搜索词的选项对象。如果这个搜索词是“foo”,这个 GET 请求的 URL 就会是 `api/heroes/?name=foo`。 + +The `HttpParams` are immutable so you'll have to use the `set()` method to update the options. + +`HttpParams` 是不可变的,所以你也要使用 `set()` 方法来修改这些选项。 ### Debouncing requests +### 请求的防抖(debounce) + The sample includes an _npm package search_ feature. +这个例子还包含了*搜索 npm 包*的特性。 + When the user enters a name in a search-box, the `PackageSearchComponent` sends a search request for a package with that name to the NPM web api. +当用户在搜索框中输入名字时,`PackageSearchComponent` 就会把一个根据名字搜索包的请求发送给 NPM 的 web api。 + Here's a pertinent excerpt from the template: +下面是模板中的相关代码片段: + The `withRefresh` option is explained [below](#cache-refresh). +[稍后](#cache-refresh) 解释了这个 `withRefresh` 选项。 +
#### _switchMap()_ The `switchMap()` operator has three important characteristics. +这个 `switchMap()` 操作符有三个重要的特征: + 1. It takes a function argument that returns an `Observable`. `PackageSearchService.search` returns an `Observable`, as other data service methods do. + 它的参数是一个返回 `Observable` 的函数。`PackageSearchService.search` 会返回 `Observable`,其它数据服务也一样。 + 2. If a previous search request is still _in-flight_ (as when the connection is poor), it cancels that request and sends a new one. + 如果以前的搜索结果仍然是*在途*状态(这会出现在慢速网络中),它会取消那个请求,并发起这个新的搜索。 + 3. It returns service responses in their original request order, even if the server returns them out of order. + 它会按照原始的请求顺序返回这些服务的响应,而不用关心服务器实际上是以乱序返回的它们。 +
If you think you'll reuse this debouncing logic, consider moving it to a utility function or into the `PackageSearchService` itself. +如果你觉得将来会复用这些防抖逻辑, +可以把它移到单独的工具函数中,或者移到 `PackageSearchService` 中。 +
### Intercepting requests and responses +### 拦截请求和响应 + _HTTP Interception_ is a major feature of `@angular/common/http`. With interception, you declare _interceptors_ that inspect and transform HTTP requests from your application to the server. The same interceptors may also inspect and transform the server's responses on their way back to the application. Multiple interceptors form a _forward-and-backward_ chain of request/response handlers. +*HTTP* 拦截机制是 `@angular/common/http` 中的主要特性之一。 +使用这种拦截机制,你可以声明*一些拦截器*,用它们监视和转换从应用发送到服务器的 HTTP 请求。 +拦截器还可以用监视和转换从服务器返回到本应用的那些响应。 +多个选择器会构成一个“请求/响应处理器”的双向链表。 + Interceptors can perform a variety of _implicit_ tasks, from authentication to logging, in a routine, standard way, for every HTTP request/response. +拦截器可以用一种常规的、标准的方式对每一次 HTTP 的请求/响应任务执行从认证到记日志等很多种*隐式*任务。 + Without interception, developers would have to implement these tasks _explicitly_ for each `HttpClient` method call. +如果没有拦截机制,那么开发人员将不得不对每次 `HttpClient` 调用*显式*实现这些任务。 + #### Write an interceptor +#### 编写拦截器 + To implement an interceptor, declare a class that implements the `intercept()` method of the `HttpInterceptor` interface. +要实现拦截器,就要实现一个实现了 `HttpInterceptor` 接口中的 `intercept()` 方法的类。 + Here is a do-nothing _noop_ interceptor that simply passes the request through without touching it: + 这里是一个什么也不做的*空白*拦截器,它只会不做任何修改的传递这个请求。 + @@ -679,14 +970,22 @@ Note the `multi: true` option. This required setting tells Angular that `HTTP_INTERCEPTORS` is a token for a _multiprovider_ that injects an array of values, rather than a single value. +注意 `multi: true` 选项。 +这个必须的选项会告诉 Angular `HTTP_INTERCEPTORS` 是一个*多重提供商*的令牌,表示它会注入一个多值的数组,而不是单一的值。 + You _could_ add this provider directly to the providers array of the `AppModule`. However, it's rather verbose and there's a good chance that you'll create more interceptors and provide them in the same way. You must also pay [close attention to the order](#interceptor-order) in which you provide these interceptors. +你*也可以*直接把这个提供商添加到 `AppModule` 中的提供商数组中,不过那样会非常啰嗦。况且,你将来还会用这种方式创建更多的拦截器并提供它们。 +你还要[特别注意提供这些拦截器的顺序](#interceptor-order)。 + Consider creating a "barrel" file that gathers all the interceptor providers into an `httpInterceptorProviders` array, starting with this first one, the `NoopInterceptor`. +认真考虑创建一个封装桶(barrel)文件,用于把所有拦截器都收集起来,一起提供给 `httpInterceptorProviders` 数组,可以先从这个 `NoopInterceptor` 开始。 + There are many more interceptors in the complete sample code. +在完整版的范例代码中还有更多的拦截器。 + #### Interceptor order +#### 拦截器的顺序 + Angular applies interceptors in the order that you provide them. If you provide interceptors _A_, then _B_, then _C_, requests will flow in _A->B->C_ and responses will flow out _C->B->A_. +Angular 会按照你提供它们的顺序应用这些拦截器。 +如果你提供拦截器的顺序是先 *A*,再 *B*,再 *C*,那么请求阶段的执行顺序就是 *A->B->C*,而响应阶段的执行顺序则是 + *C->B->A*。 + You cannot change the order or remove interceptors later. If you need to enable and disable an interceptor dynamically, you'll have to build that capability into the interceptor itself. +以后你就再也不能修改这些顺序或移除某些拦截器了。 +如果你需要动态启用或禁用某个拦截器,那就要在那个拦截器中自行实现这个功能。 + #### _HttpEvents_ You may have expected the `intercept()` and `handle()` methods to return observables of `HttpResponse` as most `HttpClient` methods do. +你可能会期望 `intercept()` 和 `handle()` 方法会像大多数 `HttpClient` 中的方法那样返回 `HttpResponse` 的可观察对象。 + Instead they return observables of `HttpEvent`. +然而并没有,它们返回的是 `HttpEvent` 的可观察对象。 + That's because interceptors work at a lower level than those `HttpClient` methods. A single HTTP request can generate multiple _events_, including upload and download progress events. The `HttpResponse` class itself is actually an event, whose type is `HttpEventType.HttpResponseEvent`. +这是因为拦截器工作的层级比那些 `HttpClient` 方法更低一些。每个 HTTP 请求都可能会生成很多个*事件*,包括上传和下载的进度事件。 +实际上,`HttpResponse` 类本身就是一个事件,它的类型(`type`)是 `HttpEventType.HttpResponseEvent`。 + Many interceptors are only concerned with the outgoing request and simply return the event stream from `next.handle()` without modifying it. +很多拦截器只关心发出的请求,而对 `next.handle()` 返回的事件流不会做任何修改。 + But interceptors that examine and modify the response from `next.handle()` will see all of these events. Your interceptor should return _every event untouched_ unless it has a _compelling reason to do otherwise_. +但那些要检查和修改来自 `next.handle()` 的响应体的拦截器希望看到所有这些事件。 +所以,你的拦截器应该返回*你没碰过的所有事件*,除非你*有充分的理由不这么做*。 + #### Immutability +#### 不可变性 + Although interceptors are capable of mutating requests and responses, the `HttpRequest` and `HttpResponse` instance properties are `readonly`, rendering them largely immutable. +虽然拦截器有能力改变请求和响应,但 `HttpRequest` 和 `HttpResponse` 实例的属性却是只读(`readonly`)的, +因此,它们在很大意义上说是不可变对象。 + They are immutable for a good reason: the app may retry a request several times before it succeeds, which means that the interceptor chain may re-process the same request multiple times. If an interceptor could modify the original request object, the re-tried operation would start from the modified request rather than the original. Immutability ensures that interceptors see the same request for each try. +有充足的理由把它们做成不可变对象:应用可能会重试发送很多次请求之后才能成功,这就意味着这个拦截器链表可能会多次重复处理同一个请求。 +如果拦截器可以修改原始的请求对象,那么重试阶段的操作就会从修改过的请求开始,而不是原始请求。 +而这种不可变性,可以确保这些拦截器在每次重试时看到的都是同样的原始请求。 + TypeScript will prevent you from setting `HttpRequest` readonly properties. +通过把 `HttpRequest` 的属性设置为只读的,TypeScript 可以防止你犯这种错误。 + ```javascript // Typescript disallows the following assignment because req.url is readonly @@ -756,6 +1093,9 @@ TypeScript will prevent you from setting `HttpRequest` readonly properties. To alter the request, clone it first and modify the clone before passing it to `next.handle()`. You can clone and modify the request in a single step as in this example. +要想修改该请求,就要先克隆它,并修改这个克隆体,然后再把这个克隆体传给 `next.handle()`。 +你可以用一步操作中完成对请求的克隆和修改,例子如下: + preserve original body @@ -803,12 +1158,19 @@ If you set the cloned request body to `null`, Angular knows you intend to clear #### Set default headers +#### 设置默认请求头 + Apps often use an interceptor to set default headers on outgoing requests. +应用通常会使用拦截器来设置外发请求的默认请求头。 + The sample app has an `AuthService` that produces an authorization token. Here is its `AuthInterceptor` that injects that service to get the token and adds an authorization header with that token to every outgoing request: +该范例应用具有一个 `AuthService`,它会生成一个认证令牌。 +在这里,`AuthInterceptor` 会注入该服务以获取令牌,并对每一个外发的请求添加一个带有该令牌的认证头: + @@ -818,6 +1180,8 @@ adds an authorization header with that token to every outgoing request: The practice of cloning a request to set new headers is so common that there's a `setHeaders` shortcut for it: +这种在克隆请求的同时设置新请求头的操作太常见了,因此它还有一个快捷方式 `setHeaders`: + @@ -830,15 +1194,15 @@ An interceptor that alters headers can be used for a number of different operati * Authentication/authorization - 认证 / 授权 + 认证 / 授权 * Caching behavior; for example, `If-Modified-Since` - 控制缓存行为。比如`If-Modified-Since` + 控制缓存行为。比如`If-Modified-Since` * XSRF protection - XSRF 防护 + XSRF 防护 #### Logging @@ -847,10 +1211,14 @@ An interceptor that alters headers can be used for a number of different operati Because interceptors can process the request and response _together_, they can do things like time and log an entire HTTP operation. +因为拦截器可以*同时*处理请求和响应,所以它们也可以对整个 HTTP 操作进行计时和记录日志。 + Consider the following `LoggingInterceptor`, which captures the time of the request, the time of the response, and logs the outcome with the elapsed time with the injected `MessageService`. +考虑下面这个 `LoggingInterceptor`,它捕获请求的发起时间、响应的接收时间,并使用注入的 `MessageService` 来发送总共花费的时间。 + @@ -939,6 +1348,8 @@ and emits again later with the updated search results. The _cache-then-refresh_ option is triggered by the presence of a **custom `x-refresh` header**. +这种*缓存并刷新*的选项是由**自定义的 `x-refresh` 头**触发的。 +
A checkbox on the `PackageSearchComponent` toggles a `withRefresh` flag, @@ -946,6 +1357,10 @@ which is one of the arguments to `PackageSearchService.search()`. That `search()` method creates the custom `x-refresh` header and adds it to the request before calling `HttpClient.get()`. +`PackageSearchComponent` 中的一个检查框会切换 `withRefresh` 标识, +它是 `PackageSearchService.search()` 的参数之一。 +`search()` 方法创建了自定义的 `x-refresh` 头,并在调用 `HttpClient.get()` 前把它添加到请求里。 +
The revised `CachingInterceptor` sets up a server request @@ -953,14 +1368,24 @@ whether there's a cached value or not, using the same `sendRequest()` method described [above](#send-request). The `results$` observable will make the request when subscribed. +修改后的 `CachingInterceptor` 会发起一个服务器请求,而不管有没有缓存的值。 +就像 [前面](#send-request) 的 `sendRequest()` 方法一样进行订阅。 +在订阅 `results$` 可观察对象时,就会发起这个请求。 + If there's no cached value, the interceptor returns `results$`. +如果没有缓存的值,拦截器直接返回 `result$`。 + If there is a cached value, the code _pipes_ the cached response onto `results$`, producing a recomposed observable that emits twice, the cached response first (and immediately), followed later by the response from the server. Subscribers see a sequence of _two_ responses. +如果有缓存的值,这些代码就会把缓存的响应加入到 `result$` 的管道中,使用重组后的可观察对象进行处理,并发出两次。 +先立即发出一次缓存的响应体,然后发出来自服务器的响应。 +订阅者将会看到一个包含这*两个*响应的序列。 + ### Listening to progress events ### 监听进度事件 @@ -969,9 +1394,15 @@ Sometimes applications transfer large amounts of data and those transfers can ta File uploads are a typical example. Give the users a better experience by providing feedback on the progress of such transfers. +有事,应用会传输大量数据,并且这些传输可能会花费很长时间。 +典型的例子是文件上传。 +可以通过在传输过程中提供进度反馈,来提升用户体验。 + To make a request with progress events enabled, you can create an instance of `HttpRequest` with the `reportProgress` option set true to enable tracking of progress events. +要想开启进度事件的响应,你可以创建一个把 `reportProgress` 选项设置为 `true` 的 `HttpRequest` 实例,以开启进度跟踪事件。 + Next, pass this request object to the `HttpClient.request()` method, which returns an `Observable` of `HttpEvents`, the same events processed by interceptors: +接下来,把这个请求对象传给 `HttpClient.request()` 方法,它返回一个 `HttpEvents` 的 `Observable`,同样也可以在拦截器中处理这些事件。 + ## Security: XSRF Protection @@ -1082,21 +1522,33 @@ setting up such mocking straightforward. Angular's HTTP testing library is designed for a pattern of testing wherein the the app executes code and makes requests first. +Angular 的 HTTP 测试库是专为其中的测试模式而设计的。在这种模式下,会首先在应用中执行代码并发起请求。 + Then a test expects that certain requests have or have not been made, performs assertions against those requests, and finally provide responses by "flushing" each expected request. +然后,每个测试会期待发起或未发起过某个请求,对这些请求进行断言, +最终对每个所预期的请求进行刷新(flush)来对这些请求提供响应。 + At the end, tests may verify that the app has made no unexpected requests. +最终,测试可能会验证这个应用不曾发起过非预期的请求。 +
You can run these sample tests in a live coding environment. +你可以到在线编程环境中运行这些范例测试。 + The tests described in this guide are in `src/testing/http-client.spec.ts`. There are also tests of an application data service that call `HttpClient` in `src/app/heroes/heroes.service.spec.ts`. +本章所讲的这些测试位于 `src/testing/http-client.spec.ts` 中。 +在 `src/app/heroes/heroes.service.spec.ts` 中还有一些测试,用于测试那些调用了 `HttpClient` 的数据服务。 +
### Setup @@ -1119,6 +1571,8 @@ along with the other symbols your tests require. Then add the `HttpClientTestingModule` to the `TestBed` and continue with the setup of the _service-under-test_. +然后把 `HTTPClientTestingModule` 添加到 `TestBed` 中,并继续设置*被测服务*。 +