博客优化:Github Page博客添加搜索功能

JS实现样式和开关,Simple-Jekll-Search实现搜索

Posted by BlackDn on January 16, 2023

“如白驹过隙,转瞬即逝。如山岳层峦,连绵不绝。”

博客优化:Github Page 博客添加搜索功能

前言

闲里偷忙瞧瞧给博客整了个搜索功能
这两天下雪了 ❄️,真好
南方孩子第一次看到积雪,激动坏了 ⛄

Github Page 简介

GitHub Pages是 Github 推出的一个静态网站托管服务,它可以从仓库获取 HTML、CSS 和 JavaScript 文件,并解析成一个网站,通过特定的域名进行访问(大多为xxx.github.io),分为个人、组织、项目三种。
所以其实很多开源项目的首页都是通过 Github Page 来实现的,比如著名开源组织Square Open Source的网站(OKHttp就是他们整的)

Github Page 规定一个账户只能创建一个 个人或组织站点,项目站点则没有限制。简单来说,我们可以把 Github Page 当成一个服务器,自然可以搭建博客。如果你也想通过 Github Page 搭建个人博客,可以参考qiubaiying这篇文章(Readme.md)。最初我也是通过这篇文章搭建的博客,在此感谢 BY 大佬。

虽然 BY 大佬为我们提供了模板,但是其中并没有搜索功能。一开始我的文章比较少还无所谓,但是随着文章的变多,寻找一些历史文章就必须通过首页-> Tag -> 文章 来进行查找,比较费时费力,特别是我还有一些记录 Git 命令或操作流程的文章,经常需要用到(想不起来就得来看看自己小抄嘛)。
于是乎,就想着来写一个搜索的功能啦,不过最初我只是有这个想法,不知道如何入手,直到看到了Weiwq 的博客,在这里感谢 Weiwq 大佬。(ps. Weiwq 的博客中还有许多有趣小功能,如置顶、中英文转换等)

Github Page 作为个人博客的优缺点

既然提到了就顺便说一下优缺点吧,这玩意我老早就想吐槽了=。=
优点比较简单,就是咱不用自己租服务器,所以基本上是白嫖,白嫖谁不爱嘛。而且博客模板(主题)也有许多开源的代码实现了,可以找找自己喜欢的样式(当然 Hexo 等也自带许多主题,有些高大上的还得收费呢)
缺点说实话就有点多了,因为用的是 Github 的服务器,再加上“网络长城”的限制,导致访问不是很稳定,常会有图裂的情况;其次就是前不久 Github Page 对其使用作出了限制,包括但不限于以下几点:

  1. GitHub Pages 的存储仓库建议不超过 1GB(软限制,Github 对每个仓库都有这个建议,毕竟太大克隆都要下载半天)
  2. GitHub Pages 的发布站点不能超过 1GB(硬限制,但是我要是能写文章超过 1GB,估计那会儿头发也掉光了)
  3. 带宽限制每月 100GB(软限制)
  4. 限制站点每小时 10 次生成(软限制)
  5. 在高并发或流量高峰期会限制速率

具体内容可见Github Pages 使用限制
但是要我说,还是钱钱重要,什么流量容量限制,对我这种没几篇文章,文章也没几个字的小众人物来说跟没有一样嘛。钱钱 💰,我的钱钱 💰

实现搜索功能

如果我的 gif 能正常加载的话,咱们的一个简单的搜索功能大概是下面这样:
导航栏的右上角多了个搜索图标,点击后弹出一个搜索页面;在搜索框中输入会动态检索匹配的文章,并允许我们点击跳转。

preview

假设我们是 BY 大佬的同门弟子,那么现在我们的博客就是通过 Jekyll 来生成的。Hexo 大家可能听的会比较多,Jekyll 和它差不多,都是静态网页的生成器,想要进一步了解的可以移步Jekyll 官网
这里先让大家知道有这么个东西,除了我们熟悉的三剑客外,等会还得靠它实现搜索功能呢。

实现静态布局

然后就正式开始实现搜索功能啦,当然先从简单的布局入手
首先我们在导航栏加一个搜索的 icon:

<!-- nav.html -->
······
<!-- Collect the nav links, forms, and other content for toggling -->
<div id="huxblog_navbar">
  <div class="navbar-collapse">
    <ul class="nav navbar-nav navbar-right">
      ······
      <!-- newly added -->
      <li class="search-icon">
        <a href="javascript:void(0)">
          <i class="fa fa-search"></i>
        </a>
      </li>
      <!-- newly added end -->
    </ul>
  </div>
</div>
<!-- /.navbar-collapse -->
······

然后我们新建一个_includes/search.html

<!-- search.html -->
<div class="search-page">
    <div class="search-icon-close-container">
      <span class="search-icon-close">
        <i class="fa fa-chevron-down"></i>
      </span>
    </div>
    <div class="search-main container">
      <div class="row">
        <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
          <form></form>
          <input type="text" id="search-input" placeholder="$ grep...">
          </form>
          <div id="search-results" class="mini-post-list"></div>
        </div>
      </div>
    </div>
  </div>

这个页面就是我们点击导航栏的搜索 icon 后弹出的页面,其内容分为两块,一个是向下的箭头图标 icon,点击它我们会回到原来的页面;然后就是搜索框和结果展示的部分。
这两处我们都使用了Font Awesome提供的图标(fa fa-searchfa fa-chevron-down

最后在default.html中引入搜索页面search.html,在body标签下添加一句: {% include search.html %}

实现点击搜索

不出意外的话,现在的导航栏上方会出现一个空白区域,那就是我们写的search.html,而右上角则是我们的导航栏和搜索图标(可能因为是白色融于背景看不见)。
接下来我们编写点击事件,点击搜索 icon,焦点给到输入框,实现动态搜索功能。

要如何实现动态搜索呢?这里我们参考了开源库Simple-Jekll-Search,它能为一个 Jekyll 博客提供搜索功能。其仓库中提供了示例(example 目录),有兴趣的话可以去进一步查看其使用。
总之,我们仿照它,新建js/simple-jekyll-search.min.js。内容有点长而且是.min.js的压缩 js 文件,可读性差,就不贴代码了,具体内容可以去我仓库的源文件复制一下:/js/simple-jekyll-search.min.js

有了simple-jekyll-search.min.js后,我们需要在_includes/footer.html中将其引用:

......
<!-- in footer.html -->
<!-- Bootstrap Core JavaScript -->
<script src="/js/bootstrap.min.js "></script>

<!-- newly added -->
<!-- Simple Jekyll Search -->
<script src="/js/simple-jekyll-search.min.js"></script>
<!-- newly added end -->

<!-- Custom Theme JavaScript -->
<script src="/js/hux-blog.min.js "></script>
......

然后在根目录下创建search.json文件,作为本地搜索结果的一个小数据源及其格式(开头的layout: null是告诉 Jekyll 让它别解析成其他文件了)

---
layout: null
---

[
  {% for post in site.posts %}
    {
      "title"    : "{{ post.title | escape }}",
      "subtitle" : "{{ post.subtitle | escape }}",
      "tags"     : "{{ post.tags | join: ', ' }}",
      "url"      : "{{ site.baseurl }}{{ post.url }}",
      "date"     : "{{ post.date }}"
    } {% unless forloop.last %},{% endunless %}
  {% endfor %}
]

最后通过 JS 实现点击弹出和关闭搜索页面的功能,这里写在了_includes/footer.html最下面,虽然不是很合规,不过可以保证我们页面渲染完了才进行绑定,避免出现控件还没渲染我们就找它的类名所导致的错误。

<!-- in footer.html -->
<!-- Simple Jekyll Search -->
<script>
  function htmlDecode(input) {
    var e = document.createElement("textarea");
    e.innerHTML = input;
    //if empty input
    return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
  }

  SimpleJekyllSearch({
    searchInput: document.getElementById("search-input"),
    resultsContainer: document.getElementById("search-results"),
    json: "/search.json",
    searchResultTemplate:
      '<div class="post-preview item"><a href="{url}"><h2 class="post-title">{title}</h2><h3 class="post-subtitle">{subtitle}</h3><hr></a></div>',
    noResultsText: "No results",
    limit: 50,
    fuzzy: false,
    templateMiddleware: function (prop, value, template) {
      if (prop === "subtitle" || prop === "title") {
        if (value.indexOf("code")) {
          return htmlDecode(value);
        } else {
          return value;
        }
      }
    },
  });

  $(document).ready(function () {
    var $searchPage = $(".search-page");
    var $searchOpen = $(".search-icon");
    var $searchClose = $(".search-icon-close");
    var $searchInput = $("#search-input");
    var $body = $("body");

    $searchOpen.on("click", function (e) {
      e.preventDefault();
      $searchPage.toggleClass("search-active");
      var prevClasses = $body.attr("class") || "";
      setTimeout(function () {
        $body.addClass("no-scroll");
      }, 400);

      if ($searchPage.hasClass("search-active")) {
        $searchClose.on("click", function (e) {
          e.preventDefault();
          $searchPage.removeClass("search-active");
          $body.attr("class", prevClasses);
        });
        $searchInput.focus();
      }
    });
  });
</script>

可以看到这里针对两种功能提供了方法。
上面的htmlDecode()SimpleJekyllSearch()是根据Simple-Jekll-Search而来的,其具体调用在simple-jekyll-search.min.js之中。
下面的方法则是实现点击 icon 之后的弹出/收起页面的功能,比较好看懂,通过经典的 JS(j Query)和 DOM 来实现。

简单来说,当我们点击首页的搜索 icon后,记录当前body标签的类名(var prevClasses = $body.attr('class') || ''),给新页面(搜索页面)的body加上'no-scroll'的类名;
然后toggleClass('search-active')会让当前页面在有'search-active'和没有这个类名之间切换(没有则添加,有则移除)。有这个类名表示在搜索页面,没有则不在;
在这之后,会进行一个判断,如果有这个类名,则说明我们进入了搜索页面,将焦点给到输入框($searchInput.focus()),并且为箭头 icon设置一个点击方法($searchClose.on()),点击箭头 icon后移除页面的'search-active'类名,并回滚body的类名(其实就是把之前加上的'no-scroll'去掉)

所以总的流程就是这样: 给搜索icon添加点击事件 -> 在首页点击搜索icon,打开搜索页 -> 添加'search-active'、'no-scroll'类名;给箭头icon添加点击事件 -> 在搜索页点击箭头icon,回到首页 -> 删掉'search-active'、'no-scroll'类名

添加样式

好了,现在我们首页顶上仍是有一片白色区域,点击搜索 icon 聚焦到搜索框,输入内容也能动态出现结果。
但是这片白色区域肯定不能就这样放着的,我们需要在点击搜索 icon 的时候将它弹出,这主要通过添加样式来实现。还记得我们在search.html里设定的很多类名,以及在footer.html中添加的'search-active'类名吧,他们代表了整个搜索页面,所以需要给他们设置样式。

虽然东西有点多,但是样式这玩意也没什么逻辑,所以可以直接按照以下步骤复制粘贴:

  1. 新建less/search.less文件,内容有点长就不贴出来了,可以直接去复制源文件:less/search.less
  2. less/hux-blog.less中引入search.less,在其开头添加一行@import "search.less";
  3. css/hux-blog.csscss/hux-blog.min.css中添加以下内容:
/* in hux-blog.css and hux-blog.min.css */
.search-page {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 100;
  background: #fff;
  -webkit-transition: all 400ms cubic-bezier(0.32, 1, 0.23, 1);
  transition: all 400ms cubic-bezier(0.32, 1, 0.23, 1);
  -webkit-transform: translate(0, 100%);
  -ms-transform: translate(0, 100%);
  transform: translate(0, 100%);
  opacity: 0;
}
.search-page.search-active {
  opacity: 1;
  -webkit-transform: translate(0, 0) scale(1, 1);
  -ms-transform: translate(0, 0) scale(1, 1);
  transform: translate(0, 0) scale(1, 1);
}
.search-page.search-active .search-main {
  opacity: 1;
}
.search-page .search-main {
  padding-top: 80px;
  height: 100%;
  opacity: 0;
  -webkit-transition: all 400ms cubic-bezier(0.32, 1, 0.23, 1) 250ms;
  transition: all 400ms cubic-bezier(0.32, 1, 0.23, 1) 250ms;
}
.search-page .search-main .row,
.search-page .search-main .row > div {
  height: 100%;
}
.search-page .search-icon-close-container {
  position: absolute;
  z-index: 1;
  padding: 16px;
  top: 0;
  right: 2px;
}
.search-page .search-icon-close-container i {
  font-size: 20px;
}
.search-page #search-input {
  font-family: "Fira Code", Menlo, Monaco, Consolas, "Courier New", monospace;
  border: none;
  outline: none;
  padding: 0;
  margin: 0;
  width: 100%;
  font-size: 30px;
  font-weight: bold;
  color: #404040;
}
@media only screen and (min-width: 768px) {
  .search-page #search-input {
    margin-left: 20px;
  }
}
.search-page #search-results {
  overflow: auto;
  height: 100%;
  -webkit-overflow-scrolling: touch;
  padding-bottom: 80px;
}
.search-icon a,
.search-icon-close {
  cursor: pointer;
  font-size: 30px;
  color: #311e3e;
  -webkit-transition: all 0.25s;
  transition: all 0.25s;
}
.search-icon a:hover,
.search-icon-close:hover {
  opacity: 0.8;
}
.search-icon,
.search-icon-close {
  font-size: 16px;
}

这里解释一下几个文件类型,css是我们熟悉的样式文件;.min.css表示压缩后的 css 文件,它通常去掉了去掉多余的注释、空格等,文件较小,易于加载,但是可读性差,通常没有空格和换行。所以其实hux-blog.min.csshux-blog.css的内容是一样的。在 VSCode 中,我们可以通过 Minify 插件快速根据 .css 文件生成 .min.css,就不需要自己手动修改 .min.css 了。
.less则是 Less 这种预处理 css 的语言的文件,和Saas 类似,允许使用变量、编写函数等,让 css 的编写更高效。

取消搜索页的外部滚动

这个时候我们的弹出/收起搜索页面和简单的搜索功能已经实现了,搜索结果的文章会随着我们在搜索框的输入动态更新。如果匹配的文章很多的话,我们可以滚动查看。
但是还有美中不足的一点,就是外部页面的滚动条仍然存在,我们仍然可以对外部页面进行滚动,虽然在视觉上没有什么影响,但是当我们推出搜索页面,就会发现外部页面的位置改变了。
为此,我们需要取消外部页面的滚动,这时候,我们之前在footer.html中所生成的'no-scroll'类名就派上用场了。

从上面的代码可知,我们实际上在进入搜索页面前记录了外部页面body的类名(实际上body没有类名),然后在进入搜索页面后body设置了'no-scroll'类名,最后退出搜索页面的时候回滚body的类名(变成进入前没有类名的状态)
所以'no-scroll'这个全新的类名就是用来取消外部滚动的,我们只用实现这个类名的样式即可。我们在hux-blog.csshux-blog.lesshux-blog.min.css这三个样式文件中加入以下代码即可:

/* in hux-blog.css, hux-blog.less, hux-blog.min.css */
.no-scroll {
  overflow-y: hidden;
}

最后

现在咱们预想的搜索功能就算实现啦
刷新一下博客来测试一下吧,记得要删掉缓存哦~

参考

  1. About GitHub Pages
  2. Jekyll
  3. Font Awesome
  4. Simple-Jekll-Search
  5. How to escape liquid template tags?