起因

我的博客使用了 hugo 作为静态生成工具,自带的主题里也没有附带搜索功能。看来,还是得自己给博客添加一个搜索功能。

经过多方查找,从 Hugo Fast Search · GitHub 找到一片详细、可用的教程(虽然后面魔改了一些)。

实际案例步骤

  1. 在 config.toml 文件做好相关配置;
  2. 添加导出 JSON 格式文件的脚本,即在 layouts/_default 目录下添加 index.json 文件;
  3. 增加依赖的 JS 脚本,包含自己的 search.js 和 fuse.js 文件;
  4. 添加相关 HTML 代码;
  5. 添加相关 CSS 样式。

配置

[params]  # 是否开启本地搜索  fastSearch = true[outputs]  # 增加 JSON 配置  home = ["HTML", "RSS", "JSON"]

添加 index.json 文件

{{- $.Scratch.Add "index" slice -}}{{- range .Site.RegularPages -}}    {{- $.Scratch.Add "index" (dict "title" .Title "permalink" .Permalink "content" .Plain) -}}{{- end -}}{{- $.Scratch.Get "index" | jsonify -}}

添加依赖

首先,可以先添加 fuse.js 依赖,它是一个功能强大的轻量级模糊搜索库,可以到 官网 访问更多信息:

{{- if .Site.Params.fastSearch -}}{{- end -}}

然后,就是添加自定义的 search.js 文件以实现搜索功能,文件放置在 assets/js 目录下。

这里的代码和 Gist 上的有些许不同,经过了自己的魔改。

var fuse; // holds our search enginevar searchVisible = false;var firstRun = true; // allow us to delay loading json data unless search activatedvar list = document.getElementById('searchResults'); // targets the 
    var first = list.firstChild; // first child of search listvar last = list.lastChild; // last child of search listvar maininput = document.getElementById('searchInput'); // input box for searchvar resultsAvailable = false; // Did we get any search results?// ==========================================// The main keyboard event listener running the show//document.addEventListener("click", event => { var cDom = document.getElementById("fastSearch"); var sDom = document.getElementById('search-click'); var tDom = event.target; if (sDom == tDom || sDom.contains(tDom)) { showSearchInput(); } else if (cDom == tDom || cDom.contains(tDom)) { // ... } else if (searchVisible) { cDom.style.display = "none" searchVisible = false; }});document.addEventListener('keydown', function(event) { // CMD-/ to show / hide Search if (event.metaKey && event.which === 191) { showSearchInput() } // Allow ESC (27) to close search box if (event.keyCode == 27) { if (searchVisible) { document.getElementById("fastSearch").style.display = "none"; document.activeElement.blur(); searchVisible = false; } } // DOWN (40) arrow if (event.keyCode == 40) { if (searchVisible && resultsAvailable) { event.preventDefault(); // stop window from scrolling if ( document.activeElement == maininput) { first.focus(); } // if the currently focused element is the main input --> focus the first
  • else if ( document.activeElement == last ) { last.focus(); } // if we're at the bottom, stay there else { document.activeElement.parentElement.nextSibling.firstElementChild.focus(); } // otherwise select the next search result } } // UP (38) arrow if (event.keyCode == 38) { if (searchVisible && resultsAvailable) { event.preventDefault(); // stop window from scrolling if ( document.activeElement == maininput) { maininput.focus(); } // If we're in the input box, do nothing else if ( document.activeElement == first) { maininput.focus(); } // If we're at the first item, go to input box else { document.activeElement.parentElement.previousSibling.firstElementChild.focus(); } // Otherwise, select the search result above the current active one } }});// ==========================================// execute search as each character is typed//document.getElementById("searchInput").onkeyup = function(e) { executeSearch(this.value);}function showSearchInput() { // Load json search index if first time invoking search // Means we don't load json unless searches are going to happen; keep user payload small unless needed if(firstRun) { loadSearch(); // loads our json data and builds fuse.js search index firstRun = false; // let's never do this again } // Toggle visibility of search box if (!searchVisible) { document.getElementById("fastSearch").style.display = "block"; // show search box document.getElementById("searchInput").focus(); // put focus in input box so you can just start typing searchVisible = true; // search visible } else { document.getElementById("fastSearch").style.display = "none"; // hide search box document.activeElement.blur(); // remove focus from search box searchVisible = false; // search not visible }}// ==========================================// fetch some json without jquery//function fetchJSONFile(path, callback) { var httpRequest = new XMLHttpRequest(); httpRequest.onreadystatechange = function() { if (httpRequest.readyState === 4) { if (httpRequest.status === 200) { var data = JSON.parse(httpRequest.responseText); if (callback) callback(data); } } }; httpRequest.open('GET', path); httpRequest.send();}// ==========================================// load our search index, only executed once// on first call of search box (CMD-/)//function loadSearch() { fetchJSONFile('/index.json', function(data){ var options = { // fuse.js options; check fuse.js website for details includeMatches: true, shouldSort: true, ignoreLocation: true, keys: [ { name: 'title', weight: 1, }, { name: 'content', weight: 0.6, }, ], }; fuse = new Fuse(data, options); // build the index from the json file });}// ==========================================// using the index we loaded on CMD-/, run// a search query (for "term") every time a letter is typed// in the search box//function executeSearch(term) { if (term.length == 0) { document.getElementById("searchResults").setAttribute("style", ""); return; } let results = fuse.search(term); // the actual query being run using fuse.js let searchItems = ''; // our results bucket if (results.length === 0) { // no results based on what was typed into the input box resultsAvailable = false; searchItems = '
  • 无结果
  • '; } else { // build our html permalinkList = [] searchItemCount = 0 for (let item in results) { if (permalinkList.includes(results[item].item.permalink)) { continue; } // 去重 permalinkList.push(results[item].item.permalink); searchItemCount += 1; title = results[item].item.title; content = results[item].item.content.slice(0, 50); for (const match of results[item].matches) { if (match.key == 'title') { startIndex = match.indices[0][0]; endIndex = match.indices[0][1] + 1; highText = '' + match.value.slice(startIndex, endIndex) + ''; title = match.value.slice(0, startIndex) + highText + match.value.slice(endIndex); } else if (match.key == 'content') { startIndex = match.indices[0][0]; endIndex = match.indices[0][1] + 1; highText = '' + match.value.slice(startIndex, endIndex) + ''; content = match.value.slice(Math.max(0, startIndex - 30), startIndex) + highText + match.value.slice(endIndex, endIndex + 30); } } searchItems = searchItems + '
  • ' + '' + title + '
    '+ content +'
  • '; // only show first 5 results if (searchItemCount >= 5) { break; } } resultsAvailable = true; } document.getElementById("searchResults").setAttribute("style", "display: block;"); document.getElementById("searchResults").innerHTML = searchItems; if (results.length > 0) { first = list.firstChild.firstElementChild; // first result container — used for checking against keyboard up/down location last = list.lastChild.firstElementChild; // last result container — used for checking against keyboard up/down location }}

最后,需要将 search.js 依赖引入,如下是引入的代码:

{{ $search := resources.Get "js/search.js" | minify | fingerprint }}

添加 HTML 代码

HTML 页面的代码分为两个部分:搜索的按钮、搜索框和结果展示。

我这里将搜索的按钮放到的菜单栏,主要是一个可点击的按钮:

{{ if .Site.Params.fastSearch -}}
  • 搜索
  • {{- end }}

    对于搜索框,我选择的是弹出式的窗口,这里比较重要的是标签的 ID 需要和 search.js 脚本一致:

    {{ if .Site.Params.fastSearch -}}
      {{- end }}

      添加 CSS 样式

      页面样式这部分,主要是看个人的喜好,这里只放出自己的样式:

      #fastSearch {    display: none;    position: fixed;    left: 50%;    top: calc(5vw + 40px);    transform: translateX(-50%);    z-index: 4;    width: 650px;    background-color: #fff;    box-shadow: 0 1px 2px #3c40434d, 0 2px 6px 2px #3c404326;    border-radius: 4px;    overflow: hidden;    input {        padding: 10px;        width: 100%;        height: 30px;        font-size: 18px;        line-height: 30px;        border: none;        outline: none;        font-family: inherit;    }    #searchResults {        display: none;        overflow-y: auto;        max-height: 60vh;        padding-left: 0;        margin: 0;        border-top: 1px dashed #ddd;        .search-highlight {            color: red;        }        li {            list-style: none;            margin: 0;            a {                text-decoration: none;                color: inherit;                padding: 6px 10px;                display: block;                font-size: 14px;                letter-spacing: .04em;            }            a:hover,            a:focus {                filter: brightness(93%);                outline: 0;                background-color: rgb(240, 240, 240);            }            .title {                font-weight: 600;            }        }        li.noSearchResult {            text-align: center;            margin: 8px 0;            color: #888;        }    }}

      样例展示

      总结

      经过两天时间的奋斗,终于是将搜索功能给上线了。

      不得不说,理想总是一开始美好,最初以为是一个完整、可用的教程,却没想到复制到代码之后就不可用了,最终是经过自己的魔改才得以使用。

      总结一下就是,没有实践就没有话语权,千万不要做管中窥豹的那个人。

      首发于翔仔的个人博客,点击查看更多。