使用Pagefind为VitePress文档添加离线全文搜索能力
前言
VitePress 相信大家都或多或少听说过或者用过了
默认 UI相比 VuePress2.x 好看,启动速度也快(由Vite驱动,当然VuePress也可以切换构建引擎至Vite)
做内容定制也相对简单,笔者的很多静态文档站点(使用VuePress1.x),文章内容多的时候启动非常的慢,于是就从之前的 VuePress 迁移到了 VitePress,并做了一个博客主题 @sugarat/theme => 之前也有过介绍一个简约风的VitePress博客主题
但是 VitePress 官方目前还没有内置开箱即用的搜索能力(相关PR还在施工中)
文档里首推使用 Algolia DocSearch, 这个需要申请,流程相对较慢,公司内网文档也无法接入使用。
推荐的另一个方案是使用 vitepress-plugin-search 基于 flexsearch 实现,但是默认的UI较丑(与 Algolia 的UI差距较大),对中文没有提供开箱即用的支持,需要进行一定的配置
目前常用的除了 flexsearch
,还有 MiniSearch
笔者之前在逛GitHub的时候还发现了一个 Pagefind(基于Rust实现,检索生成后的HTML页面内容,然后自动构建索引文件)
感觉挺有意思的,就研究使用了一番,然后将其做成了一个可直接使用的 VItePress 插件(也就是本文将介绍的)
本文主要演示下 接入步骤和效果,再简单介绍Pagefind
,最后讲解一下插件原理
接入后效果
只需要2步即可完成接入
① 安装 vitepress-plugin-pagefind
npm i vitepress-plugin-pagefind
# or
yarn add vitepress-plugin-pagefind
# or
pnpm add vitepress-plugin-pagefind
② 在.vitepress/config.ts
引入使用
import { defineConfig } from 'vitepress'
import { pagefindPlugin } from 'vitepress-plugin-pagefind'
export default defineConfig({
vite:{
plugins:[pagefindPlugin()],
}
})
UI如下(power by vue-command-palette)
搜索按钮 | 搜索框 |
---|---|
Pagefind介绍
Pagefind
是一个完全静态的搜索库,旨在在大型网站上表现良好,同时尽可能地减少用户带宽的使用,且不需要进行任何基础设施托管。
Pagefind
在Hugo,Eleventy,Jekyll,Next,Astro,SvelteKit或任何其他SSG之后运行。安装过程始终相同:Pagefind
仅需要一个包含构建的静态文件的文件夹,因此在大多数情况下,无需进行配置即可开始使用。
索引后,
Pagefind
会向您的构建文件添加静态搜索包,该包公开了可以在站点任何位置使用的JavaScript
搜索API。Pagefind
还提供了一个可无需配置即可使用的预构建UI
总结:框架无关,直接解析静态站点的产物,然后生成索引文件,提供开箱即用的API和组件,支持开箱即用的多语言站点,零配置
生成内容索引
可直接通过npx调用,指定构建后的产物目录即可
以 vitepress 默认产物目录为例
npx pagefind --source docs/.vitepress/dist
一般毫秒级就完成了页面内容的分析与pagefind需要的资源转换
默认会自动扫描指定目录下所有的html
资源,将带有data-pagefind-body
属性的元素作为索引的位置,否则的话使用<body>
作为索引位置
会自动识别 html
中的 lang
属性,使用对应的分词策略处理,目前已经内置多种语言支持
生成的相关文件默认在_pagefind
目录中,内容如下
使用内置搜索UI
在生成索引的过程中,pagefind也会把内置的搜索框UI相关资源放入其中
在页面中只需要导入相应的js/css
资源即可
<link href="/_pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/_pagefind/pagefind-ui.js" type="text/javascript"></script>
<div id="search"></div>
<script>
window.addEventListener('DOMContentLoaded', (event) => {
new PagefindUI({ element: "#search" });
});
</script>
搜索框样式如下
使用JS API
默认的搜索框样式不满足的话可以,自定义搜索框逻辑,通过JS API调用搜索能力
// 1. Initializing Pagefind
const pagefind = await import("/_pagefind/pagefind.js");
// 2. search docs
const search = await pagefind.search("hello");
// 3. Loading a result
const oneResult = await search.results[0].data();
搜索结果格式如下
interface SearchResult {
url: string;
excerpt: string;
filters: Record<string,any>;
meta: Record<string,any>;
content: string;
word_count: number;
}
一些不足
- 仅针对构建后的产物进行索引,开发环境下无法工作
- 对中文和日语等支持相对英语会差一点(区别见下截图)
- 不支跳转至标题
插件实现原理解析
这部分主要介绍 vitepress-plugin-pagefind
的关键实现部分(细节可看源码,如读者有更好的实现思路可以评论区交流)
几个关键步骤:
- 替换默认搜索组件
- 目标元素上插入检索的标识
data-pagefind-body
- 插入运行时的
script
脚本
替换默认搜索组件
通过查看默认布局组件Layout.vue
源码其中搜索组件是被VPNavBarSearch.vue
引入
咱只需要通过插件添加一个 alias
规则,将其指向自定义的组件即可
这个使用插件的 config
钩子即可
export function pagefindPlugin() {
return {
name: 'vitepress-plugin-pagefind',
enforce: 'pre',
config: () => ({
resolve: {
alias: {
'./VPNavBarSearch.vue': 'vitepress-plugin-pagefind/Search.vue'
}
}
}),
}
}
添加索引标识
由于vitepress的默认 body
元素中包含 navBar,footer,sidebar等等等内容
默认情况下每个页面的代码中都会包含这些公共内容,因此会导致检索出的内容有很多重复信息
理论上只需要检索用户编写的 markdown内容生成的部分
也就是VPContent.vue
组件渲染的内容(当然里面包含了3种情况VPHome
,VPPage
,VPDoc
这里可以不做区分)
这个通过插件的transform
钩子处理一下,构建时替换源码中VPContent
的内容
export function pagefindPlugin() {
return {
// ... other code
transform(code, id) {
if (id.endsWith('theme-default/Layout.vue')) {
return code.replace('<VPContent>', '<VPContent data-pagefind-body>')
}
return code
}
}
}
运行时的脚本注入
因为相关资源是在Build之后才会生成,所以直接在源码中 import
会提示 module not found
导致构建失败
咱可以在搜索组件里写一段脚本,在页面运行后append
到页面中,这段逻辑可以写到onBeforeMount
周期函数中
import { onBeforeMount } from 'vue'
const addInlineScript = () => {
const scriptText = `import('/_pagefind/pagefind.js')
.then((module) => {
window.__pagefind__ = module
})
.catch(() => {
console.log('not load /_pagefind/pagefind.js')
})`
const inlineScript = document.createElement('script')
inlineScript.innerHTML = scriptText
document.head.appendChild(inlineScript)
}
onBeforeMount(() => {
addInlineScript()
})
pagefind
我这里采用import(source)
动态导入,组件搜索直接使用window.__pagefind__
来进行API的调用
最后
目前这一版插件主要是将pagefind做了一个简单的内置,没有对搜索结果进行优化,也不支持多级标题的跳转
后续是准备优化一下插件的本身实现和功能
- 插件内部的hack实现替换为 VitePress 的 Build Hooks
- 搜索内容的输入输出支持自定义的转换
- 跳转支持标题
- ...
欢迎各位在评论区交流想法