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 opacity-0 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 dark:bg-[#0a0910ec]"
  >
    <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 px-4 py-2 text-white 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-05-18