利用Angular2的Observables实现交互控制的方法


Posted in Javascript onDecember 27, 2018

在Angular1.x中,我们使用Promise来处理各种异步。但是在angular2中,使用的是Reactive Extensions (Rx)的Observable。对于Promise和Observable的区别,网上有很多文章,推荐egghead.io上的这个7分钟的视频(作者 Ben Lesh)。在这个视频的介绍中,主要说的,使用Observable创建的异步任务,可以被处理,而且是延时加载的。这篇文章里,我们主要针对一些在跟服务器端交互的时候遇到的问题,来看看Observable给我们带来的特性。

实例场景

首先,我们来定义一下问题的场景。假设我们要实现一个搜索功能,有一个简单的输入框,当用户输入文字的时候,实时的利用输入的文字进行查询,并显示查询的结果。

问题

在这个简单的场景当中,一般需要考虑3个问题:

不能在用户输入每个字符的时候就触发搜索。

如果用户输入每个字符就触发搜索,一来浪费服务器资源,二来客户端频繁触发搜索,以及更新搜索结果,也会影响客户端的响应。一般这个问题,都是通过加一些延时来避免。

如果用户输入的文本没有变化,就不应该重新搜索。

假设用户输入了'foo'以后,停顿了一会,触发了搜索,再敲了一个字符'o',结果发现打错了,又删掉了这个字符。如果这个时候用户又停顿一会,导致触发了搜索,这次的文本'foo'跟之前搜索的时候的文本是一样的,所以不应该再次搜索。

要考虑服务器的异步返回的问题。

当我们使用异步的方式往服务器端发送多个请求的时候,我们需要注意接受返回的顺序是无法保证的。比如我们先后搜索了2个单词'computer', ‘car', 虽然'car'这个词是后来搜的,但是有可能服务器处理这个搜索比较快,就先返回结果。这样页面就会先显示'car'的搜索结果,然后等收到'computer'的搜索结果的时候,再显示'computer'的结果。但是,这时候在用户看来明明搜索的是'car',却显示的是另外的结果。

迎接挑战

在这个实例中,我们使用wikipedia的api接口来开发一个简单的实例,实现简单的搜索功能。

实现搜索

由于只是演示,我们的app里面只包含2个文件: app.ts 和 wikipedia-service.ts,最终版本的源文件,请参考原文提供的demo链接。

我们直接来看最初版本的WikipediaService是如何实现的:

import { Injectable } from '@angular/core';
import { URLSearchParams, Jsonp } from '@angular/http';
@Injectable()
export class WikipediaService {
constructor(private jsonp: Jsonp) {}
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 })
.toPromise()
.then((response) => response.json()[1]);
}
}

在这里版本中,使用Jsonp模块来请求api结果,它的结果应该是一个类型为Observable<Response>的对象,我们把返回的结果从Observable<Response> 转换成 Promise<Response>对象,然后使用它的then方法把结果转成json。这样,这个search方法的返回类型为Promise<Array<string>>。

注意上面,我们使用response.json()[1]方式,从原先的结果中,得到我们需要的查询结果的列表,列表里面都是string。

这个看起来很简单,在angular1.x里面,也基本都是使用$http或$resource,来返回一个Promise类型的结果。

下面就是app.ts的部分内容(因为这只是演示,所以直接在app.ts里面直接定义module和component,并且调用service,在真实的app中,应该创建相应的组件来实现):

// check the plnkr for the full list of imports
import {...} from '...';
@Component({
selector: 'my-app',
template: `
<div>
<h2>Wikipedia Search</h2>
<input #term type="text" (keyup)="search(term.value)">
<ul>
<li *ngFor="let item of items">{{item}}</li>
</ul>
</div>
`
})
export class AppComponent {
items: Array<string>;
constructor(private wikipediaService: WikipediaService) {}
search(term) {
this.wikipediaService.search(term).then(items => this.items = items);
}
}

从上面的代码也能看出,AppComponent有一个search()方法,它调用wikipediaService.search()方法,因为这个方法返回一个Promise<Array<string>>类型的结果,所以使用then(),把结果列表赋值给model对象items。上面的template里面的模板内容就是用来以列表显示查询的结果。

虽然这个实现满足了基本的查询功能,但是对于上面提到的3个问题,都没有能够解决。下面就来修改这个实现来解决上面的问题。

控制用户输入延时

我们先解决第一个问题:当用户输入的时候,不要每次输入一个字符就触发一次搜索,而是设置一个时间延时,当用户停止输入的时间超过400毫秒,就触发搜索。如果用户一直不停的输入,输入的时间间隔小于400ms就不触发。这正是'Observables'能做的事情。

为此,我们需要一个Observable<string>对象来保存用户的输入,然后就可以用这个对象提供的方法来实现延时触发的功能。我们可以利用Angular2的指令(directive)formControl。要用这个指令,需要引入ReactiveFormsModule模块。

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { JsonpModule } from '@angular/http';
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [BrowserModule, JsonpModule, ReactiveFormsModule]
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}

引入以后,我们就可以在模板里面使用FormControl来创建表单输入,并给他设置一个变量名term。

<input type="text" [formControl]="term"/>

这样,这个input组件所绑定的变量term就是FormControl的一个实例,它有一个属性valueChanges,这个属性是一个Observable<string>类型的对象。我们就可以使用Observable<string>的debounceTime方法来设置触发延时。

export class AppComponent {
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));
}
}

我们看到this.term.valueChanges是一个Observable<string>对象,通过debounceTime(400)我们设置它的事件触发延时是400毫秒。这个方法还是返回一个Observable<string>对象。然后我们就给这个对象添加一个订阅事件:

term => this.wikipediaService.search(term).then(items => this.items = items)

这是用lambda表达式写的一个方法。参数term就是Observable<string>对象经过400ms的延时设置,产生的一个用户输入的字符串。方法体就是用这个参数进行搜索,跟之前版本的处理方式一致。

在这个修改版中,我们把之前的search()方法去掉,直接在构造函数constructor(...)里面添加的,这相当于,用户在输入框的输入,是一个消息源,会经过debounceTime(400)的处理,然后产生一个消息,这个消息会发送给订阅的事件处理函数来处理,也就是搜索。所以,我们不需要一个search()方法来控制什么时候触发,而是通过类型订阅的机制来处理用户输入。

防止触发两次

现在我们再来解决第二个问题,就是经过400ms的延时以后,用户输入的搜索条件一样的情况。有了上面的Observable,这个就很简单了,Observable有一个distinctUntilChanged的方法,他会判断从消息源过来的新数据跟上次的数据是否一致,只有不一致才会触发订阅的方法。

this.term.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.subscribe(term => this.wikipediaService.search(term).then(items => this.items = items));

处理返回顺序

上面描述了服务器端异步返回数据的时候,返回顺序不一致出现的问题。对于这个问题,我们的解决办法就比较直接,也就是对于之前的请求返回的结果,直接忽略,只处理在页面上用户最后一次发起的请求的结果。说道忽略之前的请求,如果你们看了上面的视频,或者知道Promise和Observable的区别的话,就应该想到我们可以利用Observable的dispose()方法来解决。实际上,我们是利用这种'disposable'特性来解决,而不是直接调用dispose()方法。(实在不知道该怎么翻译'disposable',它的意思是我可以中止在Observable对象上的消息处理,字面的意思是可被丢弃的、一次性的。)

上面我们讲到,在service的search()方法里,我们把Jsonp返回的结果从Observable<Response> 转换成 Promise<Response>对象。为了利用Observable的特性去丢弃上一个未及时返回的结果,我们让这个方法还是返回Observable类型的结果。下面就是修改后的WikipediaService里面的search()方法。

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]);
}

注意这个方法最后用.map((response) => response.json()[1]),意思是对于原先的Response类型的结果,转换成实际的搜索结果的列表。

map()以及后面要说到的flatMap()之类的方法,是函数式编程里面常用到的方法,意思就是将原先的数据集里面的每一条数据,经过一定的处理再返回一个新的结果,也就是把一个数据集转换成另一个数据集。

现在,我们的WikipediaSerice的返回结果就不是Promise了,所以我们就需要修改app.ts,我们不能再使用then()方法来处理结果,而是使用subscribe()添加一个消息订阅方法。

this.term.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.subscribe(
term => this.wikipediaService.search(term).subscribe(
items => this.items = items
)
);

其中,第一个subscribe():

this.term.valueChanges...subscribe(term => ....)

这个是对输入框产生的查询字符串,注册一个订阅方法,来处理用户的输入。

第二个subscribe():

this.wikipediaService.search(term).subscribe(items => this.items = items));

是对从服务器端返回的数据查询结果,注册一个订阅方法,来将这个数据赋值到model上。

我们也可以用下面的方式,来避免这样使用多个subscribe:

this.term.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.flatMap(term => this.wikipediaService.search(term))
.subscribe(items => this.items = items);

我们在用户输入的字符串的Observable<string>上调用flatMap(...)方法,相当于,对用户输入的每个有效的查询条件,调用wikipediaService.search()方法。然后对这个查询返回的数据,再注册一个订阅方法。

费了这么大的篇幅,希望你明白了Observable的flatMap和subscribe用法,对于没有接触过函数式编程的人来说,这确实不好理解,但是在Angular2里面,我们将会大量使用各种函数式编程的方法。所以还是需要你花时间慢慢理解。

费了这么大功夫,上面说的似乎跟'忽略之前未及时返回的消息'好像没什么关系,那么上面的修改到底有没有解决那个问题呢。没有!确实是没有。因为我们使用flatMap,对用户输入的每个有效的查询字符串,都会调用订阅的那个处理函数,然后更新model。所以我们的问题还是没有解决。

但是到了这一步以后,解决办法就很容易了,我们只需要用switchMap代理flatMap就可以。就这么简单!这是因为,switchMap会在处理每一个新的消息的时候,就直接把上一个消息注册的订阅方法直接取消掉。

最后,再优化一下代码:

@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 AppComponent {
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));
}
}

我们直接把switchMap()的结果,赋给model对象this.items,也就是一个Observable<Array<string>>类型的数据。这样,在模板里面使用items的地方也需要修改,使用AsyncPipe就可以:

<li *ngFor="let item of items | async">{{item}}</li>

这样,模板在解析items这个model的时候,就会自动解析这个Observable的结果,再渲染页面。

Demo地址: Smart Wikipedia search using Angular 2

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JS判断不同分辨率调用不同的CSS样式文件实现思路及测试代码
Jan 23 Javascript
JQuery实现鼠标移动到图片上显示边框效果
Jan 09 Javascript
JS JQUERY实现滚动条自动滚到底的方法
Jan 09 Javascript
jquery密码强度校验
Dec 02 Javascript
jQuery调用WebMethod(PageMethod) NET2.0的方法
Apr 15 Javascript
在vscode里使用.vue代码模板的方法
Apr 28 Javascript
vue2中使用sass并配置全局的sass样式变量的方法
Sep 04 Javascript
实例分析vue循环列表动态数据的处理方法
Sep 28 Javascript
ES6 对象的新功能与解构赋值介绍
Feb 05 Javascript
微信小程序-form表单提交代码实例
Apr 29 Javascript
vue监听用户输入和点击功能
Sep 27 Javascript
JS函数式编程实现XDM一
Jun 16 Javascript
angular 用Observable实现异步调用的方法
Dec 27 #Javascript
详解CommonJS和ES6模块循环加载处理的区别
Dec 26 #Javascript
vue-router beforeEach跳转路由验证用户登录状态
Dec 26 #Javascript
Vuerouter的beforeEach与afterEach钩子函数的区别
Dec 26 #Javascript
使用Sonarqube扫描Javascript代码的示例
Dec 26 #Javascript
angular6的table组件开发的实现示例
Dec 26 #Javascript
详解VUE里子组件如何获取父组件动态变化的值
Dec 26 #Javascript
You might like
用PHP写的MySQL数据库用户认证系统代码
2007/03/22 PHP
php mail to 配置详解
2014/01/16 PHP
php通过function_exists检测函数是否存在的方法
2015/03/18 PHP
php 后端实现JWT认证方法示例
2018/09/04 PHP
拖动Html元素集合 Drag and Drop any item
2006/12/22 Javascript
javascript利用初始化数据装配模版的实现代码
2010/11/17 Javascript
借助script进行Http跨域请求:JSONP实现原理及代码
2013/03/19 Javascript
jquery next nextAll nextUntil siblings的区别介绍
2013/10/05 Javascript
JavaScript实现找出字符串中第一个不重复的字符
2014/09/03 Javascript
JavaScript strike方法入门实例(给字符串加上删除线)
2014/10/17 Javascript
JavaScript 浏览器对象模型BOM使用介绍
2015/04/13 Javascript
javascript判断元素存在和判断元素存在于实时的dom中的方法
2017/01/17 Javascript
JavaScript ES6中export、import与export default的用法和区别
2017/03/14 Javascript
JavaScript学习总结(一) ECMAScript、BOM、DOM(核心、浏览器对象模型与文档对象模型)
2018/01/07 Javascript
简单的vuex 的使用案例笔记
2018/04/13 Javascript
JS闭包原理与应用经典示例
2018/12/20 Javascript
VUE解决微信签名及SPA微信invalid signature问题(完美处理)
2019/03/29 Javascript
node.js实现带进度条的多文件上传
2020/03/27 Javascript
python 简易计算器程序,代码就几行
2009/08/29 Python
简化Python的Django框架代码的一些示例
2015/04/20 Python
python调用xlsxwriter创建xlsx的方法
2018/05/03 Python
浅析python中numpy包中的argsort函数的使用
2018/08/30 Python
python实现简单多人聊天室
2018/12/11 Python
Python钉钉报警及Zabbix集成钉钉报警的示例代码
2020/08/17 Python
用python实现一个简单的验证码
2020/12/09 Python
澳大利亚票务和娱乐市场领导者:Ticketmaster
2017/03/03 全球购物
Sephora丝芙兰菲律宾官方网站:购买化妆品和护肤品
2017/04/05 全球购物
mysql_pconnect()和mysql_connect()有什么区别
2012/05/25 面试题
JSP&Servlet技术面试题
2015/05/21 面试题
市场营销专业个人自荐信格式
2013/09/21 职场文书
临床医学专业毕业生的自我评价
2013/10/17 职场文书
生产内勤岗位职责
2013/12/07 职场文书
2014年度安全生产目标管理责任书
2014/07/25 职场文书
道士塔读书笔记
2015/06/30 职场文书
结婚喜宴祝酒词
2015/08/10 职场文书
导游词之台湾阿里山
2019/10/23 职场文书