如何使用RxJS构建搜索栏
作者选择 Mozilla Foundation 作为 Write for DOnations 计划的一部分来接受捐赠。
介绍
反应式编程是一种与异步数据流有关的范式,其中编程模型认为一切都是随时间传播的数据流。 这包括击键、HTTP 请求、要打印的文件,甚至是数组元素,这些元素可以被认为是在非常小的时间间隔内进行计时的。 这使得它非常适合 JavaScript,因为异步数据在语言中很常见。
RxJS 是 JavaScript 中用于响应式编程的流行库。 ReactiveX 是 RxJS 所在的保护伞,它在许多其他语言中都有扩展,例如 Java、Python、C++、 Swift 和 Dart。 RxJS 也被 Angular 和 React 等库广泛使用。
RxJS 的实现基于链式函数,这些函数可以感知并能够在一段时间内处理数据。 这意味着几乎可以使用接收参数和回调列表的函数来实现 RxJS 的几乎所有方面,然后在收到信号时执行它们。 围绕 RxJS 的社区已经完成了这项繁重的工作,结果是一个 API,您可以直接在任何应用程序中使用它来编写干净且可维护的代码。
在本教程中,您将使用 RxJS 构建一个功能丰富的搜索栏,向用户返回实时结果。 您还将使用 HTML 和 CSS 来设置搜索栏的格式。 最终结果将如下所示:
像搜索栏这样常见且看似简单的东西需要进行各种检查。 本教程将向您展示 RxJS 如何将一组相当复杂的需求转换为易于管理且易于理解的代码。
先决条件
在开始本教程之前,您需要以下内容:
- 支持 JavaScript 语法高亮的文本编辑器,例如 Atom、Visual Studio Code 或 Sublime Text。 这些编辑器可在 Windows、macOS 和 Linux 上使用。
- 熟悉同时使用 HTML 和 JavaScript。 在 如何将 JavaScript 添加到 HTML 中了解更多信息。
- 熟悉 JSON 数据格式,您可以在 如何在 JavaScript 中使用 JSON 中了解更多信息。
本教程的完整代码可在 Github 上找到。
第 1 步 - 创建和设置搜索栏样式
在此步骤中,您将使用 HTML 和 CSS 创建搜索栏并设置其样式。 该代码将使用 Bootstrap 中的一些常见元素来加速页面的结构化和样式化过程,以便您可以专注于添加自定义元素。 Bootstrap 是一个 CSS 框架,包含用于常见元素的模板,如排版、表单、按钮、导航、网格和其他界面组件。 您的应用程序还将使用 Animate.css 将动画添加到搜索栏。
您将从使用 nano
或您喜欢的文本编辑器创建一个名为 search-bar.html
的文件开始:
nano search-bar.html
接下来,为您的应用程序创建基本结构。 将以下 HTML 添加到新文件中:
搜索栏.html
<!DOCTYPE html> <html> <head> <title>RxJS Tutorial</title> <!-- Load CSS --> <!-- Load Rubik font --> <!-- Add Custom inline CSS --> </head> <body> <!-- Content --> <!-- Page Header and Search Bar --> <!-- Results --> <!-- Load External RxJS --> <!-- Add custom inline JavaScript --> <script> </script> </body> </html>
由于您需要整个 Bootstrap 库中的 CSS,请继续为 Bootstrap 和 Animate.css 加载 CSS。
在 Load CSS
注释下添加以下代码:
搜索栏.html
... <!-- Load CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.0/animate.min.css" /> ...
本教程将使用 Google Fonts 库中名为 Rubik 的自定义字体来设置搜索栏的样式。 通过在 Load Rubik font
注释下添加突出显示的代码来加载字体:
搜索栏.html
... <!-- Load Rubik font --> <link href="https://fonts.googleapis.com/css?family=Rubik" rel="stylesheet"> ...
接下来,将自定义 CSS 添加到 Add Custom inline CSS
注释下的页面。 这将确保页面上的标题、搜索栏和结果易于阅读和使用。
搜索栏.html
... <!-- Add Custom inline CSS --> <style> body { background-color: #f5f5f5; font-family: "Rubik", sans-serif; } .search-container { margin-top: 50px; } .search-container .search-heading { display: block; margin-bottom: 50px; } .search-container input, .search-container input:focus { padding: 16px 16px 16px; border: none; background: rgb(255, 255, 255); box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1) !important; } .results-container { margin-top: 50px; } .results-container .list-group .list-group-item { background-color: transparent; border-top: none !important; border-bottom: 1px solid rgba(236, 229, 229, 0.64); } .float-bottom-right { position: fixed; bottom: 20px; left: 20px; font-size: 20px; font-weight: 700; z-index: 1000; } .float-bottom-right .info-container .card { display: none; } .float-bottom-right .info-container:hover .card, .float-bottom-right .info-container .card:hover { display: block; } </style> ...
现在您已经拥有了所有样式,在 Page Header and Search Bar
注释下添加将定义标题和输入栏的 HTML:
搜索栏.html
... <!-- Content --> <!-- Page Header and Search Bar --> <div class="container search-container"> <div class="row justify-content-center"> <div class="col-md-auto"> <div class="search-heading"> <h2>Search for Materials Published by Author Name</h2> <p class="text-right">powered by <a href="https://www.crossref.org/">Crossref</a></p> </div> </div> </div> <div class="row justify-content-center"> <div class="col-sm-8"> <div class="input-group input-group-md"> <input id="search-input" type="text" class="form-control" placeholder="eg. Richard" aria-label="eg. Richard" autofocus> </div> </div> </div> </div> ...
这使用来自 Bootstrap 的网格系统来构建页面标题和搜索栏。 您已经为搜索栏分配了一个 search-input
标识符,您将在本教程的后面使用它来绑定到侦听器。
接下来,您将创建一个位置来显示搜索结果。 在 Results
注释下,使用 response-list
标识符创建一个 div
以在教程后面添加结果:
搜索栏.html
... <!-- Results --> <div class="container results-container"> <div class="row justify-content-center"> <div class="col-sm-8"> <ul id="response-list" class="list-group list-group-flush"></ul> </div> </div> </div> ...
此时,search-bar.html
文件将如下所示:
搜索栏.html
<!DOCTYPE html> <html> <head> <title>RxJS Tutorial</title> <!-- Load CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.0/animate.min.css" /> <!-- Load Rubik font --> <link href="https://fonts.googleapis.com/css?family=Rubik" rel="stylesheet"> <!-- Add Custom inline CSS --> <style> body { background-color: #f5f5f5; font-family: "Rubik", sans-serif; } .search-container { margin-top: 50px; } .search-container .search-heading { display: block; margin-bottom: 50px; } .search-container input, .search-container input:focus { padding: 16px 16px 16px; border: none; background: rgb(255, 255, 255); box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1) !important; } .results-container { margin-top: 50px; } .results-container .list-group .list-group-item { background-color: transparent; border-top: none !important; border-bottom: 1px solid rgba(236, 229, 229, 0.64); } .float-bottom-right { position: fixed; bottom: 20px; left: 20px; font-size: 20px; font-weight: 700; z-index: 1000; } .float-bottom-right .info-container .card { display: none; } .float-bottom-right .info-container:hover .card, .float-bottom-right .info-container .card:hover { display: block; } </style> </head> <body> <!-- Content --> <!-- Page Header and Search Bar --> <div class="container search-container"> <div class="row justify-content-center"> <div class="col-md-auto"> <div class="search-heading"> <h2>Search for Materials Published by Author Name</h2> <p class="text-right">powered by <a href="https://www.crossref.org/">Crossref</a></p> </div> </div> </div> <div class="row justify-content-center"> <div class="col-sm-8"> <div class="input-group input-group-md"> <input id="search-input" type="text" class="form-control" placeholder="eg. Richard" aria-label="eg. Richard" autofocus> </div> </div> </div> </div> <!-- Results --> <div class="container results-container"> <div class="row justify-content-center"> <div class="col-sm-8"> <ul id="response-list" class="list-group list-group-flush"></ul> </div> </div> </div> <!-- Load RxJS --> <!-- Add custom inline JavaScript --> <script> </script> </body> </html>
在这一步中,您已经使用 HTML 和 CSS 为您的搜索栏布置了基本结构。 在下一步中,您将编写一个接受搜索词并返回结果的 JavaScript 函数。
第 2 步 — 编写 JavaScript
现在您已经格式化了搜索栏,您可以编写 JavaScript 代码,作为您将在本教程后面编写的 RxJS 代码的基础。 此代码将与 RxJS 一起使用以接受搜索词并返回结果。
由于您不需要 Bootstrap 和 JavaScript 在本教程中提供的功能,因此您不会加载它们。 但是,您将使用 RxJS。 通过在 Load RxJS
注释下添加以下内容来加载 RxJS 库:
搜索栏.html
... <!-- Load RxJS --> <script src="https://unpkg.com/@reactivex/rxjs@5.0.3/dist/global/Rx.js"></script> ...
现在,您将从 HTML 中存储 div
的引用,结果将添加到其中。 在 Add custom inline JavaScript
注释下的 <script>
标记中添加突出显示的 JavaScript 代码:
搜索栏.html
... <!-- Add custom inline JavaScript --> <script> const output = document.getElementById("response-list"); </script> ...
接下来,添加代码以将来自 API 的 JSON 响应转换为 HTML 元素以显示在页面上。 此代码将首先清除搜索栏的内容,然后为搜索结果动画设置延迟。
在 <script>
标签之间添加高亮功能:
搜索栏.html
... <!-- Add custom inline JavaScript --> <script> const output = document.getElementById("response-list"); function showResults(resp) { var items = resp['message']['items'] output.innerHTML = ""; animationDelay = 0; if (items.length == 0) { output.innerHTML = "Could not find any :("; } else { items.forEach(item => { resultItem = ` <div class="list-group-item animated fadeInUp" style="animation-delay: ${animationDelay}s;"> <div class="d-flex w-100 justify-content-between"> <^> <h5 class="mb-1">${(item['title'] && item['title'][0]) || "<Title not available>"}</h5> </div> <p class="mb-1">${(item['container-title'] && item['container-title'][0]) || ""}</p> <small class="text-muted"><a href="${item['URL']}" target="_blank">${item['URL']}</a></small> <div> <p class="badge badge-primary badge-pill">${item['publisher'] || ''}</p> <p class="badge badge-primary badge-pill">${item['type'] || ''}</p> </div> </div> `; output.insertAdjacentHTML("beforeend", resultItem); animationDelay += 0.1; }); } } </script> ...
以 if
开头的代码块是一个条件循环,用于检查搜索结果,如果未找到结果则显示一条消息。 如果找到结果,则 forEach
循环将为用户提供带有动画的结果。
在这一步中,您通过编写一个可以接受结果并在页面上返回结果的函数来为 RxJS 奠定基础。 在下一步中,您将使搜索栏正常工作。
第 3 步 — 设置监听器
RxJS 关注的是数据流,在这个项目中数据流是用户输入到输入元素或搜索栏的一系列字符。 在此步骤中,您将在输入元素上添加一个侦听器以侦听更新。
首先,记下您在本教程前面添加的 search-input
标识符:
搜索栏.html
... <input id="search-input" type="text" class="form-control" placeholder="eg. Richard" aria-label="eg. Richard" autofocus> ...
接下来,创建一个变量来保存 search-input
元素的引用。 这将成为代码用于侦听输入事件的 Observable。 Observables
是 Observer
监听的未来值或事件的集合,也称为 回调函数 。
在上一步的 JavaScript 下的 <script>
标记中添加突出显示的行:
搜索栏.html
... output.insertAdjacentHTML("beforeend", resultItem); animationDelay += 0.1; }); } } let searchInput = document.getElementById("search-input"); ...
现在您已经添加了一个变量来引用输入,您将使用 fromEvent
运算符来监听事件。 这将在 DOM 或 Document Object M 模型上添加一个侦听器,用于某种事件的元素。 DOM 元素可以是页面上的 html
、body
、div
或 img
元素。 在这种情况下,您的 DOM 元素就是搜索栏。
在 searchInput
变量下添加以下突出显示的行,以将参数传递给 fromEvent
。 您的 searchInput
DOM 元素是第一个参数。 接下来是 input
事件作为第二个参数,这是代码将侦听的事件类型。
搜索栏.html
... let searchInput = document.getElementById("search-input"); Rx.Observable.fromEvent(searchInput, 'input') ...
现在你的监听器已经设置好了,只要输入元素发生任何更新,你的代码就会收到通知。 在下一步中,您将使用运算符对此类事件采取措施。
第 4 步 - 添加运算符
Operators
是具有一个任务的纯函数——对数据执行操作。 在此步骤中,您将使用运算符执行各种任务,例如缓冲 input
参数、发出 HTTP 请求和过滤结果。
您将首先确保结果在用户输入查询时实时更新。 为此,您将使用上一步中的 DOM 输入事件。 DOM 输入事件包含各种细节,但对于本教程,您对输入到目标元素中的值感兴趣。 添加以下代码以使用 pluck
运算符获取对象并返回指定键处的值:
搜索栏.html
... let searchInput = document.getElementById("search-input"); Rx.Observable.fromEvent(searchInput, 'input') .pluck('target', 'value') ...
现在事件已采用必要的格式,您将搜索词的最小值设置为三个字符。 在许多情况下,少于三个字符的任何内容都不会产生相关结果,或者用户可能仍在输入过程中。
您将使用 filter
运算符来设置最小值。 如果满足指定的条件,它将进一步向下传递数据。 将长度条件设置为大于 2
以要求至少三个字符。
搜索栏.html
... let searchInput = document.getElementById("search-input"); Rx.Observable.fromEvent(searchInput, 'input') .pluck('target', 'value') .filter(searchTerm => searchTerm.length > 2) ...
您还将确保仅以 500 毫秒的间隔发送请求,以减轻 API 服务器上的负载。 为此,您将使用 debounceTime
运算符来维持它通过流的每个事件之间的最小指定间隔。 在 filter
运算符下添加突出显示的代码:
搜索栏.html
... let searchInput = document.getElementById("search-input"); Rx.Observable.fromEvent(searchInput, 'input') .pluck('target', 'value') .filter(searchTerm => searchTerm.length > 2) .debounceTime(500) ...
如果自上次 API 调用以来没有任何更改,应用程序也应该忽略搜索词。 这将通过进一步减少发送的 API 调用的数量来优化应用程序。
例如,用户可以键入 super cars
,删除最后一个字符(使术语 super car
),然后将删除的字符添加回以将术语恢复为 super cars
. 结果,该术语没有改变,因此搜索结果不应该改变。 在这种情况下,不执行任何操作是有意义的。
您将使用 distinctUntilChanged
运算符来配置它。 该操作符记住之前通过流传递的数据,并且仅在不同时传递另一个数据。
搜索栏.html
... let searchInput = document.getElementById("search-input"); Rx.Observable.fromEvent(searchInput, 'input') .pluck('target', 'value') .filter(searchTerm => searchTerm.length > 2) .debounceTime(500) .distinctUntilChanged() ...
现在您已经规范了来自用户的输入,您将添加将使用搜索词查询 API 的代码。 为此,您将使用 AJAX 的 RxJS 实现。 AJAX 在加载页面的后台异步调用 API。 AJAX 将允许您避免使用新搜索词的结果重新加载页面,并通过从服务器获取数据来更新页面上的结果。
接下来,添加代码以使用 switchMap
将 AJAX 链接到您的应用程序。 您还将使用 map
将输入映射到输出。 此代码会将传递给它的函数应用于 Observable
发出的每个项目。
搜索栏.html
... let searchInput = document.getElementById("search-input"); Rx.Observable.fromEvent(searchInput, 'input') .pluck('target', 'value') .filter(searchTerm => searchTerm.length > 2) .debounceTime(500) .distinctUntilChanged() .switchMap(searchKey => Rx.Observable.ajax(`https://api.crossref.org/works?rows=50&query.author=${searchKey}`) .map(resp => ({ "status" : resp["status"] == 200, "details" : resp["status"] == 200 ? resp["response"] : [], "result_hash": Date.now() }) ) ) ...
此代码将 API 响应分为三个部分:
status
:API 服务器返回的 HTTP 状态码。 此代码将仅接受200
或成功响应。details
:实际收到的响应数据。 这将包含查询的搜索词的结果。result_hash
:API 服务器返回的响应的哈希值,在本教程中是一个 UNIX 时间戳。 这是随着结果变化而变化的结果散列。 唯一的哈希值将允许应用程序确定结果是否已更改并应更新。
系统出现故障,您的代码应该准备好处理错误。 要处理 API 调用中可能发生的错误,请使用 filter
运算符只接受成功的响应:
搜索栏.html
... let searchInput = document.getElementById("search-input"); Rx.Observable.fromEvent(searchInput, 'input') .pluck('target', 'value') .filter(searchTerm => searchTerm.length > 2) .debounceTime(500) .distinctUntilChanged() .switchMap(searchKey => Rx.Observable.ajax(`https://api.crossref.org/works?rows=50&query.author=${searchKey}`) .map(resp => ({ "status" : resp["status"] == 200, "details" : resp["status"] == 200 ? resp["response"] : [], "result_hash": Date.now() }) ) ) .filter(resp => resp.status !== false) ...
接下来,您将添加代码以仅在响应中检测到更改时才更新 DOM。 DOM 更新可能是一项占用大量资源的操作,因此减少更新次数将对应用程序产生积极影响。 由于 result_hash
仅在响应更改时才会更改,因此您将使用它来实现此功能。
为此,请像以前一样使用 distinctUntilChanged
运算符。 代码将使用它仅在密钥更改时接受用户输入。
搜索栏.html
... let searchInput = document.getElementById("search-input"); Rx.Observable.fromEvent(searchInput, 'input') .pluck('target', 'value') .filter(searchTerm => searchTerm.length > 2) .debounceTime(500) .distinctUntilChanged() .switchMap(searchKey => Rx.Observable.ajax(`https://api.crossref.org/works?rows=50&query.author=${searchKey}`) .map(resp => ({ "status" : resp["status"] == 200, "details" : resp["status"] == 200 ? resp["response"] : [], "result_hash": Date.now() }) ) ) .filter(resp => resp.status !== false) .distinctUntilChanged((a, b) => a.result_hash === b.result_hash) ...
您之前使用 distinctUntilChanged
运算符来查看整个数据是否已更改,但在本例中,您检查响应中的更新键。 与识别单个键中的更改相比,比较整个响应将耗费资源。 由于密钥散列代表整个响应,因此可以自信地用于识别响应更改。
该函数接受两个对象,它看到的先前值和新值。 我们检查这两个对象的哈希值,并在这两个值匹配时返回 True
,在这种情况下,数据被过滤掉,不会在管道中进一步传递。
在此步骤中,您创建了一个管道,该管道接收用户输入的搜索词,然后对其执行各种检查。 检查完成后,它会进行 API 调用并以显示结果的格式返回响应给用户。 您通过在必要时限制 API 调用来优化客户端和服务器端的资源使用。 在下一步中,您将配置应用程序以开始侦听输入元素,并将结果传递给将其呈现在页面上的函数。
第 5 步 — 通过订阅激活一切
subscribe
是链接的最终运算符,它使观察者能够看到 Observable
发出的数据事件。 它实现了以下三种方法:
onNext
:这指定接收到事件时要执行的操作。onError
:负责处理错误。 一旦调用此方法,将不会调用onNext
和onCompleted
。onCompleted
:当onNext
最后一次被调用时调用此方法。 不会有更多数据将在管道中传递。
订阅者的此签名使人们能够实现 延迟执行 ,即定义 Observable
管道并仅在您订阅它时将其设置为运动的能力。 您不会在您的代码中使用此示例,但下面将向您展示如何订阅 Observable
:
接下来,订阅 Observable
并将数据路由到负责在 UI 中呈现它的方法。
搜索栏.html
... let searchInput = document.getElementById("search-input"); Rx.Observable.fromEvent(searchInput, 'input') .pluck('target', 'value') .filter(searchTerm => searchTerm.length > 2) .debounceTime(500) .distinctUntilChanged() .switchMap(searchKey => Rx.Observable.ajax(`https://api.crossref.org/works?rows=50&query.author=${searchKey}`) .map(resp => ({ "status" : resp["status"] == 200, "details" : resp["status"] == 200 ? resp["response"] : [], "result_hash": Date.now() }) ) ) .filter(resp => resp.status !== false) .distinctUntilChanged((a, b) => a.result_hash === b.result_hash) .subscribe(resp => showResults(resp.details)); ...
进行这些更改后保存并关闭文件。
现在您已经完成了代码的编写,您可以查看和测试您的搜索栏了。 双击 search-bar.html
文件以在您的网络浏览器中打开它。 如果代码输入正确,您将看到搜索栏。
在搜索栏中输入内容以进行测试。
在此步骤中,您订阅了 Observable
以激活您的代码。 您现在拥有一个风格化且功能强大的搜索栏应用程序。
结论
在本教程中,您使用 RxJS、CSS 和 HTML 创建了一个功能丰富的搜索栏,为用户提供实时结果。 搜索栏至少需要三个字符,自动更新,并针对客户端和 API 服务器进行了优化。
可以认为是一组复杂的需求是用 18 行 RxJS 代码创建的。 该代码不仅易于阅读,而且比独立的 JavaScript 实现更简洁。 这意味着您的代码将来会更容易理解、更新和维护。
要阅读有关使用 RxJS 的更多信息,请查看 官方 API 文档 。