为什么不用插件
WordPress 自带的搜索太弱,搜「cron」能出来一堆不相关的东西。装过几个搜索插件,要么后台臃肿,要么把数据库翻个底朝天。有天晚上我盯着那个搜索框想,站点总共几百篇文章,全部标题和摘要加起来也就几百 KB,为什么不能直接在前端搜?
于是决定自己写一个。不装插件,不调外部 API,纯前端实现。目标是:输入关键词,立刻出结果,不刷新页面。
先把数据摊开
第一步是让 WordPress 吐出一份干净的索引。我在主题里加了一个小函数,用 wp_localize_script 把文章数据传给前端。只取需要的字段:标题、链接、摘要、日期、分类。摘要截前 120 个字,去掉 HTML 标签。
function quq_search_index() {
$posts = get_posts(['numberposts' => -1, 'post_status' => 'publish']);
$index = [];
foreach ($posts as $p) {
$index[] = [
'title' => get_the_title($p),
'url' => get_permalink($p),
'excerpt' => wp_trim_words(strip_tags($p->post_content), 40),
'date' => get_the_date('Y-m-d', $p),
];
}
wp_localize_script('quq-search', 'QUQ_INDEX', $index);
}
add_action('wp_enqueue_scripts', 'quq_search_index');
这样每个页面加载时都会带上一份 JSON。几百篇文章大概 200KB 左右,gzip 后更小,可以接受。如果文章上千,就得考虑分页加载或者用 fetch 按需拉取,但我暂时没这个烦恼。
前端搜索逻辑
搜索框就是一个 input,监听 input 事件,拿到关键词后遍历索引。匹配规则很简单:标题命中权重高,摘要命中权重低。用 indexOf 就够,不需要正则。
function search(query) {
const q = query.toLowerCase().trim();
if (!q) return [];
return QUQ_INDEX
.map(item => {
let score = 0;
if (item.title.toLowerCase().includes(q)) score += 10;
if (item.excerpt.toLowerCase().includes(q)) score += 3;
return { ...item, score };
})
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 10);
}
结果渲染成列表,插入到搜索框下方。点击结果跳转。整个过程没有网络请求,输入即响应。
几个小坑
第一个坑是中文分词。indexOf 对英文单词友好,但中文用户可能输入「定时任务」而文章里写的是「定时 任务」中间有空格,就搜不到。我的处理很简单:把索引里的空格和标点都去掉,查询词也去掉,再做匹配。牺牲一点精确性,换来更高的召回率。
第二个坑是性能。每次输入都遍历整个数组,几百条没问题,但输入快的时候会卡。加了个 150ms 的防抖,体验就顺了。
第三个坑是样式。搜索框默认是浏览器样式,和站点风格不搭。我直接用 CSS 重写,边框、圆角、聚焦阴影,几行搞定。结果列表用绝对定位浮在内容上方,加个半透明背景,点击外部关闭。
够用就好
现在这个搜索跑了一个多月,没出过问题。搜「cron」能直接找到那篇定时任务的文章,搜「迁移」能列出搬家记录和旧路由器那篇。它不智能,不支持模糊匹配,不会拼写纠正,但它快、简单、完全可控。
有时候做东西就是这样:不用等一个完美的方案,先把手边能用的零件拼起来,跑通了再想下一步。这个搜索以后可能会加个「按分类过滤」或者「高亮关键词」,但那是以后的事。现在它已经比我预想的好用了。


