起因
这个博客用 WordPress 搭的,主题是一款经典主题(Arke)。文章还不算多,但想找一篇旧文越来越费劲:原生 ?s= 搜索只做标题和内容的模糊匹配,不支持按分类缩小范围,没有关键词高亮,主题本身也没给搜索一个像样的入口。于是给自己提了个需求:
给博客加一个文章检索功能:能按标题、内容、分类搜,界面干净美观,别给站点增加负担和依赖。
方案选型
老规矩,先摆选项再动手:
| 方案 | 思路 | 评估 |
|---|---|---|
| A · 换主题 / 装搜索插件 | 用自带现代搜索的主题,或上 SearchWP 之类插件 | 见效最快,但要动整站外观;好用的搜索插件按年收费,为一个功能不值 |
| B · 前端轻量定制 + REST API | 主题不动,注入一段全站前端脚本,数据走 WordPress 自带的 REST API | 改动小、可随时摘除、体验完全自主可控,最终选用 |
| C · 独立全文搜索引擎 | Meilisearch / Algolia 建索引 | 对个人博客的量级是杀鸡用牛刀,运维成本不划算 |
选 B,内部代号 YSS Search。核心思路一句话:WordPress 只当数据源(REST API 本来就在),所有检索体验放在前端完成。
功能设计
两个入口,一套体验
- 导航搜索胶囊:往导航栏注入一枚白底圆角胶囊——放大镜 + 「搜索…」占位文字 +
Ctrl K键位帽,窄屏下自动收缩成纯图标; - 首页内联过滤框:首页文章列表上方再放一个过滤框,输入即筛,适合「就在首页扫一眼」的场景。
检索链路
- 输入 250ms 防抖后请求
/wp-json/wp/v2/posts?search=…&categories=…,REST 的 search 参数本身就覆盖标题 + 内容; - 客户端按权重重排:标题命中 ×3、摘要 ×2、分类 ×2、正文 ×1,比 REST 默认的相关性排序更贴合直觉;分类名也并入匹配池,所以搜「网络技术」能直接命中该分类的文章;
- 面板顶部分类 chips 从
/wp-json/wp/v2/categories动态拉取,点一下就带着分类参数重新检索; - 命中词
<mark>高亮;支持相关性 / 最新两种排序,↑↓ 选择、Enter 打开、Esc 关闭; - 降级兜底:REST 不可用时自动跳转原生
?s=搜索——功能可以变笨,但不能坏。
Ctrl+K 或 / 随时呼出面板,移动端自动切全屏。整个功能是一个自包含的 HTML 片段(CSS + JS 约 500 行),不依赖任何外部库。部署的现实:两个「没想到」
没想到一:搜索插件没有 REST API
代码注入用的是 WPCode 这款插件,本想全程用 REST API 自动化部署,结果发现 WPCode Lite 的 REST 端点全是 404——它自定义的内容类型压根没对 REST 开放。能用的只有它自带的「页头/页脚编辑器」表单。
于是把部署流程固化成一个幂等脚本,本质是模拟浏览器走表单:
- 登录 wp-admin(浏览器自动化完成,会话导出供脚本复用);
- GET 编辑器页面,取表单 nonce 和三个输入框的现值;
- 本地把新代码与旧内容做幂等合并(只替换自己的标记块,不碰别人注入的内容);
- POST 保存 → 回读校验 → 再抓线上页面验证。
没想到二:SVG 图标渲染成小黑点
上线第一版后,导航角落的搜索图标缩成了一个小黑点。原因很初学:手绘的放大镜 SVG 只有 circle 和 path,没写 fill="none" 和 stroke 样式——浏览器默认填充黑色,15px 尺寸下线稿直接糊成一坨。教训:手写 SVG 图标,填充和描边必须显式声明。
事故主角:$& 替换事故
这次最精彩的坑,值得单独一节。
部署脚本更新代码的方式最初是「删旧块、插新块」,用了正则替换:
const merged = oldContent.replace(
/<!-- MARK-START -->[\s\S]*?<!-- MARK-END -->/,
newCode // ← 问题在这里
);
而新代码里恰好有一段正则转义的工具函数:
function escReg(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
'\\$&' 是 JS 正则转义的惯用写法(把特殊字符替换成「反斜杠 + 自身」)。但当整段代码作为 replace 的替换字符串时,$& 是特殊序列,含义是「插入整个匹配」——于是旧版代码被原样插回了新代码内部,形成 46KB 的「块中块」,页面里躺着两份搜索脚本。
第一次修复,留了个尾巴
改成「成对标记删除」:找到开始标记,删到其后最近的结束标记。重新部署后体积仍有 40KB——比预期多了 15.7KB,直觉不对。
挖下去才发现:旧块当初嵌在新代码内部,成对删除删到的是旧块的结束标记,新代码的后半截被留下了——一段孤立的 </script> 加 15.7KB 裸 JS 文本。这些文本落在 <head> 里,浏览器会提前结束 head、隐式打开 body,把 JS 代码当正文渲染出来。
最终方案
// a) 循环删除所有成对标记块
while ((i = s.indexOf(START)) !== -1) {
const j = s.indexOf(END, i);
s = j > -1 ? s.slice(0, i) + s.slice(j + END.length) : s.slice(0, i);
}
// b) 兜底删除「无头残尾」:只有结束标记、没有开始标记的残留
while ((j = s.indexOf(END)) !== -1) {
s = s.slice(j + END.length);
}
// c) 三处硬校验:合并结果、表单回读、线上 HTML,
// 开始/结束标记都要「各恰好 1 份」——只查开始标记检不出残尾
三条教训
- 把代码文本传给
String.replace第二个参数之前,先想想里面有没有$&、$1、$`这些特殊序列。要么用函数形式() => code,要么用indexOf手动拼接; - 幂等脚本的完整性校验,开合标记都要断言数量,最好再配体积、行数这类硬指标;
- 修完 bug 用「总量守恒」复核:删了 A、加了 B,最终应该恰好是 B,而不是「A 的一部分 + B」。
最终效果
现在导航栏是一枚搜索胶囊,点开或 Ctrl+K 呼出全局面板:搜索框、分类 chips、排序切换、高亮列表、键盘导航一应俱全;首页文章列表上方有内联过滤框,输入即筛。整个功能零外部依赖、零后端改动,随时可以整块摘除。
回头再可以打磨的方向:把搜索词带进文章页做落地高亮、热搜词统计、拼音首字母匹配。不过对目前的文章量来说,先把「找得到」这件事做扎实就够了。
小结
这次改造最大的收获不是搜索功能本身,而是三条工程习惯:方案上,能不动后端就不动后端,把 WordPress 当数据源用;实现上,外部依赖越少越好,一个自包含片段胜过一串插件;部署上,自动化脚本必须幂等,而幂等的可信度来自严格的机器校验,不是「应该没问题」。