领先技术

键入提示

Dino Esposito

Dino Esposito在 Web 发展初期,大多数页面中都会提供一个搜索框,以帮助您在页面内或站点上快速查找内容。完善的搜索功能对于大型网站而言不可或缺。这将有助于用户绕过站点地图和体系结构快速而轻松地找到所需内容。

例如,在购物站点中,您可能希望使用查询字符串来查找产品、服务或新闻快讯。在为诸如专业体育团队等构建的网站中,搜索功能必须能够挖掘出新闻、结果、运动员名称、个人简历等等。搜索功能运作所基于的数据结构从来都不明显呈现。很显然它是特定于应用程序的。

为了避免无谓的重复工作,请考虑使用即席全文搜索引擎(如 Lucene.Net)备份您的搜索功能。像 Lucene.Net 这样的引擎将对大量基于字符串的文档编制索引,并针对该索引分析任何给定的查询字符串。如此,该引擎可允许您指定组合复杂的查询字符串。对于许多页面和站点而言,Lucene.Net 方法可能过于小题大做,但它们仍需要某种比在下拉列表中放置源源不断的各种项更为智能的搜索类型。

本文将向您介绍一个围绕 Twitter typeahead.js 而构建的、针对自动完成功能的小框架。此框架并不神奇,但它真正简化了网页中自动完成功能的使用。该框架最吸引人的方面是其允许将多个数据集组合在一起,以便在相同的页面内查询和检索不同但相关的信息。

设置 Typeahead.js

在本文中,我将阐明真实方案中使用 typeahead 的基础知识,即键入“typeahead”后于 NuGet 上找到的 typeahead 版本的使用方案,如图 1 中所示。通过 Google 搜索 typeahead 时,获取的参考、JavaScript 文件可能会较旧,或只是原始项目代码的分叉版本。文档也具有误导性。

适于 Twitter Typeahead.js 的 NuGet 包
图 1 适于 Twitter Typeahead.js 的 NuGet 包

该捆绑文件包含组成该库的所有包,包括管理本地浏览器上的提示的 Bloodhound 引擎。若要设置网页或 Razor 视图以使用 typeahead.js,只需类似 jQuery 的熟悉语法,并在所选的输入字段上激活该插件。例如:

<form action="@Url.Action("Query", "Home")" method="post">

  <input type="hidden" id="queryCode" name="queryCode" />

    <input type="text" name="queryString" id="queryString">

    <button id="queryButton" type="submit">Get</button>

</form>

<form action="@Url.Action("Query", "Home")" method="post">
  <input type="hidden" id="queryCode" name="queryCode" />
    <input type="text" name="queryString" id="queryString">
    <button id="queryButton" type="submit">Get</button>
</form>

请务必注意,若要在有用的网页上使用自动完成功能,您还需要一个隐藏的 buddy 字段,用于为选定提示收集某种唯一 ID。在很多情况下,您可以考虑使用自动完成的输入字段。在我感兴趣的方案中,使用了自动完成功能来替换无尽的下拉列表。您的站点因此可实现轻型的必应样式的搜索,而无需借助于诸如 Lucene.Net 之类的全文引擎。

在视图中包含脚本代码

若要使用 typeahead.js,请引用 jQuery 1.9.1 或更高版本和 typeahead 脚本。在视图中您将需要极少量的代码,如下所示:

$('#queryString').typeahead(
  null,
  {
    displayKey: 'value',
    source: hints.ttAdapter()
  }
});

在此情况下,您采用所有默认设置,并指示引擎使用返回数据中的 value 属性填充下拉列表。名为 value 的属性假定存在于您筛选的任何数据中。从理论上讲,您可以在任何 JavaScript 数据数组上设置自动完成。然而,在实践中,从远程源下载数据时自动完成功能最有意义。

从远程源下载会带来很多问题:同源浏览器策略、预取和缓存(只列出了少数几个)等。Twitter typeahead.js 附带了一个名为 Bloodhound 的建议引擎。这个引擎以透明方式为您完成这项工作的大部分内容。如果从 NuGet 获取捆绑 JavaScript 文件,您可以立即开始调用 Bloodhound 而无需担心它的下载和安装。之前代码段中的提示变量源自以下代码和相当标准的 Bloodhound 初始化:

var hints = new Bloodhound({
  datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
  queryTokenizer: Bloodhound.tokenizers.whitespace,
  remote: "/hint/s?query=%QUERY"
});
hints.initialize();

请注意该 remote 属性。它只是负责返回提示以显示在下拉列表中的服务器终结点。另请注意语法 %QUERY。这表示要发送到服务器以用于提示的输入字段中的字符串。换言之,%QUERY 是输入字段中任何文本的占位符。默认情况下,只要您一键入单个字符,typeahead.js 就开始获取提示。如果您想要在自动完成功能启动之前等待要进入缓冲区的字符,可将设置对象添加为插件的第一个参数:

$('#queryString').typeahead(
  {
    minLength: 2
  },
  {
    displayKey: 'value',
    source: hints.ttAdapter()
  }
});

当缓冲区已满,足以启动远程调用时,Bloodhound 开始工作。它将下载 JSON 数据并对其进行调整以适于显示。这个时候,您就具有了一个几乎不能用的自动完成引擎(该引擎将根据服务器上的一些逻辑弹出建议)。不过,在您可以在实际页面中有效使用自动完成功能之前,还有很多事情需要做。

结合使用 Typeahead.js 和 Bootstrap

任何足够复杂的插件都需要相当数量的 CSS 代码以使其美观。Typeahead.js 也不例外。该插件附带有自己的默认 UI,但您可能希望应用一些修补程序,尤其是将其与 Twitter Bootstrap 结合使用时。您可能还希望自定义诸如颜色和填充等一些可视化属性。图 2 列出了一些您可能想要用于个性化 typeahead.js 组件外观的 CSS 类。

图 2 可用于编辑以自定义 Typeahead.js 组件的 CSS 类

CSS 类 说明
twitter-typeahead 设置用户在其中键入提示的输入字段样式。
tt-hint 设置表示键入内容和第一个提示之间增量的文本的样式。仅当提示属性设置为 true(默认为 false)时使用此类。
tt-dropdown-menu 设置其中列出提示的下拉弹出窗口的样式。
tt-cursor 设置下拉框中突出显示建议的样式。
tt-highlight 设置与查询字符串匹配的文本部分的样式。

图 3 便于您了解可从自定义 CSS 类获得的内容。您还可以从功能角度自定义整体插件行为。

自定义的 CSS 类可以为您的应用实现不同的效果
图 3 自定义的 CSS 类可以为您的应用实现不同的效果

添加客户端逻辑

自动完成字段要快于任何长下拉列表。如果可供从中进行选择的项目达到数百个,则任何经典下拉列表都会很慢。因此,如果您计划使用自动完成输入字段来选择特定的值(即产品名称或客户名),则纯 typeahead.js 插件是不够的。需要采用一些额外的脚本代码以绑定至插件的选定事件:

$('#queryString').on('typeahead:selected', 
  function (e, datum) {
  $("#queryCode").val(datum.id);
});

自动完成输入的最大好处是,用户在键入一些可识别的名称后,系统将检索关联的唯一代码或 ID。此功能必须进行显式编码。在选定的事件处理程序中,您将从基准对象检索 ID 信息并将其安全地存储在隐藏字段中。当自动完成输入字段所属表单进行发布时,选定 ID 也将进行发布。基准对象(从下拉列表选取的数据项)的格式取决于从服务器端接收的数据格式。

输入字段中要显示的文本呢?在此方案中,您可能无需在输入字段中显示任何重要文本。进一步操作的相关输入是您在隐藏字段中存储的内容。显示内容由您决定。只是请注意,如果您在插件设置中指定了 displayKey 属性,则该属性的值将自动显示在输入字段中。无论如何,您都可以通过编程方式设置任何值:

$("#queryString").val(datum.label);

在某些情况下,自动完成文本框是 HTML 表单中的唯一元素。这意味着您可能希望在选定数据后可以立即开始处理。通过将以下行添加到 typeahead.js 选定事件处理程序,您将模拟在表单上单击提交按钮:

$("#queryButton").click();

假设用户开始在输入字段中键入内容,导致下拉列表显示,然后停止键入而不做任何选择。在继续进行键入时,您该怎么办?正常情况下,您会让他再次键入直到他进行了选择。一旦用户进行了选择,一些代码就会进行存储,因此在编辑恢复后必须取消所选内容。您需要一个本地变量来实现此目的:

var typeaheadItemSelected = false;
$('#queryString').on('typeahead:selected', function (e, datum) {
  $("#queryCode").val(datum.id);
  typeaheadItemSelected = true;
});

您还需要针对输入字段的焦点事件配备一个处理程序以重置任何存储的数据:

$('#queryString').on('input', function () {
  if (typeaheadItemSelected) {
    typeaheadItemSelected = false;
    $('#queryString').val(''); 
    $("#queryCode").val('');
  }
});

这一额外客户端逻辑的最终目的是确保自动完成输入字段与下拉列表起到相同的作用。

自动完成的服务器端

客户端上所有代码可以执行的操作完全依赖于从服务器返回的数据。至少,服务器终结点只是一个返回 JSON 数据的 URL。例如,一旦有了一个提供 Product 对象集合的终结点,您就可以使用 typeahead.js 的 displayKey 属性在提示的下拉列表上执行某种类型的数据绑定。在其最简单的形式中,JSON 返回控制器方法可能如下所示:

public JsonResult P(string query)
{
  var productHints = _service.GetMatchingProducts(query);
  return Json(productHints, JsonRequestBehavior.AllowGet);
}

如果希望自动完成输入字段显示同类数据项的提示,这是理想的方法。事实上,在客户端上,您可以轻松地利用内置于 typeahead.js 的模板化机制和排列自定义建议视图:

$('#queryString').typeahead(
null,
{
  templates: {
    suggestion: Handlebars.compile('<b>({{Id}}</b>: {{notes}}')
  },
  source: hints.ttAdapter()
});

templates 属性取代了 displayKey 并为下拉列表内容设置了自定义布局。图 3 中的下拉列表源自之前的代码段。排列模板时,您可能想要使用诸如 Handlebars (handlebarsjs.com) 之类的即席模板引擎。您必须独立于 typeahead.js 将 Handlebars 关联到项目。使用 Handlebars 是可选的。您始终可以通过手动 JavaScript 代码设置 HTML 模板的格式,或甚至返回具有预先设置好格式的 HTML 的服务器端对象。

如果希望自动完成输入返回异类提示(例如产品或服务),则事情会复杂一些。在这种情况下,您必须返回某个中间数据类型的数组,其中包含的信息足够用户进行选择。您通过本文获取的自动完成功能提供了一个基本的 AutoCompleteItem 类,如下所示:

public class AutoCompleteItem
{
  public String label { get; set; }
  public String id { get; set; }
  public String value { get; set; }
}

Id 属性包含一个唯一 ID。当发布时,这对于接收控制器意义非凡。它通常由两个部分组成 — 实际 ID 和将此 ID 与可能要在查询中返回的数据集(产品或服务)之一进行唯一匹配的标识符。value 属性是要在下拉列表中显示的字符串内容。另一个属性是您可能需要的其他任何事物的某种货物属性。Value 属性还可以包含用于自定义建议布局的服务器端排列的 HTML 字符串。服务器端代码还负责运行所需的所有查询以及将数据添加到 AutoCompleteItem 对象的集合。

总结

在现代网站中,每天最重要事的就是易用性。这点在面向公众的站点中很受欢迎,但对于在其中插入实际信息的网站后端更为关键。在我的网站之一的管理端中,我曾有一次遇到了包含 700 多个项的下拉列表。这个下拉列表可以使用,但是速度太慢了。我用自动完成功能进行了替换,现在它快多了。我在 bit.ly/1zubJea 中为它和其他实用工具安排了一个 GitHub 项目。


Dino Esposito 是《Microsoft .NET:Architecting Applications for the Enterprise》(Microsoft Press,2014 年)和《Programming ASP.NET MVC 5》(Microsoft Press,2014 年)的合著者。作为 JetBrains 的 Microsoft .NET Framework 和 Android 平台的技术推广人员,Esposito 经常在全球行业活动中发表演讲,并在 software2cents.wordpress.com 上以及 twitter.com/despos 上的推文中分享他对于软件的愿景。

衷心感谢以下技术专家对本文的审阅:Jon Arne Saeteras