在构建使用 Astro 或其他静态网站生成器(SSG)的网站时,一项不容忽视的挑战是如何在不牺牲性能和用户体验的前提下,实现网站内容的高效搜索。Pagefind 应运而生,为解决这一问题提供了一个全静态的解决方案。
Pagefind 简介
JavaScript 搜索 API
该静态搜索包暴露了一个 JavaScript 搜索 API,允许开发者在网站任何需要的地方轻易地调用搜索功能。这一特性尤其适用于具有复杂导航和多层次页面结构的大型网站。
预构建用户界面
除了搜索 API 外,Pagefind 还提供了一个预构建的用户界面(UI),该界面无需任何配置即可立即使用,进一步降低了实施门槛。
高效性能与带宽优化
Pagefind 的一大亮点是其对性能和带宽的优化。搜索索引被巧妙地划分为多个小块,从而确保在浏览器进行搜索时只需加载索引的一个小子集。据统计,即使对一个包含 10,000 个页面的网站进行全文搜索,总网络负载也不会超过 300KB,包括 Pagefind 库本身在内。在大多数情况下,这个数字更接近 100KB。
安装与配置
Pagefind 的安装过程异常简便,只需将其引入到包含网站已构建静态文件的文件夹中。通常情况下,这一步骤不需要任何额外配置。成功安装后,Pagefind 将自动索引网站内容,并将一个静态搜索包添加到构建文件中。
第一步:修改 package.json
-
添加脚本
在
package.json
文件的scripts
部分中,添加一个postbuild
脚本:'scripts': { 'postbuild': 'pagefind --source dist', }
这个
postbuild
脚本确保在 Astro 构建完成之后,Pagefind 会立即运行并进行网站内容的索引。 -
添加依赖
在
dependencies
部分,添加两个必要的库:'dependencies': { '@pagefind/default-ui': '^0.12.0', 'pagefind': '^0.12.0', }
这两个依赖分别是 Pagefind 的核心库和默认的用户界面库。
第二步:配置 .npmrc
在项目的根目录中,创建或编辑 .npmrc
文件,并添加以下代码:
enable-pre-post-scripts=true
这一设置确保 npm 可以执行 pre
和 post
前后缀的生命周期脚本,这在本例中是必要的,因为我们在 package.json
中设置了 postbuild
脚本。
第三步:编写 Search 组件
//src/components/Search.astro --- import '@pagefind/default-ui/css/ui.css' ---
<site-search id='search' class='ms-auto'>
<button data-open-modal disabled class='flex items-center justify-center rounded-md'>
<svg
aria-label='search'
class='h-6 w-6'
xmlns='http://www.w3.org/2000/svg'
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-linecap='round'
stroke-linejoin='round'
stroke-width='1.5'
>
<path stroke='none' d='M0 0h24v24H0z'></path>
<path d='M3 10a7 7 0 1 0 14 0 7 7 0 1 0-14 0M21 21l-6-6'></path>
</svg>
</button>
<dialog
aria-label='search'
class='h-full max-h-full w-full max-w-full border border-zinc-400 bg-white dark:bg-[#0a0910ec] shadow backdrop:backdrop-blur sm:mx-auto sm:mb-auto sm:mt-16 sm:h-max sm:max-h-[calc(100%-8rem)] sm:min-h-[15rem] sm:w-5/6 sm:max-w-[48rem] sm:rounded-md opacity-0'
>
<div class='dialog-frame flex flex-col gap-4 p-6 pt-12 sm:pt-6'>
<button
data-close-modal
class='ms-auto cursor-pointer rounded-full bg-black text-white px-4 py-2 dark:bg-white dark:text-black'
>
Close
</button>
{ import.meta.env.DEV ? (
<div class='mx-auto text-center dark:text-white'>
<p>
Search is only available in production builds. <br />
Try building and previewing the site to test it out locally.
</p>
</div>
) : (
<div class='search-container dark:text-white'>
<div id='pagefind__search' />
</div>
) }
</div>
</dialog>
</site-search>
<script>
import { animate } from 'motion'
class SiteSearch extends HTMLElement {
constructor() {
super()
const openBtn = this.querySelector<HTMLButtonElement>('button[data-open-modal]')!
const closeBtn = this.querySelector<HTMLButtonElement>('button[data-close-modal]')!
const dialog = this.querySelector('dialog')!
const dialogFrame = this.querySelector('.dialog-frame')!
const onWindowClick = (event: MouseEvent) => {
// make sure the click is outside the of the dialog
if (
document.body.contains(event.target as Node) &&
!dialogFrame.contains(event.target as Node)
)
closeModal()
}
const openModal = (event?: MouseEvent) => {
dialog.showModal()
animate(
'dialog',
{
clipPath: [
'polygon(0 0, 100% 0, 100% -200%, -200% -200%)',
'polygon(0 0, 100% 0, 100% 100%, 0% 100%)'
],
opacity: [0, 1]
},
{ duration: 0.2 }
)
document.body.classList.add('overflow-hidden')
this.querySelector('input')?.focus()
event?.stopPropagation()
window.addEventListener('click', onWindowClick)
}
const closeModal = () => {
dialog.close()
document.body.classList.remove('overflow-hidden')
window.removeEventListener('click', onWindowClick)
}
openBtn.addEventListener('click', openModal)
openBtn.disabled = false
closeBtn.addEventListener('click', closeModal)
window.addEventListener('keydown', (e) => {
if (e.key === '/' && !dialog.open) {
openModal()
e.preventDefault()
}
})
window.addEventListener('DOMContentLoaded', () => {
if (import.meta.env.DEV) return
const onIdle = window.requestIdleCallback || ((cb) => setTimeout(cb, 1))
onIdle(async () => {
// @ts-ignore
const { PagefindUI } = await import('@pagefind/default-ui')
new PagefindUI({
element: '#pagefind__search',
baseUrl: import.meta.env.BASE_URL,
bundlePath: import.meta.env.BASE_URL.replace(/\/$/, '') + '/_pagefind/',
showImages: false
})
})
})
}
}
customElements.define('site-search', SiteSearch)
</script>
<style is:global>
.dark {
--pagefind-ui-primary: #fafafa;
--pagefind-ui-text: #fff;
--pagefind-ui-background: #171717;
--pagefind-ui-border: #171717;
--pagefind-ui-tag: #171717;
}
</style>
本段代码示例展示了如何在 Astro 项目中集成 Pagefind,以提供全静态的搜索功能。代码涵盖了 HTML 结构、JavaScript 逻辑以及 CSS 样式,全面地实现了一个自定义的搜索组件。
代码结构解析
-
样式导入
import '@pagefind/default-ui/css/ui.css'
这行代码导入了 Pagefind 默认的 UI 样式,简化了样式配置的过程。
-
HTML 结构
代码定义了一个
site-search
自定义元素,该元素包括用于打开和关闭模态搜索窗口的按钮、模态窗口本身以及用于显示搜索结果的容器。 -
JavaScript 逻辑
JavaScript 部分包含了对模态窗口打开和关闭事件的处理,以及按需加载 Pagefind 搜索 UI 的逻辑。
-
CSS 样式
全局样式用于自定义 Pagefind UI 在暗模式下的外观。
特性
-
模块化设计:该组件使用了自定义元素,使其成为一个可复用和易于维护的模块。
-
按需加载:JavaScript 代码中使用了动态导入,使得 Pagefind UI 只在需要时才会被加载,从而优化了性能。
-
样式自定义:通过 CSS 变量,可轻易地在暗模式下调整 Pagefind UI 的外观。
-
无需手动触发:由于已经在
package.json
中设置了postbuild
脚本,Pagefind 将自动在每次构建后运行,无需手动触发。
第四步,导入组件
在需要的位置导入组件
---
import Search from '@/components/Search'
---
<header>
<div>
<div class='flex'>
<a href='/' class='px-4 py-2 hover:bg-neutral-200 dark:hover:bg-neutral-600'>首页</a>
</div>
<search />
</div>
</header>
第五步,运行 Astro 构建
在完成上述配置后,运行 Astro 的构建命令:
npm run build
这将触发 Astro 的构建过程,并在构建完成后,自动运行 Pagefind 的 postbuild
脚本,从而完成网站内容的索引。