Gregor Weber和我为MDN Web Docs增加了一个自动完成搜索功能,它允许你通过输入文档标题的部分内容来快速直接跳转到你要找的文档。这是关于如何实现这一功能的故事。如果你坚持到最后,我将分享一个 "复活节彩蛋 "功能,一旦你学会了它,将使你在晚宴上显得非常酷。或者,也许你只是想比凡人更快地浏览MDN。

在其最简单的形式中,输入字段有一个onkeypress 事件监听器,可以过滤每一个文档标题的完整列表(每个地区)。在写这篇文章的时候,有11,690个不同的文档标题(和它们的URL),适用于英语美国。你可以通过打开developer.mozilla.org/en-US/searc…,看到一个预览。是的,这很庞大,但要全部加载到内存中还不算太庞大。毕竟,与进行搜索的代码一起,只有在用户表示要输入东西时才会加载。而说到大小,因为文件是用Brotli压缩的,所以文件在网络上只有144KB。
实施细节
默认情况下,唯一被加载的JavaScript代码是一个小的垫片,用于观察onmouseover 和onfocus ,用于搜索<input> 字段。在整个document ,也有一个事件监听器,用来寻找某个按键。在任何时候按下/ ,就像你用鼠标光标把焦点放到<input> 领域一样。一旦焦点被触发,它做的第一件事就是下载两个JavaScript包,将<input> 字段变成更高级的东西。在其最简单的(伪)形式下,它是如何工作的。
<input
type="search"
name="q"
onfocus="startAutocomplete()"
onmouseover="startAutocomplete()"
placeholder="Site search..."
value="q">
let started = false;
function startAutocomplete() {
if (started) {
return false;
}
const script = document.createElement("script");
script.src = "/static/js/autocomplete.js";
document.head.appendChild(script);
}
然后,它加载/static/js/autocomplete.js ,这是真正神奇的地方。让我们更深入地挖掘这个伪代码。
(async function() {
const response = await fetch('/en-US/search-index.json');
const documents = await response.json();
const inputValue = document.querySelector(
'input[type="search"]'
).value;
const flex = FlexSearch.create();
documents.forEach(({ title }, i) => {
flex.add(i, title);
});
const indexResults = flex.search(inputValue);
const foundDocuments = indexResults.map((index) => documents[index]);
displayFoundDocuments(foundDocuments.slice(0, 10));
})();
正如你可能看到的,这是对其实际工作方式的过度简化,但现在还不是挖掘细节的时候。下一步是显示比赛结果。我们使用(TypeScript)React来做这个,但下面的伪代码更容易理解。
function displayFoundResults(documents) {
const container = document.createElement("ul");
documents.forEach(({url, title}) => {
const row = document.createElement("li");
const link = document.createElement("a");
link.href = url;
link.textContent = title;
row.appendChild(link);
container.appendChild(row);
});
document.querySelector('#search').appendChild(container);
}
然后用一些CSS,我们只是将其作为一个覆盖物显示在<input> 字段的下面。例如,我们根据inputValue ,突出显示每个title ,当你向上和向下导航时,各种击键事件处理程序负责突出显示相关行。
好的,让我们更深入地了解实施细节
我们只创建一次 FlexSearch 索引,并在每次新的击键时重新使用它。因为用户在等待网络时可能会输入更多的信息,所以它实际上是被动的,所以一旦所有的JavaScript和JSON XHR到达,就会执行实际的搜索。
在我们探讨这个FlexSearch 是什么之前,让我们谈谈显示的实际工作方式。为此,我们使用了一个叫做downshift的React库,它处理所有的交互、显示,并确保显示的搜索结果是可访问的。downshift 是一个成熟的库,它处理了建立这样一个小部件的无数挑战,特别是使其可访问的方面。
那么,这个FlexSearch 库是什么?它是另一个第三方,确保在标题上的搜索是以自然语言为基础的。它将自己描述为 "网络上速度最快、内存最灵活、零依赖的全文搜索库。"这比试图在一长串其他字符串中简单地寻找一个字符串要好得多,也准确得多。
决定先显示哪个结果
平心而论,如果用户输入foreac ,要把一万多个文档标题的列表减少到只包含foreac ,并不难,然后我们决定先显示哪个结果。我们实现这一目标的方式是依靠页面浏览量统计。我们记录每一个MDN网址,哪一个得到最多的页面浏览量,作为确定 "流行 "的一种形式。大多数人决定到达的文件很可能是用户正在搜索的内容。
我们生成search-index.json 文件的构建过程知道每个URLs的页面浏览数。我们实际上并不关心绝对数字,但我们关心的是相对差异。例如,我们知道 Array.prototype.forEach()(这是文件标题之一)是一个更受欢迎的页面,而不是 TypedArray.prototype.forEach(),所以我们利用这一点,对search-index.json 中的条目进行相应的排序。现在,有了FlexSearch ,我们使用数组的 "自然顺序 "作为技巧,试图给用户提供他们可能正在寻找的文件。这实际上与我们在全站搜索中对Elasticsearch 使用的技术相同。更多信息请见:MDN的网站搜索如何工作。
复活节彩蛋。如何通过URL搜索
实际上,这不是一个异想天开的复活节彩蛋,而是一个来自于这个自动完成功能需要为我们的内容创作者工作的事实。你看,当你在MDN的内容上工作时,你会启动一个本地的 "预览服务器",这是一个所有文件的完整副本,但都在本地运行,作为一个静态网站,在http://localhost:5000 。在那里,你不想依赖服务器来进行搜索。内容作者需要在文件之间快速移动,所以自动完成搜索完全是在客户端进行的,大部分原因就是如此。
在VSCode和Atom IDEs这样的工具中,你可以进行 "模糊搜索",只需输入文件路径的一部分就可以找到并打开文件。例如,搜索whmlemvo ,可以找到文件 files/**w**eb/**h**t**ml**/**e**lement/**v**ide**o**.你也可以用MDN的自动完成搜索来做这个。你的方法是将/ 作为第一个输入字符。

如果你知道一个文件的URL,但又不想准确地拼出它,它可以使你快速地直接跳到该文件。
事实上,还有另一种导航方式,那就是在浏览MDN时,先在任何地方按/ ,这就激活了自动完成搜索功能。然后你再输入/ ,你就可以去找了!
如何真正深入了解实施细节
所有这些的代码都在Yari repo中,这是一个构建和预览所有MDN内容的项目。要找到准确的代码,点击进入 client/src/search.tsx源代码,你会发现所有用于懒惰加载、搜索、预加载和显示自动完成搜索的代码。
The postMDN's autocomplete search how worksappeared first onMozilla Hacks - the Web developer blog.