30 天精通 RxJS(19): 实务范例 - 简易 Auto Complete 实作

今天我们要做一个 RxJS 的经典范例 - 自动完成 (Auto Complete),自动完成在实务上的应用非常广泛,几乎随处可见这样的功能,只要是跟表单、搜寻相关的都会看到。

虽然是个很常见的功能,但多数的工程师都只是直接套套件来完成,很少有人会自己从头到尾把完整的逻辑写一次。如果有自己实作过这个功能的工程师,应该就会知道这个功能在实作的过程中很多细节会让程式码变的非常複杂,像是要如何取消上一次发送出去的 request、要如何优化请求次数... 等等,这些小细节都会让程式码变的非常複杂且很难维护。

就让我们一起来用 RxJS 来实作这个功能吧!

需求分析

首先我们会有一个搜寻框(input#search),当我们在上面打字并停顿超过 100 毫秒就发送 HTTP Request 来取得建议选项并显示在收寻框下方(ul#suggest-list),如果使用者在前一次发送的请求还没有回来就打了下一个字,此时前一个发送的请求就要捨弃掉,当建议选项显示之后可以用滑鼠点击取建议选项代搜寻框的文字。

image.png

上面的叙述可以拆分成以下几个步骤

  • 准备 input#search 以及 ul#suggest-list 的 HTML 与 CSS
  • 在 input#search 输入文字时,等待 100 毫秒再无输入,就发送 HTTP Request
  • 当 Response 还没回来时,使用者又输入了下一个文字就捨弃前一次的并再发送一次新的 Request
  • 接受到 Response 之后显示建议选项
  • 滑鼠点击后取代 input#search 的文字

基本的 HTML 跟 CSS 笔者已经帮大家完成,大家可以直接到下面的连结接著实作:

先让我们看一下 HTML,首先在 HTML 裡有一个 input(#search),这个 input(#search) 就是要用来输入的栏位,它下方有一个 ul(#suggest-list),则是放建议选项的地方

CSS 的部分可以不用看,JS 的部分已经写好了要发送 API 的 url 跟方法getSuggestList,接著就开始实作自动完成的效果吧!

第一步,取得需要的 DOM 物件

这裡我们会用到 #search 以及 #suggest-list 这两个 DOM

const searchInput = document.getElementById('search');
const suggestList = document.getElementById('suggest-list');

第二步,建立所需的 Observable

这裡我们要监听 收寻栏位的 input 事件,以及建议选项的点击事件

const keyword = Rx.Observable.fromEvent(searchInput, 'input');
const selectItem = Rx.Observable.fromEvent(suggestList, 'click');

第三步,撰写程式逻辑

每当使用者输入文字就要发送 HTTP request,并且有新的值被输入后就捨弃前一次发送的,所以这裡用 switchMap

keyword.switchMap(e => getSuggestList(e.target.value))

这裡我们先试著订阅,看一下 API 会回传什麽样的资料

keyword
    .switchMap(e => getSuggestList(e.target.value))
    .subscribe(console.log)

在 search 栏位乱打几个字

image.png

大家可以在 console 看到资料长相这样,他会回传一个阵列带有四个元素,其中第一个元素是我们输入的值,第二个元素才是我们要的建议选项清单。

所以我们要取的是 response 阵列的第二的元素,用 switchMap 的第二个参数来选取我们要的

keyword
    .switchMap(
        e => getSuggestList(e.target.value),
        (e, res) => res[1]
    )
    .subscribe(console.log)

这时再输入文字就可以看到确实是我们要的返回值

image.png

写一个 render 方法,把阵列转成 li 并写入 suggestList

const render = (suggestArr = []) => {
    suggestList.innerHTML = suggestArr
                            .map(item => '<li>'+ item +'</li>')
                            .join('');  
}

这时我们就可用 render 方法把取得的阵列传入

const render = (suggestArr = []) => {
    suggestList.innerHTML = suggestArr
                            .map(item => '<li>'+ item +'</li>')
                            .join('');  
}

keyword
  .switchMap(
    e => getSuggestList(e.target.value),
    (e, res) => res[1]
  )
  .subscribe(list => render(list))

如此一来我们打字就能看到结果出现在 input 下方了

image.png

只是目前还不能点选,先让我们来做点选的功能,这裡点选的功能我们需要用到 delegation event 的小技巧,利用 ul 的 click 事件,来塞选是否点到了 li,如下

selectItem
  .filter(e => e.target.matches('li'))

上面我们利用 DOM 物件的 matches 方法(裡面的字串放 css 的 selector)来过滤出有点击到 li 的事件,再用 map 转出我们要的值并写入 input。

selectItem
  .filter(e => e.target.matches('li'))
  .map(e => e.target.innerText)
  .subscribe(text => searchInput.value = text)

现在我们就能点击建议清单了,但是点击后清单没有消失,这裡我们要在点击后重新 redner,所以把上面的程式码改一下

selectItem
  .filter(e => e.target.matches('li'))
  .map(e => e.target.innerText)
  .subscribe(text => { 
      searchInput.value = text;
      render();
  })

这样一来我们就完成最基本的功能了,大家可以到这裡看初步的完成品。

还记得我们前面说每次打完字要等待 100 毫秒在发送 request 吗? 这样能避免过多的 request 发送,可以降低 server 的负载也会有比较好的使用者体验,要做到这件事很简单只要加上 debounceTime(100) 就完成了

keyword
  .debounceTime(100)
  .switchMap(
    e => getSuggestList(e.target.value),
    (e, res) => res[1]
  )
  .subscribe(list => render(list))

当然这个数值可以依照需求或是请 UX 针对这个细节作调整。

这样我们就完成所有功能了,大家可以到这裡查看结果。

今日小结

我们用了不到 30 行的程式码就完成了 auto complete 的基本功能,当我们能够自己从头到尾的完成这样的功能,在面对各种不同的需求,我们就能很方便的针对需求作调整,而不会受到套件的牵制!比如说我们希望使用者打了 2 个字以上在发送 request,这时我们只要加上一行 filter 就可以了

keyword
  .filter(e => e.target.value.length > 2)
  .debounceTime(100)
  .switchMap(
    e => getSuggestList(e.target.value),
    (e, res) => res[1]
  )
  .subscribe(list => render(list))

又或者网站的使用量很大,可能 API 在量大的时候会回传失败,主管希望可以在 API 失败的时候重新尝试 3 次,我们只要加个 retry(3) 就完成了

keyword
  .filter(e => e.target.value.length > 2)
  .debounceTime(100)
  .switchMap(
    e => Rx.Observable.from(getSuggestList(e.target.value))
                      .retry(3),
    (e, res) => res[1]
  )
  .subscribe(list => render(list))

大家会发现我们的灵活度变的非常高,又同时兼顾了程式码的可读性,短短的几行程式码就完成了一个複杂的需求,这就是 RxJS 的魅力啊~

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

推荐阅读更多精彩内容