2020-01-13 10:37:43 -08:00
# Get data from a server
2017-04-01 01:57:13 +02:00
2020-02-05 14:24:27 +08:00
# 从服务端获取数据
2017-11-06 19:02:18 +01:00
In this tutorial, you'll add the following data persistence features with help from
Angular's `HttpClient` .
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
在这节课中,你将借助 Angular 的 `HttpClient` 来添加一些数据持久化特性。
2017-11-06 19:02:18 +01:00
* The `HeroService` gets hero data with HTTP requests.
2018-03-03 21:06:01 +08:00
2018-03-24 13:12:42 +08:00
`HeroService` 通过 HTTP 请求获取英雄数据。
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
* Users can add, edit, and delete heroes and save these changes over HTTP.
2018-03-03 21:06:01 +08:00
2018-03-24 13:12:42 +08:00
用户可以添加、编辑和删除英雄,并通过 HTTP 来保存这些更改。
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
* Users can search for heroes by name.
2017-02-22 18:09:39 +00:00
2018-03-24 13:12:42 +08:00
用户可以根据名字搜索英雄。
2018-03-09 13:17:13 +08:00
2017-03-27 16:08:53 +01:00
When you're done with this page, the app should look like this < live-example > < / live-example > .
2018-03-09 13:17:13 +08:00
当你完成这一章时,应用会变成这样:< live-example > < / live-example > 。
2018-03-07 15:42:49 +08:00
2017-11-06 19:02:18 +01:00
## Enable HTTP services
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
## 启用 HTTP 服务
2019-01-31 14:25:30 -05:00
`HttpClient` is Angular's mechanism for communicating with a remote server over HTTP.
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
`HttpClient` 是 Angular 通过 HTTP 与远程服务器通讯的机制。
2019-01-31 14:25:30 -05:00
Make `HttpClient` available everywhere in the app in two steps. First, add it to the root `AppModule` by importing it:
2017-04-01 01:57:13 +02:00
2020-01-11 18:10:12 +08:00
要让 `HttpClient` 在应用中随处可用,需要两个步骤。首先,用导入语句把它添加到根模块 `AppModule` 中:
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/app.module.ts" region = "import-http-client" header = "src/app/app.module.ts (HttpClientModule import)" >
< / code-example >
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
Next, still in the `AppModule` , add `HttpClient` to the `imports` array:
2018-03-03 21:06:01 +08:00
2020-01-11 18:10:12 +08:00
接下来,仍然在 `AppModule` 中,把 `HttpClientModule` 添加到 `imports` 数组中:
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/app.module.ts" region = "import-httpclientmodule" header = "src/app/app.module.ts (imports array excerpt)" >
2018-07-12 15:00:54 +02:00
< / code-example >
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
## Simulate a data server
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
## 模拟数据服务器
2019-01-31 14:25:30 -05:00
This tutorial sample mimics communication with a remote data server by using the
[In-memory Web API ](https://github.com/angular/in-memory-web-api "In-memory Web API" ) module.
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
这个教学例子会与一个使用 [内存 Web API( _In-memory Web API_) ](https://github.com/angular/in-memory-web-api "In-memory Web API" ) 模拟出的远程数据服务器通讯。
2017-11-06 19:02:18 +01:00
After installing the module, the app will make requests to and receive responses from the `HttpClient`
without knowing that the *In-memory Web API* is intercepting those requests,
applying them to an in-memory data store, and returning simulated responses.
2017-03-27 16:08:53 +01:00
2018-03-20 17:09:18 +08:00
安装完这个模块之后,应用将会通过 `HttpClient` 来发起请求和接收响应,而不用在乎实际上是这个内存 Web API 在拦截这些请求、操作一个内存数据库,并且给出仿真的响应。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
By using the In-memory Web API, you won't have to set up a server to learn about `HttpClient` .
2018-03-09 13:17:13 +08:00
2020-01-11 18:10:12 +08:00
通过使用内存 Web API, 你不用架设服务器就可以学习 `HttpClient` 了。
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
< div class = "alert is-important" >
2017-02-22 18:09:39 +00:00
2019-01-31 14:25:30 -05:00
**Important:** the In-memory Web API module has nothing to do with HTTP in Angular.
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
**重要:** 这个*内存 Web API* 模块与 Angular 中的 HTTP 模块无关。
2019-01-31 14:25:30 -05:00
If you're just reading this tutorial to learn about `HttpClient` , you can [skip over ](#import-heroes ) this step.
If you're coding along with this tutorial, stay here and add the In-memory Web API now.
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
如果你只是在*阅读*本教程来学习 `HttpClient` ,那么可以[跳过 ](#import-heroes )这一步。
如果你正在随着本教程*敲代码*,那就留下来,并加上这个*内存 Web API*。
2017-11-06 19:02:18 +01:00
< / div >
2017-05-10 10:06:04 +01:00
2019-01-31 14:25:30 -05:00
Install the In-memory Web API package from npm with the following command:
2017-03-27 16:08:53 +01:00
2020-01-11 18:10:12 +08:00
用如下命令从 `npm` 中安装这个*内存 Web API* 包(译注:请使用 0.5+ 的版本,不要使用 0.4-)
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
< code-example language = "sh" class = "code-shell" >
npm install angular-in-memory-web-api --save
< / code-example >
2017-03-27 16:08:53 +01:00
2019-01-31 14:25:30 -05:00
In the `AppModule` , import the `HttpClientInMemoryWebApiModule` and the `InMemoryDataService` class,
which you will create in a moment.
2019-05-20 22:53:10 +05:30
2020-01-11 18:10:12 +08:00
在 `AppModule` 中,导入 `HttpClientInMemoryWebApiModule` 和 `InMemoryDataService` 类,稍后你将创建它们。
2019-06-07 13:37:23 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/app.module.ts" region = "import-in-mem-stuff" header = "src/app/app.module.ts (In-memory Web API imports)" >
2019-05-20 22:53:10 +05:30
< / code-example >
2019-01-31 14:25:30 -05:00
After the `HttpClientModule` , add the `HttpClientInMemoryWebApiModule`
to the `AppModule` `imports` array and configure it with the `InMemoryDataService` .
2019-06-07 13:37:23 +08:00
2020-01-11 18:10:12 +08:00
在 `HttpClientModule` 之后,将 `HttpClientInMemoryWebApiModule` 添加到 `AppModule` 的 `imports` 数组中,并以 `InMemoryDataService` 为参数对其进行配置。
2019-05-20 22:53:10 +05:30
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/app.module.ts" header = "src/app/app.module.ts (imports array excerpt)" region = "in-mem-web-api-imports" >
< / code-example >
2019-06-07 13:37:23 +08:00
2019-01-31 14:25:30 -05:00
The `forRoot()` configuration method takes an `InMemoryDataService` class
that primes the in-memory database.
2019-05-20 22:53:10 +05:30
2020-01-11 18:10:12 +08:00
`forRoot()` 配置方法接收一个 `InMemoryDataService` 类来初始化内存数据库。
2019-06-07 13:37:23 +08:00
2019-01-31 14:25:30 -05:00
Generate the class `src/app/in-memory-data.service.ts` with the following command:
2019-05-20 22:53:10 +05:30
2020-01-11 18:10:12 +08:00
使用以下命令生成类 `src/app/in-memory-data.service.ts` :
2019-06-07 13:37:23 +08:00
2019-01-31 14:25:30 -05:00
< code-example language = "sh" class = "code-shell" >
ng generate service InMemoryData
< / code-example >
2017-03-27 16:08:53 +01:00
2019-01-31 14:25:30 -05:00
Replace the default contents of `in-memory-data.service.ts` with the following:
2018-03-09 13:17:13 +08:00
2020-01-11 18:10:12 +08:00
将 `in-memory-data.service.ts` 改为以下内容:
2017-04-01 01:57:13 +02:00
2019-07-20 20:40:17 +03:00
< code-example path = "toh-pt6/src/app/in-memory-data.service.ts" region = "init" header = "src/app/in-memory-data.service.ts" > < / code-example >
2017-02-22 18:09:39 +00:00
2019-01-31 14:25:30 -05:00
The `in-memory-data.service.ts` file replaces `mock-heroes.ts` , which is now safe to delete.
2018-03-09 13:17:13 +08:00
2020-01-11 18:10:12 +08:00
`in-memory-data.service.ts` 文件已代替了 `mock-heroes.ts` 文件,现在后者可以安全的删除了。
2017-04-01 01:57:13 +02:00
2019-01-31 14:25:30 -05:00
When the server is ready, you'll detach the In-memory Web API, and the app's requests will go through to the server.
2017-11-06 19:02:18 +01:00
2020-01-11 18:10:12 +08:00
等服务器就绪后,你就可以抛弃这个内存 Web API, 应用的请求将直接传给服务器。
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
{@a import-heroes}
2018-03-03 21:06:01 +08:00
2017-11-06 19:02:18 +01:00
## Heroes and HTTP
2017-03-27 16:08:53 +01:00
2018-03-07 15:42:49 +08:00
## 英雄与 HTTP
2019-01-31 14:25:30 -05:00
In the `HeroService` , import `HttpClient` and `HttpHeaders` :
2017-02-22 18:09:39 +00:00
2020-01-11 18:10:12 +08:00
在 `HeroService` 中,导入 `HttpClient` 和 `HttpHeaders` :
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero.service.ts" region = "import-httpclient" header = "src/app/hero.service.ts (import HTTP symbols)" >
2017-11-06 19:02:18 +01:00
< / code-example >
2017-02-22 18:09:39 +00:00
2019-01-31 14:25:30 -05:00
Still in the `HeroService` , inject `HttpClient` into the constructor in a private property called `http` .
2017-03-27 16:08:53 +01:00
2020-01-11 18:10:12 +08:00
仍然在 `HeroService` 中,把 `HttpClient` 注入到构造函数中一个名叫 `http` 的私有属性中。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero.service.ts" header = "src/app/hero.service.ts" region = "ctor" >
2017-11-06 19:02:18 +01:00
< / code-example >
2017-04-01 01:57:13 +02:00
2019-01-31 14:25:30 -05:00
Notice that you keep injecting the `MessageService` but since you'll call it so frequently, wrap it in a private `log()` method:
2017-03-27 16:08:53 +01:00
2020-01-11 18:10:12 +08:00
注意保留对 `MessageService` 的注入,但是因为你将频繁调用它,因此请把它包裹进一个私有的 `log` 方法中。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero.service.ts" header = "src/app/hero.service.ts" region = "log" >
2017-11-06 19:02:18 +01:00
< / code-example >
2017-02-22 18:09:39 +00:00
2018-06-12 07:42:17 +05:30
Define the `heroesUrl` of the form `:base/:collectionName` with the address of the heroes resource on the server.
Here `base` is the resource to which requests are made,
and `collectionName` is the heroes data object in the `in-memory-data-service.ts` .
2017-04-01 01:57:13 +02:00
2018-10-21 17:44:10 +08:00
把服务器上英雄数据资源的访问地址 `heroesURL` 定义为 `:base/:collectionName` 的形式。
这里的 `base` 是要请求的资源,而 `collectionName` 是 `in-memory-data-service.ts` 中的英雄数据对象。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero.service.ts" header = "src/app/hero.service.ts" region = "heroesUrl" >
2017-11-06 19:02:18 +01:00
< / code-example >
2019-01-31 14:25:30 -05:00
### Get heroes with `HttpClient`
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
### 通过 `HttpClient` 获取英雄
2019-01-31 14:25:30 -05:00
The current `HeroService.getHeroes()`
2017-11-06 19:02:18 +01:00
uses the RxJS `of()` function to return an array of mock heroes
as an `Observable<Hero[]>` .
2018-03-09 13:17:13 +08:00
当前的 `HeroService.getHeroes()` 使用 RxJS 的 `of()` 函数来把模拟英雄数据返回为 `Observable<Hero[]>` 格式。
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt4/src/app/hero.service.ts" region = "getHeroes-1" header = "src/app/hero.service.ts (getHeroes with RxJs 'of()')" >
2017-11-06 19:02:18 +01:00
< / code-example >
2019-01-31 14:25:30 -05:00
Convert that method to use `HttpClient` as follows:
2018-03-07 16:48:44 +08:00
2018-03-09 13:17:13 +08:00
2020-01-11 18:10:12 +08:00
把该方法转换成使用 `HttpClient` 的,代码如下:
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero.service.ts" header = "src/app/hero.service.ts" region = "getHeroes-1" >
2017-11-06 19:02:18 +01:00
< / code-example >
2017-04-01 01:57:13 +02:00
2017-03-27 16:08:53 +01:00
Refresh the browser. The hero data should successfully load from the
2017-02-22 18:09:39 +00:00
mock server.
2018-03-07 15:42:49 +08:00
刷新浏览器后,英雄数据就会从模拟服务器被成功读取。
2019-01-31 14:25:30 -05:00
You've swapped `of()` for `http.get()` and the app keeps working without any other changes
2017-11-06 19:02:18 +01:00
because both functions return an `Observable<Hero[]>` .
2017-04-01 01:57:13 +02:00
2020-01-11 18:10:12 +08:00
你用 `http.get()` 替换了 `of()` ,没有做其它修改,但是应用仍然在正常工作,这是因为这两个函数都返回了 `Observable<Hero[]>` 。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
### `HttpClient` methods return one value
2017-04-01 01:57:13 +02:00
2020-01-11 18:10:12 +08:00
### `HttpClient` 的方法返回单个值
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
All `HttpClient` methods return an RxJS `Observable` of something.
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
所有的 `HttpClient` 方法都会返回某个值的 RxJS `Observable` 。
2019-01-31 14:25:30 -05:00
HTTP is a request/response protocol.
2017-11-06 19:02:18 +01:00
You make a request, it returns a single response.
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
HTTP 是一个请求/响应式协议。你发起请求,它返回单个的响应。
2018-05-15 13:32:40 +08:00
In general, an observable _can_ return multiple values over time.
An observable from `HttpClient` always emits a single value and then completes, never to emit again.
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
通常,`Observable` *可以*在一段时间内返回多个值。
但来自 `HttpClient` 的 `Observable` 总是发出一个值,然后结束,再也不会发出其它值。
2019-01-31 14:25:30 -05:00
This particular `HttpClient.get()` call returns an `Observable<Hero[]>` ; that is, "_an observable of hero arrays_". In practice, it will only return a single hero array.
2017-03-27 16:08:53 +01:00
2020-01-11 18:10:12 +08:00
具体到这次 `HttpClient.get()` 调用,它返回一个 `Observable<Hero[]>` ,也就是“一个英雄数组的可观察对象”。在实践中,它也只会返回一个英雄数组。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
### `HttpClient.get()` returns response data
2017-03-27 16:08:53 +01:00
2020-01-11 18:10:12 +08:00
### `HttpClient.get()` 返回响应数据
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
`HttpClient.get()` returns the body of the response as an untyped JSON object by default.
2020-01-20 23:32:21 +05:30
Applying the optional type specifier, `<Hero[]>` , adds TypeScript capabilities, which reduce errors during compile time.
2017-03-27 16:08:53 +01:00
2020-01-11 18:10:12 +08:00
`HttpClient.get()` 默认情况下把响应体当做无类型的 JSON 对象进行返回。
2018-03-09 13:17:13 +08:00
如果指定了可选的模板类型 `<Hero[]>` ,就会给返回你一个类型化的对象。
2019-01-31 14:25:30 -05:00
The server's data API determines the shape of the JSON data.
2017-11-06 19:02:18 +01:00
The _Tour of Heroes_ data API returns the hero data as an array.
2017-03-27 16:08:53 +01:00
2020-01-11 18:10:12 +08:00
服务器的数据 API 决定了 JSON 数据的具体形态。
2018-03-09 13:17:13 +08:00
*英雄指南*的数据 API 会把英雄数据作为一个数组进行返回。
2018-07-19 15:00:08 -07:00
< div class = "alert is-helpful" >
2017-03-27 16:08:53 +01:00
2017-11-06 19:02:18 +01:00
Other APIs may bury the data that you want within an object.
You might have to dig that data out by processing the `Observable` result
2019-01-31 14:25:30 -05:00
with the RxJS `map()` operator.
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
其它 API 可能在返回对象中深埋着你想要的数据。
2020-01-11 18:10:12 +08:00
你可能要借助 RxJS 的 `map()` 操作符对 `Observable` 的结果进行处理,以便把这些数据挖掘出来。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
Although not discussed here, there's an example of `map()` in the `getHeroNo404()`
2017-11-06 19:02:18 +01:00
method included in the sample source code.
2017-04-01 01:57:13 +02:00
2020-01-11 18:10:12 +08:00
虽然不打算在此展开讨论,不过你可以到范例源码中的 `getHeroNo404()` 方法中找到一个使用 `map()` 操作符的例子。
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
< / div >
2017-03-27 16:08:53 +01:00
2017-11-06 19:02:18 +01:00
### Error handling
2017-03-27 16:08:53 +01:00
2018-03-07 14:35:57 +08:00
### 错误处理
2017-11-06 19:02:18 +01:00
Things go wrong, especially when you're getting data from a remote server.
The `HeroService.getHeroes()` method should catch errors and do something appropriate.
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
凡事皆会出错,特别是当你从远端服务器获取数据的时候。
`HeroService.getHeroes()` 方法应该捕获错误,并做适当的处理。
2017-11-06 19:02:18 +01:00
To catch errors, you ** "pipe" the observable** result from `http.get()` through an RxJS `catchError()` operator.
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
要捕获错误,你就要使用 RxJS 的 `catchError()` 操作符来**建立对 Observable 结果的处理管道( pipe) **。
2017-11-06 19:02:18 +01:00
Import the `catchError` symbol from `rxjs/operators` , along with some other operators you'll need later.
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
从 `rxjs/operators` 中导入 `catchError` 符号,以及你稍后将会用到的其它操作符。
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero.service.ts" header = "src/app/hero.service.ts" region = "import-rxjs-operators" >
2017-11-06 19:02:18 +01:00
< / code-example >
2017-02-22 18:09:39 +00:00
2019-01-31 14:25:30 -05:00
Now extend the observable result with the `pipe()` method and
2017-11-06 19:02:18 +01:00
give it a `catchError()` operator.
2017-02-22 18:09:39 +00:00
2020-01-11 18:10:12 +08:00
现在,使用 `pipe()` 方法来扩展 `Observable` 的结果,并给它一个 `catchError()` 操作符。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero.service.ts" region = "getHeroes-2" header = "src/app/hero.service.ts" >
2017-11-06 19:02:18 +01:00
< / code-example >
2017-02-22 18:09:39 +00:00
2017-11-15 07:32:41 -05:00
The `catchError()` operator intercepts an ** `Observable` that failed**.
2019-01-31 14:25:30 -05:00
It passes the error an error handler that can do what it wants with the error.
2017-04-01 01:57:13 +02:00
2018-03-20 17:09:18 +08:00
`catchError()` 操作符会拦截**失败的 `Observable` **。
2018-03-09 13:17:13 +08:00
它把错误对象传给*错误处理器*, *错误处理器*会处理这个错误。
2017-11-06 19:02:18 +01:00
The following `handleError()` method reports the error and then returns an
innocuous result so that the application keeps working.
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
下面的 `handleError()` 方法会报告这个错误,并返回一个无害的结果(安全值),以便应用能正常工作。
2019-01-31 14:25:30 -05:00
#### `handleError`
2017-02-22 18:09:39 +00:00
2018-10-05 11:14:26 +08:00
The following `handleError()` will be shared by many `HeroService` methods
2017-11-06 19:02:18 +01:00
so it's generalized to meet their different needs.
2017-03-27 16:08:53 +01:00
2018-10-21 17:44:10 +08:00
下面这个 `handleError()` 将会在很多 `HeroService` 的方法之间共享,所以要把它通用化,以支持这些彼此不同的需求。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
Instead of handling the error directly, it returns an error handler function to `catchError` that it
2017-11-06 19:02:18 +01:00
has configured with both the name of the operation that failed and a safe return value.
2017-04-01 01:57:13 +02:00
2020-01-11 18:10:12 +08:00
它不再直接处理这些错误,而是返回给 `catchError` 返回一个错误处理函数。还要用操作名和出错时要返回的安全值来对这个错误处理函数进行配置。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero.service.ts" header = "src/app/hero.service.ts" region = "handleError" >
2017-11-06 19:02:18 +01:00
< / code-example >
2017-03-27 16:08:53 +01:00
2019-01-31 14:25:30 -05:00
After reporting the error to the console, the handler constructs
a user friendly message and returns a safe value to the app so the app can keep working.
2017-04-01 01:57:13 +02:00
2020-01-11 18:10:12 +08:00
在控制台中汇报了这个错误之后,这个处理器会汇报一个用户友好的消息,并给应用返回一个安全值,让应用继续工作。
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
Because each service method returns a different kind of `Observable` result,
2018-10-05 11:14:26 +08:00
`handleError()` takes a type parameter so it can return the safe value as the type that the app expects.
2017-03-27 16:08:53 +01:00
2018-10-21 17:44:10 +08:00
因为每个服务方法都会返回不同类型的 `Observable` 结果,因此 `handleError()` 也需要一个类型参数,以便它返回一个此类型的安全值,正如应用所期望的那样。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
### Tap into the Observable
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
### 窥探 `Observable`
2017-11-06 19:02:18 +01:00
The `HeroService` methods will **tap** into the flow of observable values
2019-01-31 14:25:30 -05:00
and send a message, via the `log()` method, to the message area at the bottom of the page.
2017-02-22 18:09:39 +00:00
2020-01-11 18:10:12 +08:00
`HeroService` 的方法将会窥探 `Observable` 的数据流,并通过 `log()` 方法往页面底部发送一条消息。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
They'll do that with the RxJS `tap()` operator,
which looks at the observable values, does something with those values,
2017-11-06 19:02:18 +01:00
and passes them along.
2019-01-31 14:25:30 -05:00
The `tap()` call back doesn't touch the values themselves.
2017-02-22 18:09:39 +00:00
2020-01-11 18:10:12 +08:00
它们可以使用 RxJS 的 `tap()` 操作符来实现,该操作符会查看 Observable 中的值,使用那些值做一些事情,并且把它们传出来。
这种 `tap()` 回调不会改变这些值本身。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
Here is the final version of `getHeroes()` with the `tap()` that logs the operation.
2017-03-30 20:04:18 +01:00
2020-01-11 18:10:12 +08:00
下面是 `getHeroes()` 的最终版本,它使用 `tap()` 来记录各种操作。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero.service.ts" header = "src/app/hero.service.ts" region = "getHeroes" >
2017-11-06 19:02:18 +01:00
< / code-example >
2017-04-01 01:57:13 +02:00
2017-11-06 19:02:18 +01:00
### Get hero by id
2017-02-22 18:09:39 +00:00
2018-03-20 17:09:18 +08:00
### 通过 id 获取英雄
2018-03-07 15:42:49 +08:00
2018-06-12 07:42:17 +05:30
Most web APIs support a _get by id_ request in the form `:baseURL/:id` .
2018-10-21 17:44:10 +08:00
大多数的 Web API 都支持以 `:baseURL/:id` 的形式根据 id 进行获取。
2018-12-03 09:35:05 +03:00
Here, the _base URL_ is the `heroesURL` defined in the [Heroes and HTTP ](tutorial/toh-pt6#heroes-and-http ) section (`api/heroes` ) and _id_ is
2018-06-12 07:42:17 +05:30
the number of the hero that you want to retrieve. For example, `api/heroes/11` .
2019-01-31 14:25:30 -05:00
Update the `HeroService` `getHero()` method with the following to make that request:
2017-02-22 18:09:39 +00:00
2019-10-17 18:02:44 +08:00
这里的 `baseURL` 就是在 [英雄列表与 HTTP ](tutorial/toh-pt6#heroes-and-http ) 部分定义过的 `heroesURL` ( `api/heroes` )。而 `id` 则是你要获取的英雄的编号,比如,`api/heroes/11` 。
2020-01-11 18:10:12 +08:00
把 `HeroService.getHero()` 方法改成这样,以发起该请求:
2018-03-09 13:17:13 +08:00
2018-10-11 13:29:59 +02:00
< code-example path = "toh-pt6/src/app/hero.service.ts" region = "getHero" header = "src/app/hero.service.ts" > < / code-example >
2017-02-22 18:09:39 +00:00
2019-01-31 14:25:30 -05:00
There are three significant differences from `getHeroes()` :
2017-02-22 18:09:39 +00:00
2020-01-11 18:10:12 +08:00
这里和 `getHeroes()` 相比有三个显著的差异:
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
* `getHero()` constructs a request URL with the desired hero's id.
2018-03-03 21:06:01 +08:00
2020-01-11 18:10:12 +08:00
`getHero()` 使用想获取的英雄的 id 构造了一个请求 URL。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
* The server should respond with a single hero rather than an array of heroes.
2018-03-03 21:06:01 +08:00
2018-03-24 13:12:42 +08:00
服务器应该使用单个英雄作为回应,而不是一个英雄数组。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
* `getHero()` returns an `Observable<Hero>` ("_an observable of Hero objects_")
2017-11-06 19:02:18 +01:00
rather than an observable of hero _arrays_ .
2017-03-27 16:08:53 +01:00
2020-01-11 18:10:12 +08:00
所以,`getHero()` 会返回 `Observable<Hero>` (“一个可观察的*单个英雄对象*”),而不是一个可观察的英雄对象*数组*。
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
## Update heroes
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
## 修改英雄
2019-01-31 14:25:30 -05:00
Edit a hero's name in the hero detail view.
2017-11-06 19:02:18 +01:00
As you type, the hero name updates the heading at the top of the page.
But when you click the "go back button", the changes are lost.
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
在*英雄详情*视图中编辑英雄的名字。
随着输入,英雄的名字也跟着在页面顶部的标题区更新了。
但是当你点击“后退”按钮时,这些修改都丢失了。
2017-11-06 19:02:18 +01:00
If you want changes to persist, you must write them back to
2017-02-22 18:09:39 +00:00
the server.
2018-03-09 13:17:13 +08:00
如果你希望保留这些修改,就要把它们写回到服务器。
2017-03-27 16:08:53 +01:00
At the end of the hero detail template, add a save button with a `click` event
2017-03-30 20:04:18 +01:00
binding that invokes a new component method named `save()` .
2018-03-20 17:09:18 +08:00
在英雄详情模板的底部添加一个保存按钮,它绑定了一个 `click` 事件,事件绑定会调用组件中一个名叫 `save()` 的新方法:
2018-03-07 15:42:49 +08:00
2018-10-11 13:29:59 +02:00
< code-example path = "toh-pt6/src/app/hero-detail/hero-detail.component.html" region = "save" header = "src/app/hero-detail/hero-detail.component.html (save)" > < / code-example >
2017-04-01 01:57:13 +02:00
2019-01-31 14:25:30 -05:00
In the `HeroDetail` component class, add the following `save()` method, which persists hero name changes using the hero service
2017-11-06 19:02:18 +01:00
`updateHero()` method and then navigates back to the previous view.
2017-03-30 20:04:18 +01:00
2020-01-11 18:10:12 +08:00
在 `HeroDetail` 组件类中,添加如下的 `save()` 方法,它使用英雄服务中的 `updateHero()` 方法来保存对英雄名字的修改,然后导航回前一个视图。
2018-03-09 13:17:13 +08:00
2018-10-11 13:29:59 +02:00
< code-example path = "toh-pt6/src/app/hero-detail/hero-detail.component.ts" region = "save" header = "src/app/hero-detail/hero-detail.component.ts (save)" > < / code-example >
2017-04-01 01:57:13 +02:00
2019-01-31 14:25:30 -05:00
#### Add `HeroService.updateHero()`
2017-11-06 19:02:18 +01:00
2020-01-11 18:10:12 +08:00
#### 添加 `HeroService.updateHero()`
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
The overall structure of the `updateHero()` method is similar to that of
`getHeroes()` , but it uses `http.put()` to persist the changed hero
2019-01-31 14:25:30 -05:00
on the server. Add the following to the `HeroService` .
2017-11-06 19:02:18 +01:00
2018-03-20 17:09:18 +08:00
`updateHero()` 的总体结构和 `getHeroes()` 很相似,但它会使用 `http.put()` 来把修改后的英雄保存到服务器上。
2020-01-11 18:10:12 +08:00
把下列代码添加进 `HeroService` 。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero.service.ts" region = "updateHero" header = "src/app/hero.service.ts (update)" >
2017-11-06 19:02:18 +01:00
< / code-example >
2017-03-30 20:04:18 +01:00
2019-01-31 14:25:30 -05:00
The `HttpClient.put()` method takes three parameters:
2018-03-03 21:06:01 +08:00
2020-01-11 18:10:12 +08:00
`HttpClient.put()` 方法接受三个参数:
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
* the URL
2018-03-03 21:06:01 +08:00
2018-03-24 13:12:42 +08:00
URL 地址
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
* the data to update (the modified hero in this case)
2018-03-03 21:06:01 +08:00
2018-03-24 13:12:42 +08:00
要修改的数据(这里就是修改后的英雄)
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
* options
2017-02-22 18:09:39 +00:00
2018-03-24 13:12:42 +08:00
选项
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
The URL is unchanged. The heroes web API knows which hero to update by looking at the hero's `id` .
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
URL 没变。英雄 Web API 通过英雄对象的 `id` 就可以知道要修改哪个英雄。
2017-11-06 19:02:18 +01:00
The heroes web API expects a special header in HTTP save requests.
2019-01-31 14:25:30 -05:00
That header is in the `httpOptions` constant defined in the `HeroService` . Add the following to the `HeroService` class.
2017-11-06 19:02:18 +01:00
2018-03-09 13:17:13 +08:00
英雄 Web API 期待在保存时的请求中有一个特殊的头。
这个头是在 `HeroService` 的 `httpOptions` 常量中定义的。
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero.service.ts" region = "http-options" header = "src/app/hero.service.ts" >
2017-11-06 19:02:18 +01:00
< / code-example >
2017-02-22 18:09:39 +00:00
2019-01-31 14:25:30 -05:00
Refresh the browser, change a hero name and save your change. The `save()`
2019-10-27 00:00:38 +07:00
method in `HeroDetailComponent` navigates to the previous view.
2017-11-06 19:02:18 +01:00
The hero now appears in the list with the changed name.
2017-02-22 18:09:39 +00:00
2020-01-11 18:10:12 +08:00
刷新浏览器,修改英雄名,保存这些修改。在 `HeroDetailComponent` 的 `save()` 方法中导航到前一个视图。
2018-03-09 13:17:13 +08:00
现在,改名后的英雄已经显示在列表中了。
2017-11-06 19:02:18 +01:00
## Add a new hero
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
## 添加新英雄
2019-01-31 14:25:30 -05:00
To add a hero, this app only needs the hero's name. You can use an `<input>`
2017-03-27 16:08:53 +01:00
element paired with an add button.
2020-01-11 18:10:12 +08:00
要添加英雄,本应用中只需要英雄的名字。你可以使用一个和添加按钮成对的 `<input>` 元素。
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
Insert the following into the `HeroesComponent` template, just after
2017-02-22 18:09:39 +00:00
the heading:
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
把下列代码插入到 `HeroesComponent` 模板中标题的紧后面:
2018-10-11 13:29:59 +02:00
< code-example path = "toh-pt6/src/app/heroes/heroes.component.html" region = "add" header = "src/app/heroes/heroes.component.html (add)" > < / code-example >
2017-04-01 01:57:13 +02:00
2019-01-31 14:25:30 -05:00
In response to a click event, call the component's click handler, `add()` , and then
clear the input field so that it's ready for another name. Add the following to the
`HeroesComponent` class:
2017-03-27 16:08:53 +01:00
2020-01-11 18:10:12 +08:00
当点击事件触发时,调用组件的点击处理器(`add()` ),然后清空这个输入框,以便用来输入另一个名字。把下列代码添加到 `HeroesComponent` 类:
2018-03-07 15:42:49 +08:00
2018-10-11 13:29:59 +02:00
< code-example path = "toh-pt6/src/app/heroes/heroes.component.ts" region = "add" header = "src/app/heroes/heroes.component.ts (add)" > < / code-example >
2017-11-06 19:02:18 +01:00
When the given name is non-blank, the handler creates a `Hero` -like object
from the name (it's only missing the `id` ) and passes it to the services `addHero()` method.
2018-03-09 13:17:13 +08:00
当指定的名字非空时,这个处理器会用这个名字创建一个类似于 `Hero` 的对象(只缺少 `id` 属性),并把它传给服务的 `addHero()` 方法。
2019-01-31 14:25:30 -05:00
When `addHero()` saves successfully, the `subscribe()` callback
2017-11-06 19:02:18 +01:00
receives the new hero and pushes it into to the `heroes` list for display.
2020-01-11 18:10:12 +08:00
当 `addHero()` 保存成功时,`subscribe()` 的回调函数会收到这个新英雄,并把它追加到 `heroes` 列表中以供显示。
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
Add the following `addHero()` method to the `HeroService` class.
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
往 `HeroService` 类中添加 `addHero()` 方法。
2018-10-11 13:29:59 +02:00
< code-example path = "toh-pt6/src/app/hero.service.ts" region = "addHero" header = "src/app/hero.service.ts (addHero)" > < / code-example >
2017-03-27 16:08:53 +01:00
2019-01-31 14:25:30 -05:00
`addHero()` differs from `updateHero()` in two ways:
2017-03-30 20:04:18 +01:00
2020-01-11 18:10:12 +08:00
`addHero()` 和 `updateHero()` 有两点不同。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
* It calls `HttpClient.post()` instead of `put()` .
2018-03-03 21:06:01 +08:00
2018-03-24 13:12:42 +08:00
它调用 `HttpClient.post()` 而不是 `put()` 。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
* It expects the server to generate an id for the new hero,
2017-11-06 19:02:18 +01:00
which it returns in the `Observable<Hero>` to the caller.
2017-04-01 01:57:13 +02:00
2018-03-24 13:12:42 +08:00
它期待服务器为这个新的英雄生成一个 id, 然后把它通过 `Observable<Hero>` 返回给调用者。
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
Refresh the browser and add some heroes.
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
刷新浏览器,并添加一些英雄。
2017-11-06 19:02:18 +01:00
## Delete a hero
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
## 删除某个英雄
2017-11-06 19:02:18 +01:00
Each hero in the heroes list should have a delete button.
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
英雄列表中的每个英雄都有一个删除按钮。
2017-11-06 19:02:18 +01:00
Add the following button element to the `HeroesComponent` template, after the hero
2017-03-30 20:04:18 +01:00
name in the repeated `<li>` element.
2018-03-09 13:17:13 +08:00
把下列按钮(`button` )元素添加到 `HeroesComponent` 的模板中,就在每个 `<li>` 元素中的英雄名字后方。
2019-07-23 17:46:19 -04:00
< code-example path = "toh-pt6/src/app/heroes/heroes.component.html" header = "src/app/heroes/heroes.component.html" region = "delete" > < / code-example >
2017-04-01 01:57:13 +02:00
2017-11-06 19:02:18 +01:00
The HTML for the list of heroes should look like this:
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
英雄列表的 HTML 应该是这样的:
2018-10-11 13:29:59 +02:00
< code-example path = "toh-pt6/src/app/heroes/heroes.component.html" region = "list" header = "src/app/heroes/heroes.component.html (list of heroes)" > < / code-example >
2017-04-01 01:57:13 +02:00
2017-11-06 19:02:18 +01:00
To position the delete button at the far right of the hero entry,
2019-10-26 21:35:03 +07:00
add some CSS to the `heroes.component.css` . You'll find that CSS
2017-11-06 19:02:18 +01:00
in the [final review code ](#heroescomponent ) below.
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
要把删除按钮定位在每个英雄条目的最右边,就要往 `heroes.component.css` 中添加一些 CSS。你可以在下方的 [最终代码 ](#heroescomponent ) 中找到这些 CSS。
2019-01-31 14:25:30 -05:00
Add the `delete()` handler to the component class.
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
把 `delete()` 处理器添加到组件中。
2018-10-11 13:29:59 +02:00
< code-example path = "toh-pt6/src/app/heroes/heroes.component.ts" region = "delete" header = "src/app/heroes/heroes.component.ts (delete)" > < / code-example >
2017-04-01 01:57:13 +02:00
2017-11-06 19:02:18 +01:00
Although the component delegates hero deletion to the `HeroService` ,
it remains responsible for updating its own list of heroes.
The component's `delete()` method immediately removes the _hero-to-delete_ from that list,
anticipating that the `HeroService` will succeed on the server.
2017-04-01 01:57:13 +02:00
2019-04-21 11:34:47 +08:00
虽然这个组件把删除英雄的逻辑委托给了 `HeroService` ,但仍保留了更新它自己的英雄列表的职责。
2018-03-09 13:17:13 +08:00
组件的 `delete()` 方法会在 `HeroService` 对服务器的操作成功之前,先从列表中移除*要删除的英雄*。
2017-11-06 19:02:18 +01:00
There's really nothing for the component to do with the `Observable` returned by
2019-01-31 14:25:30 -05:00
`heroService.delete()` **but it must subscribe anyway** .
2017-03-30 20:04:18 +01:00
2018-03-18 07:26:14 +08:00
组件与 `heroService.delete()` 返回的 `Observable` 还完全没有关联。**必须订阅它**。
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
< div class = "alert is-important" >
2017-04-01 01:57:13 +02:00
2019-01-31 14:25:30 -05:00
If you neglect to `subscribe()` , the service will not send the delete request to the server.
As a rule, an `Observable` _does nothing_ until something subscribes.
2018-03-03 21:06:01 +08:00
2018-03-09 13:17:13 +08:00
如果你忘了调用 `subscribe()` ,本服务将不会把这个删除请求发送给服务器。
作为一条通用的规则,`Observable` 在有人订阅之前*什么都不会做*。
2017-11-06 19:02:18 +01:00
Confirm this for yourself by temporarily removing the `subscribe()` ,
clicking "Dashboard", then clicking "Heroes".
You'll see the full list of heroes again.
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
你可以暂时删除 `subscribe()` 来确认这一点。点击“Dashboard”, 然后点击“Heroes”, 就又看到完整的英雄列表了。
2017-11-06 19:02:18 +01:00
< / div >
2017-03-27 16:08:53 +01:00
2019-01-31 14:25:30 -05:00
Next, add a `deleteHero()` method to `HeroService` like this.
2017-02-22 18:09:39 +00:00
2020-01-11 18:10:12 +08:00
接下来,把 `deleteHero()` 方法添加到 `HeroService` 中,代码如下。
2018-03-09 13:17:13 +08:00
2018-10-11 13:29:59 +02:00
< code-example path = "toh-pt6/src/app/hero.service.ts" region = "deleteHero" header = "src/app/hero.service.ts (delete)" > < / code-example >
2017-04-01 01:57:13 +02:00
2019-01-31 14:25:30 -05:00
Note the following key points:
2017-11-06 19:02:18 +01:00
2018-03-09 13:17:13 +08:00
注意
2019-01-31 14:25:30 -05:00
* `deleteHero()` calls `HttpClient.delete()` .
2018-03-03 21:06:01 +08:00
2020-01-11 18:10:12 +08:00
`deleteHero()` 调用了 `HttpClient.delete()` 。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
* The URL is the heroes resource URL plus the `id` of the hero to delete.
2018-03-03 21:06:01 +08:00
2018-03-24 13:12:42 +08:00
URL 就是英雄的资源 URL 加上要删除的英雄的 `id` 。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
* You don't send data as you did with `put()` and `post()` .
2018-03-03 21:06:01 +08:00
2020-01-11 18:10:12 +08:00
你不用像 `put()` 和 `post()` 中那样发送任何数据。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
* You still send the `httpOptions` .
2017-11-06 19:02:18 +01:00
2018-03-24 13:12:42 +08:00
你仍要发送 `httpOptions` 。
2018-03-09 13:17:13 +08:00
2017-11-06 19:02:18 +01:00
Refresh the browser and try the new delete functionality.
2017-03-27 16:08:53 +01:00
2018-03-07 15:42:49 +08:00
刷新浏览器,并试一下这个新的删除功能。
2017-11-06 19:02:18 +01:00
## Search by name
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
## 根据名字搜索
2017-11-06 19:02:18 +01:00
In this last exercise, you learn to chain `Observable` operators together
so you can minimize the number of similar HTTP requests
and consume network bandwidth economically.
2017-05-10 10:06:04 +01:00
2018-03-09 13:17:13 +08:00
在最后一次练习中,你要学到把 `Observable` 的操作符串在一起,让你能将相似 HTTP 请求的数量最小化,并节省网络带宽。
2019-01-31 14:25:30 -05:00
You will add a heroes search feature to the Dashboard.
As the user types a name into a search box,
2017-11-06 19:02:18 +01:00
you'll make repeated HTTP requests for heroes filtered by that name.
Your goal is to issue only as many requests as necessary.
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
你将往*仪表盘*中加入*英雄搜索*特性。
当用户在搜索框中输入名字时,你会不断发送根据名字过滤英雄的 HTTP 请求。
你的目标是仅仅发出尽可能少的必要请求。
2019-01-31 14:25:30 -05:00
#### `HeroService.searchHeroes()`
2017-02-22 18:09:39 +00:00
2019-01-31 14:25:30 -05:00
Start by adding a `searchHeroes()` method to the `HeroService` .
2017-02-22 18:09:39 +00:00
2020-01-11 18:10:12 +08:00
先把 `searchHeroes()` 方法添加到 `HeroService` 中。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero.service.ts" region = "searchHeroes" header = "src/app/hero.service.ts" >
2017-11-06 19:02:18 +01:00
< / code-example >
2017-04-01 01:57:13 +02:00
2017-11-06 19:02:18 +01:00
The method returns immediately with an empty array if there is no search term.
2019-01-31 14:25:30 -05:00
The rest of it closely resembles `getHeroes()` , the only significant difference being
the URL, which includes a query string with the search term.
2017-04-12 21:53:18 +02:00
2018-03-09 13:17:13 +08:00
如果没有搜索词,该方法立即返回一个空数组。
剩下的部分和 `getHeroes()` 很像。
唯一的不同点是 URL, 它包含了一个由搜索词组成的查询字符串。
2017-11-06 19:02:18 +01:00
### Add search to the Dashboard
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
### 为仪表盘添加搜索功能
2019-01-31 14:25:30 -05:00
Open the `DashboardComponent` template and
add the hero search element, `<app-hero-search>` , to the bottom of the markup.
2017-05-10 10:06:04 +01:00
2020-01-11 18:10:12 +08:00
打开 `DashboardComponent` 的模板并且把用于搜索英雄的元素 `<app-hero-search>` 添加到代码的底部。
2018-03-09 13:17:13 +08:00
2019-07-20 20:40:17 +03:00
< code-example path = "toh-pt6/src/app/dashboard/dashboard.component.html" header = "src/app/dashboard/dashboard.component.html" > < / code-example >
2017-02-22 18:09:39 +00:00
2017-11-06 19:02:18 +01:00
This template looks a lot like the `*ngFor` repeater in the `HeroesComponent` template.
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
这个模板看起来很像 `HeroesComponent` 模板中的 `*ngFor` 复写器。
2019-01-31 14:25:30 -05:00
For this to work, the next step is to add a component with a selector that matches `<app-hero-search>` .
2017-04-01 01:57:13 +02:00
2020-01-11 18:10:12 +08:00
为此,下一步就是添加一个组件,它的选择器要能匹配 `<app-hero-search>` 。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
### Create `HeroSearchComponent`
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
### 创建 `HeroSearchComponent`
2017-11-06 19:02:18 +01:00
Create a `HeroSearchComponent` with the CLI.
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
使用 CLI 创建一个 `HeroSearchComponent` 。
2017-11-06 19:02:18 +01:00
< code-example language = "sh" class = "code-shell" >
ng generate component hero-search
< / code-example >
2017-02-22 18:09:39 +00:00
2019-01-31 14:25:30 -05:00
The CLI generates the three `HeroSearchComponent` files and adds the component to the `AppModule` declarations.
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
CLI 生成了 `HeroSearchComponent` 的三个文件,并把该组件添加到了 `AppModule` 的声明中。
2019-01-31 14:25:30 -05:00
Replace the generated `HeroSearchComponent` template with an `<input>` and a list of matching search results, as follows.
2017-04-01 01:57:13 +02:00
2020-01-11 18:10:12 +08:00
把生成的 `HeroSearchComponent` 的*模板*改成一个 `<input>` 和一个匹配到的搜索结果的列表。代码如下:
2018-03-09 13:17:13 +08:00
2018-10-11 13:29:59 +02:00
< code-example path = "toh-pt6/src/app/hero-search/hero-search.component.html" header = "src/app/hero-search/hero-search.component.html" > < / code-example >
2017-02-22 18:09:39 +00:00
2017-11-06 19:02:18 +01:00
Add private CSS styles to `hero-search.component.css`
as listed in the [final code review ](#herosearchcomponent ) below.
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
从下面的 [最终代码 ](#herosearchcomponent ) 中把私有 CSS 样式添加到 `hero-search.component.css` 中。
2019-01-31 14:25:30 -05:00
As the user types in the search box, an input event binding calls the
component's `search()` method with the new search box value.
2017-03-27 16:08:53 +01:00
2018-03-20 17:09:18 +08:00
当用户在搜索框中输入时,一个 *keyup* 事件绑定会调用该组件的 `search()` 方法,并传入新的搜索框的值。
2018-03-07 15:42:49 +08:00
2017-11-06 19:02:18 +01:00
{@a asyncpipe}
2019-01-31 14:25:30 -05:00
### `AsyncPipe`
2017-02-22 18:09:39 +00:00
2019-01-31 14:25:30 -05:00
The `*ngFor` repeats hero objects. Notice that the `*ngFor` iterates over a list called `heroes$` , not `heroes` . The `$` is a convention that indicates `heroes$` is an `Observable` , not an array.
2017-11-06 19:02:18 +01:00
2020-01-11 18:10:12 +08:00
`*ngFor` 会重复渲染这些英雄对象。注意,`*ngFor` 在一个名叫 `heroes$` 的列表上迭代,而不是 `heroes` 。`$` 是一个约定,表示 `heroes$` 是一个 `Observable` 而不是数组。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero-search/hero-search.component.html" header = "src/app/hero-search/hero-search.component.html" region = "async" > < / code-example >
2017-11-06 19:02:18 +01:00
2019-01-31 14:25:30 -05:00
Since `*ngFor` can't do anything with an `Observable` , use the
pipe character (`|` ) followed by `async` . This identifies Angular's `AsyncPipe` and subscribes to an `Observable` automatically so you won't have to
2017-11-06 19:02:18 +01:00
do so in the component class.
2020-01-11 18:10:12 +08:00
由于 `*ngFor` 不能直接使用 `Observable` ,所以要使用一个管道字符(`|` ),后面紧跟着一个 `async` 。这表示 Angular 的 `AsyncPipe` 管道,它会自动订阅 `Observable` ,这样你就不用在组件类中这么做了。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
### Edit the `HeroSearchComponent` class
2017-11-06 19:02:18 +01:00
2018-03-09 13:17:13 +08:00
### 修正 `HeroSearchComponent` 类
2017-11-06 19:02:18 +01:00
Replace the generated `HeroSearchComponent` class and metadata as follows.
2018-03-09 13:17:13 +08:00
修改所生成的 `HeroSearchComponent` 类及其元数据,代码如下:
2018-10-11 13:29:59 +02:00
< code-example path = "toh-pt6/src/app/hero-search/hero-search.component.ts" header = "src/app/hero-search/hero-search.component.ts" > < / code-example >
2017-11-06 19:02:18 +01:00
2019-01-31 14:25:30 -05:00
Notice the declaration of `heroes$` as an `Observable` :
2018-03-07 16:48:44 +08:00
2018-03-09 13:17:13 +08:00
注意,`heroes$` 声明为一个 `Observable`
2017-11-06 19:02:18 +01:00
< code-example
path="toh-pt6/src/app/hero-search/hero-search.component.ts"
2020-01-11 18:10:12 +08:00
header="src/app/hero-search/hero-search.component.ts" region="heroes-stream">
2017-11-06 19:02:18 +01:00
< / code-example >
2017-02-22 18:09:39 +00:00
2019-01-31 14:25:30 -05:00
You'll set it in [`ngOnInit()` ](#search-pipe ).
2017-11-06 19:02:18 +01:00
Before you do, focus on the definition of `searchTerms` .
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
你将会在 [`ngOnInit()` ](#search-pipe ) 中设置它,在此之前,先仔细看看 `searchTerms` 的定义。
2019-01-31 14:25:30 -05:00
### The `searchTerms` RxJS subject
2017-02-22 18:09:39 +00:00
2018-03-09 13:17:13 +08:00
### RxJS `Subject` 类型的 `searchTerms`
2019-01-31 14:25:30 -05:00
The `searchTerms` property is an RxJS `Subject` .
2017-03-30 20:04:18 +01:00
2020-01-11 18:10:12 +08:00
`searchTerms` 属性是 RxJS 的 `Subject` 类型。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero-search/hero-search.component.ts" header = "src/app/hero-search/hero-search.component.ts" region = "searchTerms" > < / code-example >
2017-04-01 01:57:13 +02:00
2019-01-31 14:25:30 -05:00
A `Subject` is both a source of observable values and an `Observable` itself.
2017-11-06 19:02:18 +01:00
You can subscribe to a `Subject` as you would any `Observable` .
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
`Subject` 既是可观察对象的数据源,本身也是 `Observable` 。
你可以像订阅任何 `Observable` 一样订阅 `Subject` 。
2017-11-06 19:02:18 +01:00
You can also push values into that `Observable` by calling its `next(value)` method
as the `search()` method does.
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
你还可以通过调用它的 `next(value)` 方法往 `Observable` 中推送一些值,就像 `search()` 方法中一样。
2019-01-31 14:25:30 -05:00
The event binding to the textbox's `input` event calls the `search()` method.
2017-04-01 01:57:13 +02:00
2020-01-11 18:10:12 +08:00
文本框的 `input` 事件的*事件绑定*会调用 `search()` 方法。
2018-03-09 13:17:13 +08:00
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero-search/hero-search.component.html" header = "src/app/hero-search/hero-search.component.html" region = "input" > < / code-example >
2017-02-22 18:09:39 +00:00
2019-01-31 14:25:30 -05:00
Every time the user types in the textbox, the binding calls `search()` with the textbox value, a "search term".
2017-11-06 19:02:18 +01:00
The `searchTerms` becomes an `Observable` emitting a steady stream of search terms.
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
每当用户在文本框中输入时,这个事件绑定就会使用文本框的值(搜索词)调用 `search()` 函数。
`searchTerms` 变成了一个能发出搜索词的稳定的流。
2017-11-06 19:02:18 +01:00
{@a search-pipe}
2017-04-01 01:57:13 +02:00
2017-11-06 19:02:18 +01:00
### Chaining RxJS operators
2018-03-09 13:17:13 +08:00
### 串联 RxJS 操作符
2017-11-06 19:02:18 +01:00
Passing a new search term directly to the `searchHeroes()` after every user keystroke would create an excessive amount of HTTP requests,
2019-01-31 14:25:30 -05:00
taxing server resources and burning through data plans.
2017-04-01 01:57:13 +02:00
2020-01-11 18:10:12 +08:00
如果每当用户击键后就直接调用 `searchHeroes()` 将导致创建海量的 HTTP 请求,浪费服务器资源并干扰数据调度计划。
2018-03-09 13:17:13 +08:00
2018-05-15 13:32:40 +08:00
Instead, the `ngOnInit()` method pipes the `searchTerms` observable through a sequence of RxJS operators that reduce the number of calls to the `searchHeroes()` ,
ultimately returning an observable of timely hero search results (each a `Hero[]` ).
2017-11-06 19:02:18 +01:00
2018-03-09 13:17:13 +08:00
应该怎么做呢?`ngOnInit()` 往 `searchTerms` 这个可观察对象的处理管道中加入了一系列 RxJS 操作符,用以缩减对 `searchHeroes()` 的调用次数,并最终返回一个可及时给出英雄搜索结果的可观察对象(每次都是 `Hero[]` )。
2019-01-31 14:25:30 -05:00
Here's a closer look at the code.
2017-11-06 19:02:18 +01:00
2018-03-09 13:17:13 +08:00
代码如下:
2019-01-31 14:25:30 -05:00
< code-example path = "toh-pt6/src/app/hero-search/hero-search.component.ts" header = "src/app/hero-search/hero-search.component.ts" region = "search" >
2018-03-03 21:06:01 +08:00
< / code-example >
2017-03-27 16:08:53 +01:00
2019-01-31 14:25:30 -05:00
Each operator works as follows:
2017-03-27 16:08:53 +01:00
2020-01-11 18:10:12 +08:00
各个操作符的工作方式如下:
2017-03-27 16:08:53 +01:00
* `debounceTime(300)` waits until the flow of new string events pauses for 300 milliseconds
before passing along the latest string. You'll never make requests more frequently than 300ms.
2017-11-06 19:02:18 +01:00
2018-03-24 13:12:42 +08:00
在传出最终字符串之前,`debounceTime(300)` 将会等待,直到新增字符串的事件暂停了 300 毫秒。
2018-03-09 13:17:13 +08:00
你实际发起请求的间隔永远不会小于 300ms。
2018-03-07 15:42:49 +08:00
2018-05-15 13:32:40 +08:00
* `distinctUntilChanged()` ensures that a request is sent only if the filter text changed.
2017-11-06 19:02:18 +01:00
2018-05-15 13:32:40 +08:00
`distinctUntilChanged()` 会确保只在过滤条件变化时才发送请求。
2018-03-07 15:42:49 +08:00
2019-01-31 14:25:30 -05:00
* `switchMap()` calls the search service for each search term that makes it through `debounce()` and `distinctUntilChanged()` .
2017-03-27 16:08:53 +01:00
It cancels and discards previous search observables, returning only the latest search service observable.
2020-01-11 18:10:12 +08:00
`switchMap()` 会为每个从 `debounce()` 和 `distinctUntilChanged()` 中通过的搜索词调用搜索服务。
2018-03-07 15:42:49 +08:00
它会取消并丢弃以前的搜索可观察对象,只保留最近的。
2018-07-19 15:00:08 -07:00
< div class = "alert is-helpful" >
2017-03-27 16:08:53 +01:00
2017-11-06 19:02:18 +01:00
With the [switchMap operator ](http://www.learnrxjs.io/operators/transformation/switchmap.html ),
every qualifying key event can trigger an `HttpClient.get()` method call.
2017-05-10 10:06:04 +01:00
Even with a 300ms pause between requests, you could have multiple HTTP requests in flight
and they may not return in the order sent.
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
借助 [switchMap 操作符 ](http://www.learnrxjs.io/operators/transformation/switchmap.html ),
每个有效的击键事件都会触发一次 `HttpClient.get()` 方法调用。
即使在每个请求之间都有至少 300ms 的间隔,仍然可能会同时存在多个尚未返回的 HTTP 请求。
2017-11-06 19:02:18 +01:00
`switchMap()` preserves the original request order while returning only the observable from the most recent HTTP method call.
2017-05-10 10:06:04 +01:00
Results from prior calls are canceled and discarded.
2018-03-18 07:26:14 +08:00
2018-03-09 13:17:13 +08:00
`switchMap()` 会记住原始的请求顺序,只会返回最近一次 HTTP 方法调用的结果。
以前的那些请求都会被取消和舍弃。
2017-04-01 01:57:13 +02:00
2019-01-31 14:25:30 -05:00
Note that canceling a previous `searchHeroes()` Observable
2017-05-10 10:06:04 +01:00
doesn't actually abort a pending HTTP request.
2017-11-06 19:02:18 +01:00
Unwanted results are simply discarded before they reach your application code.
2017-03-27 16:08:53 +01:00
2020-01-11 18:10:12 +08:00
注意,取消前一个 `searchHeroes()` 可观察对象并不会中止尚未完成的 HTTP 请求。
2018-03-09 13:17:13 +08:00
那些不想要的结果只会在它们抵达应用代码之前被舍弃。
2017-04-10 16:51:13 +01:00
< / div >
2017-03-27 16:08:53 +01:00
2017-11-06 19:02:18 +01:00
Remember that the component _class_ does not subscribe to the `heroes$` _observable_ .
That's the job of the [`AsyncPipe` ](#asyncpipe ) in the template.
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
记住,组件类中并没有订阅 `heroes$` 这个可观察对象,而是由模板中的 [`AsyncPipe` ](#asyncpipe ) 完成的。
2017-11-06 19:02:18 +01:00
#### Try it
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
#### 试试看
2017-11-06 19:02:18 +01:00
Run the app again. In the *Dashboard* , enter some text in the search box.
If you enter characters that match any existing hero names, you'll see something like this.
2017-03-27 16:08:53 +01:00
2018-03-09 13:17:13 +08:00
再次运行本应用。在这个 *仪表盘* 中,在搜索框中输入一些文字。如果你输入的字符匹配上了任何现有英雄的名字,你将会看到如下效果:
2019-11-11 14:47:51 -08:00
< div class = "lightbox" >
2017-11-06 19:02:18 +01:00
< img src = 'generated/images/guide/toh/toh-hero-search.png' alt = "Hero Search Component" >
2019-11-11 14:47:51 -08:00
< / div >
2017-03-27 16:08:53 +01:00
2017-11-06 19:02:18 +01:00
## Final code review
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
## 查看最终代码
2017-11-06 19:02:18 +01:00
Your app should look like this < live-example > < / live-example > .
2017-03-27 16:08:53 +01:00
2018-03-09 13:30:42 +08:00
你的应用现在变成了这样:< live-example > < / live-example > 。
2018-03-07 15:42:49 +08:00
2017-11-06 19:02:18 +01:00
Here are the code files discussed on this page (all in the `src/app/` folder).
2017-04-01 01:57:13 +02:00
2018-03-09 13:17:13 +08:00
本文讨论过的代码文件如下(都位于 `src/app/` 文件夹中)。
2017-11-06 19:02:18 +01:00
{@a heroservice}
2018-03-07 11:25:56 +08:00
2017-11-06 19:02:18 +01:00
{@a inmemorydataservice}
2018-03-07 11:25:56 +08:00
2017-11-06 19:02:18 +01:00
{@a appmodule}
2018-03-03 21:06:01 +08:00
2019-01-31 14:25:30 -05:00
#### `HeroService`, `InMemoryDataService`, `AppModule`
2017-02-22 18:09:39 +00:00
2017-11-06 19:02:18 +01:00
< code-tabs >
2019-01-31 14:25:30 -05:00
< code-pane
header="hero.service.ts"
2017-11-06 19:02:18 +01:00
path="toh-pt6/src/app/hero.service.ts">
< / code-pane >
2019-01-31 14:25:30 -05:00
< code-pane
2018-10-11 13:29:59 +02:00
header="in-memory-data.service.ts"
2017-11-06 19:02:18 +01:00
path="toh-pt6/src/app/in-memory-data.service.ts">
< / code-pane >
2019-01-31 14:25:30 -05:00
< code-pane
header="app.module.ts"
2017-11-06 19:02:18 +01:00
path="toh-pt6/src/app/app.module.ts">
< / code-pane >
< / code-tabs >
2017-02-22 18:09:39 +00:00
2017-11-06 19:02:18 +01:00
{@a heroescomponent}
2018-03-03 21:06:01 +08:00
2019-01-31 14:25:30 -05:00
#### `HeroesComponent`
2017-04-01 01:57:13 +02:00
2017-11-06 19:02:18 +01:00
< code-tabs >
2019-01-31 14:25:30 -05:00
< code-pane
header="heroes/heroes.component.html"
2017-11-06 19:02:18 +01:00
path="toh-pt6/src/app/heroes/heroes.component.html">
< / code-pane >
2019-01-31 14:25:30 -05:00
< code-pane
header="heroes/heroes.component.ts"
2017-11-06 19:02:18 +01:00
path="toh-pt6/src/app/heroes/heroes.component.ts">
< / code-pane >
2019-01-31 14:25:30 -05:00
< code-pane
header="heroes/heroes.component.css"
2017-11-06 19:02:18 +01:00
path="toh-pt6/src/app/heroes/heroes.component.css">
< / code-pane >
< / code-tabs >
2017-03-27 16:08:53 +01:00
2017-11-06 19:02:18 +01:00
{@a herodetailcomponent}
2018-03-03 21:06:01 +08:00
2019-01-31 14:25:30 -05:00
#### `HeroDetailComponent`
2017-03-27 16:08:53 +01:00
2017-11-06 19:02:18 +01:00
< code-tabs >
2019-01-31 14:25:30 -05:00
< code-pane
2018-10-11 13:29:59 +02:00
header="hero-detail/hero-detail.component.html"
2017-11-06 19:02:18 +01:00
path="toh-pt6/src/app/hero-detail/hero-detail.component.html">
< / code-pane >
2019-01-31 14:25:30 -05:00
< code-pane
header="hero-detail/hero-detail.component.ts"
2017-11-06 19:02:18 +01:00
path="toh-pt6/src/app/hero-detail/hero-detail.component.ts">
< / code-pane >
< / code-tabs >
2017-04-01 01:57:13 +02:00
2018-06-12 07:42:17 +05:30
{@a dashboardcomponent}
2019-01-31 14:25:30 -05:00
#### `DashboardComponent`
2018-06-12 07:42:17 +05:30
< code-tabs >
2019-01-31 14:25:30 -05:00
< code-pane
2018-10-11 13:29:59 +02:00
header="src/app/dashboard/dashboard.component.html"
2018-06-12 07:42:17 +05:30
path="toh-pt6/src/app/dashboard/dashboard.component.html">
< / code-pane >
< / code-tabs >
2017-11-06 19:02:18 +01:00
{@a herosearchcomponent}
2018-03-03 21:06:01 +08:00
2019-01-31 14:25:30 -05:00
#### `HeroSearchComponent`
2017-02-22 18:09:39 +00:00
2017-11-06 19:02:18 +01:00
< code-tabs >
2019-01-31 14:25:30 -05:00
< code-pane
2018-10-11 13:29:59 +02:00
header="hero-search/hero-search.component.html"
2017-11-06 19:02:18 +01:00
path="toh-pt6/src/app/hero-search/hero-search.component.html">
< / code-pane >
2019-01-31 14:25:30 -05:00
< code-pane
2018-10-11 13:29:59 +02:00
header="hero-search/hero-search.component.ts"
2017-11-06 19:02:18 +01:00
path="toh-pt6/src/app/hero-search/hero-search.component.ts">
< / code-pane >
2019-01-31 14:25:30 -05:00
< code-pane
2018-10-11 13:29:59 +02:00
header="hero-search/hero-search.component.css"
2017-11-06 19:02:18 +01:00
path="toh-pt6/src/app/hero-search/hero-search.component.css">
< / code-pane >
< / code-tabs >
2017-02-22 18:09:39 +00:00
2017-09-27 16:45:47 -04:00
## Summary
2017-02-22 18:09:39 +00:00
2018-03-03 21:06:01 +08:00
## 小结
2017-03-27 16:08:53 +01:00
You're at the end of your journey, and you've accomplished a lot.
2018-03-03 21:06:01 +08:00
2018-03-09 13:30:42 +08:00
旅程即将结束,不过你已经收获颇丰。
2018-03-07 15:42:49 +08:00
2017-04-01 01:57:13 +02:00
* You added the necessary dependencies to use HTTP in the app.
2018-03-03 21:06:01 +08:00
2018-03-24 13:12:42 +08:00
你添加了在应用程序中使用 HTTP 的必备依赖。
2018-03-07 15:42:49 +08:00
2017-04-01 01:57:13 +02:00
* You refactored `HeroService` to load heroes from a web API.
2018-03-03 21:06:01 +08:00
2018-03-24 13:12:42 +08:00
你重构了 `HeroService` ,以通过 web API 来加载英雄数据。
2018-03-07 15:42:49 +08:00
2017-04-01 01:57:13 +02:00
* You extended `HeroService` to support `post()` , `put()` , and `delete()` methods.
2018-03-03 21:06:01 +08:00
2018-03-24 13:12:42 +08:00
你扩展了 `HeroService` 来支持 `post()` 、`put()` 和 `delete()` 方法。
2018-03-07 15:42:49 +08:00
2017-04-01 01:57:13 +02:00
* You updated the components to allow adding, editing, and deleting of heroes.
2018-03-03 21:06:01 +08:00
2018-03-24 13:12:42 +08:00
你修改了组件,以允许用户添加、编辑和删除英雄。
2018-03-07 15:42:49 +08:00
2017-04-01 01:57:13 +02:00
* You configured an in-memory web API.
2018-03-03 21:06:01 +08:00
2018-03-24 13:12:42 +08:00
你配置了一个内存 Web API。
2018-03-07 15:42:49 +08:00
2018-05-15 13:32:40 +08:00
* You learned how to use observables.
2017-03-27 16:08:53 +01:00
2018-03-24 13:12:42 +08:00
你学会了如何使用“可观察对象”。
2018-03-07 15:42:49 +08:00
2017-11-06 19:02:18 +01:00
This concludes the "Tour of Heroes" tutorial.
2017-05-04 14:30:29 -07:00
You're ready to learn more about Angular development in the fundamentals section,
starting with the [Architecture ](guide/architecture "Architecture" ) guide.
2018-03-09 13:17:13 +08:00
《英雄指南》教程结束了。
如果你准备开始学习 Angular 开发的原理,请开始 [架构 ](guide/architecture "Architecture" ) 一章。