2018-05-15

根据名字搜索

https://angular.io/tutorial/toh-pt6#search-by-name

在最后一次练习中,你要学到把 Observable 的操作符串在一起,让你能将相似 HTTP 请求的数量最小化,并节省网络带宽。

你将往仪表盘中加入英雄搜索特性。 当用户在搜索框中输入名字时,你会不断发送根据名字过滤英雄的 HTTP 请求。 你的目标是仅仅发出尽可能少的必要请求。

HeroService.searchHeroes

先把 searchHeroes 方法添加到 HeroService 中。

<code-example path="toh-pt6/src/app/hero.service.ts" region="searchHeroes" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><header class="ng-star-inserted" style="background-color: rgb(30, 136, 229); border-radius: 5px 5px 0px 0px; color: rgb(250, 250, 250); font-size: 16px; padding: 8px 16px;">src/app/hero.service.ts</header>

<aio-code class="headed-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy /* GET heroes whose name contains search term */ searchHeroes(term: string): Observable<Hero[]> { if (!term.trim()) { // if not search term, return empty hero array. return of([]); } return this.http.get<Hero[]>(api/heroes/?name=${term}).pipe( tap(_ => this.log(found heroes matching "${term}")), catchError(this.handleError<Hero[]>('searchHeroes', [])) ); }</pre></aio-code></code-example>

如果没有搜索词,该方法立即返回一个空数组。 剩下的部分和 getHeroes() 很像。 唯一的不同点是 URL,它包含了一个由搜索词组成的查询字符串。

为仪表盘添加搜索功能

打开 DashboardComponent模板并且把用于搜索英雄的元素 <app-hero-search> 添加到 DashboardComponent模板的底部。

<code-example path="toh-pt6/src/app/dashboard/dashboard.component.html" linenums="false" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><header class="ng-star-inserted" style="background-color: rgb(30, 136, 229); border-radius: 5px 5px 0px 0px; color: rgb(250, 250, 250); font-size: 16px; padding: 8px 16px;">src/app/dashboard/dashboard.component.html</header>

<aio-code class="headed-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy <h3>Top Heroes</h3> <div class="grid grid-pad"> <[a](https://angular.cn/api/router/RouterLinkWithHref) *[ngFor](https://angular.cn/api/common/NgForOf)="let hero of heroes" class="col-1-4" [routerLink](https://angular.cn/api/router/RouterLink)="/detail/{{hero.id}}"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </[a](https://angular.cn/api/router/RouterLinkWithHref)> </div> <app-hero-search></app-hero-search></pre></aio-code></code-example>

这个模板看起来很像 HeroesComponent 模板中的 *[ngFor](https://angular.cn/api/common/NgForOf) 复写器。

很不幸,添加这个元素让本应用挂了。 Angular 找不到哪个组件的选择器能匹配上 <app-hero-search>

HeroSearchComponent 还不存在,这就解决。

创建 HeroSearchComponent

使用 CLI 创建一个 HeroSearchComponent

<code-example language="sh" class="code-shell" ng-version="5.2.0" style="clear: both; display: block; background-color: rgb(51, 51, 51); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><aio-code class="simple-code"><pre class="prettyprint lang-sh" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy ng generate component hero-search</pre></aio-code></code-example>

CLI 生成了 HeroSearchComponent 的三个文件,并把该组件添加到了 AppModule 的声明中。

把生成的 HeroSearchComponent模板改成一个输入框和一个匹配到的搜索结果的列表。代码如下:

<code-example path="toh-pt6/src/app/hero-search/hero-search.component.html" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><header class="ng-star-inserted" style="background-color: rgb(30, 136, 229); border-radius: 5px 5px 0px 0px; color: rgb(250, 250, 250); font-size: 16px; padding: 8px 16px;">src/app/hero-search/hero-search.component.html</header>

<aio-code class="headed-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy <div id="search-component"> <h4>Hero Search</h4> <input #searchBox id="search-box" (keyup)="search(searchBox.value)" /> <ul class="search-result"> <li *[ngFor](https://angular.cn/api/common/NgForOf)="let hero of heroes$ | [async](https://angular.cn/api/core/testing/async)" > <[a](https://angular.cn/api/router/RouterLinkWithHref) [routerLink](https://angular.cn/api/router/RouterLink)="/detail/{{hero.id}}"> {{hero.name}} </[a](https://angular.cn/api/router/RouterLinkWithHref)> </li> </ul> </div></pre></aio-code></code-example>

从下面的 最终代码 中把私有 CSS 样式添加到 hero-search.component.css 中。

当用户在搜索框中输入时,一个 keyup 事件绑定会调用该组件的 search() 方法,并传入新的搜索框的值。

AsyncPipe

如你所愿,*[ngFor](https://angular.cn/api/common/NgForOf) 重复渲染出了这些英雄。

仔细看,你会发现 *[ngFor](https://angular.cn/api/common/NgForOf) 是在一个名叫 heroes$ 的列表上迭代,而不是 heroes

<code-example path="toh-pt6/src/app/hero-search/hero-search.component.html" region="async" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><aio-code class="simple-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy <li *[ngFor](https://angular.cn/api/common/NgForOf)="let hero of heroes$ | [async](https://angular.cn/api/core/testing/async)" ></pre></aio-code></code-example>

$ 是一个命名惯例,用来表明 heroes$ 是一个 Observable,而不是数组。

*[ngFor](https://angular.cn/api/common/NgForOf) 不能直接使用 Observable。 不过,它后面还有一个管道字符(|),后面紧跟着一个 [async](https://angular.cn/api/core/testing/async),它表示 Angular 的 [AsyncPipe](https://angular.cn/api/common/AsyncPipe)

[AsyncPipe](https://angular.cn/api/common/AsyncPipe) 会自动订阅到 Observable,这样你就不用再在组件类中订阅了。

修正 HeroSearchComponent

修改所生成的 HeroSearchComponent 类及其元数据,代码如下:

<code-example path="toh-pt6/src/app/hero-search/hero-search.component.ts" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><header class="ng-star-inserted" style="background-color: rgb(30, 136, 229); border-radius: 5px 5px 0px 0px; color: rgb(250, 250, 250); font-size: 16px; padding: 8px 16px;">src/app/hero-search/hero-search.component.ts</header>

<aio-code class="headed-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy import { [Component](https://angular.cn/api/core/Component), [OnInit](https://angular.cn/api/core/OnInit) } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; import { Hero } from '../hero'; import { HeroService } from '../hero.service'; @[Component](https://angular.cn/api/core/Component)({ selector: 'app-hero-search', templateUrl: './hero-search.component.html', styleUrls: [ './hero-search.component.css' ] }) export class HeroSearchComponent implements [OnInit](https://angular.cn/api/core/OnInit) { heroes$: Observable<Hero[]>; private searchTerms = new Subject<string>(); constructor(private heroService: HeroService) {} // Push [a](https://angular.cn/api/router/RouterLinkWithHref) search term into the observable stream. search(term: string): void { this.searchTerms.next(term); } ngOnInit(): void { this.heroes$ = this.searchTerms.pipe( // wait 300ms after each keystroke before considering the term debounceTime(300), // ignore new term if same as previous term distinctUntilChanged(), // switch to new search observable each time the term changes switchMap((term: string) => this.heroService.searchHeroes(term)), ); } }</pre></aio-code></code-example>

注意,heroes$ 声明为一个 Observable

<code-example path="toh-pt6/src/app/hero-search/hero-search.component.ts" region="heroes-stream" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><aio-code class="simple-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy heroes$: Observable<Hero[]>;</pre></aio-code></code-example>

你将会在 ngOnInit() 中设置它,在此之前,先仔细看看 searchTerms 的定义。

RxJS Subject 类型的 searchTerms

searchTerms 属性声明成了 RxJS 的 Subject 类型。

<code-example path="toh-pt6/src/app/hero-search/hero-search.component.ts" region="searchTerms" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><aio-code class="simple-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy private searchTerms = new Subject<string>(); // Push [a](https://angular.cn/api/router/RouterLinkWithHref) search term into the observable stream. search(term: string): void { this.searchTerms.next(term); }</pre></aio-code></code-example>

Subject 既是可观察对象的数据源,本身也是 Observable。 你可以像订阅任何 Observable 一样订阅 Subject

你还可以通过调用它的 next(value) 方法往 Observable 中推送一些值,就像 search() 方法中一样。

search() 是通过对文本框的 keystroke 事件的事件绑定来调用的。

<code-example path="toh-pt6/src/app/hero-search/hero-search.component.html" region="input" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><aio-code class="simple-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy <input #searchBox id="search-box" (keyup)="search(searchBox.value)" /></pre></aio-code></code-example>

每当用户在文本框中输入时,这个事件绑定就会使用文本框的值(搜索词)调用 search() 函数。searchTerms 变成了一个能发出搜索词的稳定的流。

串联 RxJS 操作符

如果每当用户击键后就直接调用 searchHeroes() 将导致创建海量的 HTTP 请求,浪费服务器资源并消耗大量网络流量。

应该怎么做呢?ngOnInit()searchTerms 这个可观察对象的处理管道中加入了一系列 RxJS 操作符,用以缩减对 searchHeroes() 的调用次数,并最终返回一个可及时给出英雄搜索结果的可观察对象(每次都是 Hero[] )。

代码如下:

<code-example path="toh-pt6/src/app/hero-search/hero-search.component.ts" region="search" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><aio-code class="simple-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy this.heroes$ = this.searchTerms.pipe( // wait 300ms after each keystroke before considering the term debounceTime(300), // ignore new term if same as previous term distinctUntilChanged(), // switch to new search observable each time the term changes switchMap((term: string) => this.heroService.searchHeroes(term)), );</pre></aio-code></code-example>

  • 在传出最终字符串之前,debounceTime(300) 将会等待,直到新增字符串的事件暂停了 300 毫秒。 你实际发起请求的间隔永远不会小于 300ms。

  • distinctUntilChanged 会确保只在过滤条件变化时才发送请求。

  • switchMap() 会为每个从 debouncedistinctUntilChanged 中通过的搜索词调用搜索服务。 它会取消并丢弃以前的搜索可观察对象,只保留最近的。

借助 switchMap 操作符, 每个有效的击键事件都会触发一次 [HttpClient](https://angular.cn/api/common/http/HttpClient).get() 方法调用。 即使在每个请求之间都有至少 300ms 的间隔,仍然可能会同时存在多个尚未返回的 HTTP 请求。

switchMap() 会记住原始的请求顺序,只会返回最近一次 HTTP 方法调用的结果。 以前的那些请求都会被取消和舍弃。

注意,取消前一个 searchHeroes() 可观察对象并不会中止尚未完成的 HTTP 请求。 那些不想要的结果只会在它们抵达应用代码之前被舍弃。

记住,组件类中并没有订阅 heroes$ 这个可观察对象,而是由模板中的 AsyncPipe 完成的。

试试看

再次运行本应用。在这个 仪表盘 中,在搜索框中输入一些文字。如果你输入的字符匹配上了任何现有英雄的名字,你将会看到如下效果:

<figure style="background: rgb(255, 255, 255); padding: 20px; display: inline-block; box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 5px 0px; margin: 0px 0px 14px; border-radius: 4px; color: rgba(0, 0, 0, 0.87); font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
Hero Search Component

</figure>

这里出现了 | async

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341