✨Kevin‘s Blog🪐

为 Astro 博客添加离线搜索功能

📅发布日期:7/14/2025

第 8 课:为 Astro 博客添加离线搜索功能,我们将一步步构建一个不依赖后端、支持关键词模糊匹配的搜索系统,基于构建阶段生成的 JSON 索引 + 前端加载 + Fuse.js 实现。


🎯 课程目标

你将学到:


🧱 第一步:构建时生成索引文件

Astro 的 src/content/blog/*.md 数据可以在构建阶段统一导出并保存为 JSON 供前端使用。

新建 src/scripts/generateSearchIndex.ts

// src/scripts/generateSearchIndex.ts
import fs from 'fs';
import path from 'path';
import { getCollection } from 'astro:content';

async function generateIndex() {
  const posts = await getCollection('blog');

  const index = posts.map((post) => ({
    slug: `/blog/${post.slug}/`,
    title: post.data.title,
    description: post.data.description,
    body: post.body, // 原始 markdown
    tags: post.data.tags || [],
  }));

  const outputPath = path.resolve('./public/search-index.json');
  fs.writeFileSync(outputPath, JSON.stringify(index, null, 2));
  console.log('✅ 搜索索引已生成');
}

generateIndex();

⏱️ 添加构建钩子

修改你的 package.json

{
  "scripts": {
    "dev": "astro dev",
    "build": "astro build && tsx src/scripts/generateSearchIndex.ts",
    "preview": "astro preview"
  }
}

现在,每次构建后,都会自动生成 /public/search-index.json


🧪 第二步:加载索引并搜索

新建 src/components/SearchBox.astro

---
import { useEffect, useState } from 'react';
import Fuse from 'fuse.js';

const [query, setQuery] = useState('');
const [results, setResults] = useState([]);

useEffect(() => {
  let fuse;
  fetch('/search-index.json')
    .then(res => res.json())
    .then(data => {
      fuse = new Fuse(data, {
        keys: ['title', 'description', 'body'],
        threshold: 0.3,
      });
    });

  const handleSearch = () => {
    if (fuse && query) {
      setResults(fuse.search(query).map(result => result.item));
    } else {
      setResults([]);
    }
  };

  const timeout = setTimeout(handleSearch, 300);
  return () => clearTimeout(timeout);
}, [query]);
---

<div class="search-box">
  <input
    type="text"
    placeholder="搜索文章…"
    value={query}
    onInput={(e) => setQuery(e.currentTarget.value)}
  />

  <ul>
    {results.map((post) => (
      <li>
        <a href={post.slug}>{post.title}</a>
      </li>
    ))}
  </ul>
</div>

📦 安装依赖

npm install fuse.js

🎨 第三步:样式与优化

你可以在 global.css 中添加样式:

.search-box {
  margin: 2rem 0;
}

.search-box input {
  width: 100%;
  padding: 0.5rem;
  font-size: 1rem;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.search-box ul {
  list-style: none;
  padding-left: 0;
  margin-top: 1rem;
}

.search-box li {
  margin-bottom: 0.5rem;
}

🧠 原理讲解

阶段原理
构建阶段getCollection() 获取所有博客元数据和正文内容,写入静态 JSON 文件
加载阶段前端页面加载 JSON 数据到内存,使用 Fuse.js 创建搜索引擎实例
搜索阶段每次输入关键字后,运行 fuse.search() 返回匹配项,并渲染结果列表

✅ 小结

离线搜索 = 内容结构化 + 本地索引加载 + 前端模糊匹配。

相比 Algolia 等服务端方案,它: