Astro文章搜索,创建简单的搜索组件

发布于 #Astro

Astro文章搜索,创建简单的搜索组件

需要为整个站点的文字提供一个简单的搜索功能,主要是从文章的title和tag和发布时间等检索。

设想是根据全部文章的json文件检索对应的entry.data内容。实现可以参考Api端点和js简单正则匹配。

利用静态端点APi生成我们搜索的数据源json

在pages文件夹下创建对应的content.json.ts文件。当我们生成dist文件时,会自动生成一个content.json文件。

具体内容如下:

import { getAllPosts } from '@assets/ts/utils';
import { getCollection } from 'astro:content';

export const GET = async () => {

  const blogEntries = await getAllPosts();
  const content = blogEntries.map((entry) => ({
      slug: entry.slug,
      data: {
          ...entry.data,
          pubDate: entry.data.pubDate.toLocaleDateString("zh-cn", {
              year: "numeric",
              month: "2-digit",
              day: "numeric",
          })
      },
  }));
  return new Response(JSON.stringify({ posts: content }));
};

后续会生成如下:

{"posts": [
        {
            "slug": "2024-02",
            "data": {
                "title": "Astro实现分页",
                "pubDate": "2024/06/11",
                "author": "霁",
                "heroImage": null,
                "slug": "2024-02",
                "categories": [
                    "JavaScript"
                ],
                "tags": [
                    "Astro"
                ]
            }
        },
        {
            "slug": "2024-01",
            "data": {
                "title": "从Jekyll迁移到Astro要做什么?",
                "pubDate": "2024/06/10",
                "author": "霁",
                "heroImage": null,
                "slug": "2024-01",
                "categories": [
                    "JavaScript"
                ],
                "tags": [
                    "Astro"
                ]
            }
        }
    ]
}

创建简单的搜索框组件

需要包括input输入框,结果显示区域,具体样式可自定义

---
import { config } from '@assets/ts/consts';
---

<search>
 <span>
   <svg class='size-5' viewBox='0 0 24 24' fill='none'>
     <path
       d='M21 21L15 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z'
       stroke='currentColor'
       stroke-width='2'
       stroke-linecap='round'
       stroke-linejoin='round'>
     </path>
   </svg>
 </span>
 <input
   type='text'
   id='search-input'
   data-json={config.simple_search.json}
   placeholder={config.simple_search.placeholder}
 />
 <ul id='results-container' class='divide-y bg-site-light dark:bg-site-dark'>
   <!-- 这里插入搜索结果列表项 -->
 </ul>
</search>

<script>
 import { SimpleSearch } from '@assets/ts/utils';
 // 初始化搜索功能
 document.addEventListener('astro:page-load', function () {
   SimpleSearch({
     searchInput: document.getElementById('search-input'),
     resultsContainer: document.getElementById('results-container'),
     json: `${document.getElementById('search-input')?.dataset.json}` // 你的博客文章数据
   }).init();
 });
</script>

<style>
 search {
   @apply relative flex h-full w-full justify-center max-sm:px-5;
 }
 span {
   @apply absolute inset-y-0 left-0 flex items-center max-sm:left-5;
 }
 input {
   @apply my-1 h-12 w-full flex-auto border-b-2 border-transparent bg-inherit pl-10 text-inherit focus:border-b-primary-light focus:outline-none max-sm:pl-8;
 }
 ul {
   @apply absolute inset-x-0 top-full flex max-h-72 flex-col overflow-y-auto shadow;
 }
</style>

实现对应的SimpleSearch

需要根据输入框的输入去匹配content.json,具体做法就是类似fetch后台的形式。

可以参考如下

const SimpleSearch = (options) => {
  const defaults = {
      searchInput: null,
      resultsContainer: null,
      json: null,
      searchResultTemplate: "",
      noResultsText: `没有找到匹配的文章`,
      limit: 10,
      fuzzy: false,
  };
  // 合并用户提供的选项和默认选项
  const settings = Object.assign({}, defaults, options);
  const regexPost = (data, query) => {
      const regex = new RegExp(query.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"), "gi");
      const results: any[] = [];
      data.forEach((post) => {
          const { data } = post;
          if (
              (settings.fuzzy &&
                  (data.title.match(regex) || data.tags.join(",").match(regex) || data.pubDate.match(regex))) ||
              (!settings.fuzzy &&
                  (data.title.toLowerCase().includes(query.toLowerCase()) ||
                      data.tags.join(",").toLowerCase().includes(query.toLowerCase()) ||
                      data.pubDate.toLowerCase().includes(query.toLowerCase())))
          ) {
              results.push(post);
          }
      });
      renderResults(results.slice(0, settings.limit));
  };

  // 执行搜索的函数
  async function search(query) {
      const results: any[] = [];
      if (query === "") {
          settings.resultsContainer.innerHTML = "";
          return;
      }

      // 尝试从 localStorage 中获取缓存的 JSON 数据
      const cachedData = sessionStorage.getItem("searchCache");
      if (cachedData) {
          // 解析缓存数据
          const parsedData = JSON.parse(cachedData);
          // 检查数据是否已过期
          const now = new Date().getTime();
          const expirationTime = parsedData.expirationTime || now;
          if (now < expirationTime) {
              results.push(...parsedData.data);
          }
      }
      if (results.length > 0) {
          regexPost(results, query);
      } else {
          const content = await fetch(settings.json);
          const { posts } = await content.json();
          // 设置数据的有效期
          const expirationTime = new Date(Date.now() + 1000 * 60 * 60 * 24).getTime(); // 设置有效期为 24 小时
          // 更新缓存数据并存储
          const updatedData = {
              data: posts,
              expirationTime: expirationTime,
          };
          sessionStorage.setItem("searchCache", JSON.stringify(updatedData));
          regexPost(posts, query);
      }
  }

  // 渲染搜索结果的函数
  function renderResults(results) {
      const container = settings.resultsContainer;
      container.innerHTML = "";

      if (results.length === 0) {
          container.innerHTML = `${settings.noResultsText}`;
      } else {
          results.forEach((result) => {
              let tagHTML = "";
              //   // 遍历标签数组,为每个标签生成 HTML 元素
              const { slug, data } = result;
              const { title, pubDate, tags } = data;
              tags.forEach((tag) => {
                  tagHTML += `<a href="/tags/${tag}" class="tag">#${tag}</a>`;
              });
              container.innerHTML += `<li class="flex flex-col flex-auto justify-between p-5 space-y-2">
                                      <a class="inline-block" href="/blog/${slug}/" title="${title}">
                                          <h2>${title}</h2>
                                      </a>
                                      <div class="flex justify-between items-center w-full ">
                                          <div class="flex justify-around space-x-2">${tagHTML}</div>
                                          <time>${pubDate}</time>
                                      </div>
                                  </li>
                                  `;
          });
      }
  }

  // 监听整个文档的点击事件
  document.addEventListener("click", function (event) {
      const inputElement = settings.searchInput; // 替换成你的输入框的实际 ID
      const resultsContainer = settings.resultsContainer; // 替换成你搜索结果容器的实际 ID

      // 检查点击事件的目标是否是输入框以外的元素,并且搜索结果容器是可见的
      if (
          event.target !== inputElement &&
          !inputElement.contains(event.target) &&
          event.target !== resultsContainer &&
          !resultsContainer.contains(event.target)
      ) {
          // 销毁搜索结果
          destroyResults();
      }
  });

  // 销毁搜索结果的函数
  function destroyResults() {
      const resultsContainer = settings.resultsContainer; // 替换成你搜索结果容器的实际 ID
      resultsContainer.innerHTML = ""; // 清空搜索结果容器的内容,或者隐藏搜索结果容器
  }

  function debounce(func, delay) {
      let debounceTimer;
      return function (...args) {
          clearTimeout(debounceTimer);
          debounceTimer = setTimeout(() => func.apply(this, args), delay);
      };
  }

  const handleInputDebounced = debounce(search, 300);

  // 初始化搜索功能
  function init() {
      const input = settings.searchInput;
      if (!input) return;

      input.addEventListener("input", function (event) {
          handleInputDebounced(event.target.value);
      });
  }

  // 返回公共方法
  return {
      init: init,
      search: search,
  };
}
相关文章

Astro实现分页

为需要分页的page创建对应文件 例如创建archives/[…page].astro 使用pag...

发布于 #Astro