这个博客没有后端,却能站内搜索。静态站既没有数据库也没有服务端接口,搜索这件「天生需要查询」的事,是怎么做到的?答案是:把索引在构建期生成好,查询全放到浏览器端做。

思路:索引前置

动态站的搜索是「用户输入 → 后端查库 → 返回结果」。静态站没有后端,于是换个思路:在生成网站的时候,把所有文章的标题、正文、链接打包成一个 JSON 文件(本站用 hexo-generator-searchdb 生成 search.json),随站点一起部署。浏览器加载这个 JSON,搜索就在前端内存里完成。

1
2
3
4
[
{ "title": "PJAX 无刷新导航", "url": "/2026/05/12/pjax/", "content": "这个博客点击菜单..." },
{ "title": "HTTP 缓存全解", "url": "/2026/04/16/http-cache/", "content": "为什么改了 CSS..." }
]

最朴素的实现

数据量不大时,直接 includes 子串匹配就够用:

1
2
3
4
5
6
7
8
9
10
11
let data = null;
async function load() {
data = await fetch('/search.json').then(r => r.json());
}
function search(q) {
q = q.trim().toLowerCase();
if (!q) return [];
return data
.filter(p => (p.title + ' ' + p.content).toLowerCase().includes(q))
.slice(0, 10);
}

本站正是这么做的:弹出搜索框时才懒加载 search.json,输入时实时过滤,取前 10 条。几十上百篇文章的规模,这种暴力匹配在现代浏览器里毫秒级完成,体验完全够。

几个体验细节

  • 懒加载索引:别在首页就下载 search.json,等用户真的打开搜索框再 fetch,省首屏流量。
  • 防抖:输入事件触发很频繁,高频场景可以加 debounce 限流;但纯内存子串匹配其实很快,可不加。
  • 结果高亮:把命中的关键词用 <mark> 包起来,结果一目了然。
  • 截断摘要:正文很长,结果里只展示命中位置附近的一小段。

什么时候需要更专业的方案

子串匹配有两个短板:一是不分词(搜「前端性能」不会命中「前端的性能」),二是数据量大时 JSON 体积和遍历成本都上来了。这时可以升级:

  • 倒排索引:预先把「词 → 出现在哪些文档」的映射建好,查询时查词表而非遍历全文。Fuse.jsLunr.jsFlexSearch 都是前端可用的库。
  • Pagefind:专为静态站设计,构建期分片建索引,查询时按需下载分片,万篇文章也不卡。
  • Algolia:托管搜索服务,免费额度够个人博客用,但引入了外部依赖。

小结

静态站搜索的精髓是「把索引提前到构建期,把查询下放到客户端」。小博客用一个 search.json + 子串匹配就够优雅;规模大了再上倒排索引或 Pagefind。无后端不等于没搜索,只是把计算挪了个位置。