<!DOCTYPE html><html lang="zh" data-theme="dark"><head><meta charset="utf-8"><meta name="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Hexo 本地搜索插件教程 · Cytrogen 的个人博客</title><meta name="description" content="本文是一篇详细的 Hexo 本地搜索功能实现教程。文章指导你如何使用 hexo-generator-search 插件生成站点内容的索引文件,并结合 jQuery 和自定义 JavaScript 脚本,一步步构建出一个功能完善、无需后端的本地搜索引擎。内容涵盖插件配置、搜索弹窗的 Pug 视图与 CSS 样式编写、交互逻辑实现,以及核心的 Ajax 搜索与关键词高亮脚本。跟随本教程,可以轻松为你的博客添加一个实用的搜索功能,提升用户体验。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/49c3.html"><link rel="webmention" href="https://webmention.io/cytrogen.icu/webmention"><link rel="me" href="https://m.otter.homes/@Cytrogen"><link rel="me" href="https://github.com/cytrogen"><meta name="fediverse:creator" content="@Cytrogen@m.otter.homes"><link rel="preload" href="../fonts/opensans-regular-latin.woff2" as="font" type="font/woff2" crossorigin="anonymous"><style>@font-face {
font-family: 'Open Sans';
src: url('../fonts/opensans-regular-latin.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
size-adjust: 107%;
ascent-override: 97%;
descent-override: 25%;
line-gap-override: 0%;
}
</style><script>(function() {
try {
// 优先级:用户选择 > 系统偏好 > 默认浅色
const saved = localStorage.getItem('theme');
const theme = saved ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
} catch (error) {
// 失败时使用默认主题,不阻塞渲染
document.documentElement.setAttribute('data-theme', 'light');
}
})();
</script><link rel="stylesheet" href="../css/ares.css"><script data-netlify-skip-bundle="true">(function() {
document.addEventListener('DOMContentLoaded', function() {
const theme = document.documentElement.getAttribute('data-theme');
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper && theme) {
pageWrapper.setAttribute('data-theme', theme);
}
});
})();
</script><!-- hexo injector head_end start -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hexo-math@4.0.0/dist/style.css">
<!-- hexo injector head_end end --><meta name="generator" content="Hexo 8.1.1"><link rel="alternate" href="atom.xml" title="Cytrogen 的个人博客" type="application/atom+xml">
</head><body><div id="page-wrapper"><a class="skip-link" href="#main-content">跳到主要内容</a><div class="wrap"><header><a class="logo-link" href="../index.html"><img src="../favicon.png" alt="logo"></a><div class="h-card visually-hidden"><img class="u-photo" src="https://cytrogen.icu/favicon.png" alt="Cytrogen"><a class="p-name u-url u-uid" href="https://cytrogen.icu">Cytrogen</a><p class="p-note">Cytrogen 的个人博客,Cytrogen's Blog</p><a class="u-url" rel="me noopener" target="_blank" href="https://m.otter.homes/@Cytrogen">Mastodon</a><a class="u-url" rel="me noopener" target="_blank" href="https://github.com/cytrogen">GitHub</a></div><nav class="site-nav"><div class="nav-main"><div class="nav-primary"><ul class="nav-list hidden-mobile"><li class="nav-item"><a class="nav-link" href="../index.html">首页</a></li></ul><div class="nav-tools"><div class="language-menu"><button class="language-toggle" type="button"><svg class="icon icon-globe" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855A7.97 7.97 0 0 0 10.855 12H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z"></path></svg><span>中文</span></button><div class="language-dropdown"></div></div></div><div class="nav-controls"><div class="more-menu hidden-mobile"><button class="more-toggle" type="button"><span>更多</span><svg class="icon icon-chevron-down" width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 8.825c-.2 0-.4-.1-.5-.2l-3.3-3.3c-.3-.3-.3-.8 0-1.1s.8-.3 1.1 0l2.7 2.7 2.7-2.7c.3-.3.8-.3 1.1 0s.3.8 0 1.1l-3.3 3.3c-.1.1-.3.2-.5.2z"></path></svg></button><div class="more-dropdown"><ul class="dropdown-list"><li class="dropdown-item"><a class="nav-link" href="../archives/index.html">归档</a></li><li class="dropdown-item"><a class="nav-link" href="../categories/index.html">分类</a></li><li class="dropdown-item"><a class="nav-link" href="../tags/index.html">标签</a></li><li class="dropdown-item"><a class="nav-link" href="../about/index.html">关于</a></li><li class="dropdown-item"><a class="nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></div><div class="theme-switcher"><button class="theme-toggle" type="button" role="switch" aria-pressed="false" aria-label="切换主题"><div class="theme-icon moon-icon"><svg class="icon icon-moon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"></path></svg></div><div class="theme-icon sun-icon"><svg class="icon icon-sun" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"></path></svg></div></button></div><details class="mobile-menu-details hidden-desktop"><summary class="hamburger-menu" aria-label="nav.menu"><svg class="icon icon-bars" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"></path></svg><span class="menu-text">nav.menu</span></summary><div class="mobile-menu-dropdown"><ul class="mobile-nav-list"><li class="mobile-nav-item"><a class="mobile-nav-link" href="../index.html">首页</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../archives/index.html">归档</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../categories/index.html">分类</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../tags/index.html">标签</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../about/index.html">关于</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></details></div></div></div></nav></header><main class="container" id="main-content" tabindex="-1"><div class="post"><article class="post-block h-entry"><div class="post-meta p-author h-card visually-hidden"><img class="author-avatar u-photo" src="../favicon.png" alt="Cytrogen"><span class="p-name">Cytrogen</span><a class="u-url" href="https://cytrogen.icu">https://cytrogen.icu</a></div><a class="post-permalink u-url u-uid visually-hidden" href="https://cytrogen.icu/posts/49c3.html">永久链接</a><div class="p-summary visually-hidden"><p>随着文章数量的增加,我们的博客网站自然是不能只有归档或者标签页的,更别提有时候我们不记得标题、只记得文章内一个简短的词汇。</p>
<p>一个简单的本地搜索栏可以帮到我们。</p></div><div class="visually-hidden"><a class="p-category" href="../categories/%E7%BC%96%E7%A8%8B%E7%AC%94%E8%AE%B0/">编程笔记</a><a class="p-category" href="../tags/Hexo/">Hexo</a><a class="p-category" href="../tags/JavaScript/">JavaScript</a></div><h1 class="post-title p-name">Hexo 本地搜索插件教程</h1><div class="post-info"><time class="post-date dt-published" datetime="2024-07-03T05:00:00.000Z">7/3/2024</time><time class="dt-updated visually-hidden" datetime="2026-02-09T17:16:54.709Z"></time></div><div class="post-content e-content"><html><head></head><body><p>随着文章数量的增加,我们的博客网站自然是不能只有归档或者标签页的,更别提有时候我们不记得标题、只记得文章内一个简短的词汇。</p>
<p>一个简单的本地搜索栏可以帮到我们。</p>
<span id="more"></span>
<div class="danger">
<ul>
<li>
<p>本文章使用到了 <code>Hexo-Generator-Search</code> 库。</p>
<p><a target="_blank" rel="noopener" href="https://github.com/wzpan/hexo-generator-search"><img src="https://gh-card.dev/repos/wzpan/hexo-generator-search.svg" alt="wzpan/hexo-generator-search - GitHub"></a></p>
</li>
<li>
<p>本文章使用的是 Pug 模板语言。</p>
</li>
</ul>
</div>
<h1 id="安装依赖"><a class="markdownIt-Anchor" href="#安装依赖"></a> 安装依赖</h1>
<figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --save hexo-generator-search</span><br></pre></td></tr></tbody></table></figure>
<p><code>Hexo-Generator-Search</code> 会生成搜索索引文件,其中包含文章的所有必要数据,可用于为博客编写本地搜索引擎。它支持 XML 和 JSON 格式输出,我们这里会使用 XML。</p>
<blockquote>
<p>两者的输出区别可见官方仓库的示例。</p>
</blockquote>
<p>在博客根目录中的 <code>_config.yml</code> 文件内添加以下配置项:</p>
<figure class="highlight yaml"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">search:</span></span><br><span class="line"> <span class="attr">path:</span> <span class="string">search.xml</span></span><br><span class="line"> <span class="attr">field:</span> <span class="string">post</span></span><br><span class="line"> <span class="attr">content:</span> <span class="literal">true</span></span><br><span class="line"> <span class="attr">template:</span> <span class="string">./search.xml</span></span><br></pre></td></tr></tbody></table></figure>
<ul>
<li><code>path</code>:文件路径。默认为 <code>search.xml</code>。如果文件扩展名为 <code>.json</code>,则输出格式为 JSON。否则将输出 XML 格式文件。
<ul>
<li>值得注意的是,这里的路径指的是 <code>public</code> 路径。</li>
</ul>
</li>
<li><code>field</code>:您要搜索的搜索范围,您可以选择:
<ul>
<li><code>post</code>(默认):仅涵盖博客的所有文章。</li>
<li><code>page</code>:只涵盖博客的所有页面。</li>
<li><code>all</code>:将涵盖博客的所有文章和页面。
<ul>
<li><strong>页面</strong> 指的是 Hexo 中 <code>archive</code>、<code>tags</code> 等页面。</li>
</ul>
</li>
</ul>
</li>
<li><code>content</code>:是否包含每篇文章的全部内容。如果为 <code>false</code>,则生成的结果只包括标题和其他元信息,而不包括正文。默认为 <code>true</code>。</li>
<li><code>template</code>(可选):自定义 XML 模板的路径。</li>
</ul>
<p>在博客根目录中添加 <code>search.xml</code>,用作生成搜索索引的模板:</p>
<figure class="highlight xml"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><?xml version=<span class="string">"1.0"</span> encoding=<span class="string">"utf-8"</span>?></span></span><br><span class="line"><span class="tag"><<span class="name">search</span>></span> </span><br><span class="line"> {% if posts %}</span><br><span class="line"> {% for post in posts.toArray() %}</span><br><span class="line"> {% if post.indexing == undefined or post.indexing %}</span><br><span class="line"> <span class="tag"><<span class="name">entry</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">title</span>></span>{{ post.title }}<span class="tag"></<span class="name">title</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">link</span> <span class="attr">href</span>=<span class="string">"{{ (url + post.path) | uriencode }}"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">url</span>></span>{{ (url + post.path) | uriencode }}<span class="tag"></<span class="name">url</span>></span></span><br><span class="line"> {% if content %}</span><br><span class="line"> <span class="tag"><<span class="name">content</span> <span class="attr">type</span>=<span class="string">"html"</span>></span><![CDATA[{{ post.content | noControlChars | safe }}]]><span class="tag"></<span class="name">content</span>></span></span><br><span class="line"> {% endif %}</span><br><span class="line"> {% if post.categories and post.categories.length>0 %}</span><br><span class="line"> <span class="tag"><<span class="name">categories</span>></span></span><br><span class="line"> {% for cate in post.categories.toArray() %}</span><br><span class="line"> <span class="tag"><<span class="name">category</span>></span> {{ cate.name }} <span class="tag"></<span class="name">category</span>></span></span><br><span class="line"> {% endfor %}</span><br><span class="line"> <span class="tag"></<span class="name">categories</span>></span></span><br><span class="line"> {% endif %}</span><br><span class="line"> {% if post.tags and post.tags.length>0 %}</span><br><span class="line"> <span class="tag"><<span class="name">tags</span>></span></span><br><span class="line"> {% for tag in post.tags.toArray() %}</span><br><span class="line"> <span class="tag"><<span class="name">tag</span>></span> {{ tag.name }} <span class="tag"></<span class="name">tag</span>></span></span><br><span class="line"> {% endfor %}</span><br><span class="line"> <span class="tag"></<span class="name">tags</span>></span></span><br><span class="line"> {% endif %}</span><br><span class="line"> <span class="tag"></<span class="name">entry</span>></span></span><br><span class="line"> {% endif %}</span><br><span class="line"> {% endfor %}</span><br><span class="line"> {% endif %}</span><br><span class="line"> {% if pages %}</span><br><span class="line"> {% for page in pages.toArray() %}</span><br><span class="line"> {% if post.indexing == undefined or post.indexing %}</span><br><span class="line"> <span class="tag"><<span class="name">entry</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">title</span>></span>{{ page.title }}<span class="tag"></<span class="name">title</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">link</span> <span class="attr">href</span>=<span class="string">"{{ (url + page.path) | uriencode }}"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">url</span>></span>{{ (url + page.path) | uriencode }}<span class="tag"></<span class="name">url</span>></span></span><br><span class="line"> {% if content %}</span><br><span class="line"> <span class="tag"><<span class="name">content</span> <span class="attr">type</span>=<span class="string">"html"</span>></span><![CDATA[{{ page.content | noControlChars | safe }}]]><span class="tag"></<span class="name">content</span>></span></span><br><span class="line"> {% endif %}</span><br><span class="line"> <span class="tag"></<span class="name">entry</span>></span></span><br><span class="line"> {% endif %}</span><br><span class="line"> {% endfor %}</span><br><span class="line"> {% endif %}</span><br><span class="line"><span class="tag"></<span class="name">search</span>></span></span><br></pre></td></tr></tbody></table></figure>
<p>官方仓库中提到,当运行以下命令后就可以在 <code>public</code> 路径中看到生成的结果:</p>
<figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo g</span><br></pre></td></tr></tbody></table></figure>
<h1 id="显示结果"><a class="markdownIt-Anchor" href="#显示结果"></a> 显示结果</h1>
<p>首先需要清楚一件事:<code>Hexo-Generator-Search</code> <strong>仅</strong> 生成搜索索引文件!你要如何使用这个文件就是你自己的事情,包括写不写搜索栏、怎么写搜索结果等。此处仅讲解我自己的方案,极大参考了 <code>Hexo-Theme-Freemind</code> 的写法。</p>
<p><a target="_blank" rel="noopener" href="https://github.com/wzpan/hexo-theme-freemind"><img src="https://gh-card.dev/repos/wzpan/hexo-theme-freemind.svg" alt="wzpan/hexo-theme-freemind - GitHub"></a></p>
<blockquote>
<p>部分主题自带了本地搜索功能,建议先看一下你使用的主题是否有内置。</p>
</blockquote>
<p><code>Hexo-Theme-Freemind</code> 使用了 AJAX 和 jQuery。从网上找到最新的 jQuery CDN(要 <code>minified</code> 的),并将 <code>script</code> 标签写在 <code>head.pug</code> 中:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">script(src='https://code.jquery.com/jquery-3.7.1.min.js', integrity='sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=', crossorigin='anonymous')</span><br></pre></td></tr></tbody></table></figure>
<p>这样,我们就可以使用 jQuery 了。</p>
<blockquote>
<p>jQuery 是一个轻量级的 JavaScript 库,使得开发者在网站上使用 JavaScript 更容易更方便。</p>
<p>例如,我们想要在页面加载时添加一个动画效果,就可以写:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">script.</span><br><span class="line"> $(document).ready(function() {</span><br><span class="line"> $("body").fadeIn(2000);</span><br><span class="line"> });</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>$</code> 符号在 jQuery 中代表一个函数,也是 jQuery 对象的别名,可以靠它来选择和操作 HTML 元素。</p>
</blockquote>
</blockquote>
<blockquote>
<p>AJAX 是一种在客户端创建异步 Web 应用程序的 Web 开发技术。它允许 Web 应用程序在不干扰现有页面显示和行为的情况下,异步地从服务器发送和检索数据。这意味着可以更新网页的部分内容,而无需重新加载整个页面。</p>
</blockquote>
<h2 id="编写搜索视图"><a class="markdownIt-Anchor" href="#编写搜索视图"></a> 编写搜索视图</h2>
<p>搜索视图是用于显示搜索表单和搜索结果的地方。最直观的说法就是用户看到的搜索栏。</p>
<blockquote>
<p><code>Hexo-Theme-Freemind</code> 使用的是 EJS 模板语言,感兴趣的可以 <a target="_blank" rel="noopener" href="https://github.com/wzpan/hexo-theme-freemind/blob/master/layout/_widget/search.ejs#L8">看一眼</a>。</p>
</blockquote>
<p>由于我使用的是单列主题 <code>Hexo-Theme-Hermes</code>,整个页面就没有什么侧边栏等地方存放搜索栏。</p>
<p><a target="_blank" rel="noopener" href="https://github.com/claymcleod/hexo-theme-hermes"><img src="https://gh-card.dev/repos/claymcleod/hexo-theme-hermes.svg" alt="claymcleod/hexo-theme-hermes - GitHub"></a></p>
<p>而多列主题(例如 <code>Hexo-Theme-Freemind</code>)就可以在侧边栏内直接塞入搜索栏。</p>
<p><img src="/posts/49c3/1.png" alt="img.png"></p>
<p>为了不破坏单列主题的核心概念,我决定在导航栏内添加一个搜索图标。当用户点击图标后,一个搜索窗口会弹出来,内含搜索栏以及显示搜索结果的子容器。用户再次点击搜索图标或者点击搜索窗口外的区域都会导致搜索窗口消失。</p>
<p>搜索图标我使用的是 FontAwesome 的 <code>fas fa-search</code>。从 FontAwesome 那边注册、拿到属于自己的 kit 后,在 <code>head.pug</code> 中添加:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">script(src="https://kit.fontawesome.com/########.js", crossorigin="anonymous")</span><br></pre></td></tr></tbody></table></figure>
<p><code>Hexo-Theme-Hermes</code> 的导航栏写法如下:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">ul.nav.nav-list</span><br><span class="line"> each value, key in theme.menu</span><br><span class="line"> li.nav-list-item</span><br><span class="line"> - var re = /^(http|https):\/\/*/gi;</span><br><span class="line"> - var tar = re.test(value) ? "_blank" : "_self"</span><br><span class="line"> - var act = !re.test(value) && "/"+page.current_url === value</span><br><span class="line"> a.nav-list-link(class={active: act} href=url_for(value) target=tar)</span><br><span class="line"> != key.toUpperCase()</span><br></pre></td></tr></tbody></table></figure>
<p>它是根据 <code>theme</code> 路径的配置项依次添加 <code>li</code> 标签,我们的搜索图标可以直接添加到 <code>ul</code> 标签的内部(和 <code>each value, key in theme.menu</code> 同级):</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">if config.search</span><br><span class="line"> li.nav-list-item#search-icon</span><br><span class="line"> i.fas.fa-search</span><br></pre></td></tr></tbody></table></figure>
<p>CSS 样式:</p>
<figure class="highlight css"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-id">#search-icon</span> {</span><br><span class="line"> <span class="attribute">cursor</span>: pointer;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>cursor</code> 属性设置鼠标指针在元素上方时显示的光标类型。<code>pointer</code> 表示光标将显示为一个指向链接的手指图标,通常在链接或可点击元素上使用,以向用户表明该元素可以被点击。这有助于提高用户界面的可用性。</p>
</blockquote>
<p><img src="/posts/49c3/2.png" alt="img.png"></p>
<p>接着我们来快速写一个显示搜索栏和搜索结果的容器。我要求不多,就是一个方方正正的小窗口在页面正中间。</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">div#search-popup.hidden</span><br><span class="line"> div#search-panel</span><br><span class="line"> input(type="text", id="local-search-input", name="q", results="0", placeholder=__('搜索...'))</span><br><span class="line"> div#local-search-results</span><br></pre></td></tr></tbody></table></figure>
<p>CSS 样式:</p>
<figure class="highlight css"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.hidden</span> {</span><br><span class="line"> <span class="attribute">display</span>: none;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="selector-id">#search-popup</span> {</span><br><span class="line"> <span class="attribute">position</span>: fixed;</span><br><span class="line"> <span class="attribute">top</span>: <span class="number">50%</span>;</span><br><span class="line"> <span class="attribute">left</span>: <span class="number">50%</span>;</span><br><span class="line"> <span class="attribute">transform</span>: <span class="built_in">translate</span>(-<span class="number">50%</span>, -<span class="number">50%</span>);</span><br><span class="line"> <span class="attribute">width</span>: <span class="number">75%</span>;</span><br><span class="line"> <span class="attribute">height</span>: <span class="number">50%</span>;</span><br><span class="line"> <span class="attribute">padding</span>: <span class="number">20px</span>;</span><br><span class="line"> <span class="attribute">border</span>: <span class="number">3px</span> solid <span class="number">#ccc</span>;</span><br><span class="line"> <span class="attribute">background-color</span>: <span class="number">#fff</span>;</span><br><span class="line"> <span class="attribute">text-align</span>: center;</span><br><span class="line"> <span class="attribute">z-index</span>: <span class="number">10</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="selector-id">#search-popup</span> <span class="selector-tag">input</span><span class="selector-attr">[type=<span class="string">"text"</span>]</span> {</span><br><span class="line"> <span class="attribute">display</span>: block;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>该窗口必须初始状态为隐藏,显现后则是一个固定在屏幕中心的弹出窗口,文本内容居中对齐。如果需要输入文本,输入框就会块级显示。</p>
<p>因为我希望该弹窗在 Z 轴上的位置优先于其他,我就写了个 <code>z-index: 10</code>(实际测试时发现 <code>position: absolute</code> 的类会优先于 <code>#search-popup</code>,才决定加上的)。</p>
<figure class="highlight css"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-id">#search-panel</span> {</span><br><span class="line"> <span class="attribute">display</span>: flex;</span><br><span class="line"> <span class="attribute">flex-direction</span>: column;</span><br><span class="line"> <span class="attribute">padding</span>: <span class="number">0</span>;</span><br><span class="line"> <span class="attribute">margin</span>: <span class="number">0</span>;</span><br><span class="line"> <span class="attribute">height</span>: <span class="number">100%</span>;</span><br><span class="line"> <span class="attribute">width</span>: <span class="number">100%</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>#search-panel</code> 的主要目的是将搜索栏和搜索结果以更简单的方式分开。Flexbox 布局好用、爱用、不用不会写。</p>
<figure class="highlight css"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-id">#local-search-input</span> {</span><br><span class="line"> <span class="attribute">width</span>: <span class="number">100%</span>;</span><br><span class="line"> <span class="attribute">margin-bottom</span>: <span class="number">1rem</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="selector-id">#local-search-results</span> {</span><br><span class="line"> <span class="attribute">overflow-y</span>: auto;</span><br><span class="line"> <span class="attribute">flex-grow</span>: <span class="number">1</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>一些之后会用到的 CSS 样式:</p>
<figure class="highlight css"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">em</span><span class="selector-class">.search-keyword</span> {</span><br><span class="line"> <span class="attribute">border-bottom</span>: <span class="number">1px</span> dashed <span class="number">#4088b8</span>;</span><br><span class="line"> <span class="attribute">font-weight</span>: bold;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="selector-tag">ul</span><span class="selector-class">.search-result-list</span> {</span><br><span class="line"> <span class="attribute">padding-left</span>: <span class="number">10px</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="selector-tag">a</span><span class="selector-class">.search-result-title</span> {</span><br><span class="line"> <span class="attribute">font-weight</span>: bold;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="selector-tag">p</span><span class="selector-class">.search-result</span> {</span><br><span class="line"> <span class="attribute">color</span>: <span class="number">#555</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>最终结果:</p>
<p><img src="/posts/49c3/3.png" alt="img.png"></p>
<blockquote>
<p>写的很随便也不好看,未来再美化吧。能用就行。</p>
</blockquote>
<h4 id="添加弹出窗口的显示逻辑"><a class="markdownIt-Anchor" href="#添加弹出窗口的显示逻辑"></a> 添加弹出窗口的显示逻辑</h4>
<p>只是单单写一个图标和窗口还不够,我们还没有添加窗口的显示逻辑。先前说了,我希望的是点击图标之后,窗口会显示;再次点击图标,窗口会隐藏。</p>
<p>在 Pug 文件中添加 <code>script</code> 标签:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">script.</span><br><span class="line"> document.getElementById('search-icon').addEventListener('click', function(event) {</span><br><span class="line"> const popup = document.getElementById('search-popup');</span><br><span class="line"> if (popup.classList.contains('hidden')) {</span><br><span class="line"> popup.classList.remove('hidden');</span><br><span class="line"> } else {</span><br><span class="line"> popup.classList.add('hidden');</span><br><span class="line"> }</span><br><span class="line"> event.stopPropagation();</span><br><span class="line"> });</span><br><span class="line"> document.getElementById('search-popup').addEventListener('click', function(event) {</span><br><span class="line"> event.stopPropagation();</span><br><span class="line"> });</span><br><span class="line"> document.addEventListener('click', function() {</span><br><span class="line"> const popup = document.getElementById('search-popup');</span><br><span class="line"> if (!popup.classList.contains('hidden')) {</span><br><span class="line"> popup.classList.add('hidden');</span><br><span class="line"> }</span><br><span class="line"> });</span><br></pre></td></tr></tbody></table></figure>
<ol>
<li>当用户点击 ID 为 <code>search-icon</code> 的元素时,会触发事件监听器。监听器会首先获取 ID 为 <code>search-popup</code> 的元素,然后检查该元素是否包含 <code>hidden</code> 类。如果是,就移除这个类;反之添加这个类。最后调用 <code>event.stopPropagation()</code> 来阻止事件冒泡、传播到父元素。</li>
<li>当用户点击 ID 为 <code>search-popup</code> 的元素时,也会触发一个事件监听器。这个监听器只会做一件事,那就是调用 <code>event.stopPropagation()</code> 来阻止事件冒泡。这样做的目的是防止当用户在弹出窗口上点击时,触发下面的文档点击事件监听器。</li>
<li>当用户点击文档的任何地方时,会触发一个事件监听器。它首先会获取 ID 为 <code>search-popup</code> 的元素,然后检查是否包含 <code>hidden</code> 类。如果不是,就添加这个类。这样的话,每当用户点击弹出窗口以外的任何地方时,弹出窗口就会隐藏。</li>
</ol>
<h2 id="编写搜索脚本"><a class="markdownIt-Anchor" href="#编写搜索脚本"></a> 编写搜索脚本</h2>
<p>搜索脚本会告诉浏览器如何抓取搜索数据,并过滤出我们要搜索的内容。</p>
<blockquote>
<p>这里我近乎是照搬了 <code>Hexo-Theme-Freemind</code> 的 <a target="_blank" rel="noopener" href="https://github.com/wzpan/hexo-theme-freemind/blob/master/source/js/search.js">写法</a>,只修改了一丢丢细节。</p>
<p>未来可能会尝试改进这段代码。先挖坑了。</p>
</blockquote>
<figure class="highlight js"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> searchFunc = <span class="keyword">function</span> (<span class="params">path, search_id, content_id</span>) {</span><br><span class="line"> <span class="string">'use strict'</span>;</span><br><span class="line"> $.<span class="title function_">ajax</span>({</span><br><span class="line"> <span class="attr">url</span>: path,</span><br><span class="line"> <span class="attr">dataType</span>: <span class="string">"xml"</span>,</span><br><span class="line"> <span class="attr">success</span>: <span class="keyword">function</span> (<span class="params">xmlResponse</span>) {</span><br><span class="line"> <span class="keyword">const</span> datas = $(<span class="string">"entry"</span>, xmlResponse).<span class="title function_">map</span>(<span class="keyword">function</span> (<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="attr">title</span>: $(<span class="string">"title"</span>, <span class="variable language_">this</span>).<span class="title function_">text</span>(),</span><br><span class="line"> <span class="attr">content</span>: $(<span class="string">"content"</span>, <span class="variable language_">this</span>).<span class="title function_">text</span>(),</span><br><span class="line"> <span class="attr">url</span>: $(<span class="string">"url"</span>, <span class="variable language_">this</span>).<span class="title function_">text</span>()</span><br><span class="line"> };</span><br><span class="line"> }).<span class="title function_">get</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> $input = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(search_id);</span><br><span class="line"> <span class="keyword">if</span> (!$input) <span class="keyword">return</span>;</span><br><span class="line"> <span class="keyword">const</span> $resultContent = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(content_id);</span><br><span class="line"> <span class="keyword">if</span> ($(<span class="string">"#local-search-input"</span>).<span class="property">length</span> > <span class="number">0</span>) {</span><br><span class="line"> $input.<span class="title function_">addEventListener</span>(<span class="string">'input'</span>, <span class="keyword">function</span> (<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">let</span> str = <span class="string">'<ul class=\"search-result-list\">'</span>;</span><br><span class="line"> <span class="keyword">const</span> keywords = <span class="variable language_">this</span>.<span class="property">value</span>.<span class="title function_">trim</span>().<span class="title function_">toLowerCase</span>().<span class="title function_">split</span>(<span class="regexp">/[\s\-]+/</span>);</span><br><span class="line"> $resultContent.<span class="property">innerHTML</span> = <span class="string">""</span>;</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">value</span>.<span class="title function_">trim</span>().<span class="property">length</span> <= <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> datas.<span class="title function_">forEach</span>(<span class="keyword">function</span> (<span class="params">data</span>) {</span><br><span class="line"> <span class="keyword">let</span> isMatch = <span class="literal">true</span>;</span><br><span class="line"> <span class="keyword">const</span> content_index = [];</span><br><span class="line"> <span class="keyword">if</span> (!data.<span class="property">title</span> || data.<span class="property">title</span>.<span class="title function_">trim</span>() === <span class="string">''</span>) {</span><br><span class="line"> data.<span class="property">title</span> = <span class="string">"Untitled"</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> data_title = data.<span class="property">title</span>.<span class="title function_">trim</span>();</span><br><span class="line"> <span class="keyword">const</span> data_content = data.<span class="property">content</span>.<span class="title function_">trim</span>().<span class="title function_">replace</span>(<span class="regexp">/<[^>]+>/g</span>, <span class="string">""</span>);</span><br><span class="line"> <span class="keyword">const</span> data_url = data.<span class="property">url</span>;</span><br><span class="line"> <span class="keyword">let</span> index_title = -<span class="number">1</span>;</span><br><span class="line"> <span class="keyword">let</span> index_content = -<span class="number">1</span>;</span><br><span class="line"> <span class="keyword">let</span> first_occur = -<span class="number">1</span>;</span><br><span class="line"> <span class="keyword">if</span> (data_content !== <span class="string">''</span>) {</span><br><span class="line"> keywords.<span class="title function_">forEach</span>(<span class="keyword">function</span> (<span class="params">keyword, i</span>) {</span><br><span class="line"> index_title = data_title.<span class="title function_">toLowerCase</span>().<span class="title function_">indexOf</span>(keyword.<span class="title function_">toLowerCase</span>());</span><br><span class="line"> index_content = data_content.<span class="title function_">toLowerCase</span>().<span class="title function_">indexOf</span>(keyword.<span class="title function_">toLowerCase</span>());</span><br><span class="line"> <span class="keyword">if</span> (index_title < <span class="number">0</span> && index_content < <span class="number">0</span>) {</span><br><span class="line"> isMatch = <span class="literal">false</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">if</span> (index_content < <span class="number">0</span>) {</span><br><span class="line"> index_content = <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (i === <span class="number">0</span>) {</span><br><span class="line"> first_occur = index_content;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> isMatch = <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (isMatch) {</span><br><span class="line"> str += <span class="string">"<li><a href='"</span> + data_url + <span class="string">"' class='search-result-title'>"</span> + data_title + <span class="string">"</a>"</span>;</span><br><span class="line"> <span class="keyword">const</span> content = data.<span class="property">content</span>.<span class="title function_">trim</span>().<span class="title function_">replace</span>(<span class="regexp">/<[^>]+>/g</span>, <span class="string">""</span>);</span><br><span class="line"> <span class="keyword">if</span> (first_occur >= <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">let</span> start = first_occur - <span class="number">20</span>;</span><br><span class="line"> <span class="keyword">let</span> end = first_occur + <span class="number">80</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (start < <span class="number">0</span>) {</span><br><span class="line"> start = <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (start === <span class="number">0</span>) {</span><br><span class="line"> end = <span class="number">100</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (end > content.<span class="property">length</span>) {</span><br><span class="line"> end = content.<span class="property">length</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">let</span> match_content = content.<span class="title function_">substring</span>(start, end);</span><br><span class="line"> </span><br><span class="line"> keywords.<span class="title function_">forEach</span>(<span class="keyword">function</span> (<span class="params">keyword</span>) {</span><br><span class="line"> <span class="keyword">const</span> regS = <span class="keyword">new</span> <span class="title class_">RegExp</span>(keyword, <span class="string">"gi"</span>);</span><br><span class="line"> match_content = match_content.<span class="title function_">replace</span>(regS, <span class="keyword">function</span>(<span class="params">match</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"<em class=\"search-keyword\">"</span> + match + <span class="string">"</em>"</span>;</span><br><span class="line"> });</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> str += <span class="string">"<p class=\"search-result\">"</span> + match_content + <span class="string">"...</p>"</span></span><br><span class="line"> }</span><br><span class="line"> str += <span class="string">"</li>"</span>;</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> str += <span class="string">"</ul>"</span>;</span><br><span class="line"> $resultContent.<span class="property">innerHTML</span> = str;</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure>
<ol>
<li>函数定义和 AJAX 请求:<figure class="highlight js"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> searchFunc = <span class="keyword">function</span> (<span class="params">path, search_id, content_id</span>) {</span><br><span class="line"> <span class="string">'use strict'</span>;</span><br><span class="line"> $.<span class="title function_">ajax</span>({</span><br><span class="line"> <span class="attr">url</span>: path,</span><br><span class="line"> <span class="attr">dataType</span>: <span class="string">"xml"</span>,</span><br><span class="line"> <span class="attr">success</span>: <span class="keyword">function</span> (<span class="params">xmlResponse</span>) {</span><br><span class="line"> <span class="comment">// ... 后续代码</span></span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure>
这段代码定义了一个名为 <code>searchFunc</code> 的函数,它接受三个参数:
<ul>
<li><code>path</code>:XML 文件的路径</li>
<li><code>search_id</code>:搜索栏的 ID</li>
<li><code>content_id</code>:搜索结果显示区域的 ID<br>
函数使用 jQuery 的 AJAX 方法从指定的 <code>path</code> 获取 XML 数据。</li>
</ul>
</li>
<li>数据处理:<figure class="highlight js"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> datas = $(<span class="string">"entry"</span>, xmlResponse).<span class="title function_">map</span>(<span class="keyword">function</span> (<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="attr">title</span>: $(<span class="string">"title"</span>, <span class="variable language_">this</span>).<span class="title function_">text</span>(),</span><br><span class="line"> <span class="attr">content</span>: $(<span class="string">"content"</span>, <span class="variable language_">this</span>).<span class="title function_">text</span>(),</span><br><span class="line"> <span class="attr">url</span>: $(<span class="string">"url"</span>, <span class="variable language_">this</span>).<span class="title function_">text</span>()</span><br><span class="line"> };</span><br><span class="line">}).<span class="title function_">get</span>();</span><br></pre></td></tr></tbody></table></figure>
处理从 XML 响应中获取的数据,将每个 <code>entry</code> 元素转换为包含 <code>title</code>、<code>content</code> 和 <code>url</code> 的对象数组。<br>
这里使用了 jQuery 的 <code>map</code> 方法来遍历 XML 元素,并使用 <code>get()</code> 来将结果转换为普通数组。</li>
<li>搜索输入监听:<figure class="highlight js"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> $input = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(search_id);</span><br><span class="line"><span class="keyword">if</span> (!$input) <span class="keyword">return</span>;</span><br><span class="line"><span class="keyword">const</span> $resultContent = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(content_id);</span><br><span class="line"><span class="keyword">if</span> ($(<span class="string">"#local-search-input"</span>).<span class="property">length</span> > <span class="number">0</span>) {</span><br><span class="line"> $input.<span class="title function_">addEventListener</span>(<span class="string">'input'</span>, <span class="keyword">function</span> (<span class="params"></span>) {</span><br><span class="line"> <span class="comment">// ... 搜索逻辑</span></span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
设置对搜索输入框的监听。当用户在搜索栏中输入内容时就会触发搜索。<br>
这段代码混合使用了原生 JavaScript(<code>document.getElementById</code> 和 <code>addEventListener</code>)和 jQuery(<code>$("local-search-input").length</code>)。</li>
<li>搜索逻辑:<figure class="highlight js"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> str = <span class="string">'<ul class=\"search-result-list\">'</span>;</span><br><span class="line"><span class="keyword">const</span> keywords = <span class="variable language_">this</span>.<span class="property">value</span>.<span class="title function_">trim</span>().<span class="title function_">toLowerCase</span>().<span class="title function_">split</span>(<span class="regexp">/[\s\-]+/</span>);</span><br><span class="line">$resultContent.<span class="property">innerHTML</span> = <span class="string">""</span>;</span><br><span class="line"><span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">value</span>.<span class="title function_">trim</span>().<span class="property">length</span> <= <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line">datas.<span class="title function_">forEach</span>(<span class="keyword">function</span> (<span class="params">data</span>) {</span><br><span class="line"> <span class="comment">// ... 匹配逻辑</span></span><br><span class="line">});</span><br><span class="line">str += <span class="string">"</ul>"</span>;</span><br><span class="line">$resultContent.<span class="property">innerHTML</span> = str;</span><br></pre></td></tr></tbody></table></figure>
这是搜索功能的核心逻辑。将输入的搜索词与数据进行匹配,并构建搜索结果 HTML。</li>
<li>匹配和高亮显示:<figure class="highlight js"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (isMatch) {</span><br><span class="line"> str += <span class="string">"<li><a href='"</span> + data_url + <span class="string">"' class='search-result-title'>"</span> + data_title + <span class="string">"</a>"</span>;</span><br><span class="line"> <span class="keyword">const</span> content = data.<span class="property">content</span>.<span class="title function_">trim</span>().<span class="title function_">replace</span>(<span class="regexp">/<[^>]+>/g</span>, <span class="string">""</span>);</span><br><span class="line"> <span class="keyword">if</span> (first_occur >= <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// ... 截取匹配内容</span></span><br><span class="line"> keywords.<span class="title function_">forEach</span>(<span class="keyword">function</span> (<span class="params">keyword</span>) {</span><br><span class="line"> <span class="keyword">const</span> regS = <span class="keyword">new</span> <span class="title class_">RegExp</span>(keyword, <span class="string">"gi"</span>);</span><br><span class="line"> match_content = match_content.<span class="title function_">replace</span>(regS, <span class="keyword">function</span>(<span class="params">match</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"<em class=\"search-keyword\">"</span> + match + <span class="string">"</em>"</span>;</span><br><span class="line"> });</span><br><span class="line"> });</span><br><span class="line"> str += <span class="string">"<p class=\"search-result\">"</span> + match_content + <span class="string">"...</p>"</span></span><br><span class="line"> }</span><br><span class="line"> str += <span class="string">"</li>"</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
处理匹配结果的显示和关键词高亮。此处使用了正则表达式去除 HTML 标签,使用 <code>substring</code> 截取匹配上下文,然后使用 <code>replace</code> 和正则表达式实现关键词高亮。</li>
</ol>
<p>将 <code>search.js</code> 放在 <code>theme/hermes/source/js</code> 路径下,运行 <code>hexo g</code> 之后便会出现在 <code>public/js</code> 路径下。</p>
<h2 id="连接视图和脚本"><a class="markdownIt-Anchor" href="#连接视图和脚本"></a> 连接视图和脚本</h2>
<p>有了搜索视图和搜索脚本后,我们就可以把两者连接在一起。</p>
<p>在 Pug 文件中添加 <code>script</code> 标签:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">if config.search</span><br><span class="line"> script(src="/js/search.js")</span><br><span class="line"> script.</span><br><span class="line"> let search_path = "#{config.search.path}";</span><br><span class="line"> if (search_path.length === 0) {</span><br><span class="line"> search_path = "search.xml";</span><br><span class="line"> }</span><br><span class="line"> const path = "#{config.root}" + search_path;</span><br><span class="line"> searchFunc(path, 'local-search-input', 'local-search-results');</span><br></pre></td></tr></tbody></table></figure>
<p>首先是引入搜索脚本 <code>search.js</code>。前面说过,该脚本在 <code>public</code> 路径下时会是在 <code>js</code> 路径下。接着调用 <code>search.js</code> 中我们定义好的 <code>searchFunc</code> 函数。</p>
<p>最终效果:</p>
<p><img src="/posts/49c3/4.png" alt="img.png"></p>
</body></html></div></article></div></main><footer><div class="paginator"><a class="prev" href="41a3.html">上一篇</a><a class="next" href="75ba.html">下一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/49c3.html" data-full-url="https://cytrogen.icu/posts/49c3.html" data-mode="static">
<h3 class="webmention-title">Webmentions (<span class="webmention-count">0</span>)</h3>
<div class="webmention-list"></div>
<span>暂无 Webmentions</span>
</div><div class="copyright"><p class="footer-links"><a href="../friends/index.html">友链</a><span class="footer-separator"> ·</span><a href="../links/index.html">邻邦</a><span class="footer-separator"> ·</span><a href="../contact/index.html">联络</a><span class="footer-separator"> ·</span><a href="../colophon/index.html">营造记</a><span class="footer-separator"> ·</span><a href="../atom.xml">RSS订阅</a></p><p>© 2025 - 2026 <a href="https://cytrogen.icu">Cytrogen</a>, powered by <a href="https://hexo.io/" target="_blank">Hexo</a> and <a href="https://github.com/cytrogen/hexo-theme-ares" target="_blank">hexo-theme-ares</a>.</p><p><a href="https://blogscn.fun" target="_blank" rel="noopener">BLOGS·CN</a></p></div></footer></div></div><a class="back-to-top" href="#top" aria-label="返回顶部"><svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M3.293 9.707a1 1 0 010-1.414L9.586 2a2 2 0 012.828 0l6.293 6.293a1 1 0 01-1.414 1.414L11 3.414V17a1 1 0 11-2 0V3.414L2.707 9.707a1 1 0 01-1.414 0z"></path></svg></a><script>document.addEventListener('DOMContentLoaded', function() {
const codeBlocks = document.querySelectorAll('figure.highlight');
codeBlocks.forEach(block => {
let caption = block.querySelector('figcaption');
if (!caption) {
caption = document.createElement('figcaption');
block.insertBefore(caption, block.firstChild);
}
const info = document.createElement('div');
info.className = 'info';
const filename = caption.querySelector('span');
if (filename) {
filename.className = 'filename';
info.appendChild(filename);
}
const lang = block.className.split(' ')[1];
if (lang) {
const langSpan = document.createElement('span');
langSpan.className = 'lang-name';
langSpan.textContent = lang;
info.appendChild(langSpan);
}
const sourceLink = caption.querySelector('a');
if (sourceLink) {
sourceLink.className = 'source-link';
info.appendChild(sourceLink);
}
const actions = document.createElement('div');
actions.className = 'actions';
const codeHeight = block.scrollHeight;
const threshold = 300;
if (codeHeight > threshold) {
block.classList.add('folded');
const toggleBtn = document.createElement('button');
toggleBtn.textContent = '展开';
toggleBtn.addEventListener('click', () => {
block.classList.toggle('folded');
toggleBtn.textContent = block.classList.contains('folded') ? '展开' : '折叠';
});
actions.appendChild(toggleBtn);
}
const copyBtn = document.createElement('button');
copyBtn.textContent = '复制';
copyBtn.addEventListener('click', async () => {
const codeLines = block.querySelectorAll('.code .line');
const code = Array.from(codeLines)
.map(line => line.textContent)
.join('\n')
.replace(/\n\n/g, '\n');
try {
await navigator.clipboard.writeText(code);
copyBtn.textContent = '已复制';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '复制';
copyBtn.classList.remove('copied');
}, 3000);
} catch (err) {
console.error('复制失败:', err);
copyBtn.textContent = '复制失败';
setTimeout(() => {
copyBtn.textContent = '复制';
}, 3000);
}
});
actions.appendChild(copyBtn);
caption.innerHTML = '';
caption.appendChild(info);
caption.appendChild(actions);
const markedLines = block.getAttribute('data-marked-lines');
if (markedLines) {
const lines = markedLines.split(',');
lines.forEach(range => {
if (range.includes('-')) {
const [start, end] = range.split('-').map(Number);
for (let i = start; i <= end; i++) {
const line = block.querySelector(`.line-${i}`);
if (line) line.classList.add('marked');
}
} else {
const line = block.querySelector(`.line-${range}`);
if (line) line.classList.add('marked');
}
});
}
});
});</script><script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.querySelector('.theme-toggle');
if (!themeToggle) return;
const getCurrentTheme = () => {
return document.documentElement.getAttribute('data-theme') || 'light';
};
const updateUI = (theme) => {
const isDark = theme === 'dark';
themeToggle.setAttribute('aria-pressed', isDark.toString());
};
const setTheme = (theme) => {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper) {
pageWrapper.setAttribute('data-theme', theme);
}
// Find and remove the temporary anti-flicker style tag if it exists.
// This ensures the main stylesheet takes full control after the initial load.
const antiFlickerStyle = document.getElementById('anti-flicker-style');
if (antiFlickerStyle) {
antiFlickerStyle.remove();
}
localStorage.setItem('theme', theme);
updateUI(theme);
};
const toggleTheme = () => {
const current = getCurrentTheme();
const newTheme = current === 'light' ? 'dark' : 'light';
setTheme(newTheme);
};
updateUI(getCurrentTheme());
themeToggle.addEventListener('click', toggleTheme);
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', function(e) {
if (!localStorage.getItem('theme')) {
const theme = e.matches ? 'dark' : 'light';
setTheme(theme);
}
});
}
});
})();
</script><script src="../js/details-toggle.js" defer></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const backToTopBtn = document.querySelector('.back-to-top');
if (!backToTopBtn) return;
const toggleButtonVisibility = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const shouldShow = scrollTop > 200;
if (shouldShow) {
backToTopBtn.classList.add('is-visible');
} else {
backToTopBtn.classList.remove('is-visible');
}
};
let ticking = false;
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
toggleButtonVisibility();
ticking = false;
});
ticking = true;
}
};
const scrollToTop = (event) => {
event.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
window.addEventListener('scroll', handleScroll);
backToTopBtn.addEventListener('click', scrollToTop);
toggleButtonVisibility();
});
})();</script></body></html>