Day20 - Astro Series: Search Functionality

Astro 系列文第二十日:实现搜索功能

一个漂亮的渐变背景上面有一句标题:「实现搜索功能」

前言

之前通过建立静态端点实现了自己的 RSS Feed 算是一个简单的练习起步,这次更进一步通过建立整个集合的数据来完成「集合文章搜索功能」。

定义问题

随着文章越来越多会需要一个查询文章的功能,通过用户输入文章相关的信息,比对文章信息(标题、文章短描述)并显示出最相关的文章。通过以上描述统整出会需要以下几个要点:

  1. 用户可以输入查询的界面
  2. 获取当前集合数据的方式
  3. 可以模糊比对输入内容与当前所有数据的系统
  4. 显示查询结果的界面

实作

统整下来,以上的问题会需要较为频繁地操作 DOM 且有一定的复杂程度,因此会引入框架来协助处理数据与界面更新的问题,避免纠结在一些底层的 DOM 操作上面,这里我使用 Preact(类似轻量化的 React),可以选择自己习惯的框架即可。

第一步:制作输入查询的界面

创建一个 jsx 组件,制作一个 <input>,只在用户第一次 Focus 的时候抓取要比对的数据,并且切换 isLoading 的状态。

function getPosts() {
setIsLoading(true);
return fetch('/search.json')
.then((res) => {
if (!res.ok) {
throw new Error('Network response was not ok');
}
return res.json();
})
.catch((err) => console.error(err))
.finally(setIsLoading(false));
}
async function handleInputFocus() {
setIsSearching(true);
if (!posts.length) {
setPosts(await getPosts());
}
}

并且在 Blur 的时候,如果目标在搜索组件之外就将 isSearching 的状态切换为否。

function handleInputBlur(e) {
const { currentTarget } = e;
// 給瀏覽器時間去 Focus 下一個元素
requestAnimationFrame(() => {
if (!currentTarget.contains(document.activeElement)) {
setIsSearching(false);
}
});
}

由于在手机尺寸并不会显示这个区块的功能,因此可以设定只有在设备尺寸 1024px 以上才加载相关的客户端 JavaScript,达成「有用到才加载」,节省不必要的浪费。

<Search client:media="(min-width: 1024px)" />

第二步:制作数据端点与索取数据

由于前面 getPosts 会期望网站的根部有 search.json 这样一个文件,因此需要制作一个端点并产生一个要搜索数据的 JSON 文件,这里以我的博客为例,内容包含了文章的:链接、标题、繁中标题、短介绍、主题色。

search.json.ts
import { getCollection } from 'astro:content';
async function getPosts() {
const avaliablePosts = await getCollection('post', (post) => (import.meta.env.PROD ? !post.data.isDraft : true));
return avaliablePosts.map((post) => ({
slug: post.slug,
title: post.data.title,
headline: post.data.titleTC,
excerpt: post.data.excerpt,
themeColor: post.data.themeColor,
}));
}
export async function GET() {
return new Response(JSON.stringify(await getPosts()), {
status: 200,
headers: {
'content-type': 'application/json',
},
});
}

第三步:模糊搜索比对结果

每当用户输入时如果 fuse 实例并没有被创建过,就创建一次到 fuseInstance 当中,并且通过给 fuse 实例的搜索方法传入「目前查询的词汇」可以得到模糊比对出来最贴近用户搜索的文章。

let fuseInstance;
function handleInputChange(e) {
if (!fuseInstance) {
fuseInstance = new Fuse(posts, {
includeScore: true,
shouldSort: true,
threshold: 0.5,
keys: [
{
name: 'titleTC',
weight: 1,
},
{
name: 'title',
weight: 1,
},
],
});
}
setResults(fuseInstance.search(e.target.value));
}

第四步:将结果展示到界面当中

基本上你只需要把 results 这笔数据展示到界面上即可。

总结

由于我有使用到 Preact🔗Fuse.js🔗,但还是希望这篇文章能够尽量着重在 Astro 本身,因此有些描述会省略掉,可以了解其中大概念即可。

延伸阅读