Hexo_搜索功能实现
参考资料
资源
- remixicon:图标
- hexo-generator-search:插件仓库
- Bootstrap:bootstrap框架
- bytedance:字节CDN
文档
- 给hexo添加本地搜索:插件启发
- 为Hexo博客添加全文搜索:代码提供,在此基础上修改
- Bootstrap模态框:bootstrap框架V5版本模态框文档
前言
花费点时间弄下博客的搜索功能吧,讲下大体的思路
前端上使用bootstrap
,好处是响应式框架不用自己额外做手机端的适配比较舒服,坏处是单纯为了实现模态框,引入整个框架,有点大炮打蚊子的既视感(只引入模态框相关的 CSS 和 JS,但不知道为啥不生效…)
后端的话,数据通过插件hexo-generator-search
生成,检索的话直接在页面执行ajax
不刷新请求加载数据
总体流程
- 插件安装:
hexo-generator-search
插件安装 - 搜索部件:搜索按钮+搜索框的实现
- 搜索功能:使用
ajax
实现 - 样式调整:样式的简单调整
插件安装
进入站点根目录,执行插件安装命令
npm install hexo-generator-search --save
修改站点配置文件_config.yml
,添加配置
注意:非主题配置文件,而是站点根目录下的_config.yml
search:
path: search.xml
field: post
这里只用设置2个即可,如果需要更多自定义,例如生成json
、排除文章被搜索之类的,具体参考1.2的文档,讲的比较详细
搜索部件
搜索通过2个部件组成,一个搜索框,一个搜索按钮
我们需要完成点击按钮然后出现搜索框
搜索框
直接使用Bootstrap的中等模态框,支持响应式,也不用去重头设计实现
因为是全局通用,所以需要在使用的主题的布局文件layout.ejs
中添加
示例:
在layout.ejs
中添加以下代码
<!--Hexo\themes\base\layout\layout.ejs -->
<%- partial('_partials/search-modal.ejs') %>
模态框search-modal.ejs
代码如下
<!-- Hexo\themes\base\layout\_partials\search-modal.ejs -->
<div class="modal fade" id="searchModal">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">搜索</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="text" class="form-control" id="searchInput" placeholder="Keyword">
<div class="search-content" id="searchResult"></div>
</div>
</div>
</div>
</div>
布局文件夹目录结构(修改5行对应文件,新增11行文件)
layout
├── archive.ejs
├── category.ejs
├── index.ejs
├── layout.ejs <----
├── page.ejs
├── post.ejs
├── tag.ejs
├── tags.ejs
└── _partials
└── search-modal.ejs <----
搜索按钮
图标可以使用Bootstrap
、fontawesome
、阿里巴巴图标库等
我这里为了主题风格统一,使用主题统一的remixicon
<i class="ri-search-line"></i>
然后使用a
标签包围包围即可,使用data-bs-toggle
、data-bs-target
与搜索框绑定bootstrap
会自动处理相应事件(点击打开搜索框)
<a class="imqi1-header-links-item" data-bs-toggle="modal" data-bs-target="#searchModal"><i class="ri-search-line"></i> 搜索</a>
之后添加到导航栏即可
搜索功能实现
在主题资源文件夹下,新建一个search.js
代码方面有两版,一版使用原生JavaScript实现,一版使用Jquery
实现Jquery
版本来自「王郁的小站」博主提供的代码,在此基础上修改(截断字符、修改标签为span
)
如果不想要引入额外的库Jquery
,就使用原生,根据自己选择即可
原生版本
// Hexo\themes\base\source\js\search.js
var searchFunc = function (path, search_id, content_id) {
"use strict";
fetch(path)
.then(function (response) {
// 检查响应是否 OK
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.text();
})
.then(function (xmlString) {
// 使用 DOMParser 解析 XML 字符串
var parser = new DOMParser();
var xmlResponse = parser.parseFromString(xmlString, "text/xml");
// get the contents from search data
var datas = Array.from(xmlResponse.querySelectorAll("entry")).map(
function (entry) {
return {
title: entry.querySelector("title").textContent,
content: entry.querySelector("content").textContent,
url: entry.querySelector("url").textContent,
};
}
);
var $input = document.getElementById(search_id);
var $resultContent = document.getElementById(content_id);
$input.addEventListener("input", function () {
var str = '<ul class="search-result-list">';
var keywords = this.value
.trim()
.toLowerCase()
.split(/[\s\-]+/);
$resultContent.innerHTML = "";
if (this.value.trim().length <= 0) {
return;
}
// perform local searching
datas.forEach(function (data) {
var isMatch = true;
var content_index = [];
if (!data.title || data.title.trim() === "") {
data.title = "Untitled";
}
var data_title = data.title.trim().toLowerCase();
var data_content = data.content
.trim()
.replace(/<[^>]+>/g, "")
.toLowerCase();
var data_url = data.url;
var index_title = -1;
var index_content = -1;
var first_occur = -1;
// only match artiles with not empty contents
if (data_content !== "") {
keywords.forEach(function (keyword, i) {
index_title = data_title.indexOf(keyword);
index_content = data_content.indexOf(keyword);
if (index_title < 0 && index_content < 0) {
isMatch = false;
} else {
if (index_content < 0) {
index_content = 0;
}
if (i == 0) {
first_occur = index_content;
}
}
});
} else {
isMatch = false;
}
// show search results
if (isMatch) {
str +=
"<li><a href='" +
data_url +
"' class='search-result-title'>" +
data_title +
"</a>";
var content = data.content.trim().replace(/<[^>]+>/g, "");
if (first_occur >= 0) {
// cut out 100 characters
var start = first_occur - 20;
var end = first_occur + 80;
if (start < 0) {
start = 0;
}
if (start == 0) {
end = 100;
}
if (end > content.length) {
end = content.length;
}
var match_content = content.substr(start, end);
// highlight all keywords
keywords.forEach(function (keyword) {
var regS = new RegExp(keyword, "gi");
match_content = match_content.replace(
regS,
'<span class="search-keyword">' + keyword + "</span>"
);
match_content = match_content.slice(0, 200);
});
str += '<p class="search-result">' + match_content + "...</p>";
}
str += "</li>";
}
});
str += "</ul>";
$resultContent.innerHTML = str;
});
})
.catch(function (error) {
console.error("Error fetching the XML file:", error);
});
};
jquery
版本
// Hexo\themes\base\source\js\search.js
// A local search script with the help of [hexo-generator-search](https://github.com/PaicHyperionDev/hexo-generator-search)
// Copyright (C) 2015
// Joseph Pan <http://github.com/wzpan>
// Shuhao Mao <http://github.com/maoshuhao>
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
// 02110-1301 USA
//
var searchFunc = function (path, search_id, content_id) {
"use strict";
$.ajax({
url: path,
dataType: "xml",
success: function (xmlResponse) {
// get the contents from search data
var datas = $("entry", xmlResponse)
.map(function () {
return {
title: $("title", this).text(),
content: $("content", this).text(),
url: $("url", this).text(),
};
})
.get();
var $input = document.getElementById(search_id);
var $resultContent = document.getElementById(content_id);
$input.addEventListener("input", function () {
var str = '<ul class="search-result-list">';
var keywords = this.value
.trim()
.toLowerCase()
.split(/[\s\-]+/);
$resultContent.innerHTML = "";
if (this.value.trim().length <= 0) {
return;
}
// perform local searching
datas.forEach(function (data) {
var isMatch = true;
var content_index = [];
if (!data.title || data.title.trim() === "") {
data.title = "Untitled";
}
var data_title = data.title.trim().toLowerCase();
var data_content = data.content
.trim()
.replace(/<[^>]+>/g, "")
.toLowerCase()
var data_url = data.url;
var index_title = -1;
var index_content = -1;
var first_occur = -1;
// only match artiles with not empty contents
if (data_content !== "") {
keywords.forEach(function (keyword, i) {
index_title = data_title.indexOf(keyword);
index_content = data_content.indexOf(keyword);
if (index_title < 0 && index_content < 0) {
isMatch = false;
} else {
if (index_content < 0) {
index_content = 0;
}
if (i == 0) {
first_occur = index_content;
}
// content_index.push({index_content:index_content, keyword_len:keyword_len});
}
});
} else {
isMatch = false;
}
// show search results
if (isMatch) {
str +=
"<li><a href='" +
data_url +
"' class='search-result-title'>" +
data_title +
"</a>";
var content = data.content.trim().replace(/<[^>]+>/g, "");
if (first_occur >= 0) {
// cut out 100 characters
var start = first_occur - 20;
var end = first_occur + 80;
if (start < 0) {
start = 0;
}
if (start == 0) {
end = 100;
}
if (end > content.length) {
end = content.length;
}
var match_content = content.substr(start, end);
// highlight all keywords
keywords.forEach(function (keyword) {
var regS = new RegExp(keyword, "gi");
match_content = match_content.replace(
regS,
'<span class="search-keyword">' + keyword + "</span>"
);
match_content = match_content.slice(0,200);
});
str += '<p class="search-result">' + match_content + "...</p>";
}
str += "</li>";
}
});
str += "</ul>";
$resultContent.innerHTML = str;
});
},
});
};
之后布局文件layout.ejs
中引入对应的脚本
<body>
<!-- 脚本 -->
<script src="/js/search.js"></script>
<script>searchFunc('<%= url_for('search.xml') %>', 'searchInput', 'searchResult');</script>
</body>
样式调整
根据实际需要调整即可
/* 搜索 - 关键字高亮 */
.search-keyword{
color: #c7254e;
}
/* 搜索 - 标题高亮 */
.search-result-list a{
color: #3498db;
}
/* 搜索 - 搜索结果排版 */
#searchResult {
margin-top: 23px;
}