前言
之前通过建立静态端点实现了自己的 RSS Feed 算是一个简单的练习起步,这次更进一步通过建立整个集合的数据来完成「集合文章搜索功能」。
定义问题
随着文章越来越多会需要一个查询文章的功能,通过用户输入文章相关的信息,比对文章信息(标题、文章短描述)并显示出最相关的文章。通过以上描述统整出会需要以下几个要点:
- 用户可以输入查询的界面
- 获取当前集合数据的方式
- 可以模糊比对输入内容与当前所有数据的系统
- 显示查询结果的界面
实作
统整下来,以上的问题会需要较为频繁地操作 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 文件,这里以我的博客为例,内容包含了文章的:链接、标题、繁中标题、短介绍、主题色。
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 本身,因此有些描述会省略掉,可以了解其中大概念即可。
延伸阅读
- Content Collections - Astro DOCS
- Dynamic Routes - Astro DOCS
- Day20 - 實作搜尋功能 - 相同文章同步發布於 iThome 鐵人賽中