如何使用RxJS构建搜索栏

来自菜鸟教程
跳转至:导航、​搜索

作者选择 Mozilla Foundation 作为 Write for DOnations 计划的一部分来接受捐赠。

介绍

反应式编程是一种与异步数据流有关的范式,其中编程模型认为一切都是随时间传播的数据流。 这包括击键、HTTP 请求、要打印的文件,甚至是数组元素,这些元素可以被认为是在非常小的时间间隔内进行计时的。 这使得它非常适合 JavaScript,因为异步数据在语言中很常见。

RxJSJavaScript 中用于响应式编程的流行库。 ReactiveX 是 RxJS 所在的保护伞,它在许多其他语言中都有扩展,例如 JavaPythonC++SwiftDart。 RxJS 也被 Angular 和 React 等库广泛使用。

RxJS 的实现基于链式函数,这些函数可以感知并能够在一段时间内处理数据。 这意味着几乎可以使用接收参数和回调列表的函数来实现 RxJS 的几乎所有方面,然后在收到信号时执行它们。 围绕 RxJS 的社区已经完成了这项繁重的工作,结果是一个 API,您可以直接在任何应用程序中使用它来编写干净且可维护的代码。

在本教程中,您将使用 RxJS 构建一个功能丰富的搜索栏,向用户返回实时结果。 您还将使用 HTML 和 CSS 来设置搜索栏的格式。 最终结果将如下所示:

像搜索栏这样常见且看似简单的东西需要进行各种检查。 本教程将向您展示 RxJS 如何将一组相当复杂的需求转换为易于管理且易于理解的代码。

先决条件

在开始本教程之前,您需要以下内容:

本教程的完整代码可在 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]) || "&lt;Title not available&gt;"}</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 元素的引用。 这将成为代码用于侦听输入事件的 ObservableObservablesObserver 监听的未来值或事件的集合,也称为 回调函数

在上一步的 JavaScript 下的 <script> 标记中添加突出显示的行:

搜索栏.html

...
      output.insertAdjacentHTML("beforeend", resultItem);
      animationDelay += 0.1; 

    });
  }
}


      let searchInput = document.getElementById("search-input");
...

现在您已经添加了一个变量来引用输入,您将使用 fromEvent 运算符来监听事件。 这将在 DOMDocument Object M 模型上添加一个侦听器,用于某种事件的元素。 DOM 元素可以是页面上的 htmlbodydivimg 元素。 在这种情况下,您的 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:负责处理错误。 一旦调用此方法,将不会调用 onNextonCompleted
  • 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 文档