Astro 优化之搜索-Pagefind

iNote-Astro 优化之搜索

在构建使用 Astro 或其他静态网站生成器(SSG)的网站时,一项不容忽视的挑战是如何在不牺牲性能和用户体验的前提下,实现网站内容的高效搜索。Pagefind 应运而生,为解决这一问题提供了一个全静态的解决方案。

Pagefind 简介

JavaScript 搜索 API

该静态搜索包暴露了一个 JavaScript 搜索 API,允许开发者在网站任何需要的地方轻易地调用搜索功能。这一特性尤其适用于具有复杂导航和多层次页面结构的大型网站。

预构建用户界面

除了搜索 API 外,Pagefind 还提供了一个预构建的用户界面(UI),该界面无需任何配置即可立即使用,进一步降低了实施门槛。

高效性能与带宽优化

Pagefind 的一大亮点是其对性能和带宽的优化。搜索索引被巧妙地划分为多个小块,从而确保在浏览器进行搜索时只需加载索引的一个小子集。据统计,即使对一个包含 10,000 个页面的网站进行全文搜索,总网络负载也不会超过 300KB,包括 Pagefind 库本身在内。在大多数情况下,这个数字更接近 100KB。

安装与配置

Pagefind 的安装过程异常简便,只需将其引入到包含网站已构建静态文件的文件夹中。通常情况下,这一步骤不需要任何额外配置。成功安装后,Pagefind 将自动索引网站内容,并将一个静态搜索包添加到构建文件中。

第一步:修改 package.json

  1. 添加脚本

    package.json 文件的 scripts 部分中,添加一个 postbuild 脚本:

    'scripts': {
      'postbuild': 'pagefind --source dist',
    }
    

    这个 postbuild 脚本确保在 Astro 构建完成之后,Pagefind 会立即运行并进行网站内容的索引。

  2. 添加依赖

    dependencies 部分,添加两个必要的库:

    'dependencies': {
      '@pagefind/default-ui': '^0.12.0',
      'pagefind': '^0.12.0',
    }
    

    这两个依赖分别是 Pagefind 的核心库和默认的用户界面库。

第二步:配置 .npmrc

在项目的根目录中,创建或编辑 .npmrc 文件,并添加以下代码:

enable-pre-post-scripts=true

这一设置确保 npm 可以执行 prepost 前后缀的生命周期脚本,这在本例中是必要的,因为我们在 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 样式,全面地实现了一个自定义的搜索组件。

代码结构解析

  1. 样式导入

    import '@pagefind/default-ui/css/ui.css'

    这行代码导入了 Pagefind 默认的 UI 样式,简化了样式配置的过程。

  2. HTML 结构

    代码定义了一个 site-search 自定义元素,该元素包括用于打开和关闭模态搜索窗口的按钮、模态窗口本身以及用于显示搜索结果的容器。

  3. JavaScript 逻辑

    JavaScript 部分包含了对模态窗口打开和关闭事件的处理,以及按需加载 Pagefind 搜索 UI 的逻辑。

  4. CSS 样式

    全局样式用于自定义 Pagefind UI 在暗模式下的外观。

特性

  1. 模块化设计:该组件使用了自定义元素,使其成为一个可复用和易于维护的模块。

  2. 按需加载:JavaScript 代码中使用了动态导入,使得 Pagefind UI 只在需要时才会被加载,从而优化了性能。

  3. 样式自定义:通过 CSS 变量,可轻易地在暗模式下调整 Pagefind UI 的外观。

  4. 无需手动触发:由于已经在 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 脚本,从而完成网站内容的索引。

人世间 博客
发表于 2023-09-08,更新于 2024-04-25 阅读量: