我们通过一个具体的例子来阐述。
考虑您正在构建一个搜索输入掩码,该掩码应在您键入时立即显示结果。
如果您曾经构建过这样的东西,那么您可能会意识到该任务带来的挑战。
- 不要在每次击键时都点击搜索端点
将搜索端点视为您按请求付费。不管它是不是你自己的硬件。我们不应该比需要的更频繁地敲击搜索端点。基本上我们只想在用户停止输入后点击它,而不是每次击键时点击它。
- 不要在后续请求中使用相同的查询参数命中搜索端点
假设您键入 foo,停止,键入另一个 o,然后立即退格并返回到 foo。这应该只是一个带有 foo 一词的请求,而不是两个,即使我们在搜索框中有 foo 后从技术上讲停止了两次。
3.处理乱序响应
当我们同时有多个请求进行中时,我们必须考虑它们以意外顺序返回的情况。考虑我们首先键入 computer,停止,请求发出,我们键入 car,停止,请求发出。现在我们有两个正在进行的请求。不幸的是,在为 car 携带结果的请求之后,为 computer 携带结果的请求又回来了。这可能是因为它们由不同的服务器提供服务。如果我们不正确处理此类情况,我们最终可能会显示 computer 的结果,而搜索框会显示 car.
我们将使用免费和开放的维基百科 API 来编写一个小演示。
为简单起见,我们的演示将只包含两个文件:app.ts 和 wikipedia-service.ts。 不过,在现实世界中,我们很可能会将事情进一步拆分。
让我们从一个基于 Promise 的实现开始,它不处理任何描述的边缘情况。
这就是我们的 WikipediaService 的样子。
使用了 jsonp 这个 Angular HTTP 服务:
上图将来自 angular/http 库中的 jsonp 返回的对象,使用 toPromise 方法转换成了 promise.
简单地说,我们正在注入 Jsonp 服务,以使用给定的搜索词针对维基百科 API 发出 GET 请求。 请注意,我们调用 toPromise 是为了从 Observable<Response> 到 Promise<Response>。 通过 then-chaining 我们最终得到一个 Promise<Array<string>> 作为我们搜索方法的返回类型。
到目前为止一切顺利,让我们看看保存我们的 App 组件的 app.ts 文件。看一下 wiki service 如何被消费的:
这里也没什么惊喜。 我们注入我们的 WikipediaService 并通过搜索方法向模板公开它的功能。 模板简单地绑定到 keyup 并调用 search(term.value) 利用 Angular 的很棒的模板引用功能。
我们解开 WikipediaService 的搜索方法返回的 Promise 的结果,并将其作为一个简单的字符串数组公开给模板,这样我们就可以让 *ngFor 循环遍历它并为我们构建一个列表。
不幸的是,这个实现没有解决我们想要处理的任何所描述的边缘情况。 让我们重构我们的代码,使其符合预期的行为。
让我们更改我们的代码,不要在每次击键时敲击端点,而是仅在用户停止输入 400 毫秒时发送请求。 这就是 Observables 真正闪耀的地方。 Reactive Extensions (Rx) 提供了广泛的运算符,让我们可以改变 Observables 的行为并创建具有所需语义的新 Observables。
为了揭示这样的超能力,我们首先需要获得一个 Observable<string> ,它携带用户输入的搜索词。 我们可以利用 Angular 的 formControl 指令,而不是手动绑定到 keyup 事件。 要使用此指令,我们首先需要将 ReactiveFormsModule 导入到我们的应用程序模块中。
导入后,我们可以在模板中使用 formControl 并将其设置为名称“term”。
<input type="text" [formControl]="term"/>
在我们的组件中,我们从@angular/form 创建了一个 FormControl 的实例,并将其公开为组件上名称 term 下的一个字段。
在幕后,术语会自动公开一个 Observable<string> 作为我们可以订阅的属性 valueChanges。 现在我们有了一个 Observable<string>,驯服用户输入就像在我们的 Observable 上调用 debounceTime(400) 一样简单。 这将返回一个新的 Observable<string>,它只会在 400 毫秒内没有新值出现时才发出新值。
这个新的对象会在我们期望的时间间隔之后,才会发生新值。至于如何控制时间间隔,对前端开发人员来说是一个黑盒子。
export class App { items: Array<string>; term = new FormControl(); constructor(private wikipediaService: WikipediaService) { this.term.valueChanges .debounceTime(400) .subscribe(term => this.wikipediaService.search(term).then(items => this.items = items)); } }
正如我们所说,对我们的应用程序已经显示结果的搜索词发出另一个请求将是一种资源浪费。幸运的是,Rx 简化了许多几乎不需要提及的操作。为了实现所需的行为,我们所要做的就是在我们调用 debounceTime(400) 之后立即调用 distinctUntilChanged 运算符。同样,我们将返回一个 Observable<string> ,但它忽略了与前一个相同的值。
处理无序响应
处理无序响应可能是一项棘手的任务。基本上,我们需要一种方法来表示,一旦我们发出新请求,我们就不再对之前进行中的请求的结果感兴趣。换句话说:一旦我们开始一个新的请求,就取消所有先前的请求。正如我在开头简要提到的,Observables 是一次性的,这意味着我们可以取消订阅它们。
这是我们想要更改 WikipediaService 以返回 Observable<Array<string>> 而不是 Promise<Array<string>> 的地方。这就像删除 toPromise 并使用 map 而不是 then 一样简单。
search (term: string) { var search = new URLSearchParams() search.set('action', 'opensearch'); search.set('search', term); search.set('format', 'json'); return this.jsonp .get('http://en.wikipedia.org/w/api.php?callback=JSONP_CALLBACK', { search }) .map((response) => response.json()[1]); }
现在我们的 WikipediaSerice 返回一个 Observable 而不是 Promise,我们只需要在我们的 App 组件中将 then 替换为 subscribe 。
this.term.valueChanges .debounceTime(400) .distinctUntilChanged() .subscribe(term => this.wikipediaService.search(term).subscribe(items => this.items = items));
但是现在我们有两个 subscribe 调用。 这是不必要的冗长,通常是需要代码重构的标志。 好消息是,现在搜索返回一个 Observable<Array>,我们可以简单地使用 flatMap 通过组合 Observables 来将 Observable 投影到所需的 Observable<Array> 中。
this.term.valueChanges .debounceTime(400) .distinctUntilChanged() .flatMap(term => this.wikipediaService.search(term)) .subscribe(items => this.items = items);
你可能想知道 flatMap 是做什么的,为什么我们不能在这里使用 map。
答案很简单。 map 操作符需要一个函数,它接受一个值 T 并返回一个值 U。例如一个接受一个字符串并返回一个数字的函数。 因此,当您使用 map 时,您会从 Observable<T> 获得一个 Observable<U>。 但是,我们的搜索方法本身会生成一个 Observable。 因此,来自我们在 distinctUntilChanged 之后的 Observable,map 会将我们带到 Observable<Observable<Array>。 这不是我们想要的。
另一方面, flatMap 运算符需要一个函数,它接受一个 T 并返回一个 Observable<U> 并为我们生成一个 Observable<U>。
注意:这并不完全正确,但它有助于简化。
这完全符合我们的情况。 我们有一个 Observable<string>,然后使用一个函数调用 flatMap,该函数接受一个字符串并返回一个 Observable<Array<string>>。
现在我们已经掌握了语义,还有一个小技巧可以用来节省一些打字的时间。 我们可以让 Angular 直接在模板中为我们解包,而不是手动订阅 Observable。 我们要做的就是在我们的模板中使用 AsyncPipe 并公开 Observable<Array<string>> 而不是 Array<string>。
@Component({ selector: 'my-app', template: ` <div> <h2>Wikipedia Search</h2> <input type="text" [formControl]="term"/> <ul> <li *ngFor="let item of items | async">{{item}}</li> </ul> </div> ` }) export class App { items: Observable<Array<string>>; term = new FormControl(); constructor(private wikipediaService: WikipediaService) { this.items = this.term.valueChanges .debounceTime(400) .distinctUntilChanged() .switchMap(term => this.wikipediaService.search(term)); } }