` 的可观察对象。
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
req.url = req.url.replace('http://', 'https://');
```
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()`。
你可以用一步操作中完成对请求的克隆和修改,例子如下:
The `clone()` method's hash argument allows you to mutate specific properties of the request while copying the others.
这个 `clone()` 方法的哈希型参数允许你在复制出克隆体的同时改变该请求的某些特定属性。
##### The request body
##### 请求体
The `readonly` assignment guard can't prevent deep updates and, in particular,
it can't prevent you from modifying a property of a request body object.
`readonly` 这种赋值保护,无法防范深修改(修改子对象的属性),也不能防范你修改请求体对象中的属性。
```javascript
req.body.name = req.body.name.trim(); // bad idea!
```
If you must mutate the request body, copy it first, change the copy,
`clone()` the request, and set the clone's body with the new body, as in the following example.
如果你必须修改请求体,那么就要先复制它,然后修改这个复本,`clone()` 这个请求,然后把这个请求体的复本作为新的请求体,例子如下:
##### Clearing the request body
##### 清空请求体
Sometimes you need to clear the request body rather than replace it.
If you set the cloned request body to `undefined`, Angular assumes you intend to leave the body as is.
That is not what you want.
If you set the cloned request body to `null`, Angular knows you intend to clear the request body.
有时你需要清空请求体,而不是替换它。
如果你把克隆后的请求体设置成 `undefined`,Angular 会认为你是想让这个请求体保持原样。
这显然不是你想要的。
但如果把克隆后的请求体设置成 `null`,那 Angular 就知道你是想清空这个请求体了。
```javascript
newReq = req.clone({ ... }); // body not mentioned => preserve original body
newReq = req.clone({ body: undefined }); // preserve original body
newReq = req.clone({ body: null }); // clear the body
```
#### 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` 会注入该服务以获取令牌,并对每一个外发的请求添加一个带有该令牌的认证头:
The practice of cloning a request to set new headers is so common that
there's a `setHeaders` shortcut for it:
这种在克隆请求的同时设置新请求头的操作太常见了,因此它还有一个快捷方式 `setHeaders`:
An interceptor that alters headers can be used for a number of different operations, including:
这种可以修改头的拦截器可以用于很多不同的操作,比如:
* Authentication/authorization
认证 / 授权
* Caching behavior; for example, `If-Modified-Since`
控制缓存行为。比如 `If-Modified-Since`
* XSRF protection
XSRF 防护
#### Logging
#### 记日志
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` 来发送总共花费的时间。
The RxJS `tap` operator captures whether the request succeed or failed.
The RxJS `finalize` operator is called when the response observable either errors or completes (which it must),
and reports the outcome to the `MessageService`.
RxJS 的 `tap` 操作符会捕获请求成功了还是失败了。
RxJS 的 `finalize` 操作符无论在响应成功还是失败时都会调用(这是必须的),然后把结果汇报给 `MessageService`。
Neither `tap` nor `finalize` touch the values of the observable stream returned to the caller.
在这个可观察对象的流中,无论是 `tap` 还是 `finalize` 接触过的值,都会照常发送给调用者。
#### Caching
#### 缓存
Interceptors can handle requests by themselves, without forwarding to `next.handle()`.
拦截器还可以自行处理这些请求,而不用转发给 `next.handle()`。
For example, you might decide to cache certain requests and responses to improve performance.
You can delegate caching to an interceptor without disturbing your existing data services.
比如,你可能会想缓存某些请求和响应,以便提升性能。
你可以把这种缓存操作委托给某个拦截器,而不破坏你现有的各个数据服务。
The `CachingInterceptor` demonstrates this approach.
`CachingInterceptor` 演示了这种方式。
The `isCachable()` function determines if the request is cachable.
In this sample, only GET requests to the npm package search api are cachable.
`isCachable()` 函数用于决定该请求是否允许缓存。
在这个例子中,只有发到 npm 包搜索 API 的 GET 请求才是可以缓存的。
If the request is not cachable, the interceptor simply forwards the request
to the next handler in the chain.
如果该请求是不可缓存的,该拦截器只会把该请求转发给链表中的下一个处理器。
If a cachable request is found in the cache, the interceptor returns an `of()` _observable_ with
the cached response, by-passing the `next` handler (and all other interceptors downstream).
如果可缓存的请求在缓存中找到了,该拦截器就会通过 `of()` 函数返回一个已缓存的响应体的*可观察对象*,然后把它传给 `next` 处理器(以及所有其它下游拦截器)。
If a cachable request is not in cache, the code calls `sendRequest`.
如果可缓存的请求在缓存中没找到,代码就会调用 `sendRequest`。
{@a send-request}
The `sendRequest` function creates a [request clone](#immutability) without headers
because the npm api forbids them.
`sendRequest` 函数创建了一个不带请求头的[请求克隆体](#immutability),因为 npm API 不会接受它们。
It forwards that request to `next.handle()` which ultimately calls the server and
returns the server's response.
它会把这个请求转发给 `next.handle()`,它最终会调用服务器,并且返回服务器的响应。
Note how `sendRequest` _intercepts the response_ on its way back to the application.
It _pipes_ the response through the `tap()` operator,
whose callback adds the response to the cache.
注意 `sendRequest` 是如何在发回给应用之前*拦截这个响应的*。
它会通过 `tap()` 操作符对响应进行管道处理,并在其回调中把响应加到缓存中。
The original response continues untouched back up through the chain of interceptors
to the application caller.
然后,原始的响应会通过这些拦截器链,原封不动的回到服务器的调用者那里。
Data services, such as `PackageSearchService`, are unaware that
some of their `HttpClient` requests actually return cached responses.
数据服务,比如 `PackageSearchService`,并不知道它们收到的某些 `HttpClient` 请求实际上是从缓存的请求中返回来的。
{@a cache-refresh}
#### Return a multi-valued _Observable_
#### 返回多值可观察对象
The `HttpClient.get()` method normally returns an _observable_
that either emits the data or an error.
Some folks describe it as a "_one and done_" observable.
`HttpClient.get()` 方法正常情况下只会返回一个*可观察对象*,它或者发出数据,或者发出错误。
有些人说它是“一次性完成”的可观察对象。
But an interceptor can change this to an _observable_ that emits more than once.
但是拦截器也可以把这个修改成发出多个值的*可观察对象*。
A revised version of the `CachingInterceptor` optionally returns an _observable_ that
immediately emits the cached response, sends the request to the NPM web API anyway,
and emits again later with the updated search results.
修改后的 `CachingInterceptor` 版本可以返回一个立即发出缓存的响应,然后仍然把请求发送到 NPM 的 Web API,然后再把修改过的搜索结果重新发出一次。
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,
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
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
### 监听进度事件
Sometimes applications transfer large amounts of data and those transfers can take a long time.
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` 实例,以开启进度跟踪事件。
Every progress event triggers change detection, so only turn them on if you truly intend to report progress in the UI.
每个进度事件都会触发变更检测,所以,你应该只有当确实希望在 UI 中报告进度时才打开这个选项。
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`,同样也可以在拦截器中处理这些事件。
The `getEventMessage` method interprets each type of `HttpEvent` in the event stream.
`getEventMessage` 方法会解释事件流中的每一个 `HttpEvent` 类型。
The sample app for this guide doesn't have a server that accepts uploaded files.
The `UploadInterceptor` in `app/http-interceptors/upload-interceptor.ts`
intercepts and short-circuits upload requests
by returning an observable of simulated events.
这个范例应用中并没有一个用来接收上传的文件的真实的服务器。
`app/http-interceptors/upload-interceptor.ts` 中的 `UploadInterceptor` 会拦截并短路掉上传请求,改为返回一个带有各个模拟事件的可观察对象。
## Security: XSRF Protection
## 安全:XSRF 防护
[Cross-Site Request Forgery (XSRF)](https://en.wikipedia.org/wiki/Cross-site_request_forgery) is an attack technique by which the attacker can trick an authenticated user into unknowingly executing actions on your website. `HttpClient` supports a [common mechanism](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Cookie-to-Header_Token) used to prevent XSRF attacks. When performing HTTP requests, an interceptor reads a token from a cookie, by default `XSRF-TOKEN`, and sets it as an HTTP header, `X-XSRF-TOKEN`. Since only code that runs on your domain could read the cookie, the backend can be certain that the HTTP request came from your client application and not an attacker.
[跨站请求伪造 (XSRF)](https://en.wikipedia.org/wiki/Cross-site_request_forgery)是一个攻击技术,它能让攻击者假冒一个已认证的用户在你的网站上执行未知的操作。`HttpClient` 支持一种[通用的机制](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Cookie-to-Header_Token)来防范 XSRF 攻击。当执行 HTTP 请求时,一个拦截器会从 cookie 中读取 XSRF 令牌(默认名字为 `XSRF-TOKEN`),并且把它设置为一个 HTTP 头 `X-XSRF-TOKEN`,由于只有运行在你自己的域名下的代码才能读取这个 cookie,因此后端可以确认这个 HTTP 请求真的来自你的客户端应用,而不是攻击者。
By default, an interceptor sends this cookie on all mutating requests (POST, etc.)
to relative URLs but not on GET/HEAD requests or
on requests with an absolute URL.
默认情况下,拦截器会在所有的修改型请求中(比如 POST 等)把这个 cookie 发送给使用相对 URL 的请求。但不会在 GET/HEAD 请求中发送,也不会发送给使用绝对 URL 的请求。
To take advantage of this, your server needs to set a token in a JavaScript readable session cookie called `XSRF-TOKEN` on either the page load or the first GET request. On subsequent requests the server can verify that the cookie matches the `X-XSRF-TOKEN` HTTP header, and therefore be sure that only code running on your domain could have sent the request. The token must be unique for each user and must be verifiable by the server; this prevents the client from making up its own tokens. Set the token to a digest of your site's authentication
cookie with a salt for added security.
要获得这种优点,你的服务器需要在页面加载或首个 GET 请求中把一个名叫 `XSRF-TOKEN` 的令牌写入可被 JavaScript 读到的会话 cookie 中。
而在后续的请求中,服务器可以验证这个 cookie 是否与 HTTP 头 `X-XSRF-TOKEN` 的值一致,以确保只有运行在你自己域名下的代码才能发起这个请求。这个令牌必须对每个用户都是唯一的,并且必须能被服务器验证,因此不能由客户端自己生成令牌。把这个令牌设置为你的站点认证信息并且加了盐(salt)的摘要,以提升安全性。
In order to prevent collisions in environments where multiple Angular apps share the same domain or subdomain, give each application a unique cookie name.
为了防止多个 Angular 应用共享同一个域名或子域时出现冲突,要给每个应用分配一个唯一的 cookie 名称。
*Note that `HttpClient` supports only the client half of the XSRF protection scheme.*
Your backend service must be configured to set the cookie for your page, and to verify that
the header is present on all eligible requests.
If not, Angular's default protection will be ineffective.
*注意,`HttpClient` 支持的只是 XSRF 防护方案的客户端这一半。* 你的后端服务必须配置为给页面设置 cookie ,并且要验证请求头,以确保全都是合法的请求。否则,Angular 默认的这种防护措施就会失效。
### Configuring custom cookie/header names
### 配置自定义 cookie/header 名称
If your backend service uses different names for the XSRF token cookie or header,
use `HttpClientXsrfModule.withOptions()` to override the defaults.
如果你的后端服务中对 XSRF 令牌的 cookie 或 头使用了不一样的名字,就要使用 `HttpClientXsrfModule.withConfig()` 来覆盖掉默认值。
## Testing HTTP requests
## 测试 HTTP 请求
Like any external dependency, the HTTP backend needs to be mocked
so your tests can simulate interaction with a remote server.
The `@angular/common/http/testing` library makes
setting up such mocking straightforward.
如同所有的外部依赖一样,HTTP 后端也需要在良好的测试实践中被 Mock 掉。`@angular/common/http` 提供了一个测试库 `@angular/common/http/testing`,它让你可以直截了当的进行这种 Mock 。
### Mocking philosophy
### Mock 方法论
Angular's HTTP testing library is designed for a pattern of testing wherein
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
### 搭建环境
To begin testing calls to `HttpClient`,
import the `HttpClientTestingModule` and the mocking controller, `HttpTestingController`,
along with the other symbols your tests require.
要开始测试那些通过 `HttpClient` 发起的请求,就要导入 `HttpClientTestingModule` 模块,并把它加到你的 `TestBed` 设置里去,代码如下:
Then add the `HttpClientTestingModule` to the `TestBed` and continue with
the setup of the _service-under-test_.
然后把 `HTTPClientTestingModule` 添加到 `TestBed` 中,并继续设置*被测服务*。
Now requests made in the course of your tests will hit the testing backend instead of the normal backend.
现在,在测试中发起的这些请求将会被这些测试后端(testing backend)处理,而不是标准的后端。
This setup also calls `TestBed.get()` to inject the `HttpClient` service and the mocking controller
so they can be referenced during the tests.
这种设置还会调用 `TestBed.get()`,来获取注入的 `HttpClient` 服务和模拟对象的控制器 `HttpTestingController`,以便在测试期间引用它们。
### Expecting and answering requests
### 期待并回复请求
Now you can write a test that expects a GET Request to occur and provides a mock response.
现在,你就可以编写测试,等待 GET 请求并给出模拟响应。
The last step, verifying that no requests remain outstanding, is common enough for you to move it into an `afterEach()` step:
最后一步,验证没有发起过预期之外的请求,足够通用,因此你可以把它移到 `afterEach()` 中:
#### Custom request expectations
#### 自定义对请求的预期
If matching by URL isn't sufficient, it's possible to implement your own matching function.
For example, you could look for an outgoing request that has an authorization header:
如果仅根据 URL 匹配还不够,你还可以自行实现匹配函数。
比如,你可以验证外发的请求是否带有某个认证头:
As with the previous `expectOne()`,
the test will fail if 0 or 2+ requests satisfy this predicate.
和前面根据 URL 进行测试时一样,如果零或两个以上的请求匹配上了这个期待,它就会抛出异常。
#### Handling more than one request
#### 处理一个以上的请求
If you need to respond to duplicate requests in your test, use the `match()` API instead of `expectOne()`.
It takes the same arguments but returns an array of matching requests.
Once returned, these requests are removed from future matching and
you are responsible for flushing and verifying them.
如果你需要在测试中对重复的请求进行响应,可以使用 `match()` API 来代替 `expectOne()`,它的参数不变,但会返回一个与这些请求相匹配的数组。一旦返回,这些请求就会从将来要匹配的列表中移除,你要自己验证和刷新(flush)它。
### Testing for errors
### 测试对错误的预期
You should test the app's defenses against HTTP requests that fail.
你还要测试应用对于 HTTP 请求失败时的防护。
Call `request.flush()` with an error message, as seen in the following example.
调用 `request.flush()` 并传入一个错误信息,如下所示:
Alternatively, you can call `request.error()` with an `ErrorEvent`.
另外,你还可以使用 `ErrorEvent` 来调用 `request.error()`.