写在前面
最近一直想折腾一下博客的全文本搜索, 但是找了很多博客都比较老旧了, 尤其是使用 Luna.js 以及使用 simple-Jekyll-Search 等插件的, 基本上最近的都在 3 年前了… 并且 simple 这个库不更新了, 看知乎(为Jekyll+GitHub Pages添加全文搜索功能)还有一些特殊字符提取的bug.
后来看到了这篇文章:
感觉可行! 于是就一点一点配置了.
后来不知道为什么, (一开始猜测可能是我的博客内容比较多) 总是显示匹配数量为 0, 但是后来我精简了一下用例, 还是不行.
百思不得解的时候我联系了作者,他提供了一个办法: 浏览器控制台调试, 不得不说, 真的有用!
最后的问题竟然是一个小小的
&
…
下面主要说一下我在我的博客上成功配置作者的 js 搜索脚本的方法以及一些存在的问题, 具体的话还是看作者提供的思路, 非常好用!
步骤
添加 js 文件
下面的内容放在/assets/js/search.js
中即可:(根目录是博客目录)
/**
* 网站文章内容搜索功能实现
* Copyright (c) 2020 knightyun.
* <https://github.com/knightyun/knightyun.github.io/assets/js/search.js>
* @todo 多关键词搜索
*/
// 获取搜索框、搜索按钮、清空搜索、结果输出对应的元素
var elSearchBox = document.querySelector('.search'),
elSearchBtn = document.querySelector('.search-start'),
elSearchClear = document.querySelector('.search-clear'),
elSearchInput = document.querySelector('.search-input'),
elSearchResults = document.querySelector('.search-results');
// 声明保存文章的标题、链接、内容的数组变量
var searchValue = '', arrItems = [], arrContents = [], arrLinks = [],
arrTitles = [], arrResults = [], indexItem = [], itemLength = 0;
var tmpDiv = document.createElement('div'),
tmpAnchor = document.createElement('a');
var isSearchFocused = false;
// ajax 的兼容写法
var xhr = new XMLHttpRequest() || new ActiveXObject('Microsoft.XMLHTTP');
// 获取根目录下 feed.xml 文件内的数据
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
var xml = xhr.responseXML;
if (!xml) // xml 验证
return;
arrItems = xml.getElementsByTagName('item');
itemLength = arrItems.length;
// 遍历并保存所有文章对应的标题、链接、内容到对应的数组中
// 同时过滤掉 HTML 标签
for (i = 0; i < itemLength; i++) {
arrContents[i] = arrItems[i]
.getElementsByTagName('description')[0]
.childNodes[0]
.nodeValue.replace(/<.*?>/g, '');
arrLinks[i] = arrItems[i]
.getElementsByTagName('link')[0]
.childNodes[0]
.nodeValue.replace(/<.*?>/g, '');
arrTitles[i] = arrItems[i]
.getElementsByTagName('title')[0]
.childNodes[0]
.nodeValue.replace(/<.*?>/g, '');
// console.log(arrItems[i]);
// console.log(arrItems[i].getElementsByTagName('title')[0]);
// console.log(arrItems[i].getElementsByTagName('title')[0].childNodes[0]);
// console.log(arrItems[i]
// .getElementsByTagName('title')[0]
// .childNodes[0]
// .nodeValue.replace(/<.*?>/g, ''));
// console.log(arrTitles[i]);
}
// 内容加载完毕后显示搜索框
elSearchBox.style.display = 'block';
}
};
xhr.open('get', '/feed.xml', true);
xhr.send();
// 绑定按钮事件
elSearchBtn.onclick = searchConfirm;
elSearchClear.onclick = searchClear;
// 输入框内容变化后就开始匹配,可以不用点按钮
// 经测试,onkeydown, onchange 等方法效果不太理想,
// 存在输入延迟等问题,最后发现触发 input 事件最理想,
// 并且可以处理中文输入法拼写的变化
elSearchInput.oninput = function() { setTimeout(searchConfirm, 0); };
elSearchInput.onfocus = function() { isSearchFocused = true; };
elSearchInput.onblur = function() { isSearchFocused = false; };
/** 搜索确认 */
function searchConfirm() {
if (elSearchInput.value == '') {
searchClear();
} else if (elSearchInput.value.search(/^\s+$/) >= 0) {
// 检测输入值全是空白的情况
searchInit();
var itemDiv = tmpDiv.cloneNode(true);
itemDiv.innerText = '请输入有效内容...';
elSearchResults.appendChild(itemDiv);
} else {
// 合法输入值的情况
searchInit();
searchValue = elSearchInput.value;
// 在标题、内容中匹配搜索值
searchMatching(arrTitles, arrContents, searchValue);
}
}
/** 搜索清空 */
function searchClear() {
elSearchInput.value = '';
elSearchClear.style.display = 'none';
elSearchResults.style.display = 'none';
elSearchResults.classList.remove('result-item');
}
/** 每次搜索完成后的初始化 */
function searchInit() {
arrResults = [];
indexItem = [];
elSearchResults.innerHTML = '';
elSearchClear.style.display = 'block';
elSearchResults.style.display = 'block';
elSearchResults.classList.add('result-item');
}
/**
* 匹配搜索内容
* @param {string[]} arrTitles - 所有文章标题
* @param {string[]} arrContents - 所有文件内容
* @param {string} input - 搜索内容
*/
function searchMatching(arrTitles, arrContents, input) {
var inputReg;
try {
// 转换为正则表达式,忽略输入大小写
inputReg = new RegExp(input, 'i');
} catch (_) {
var errorInputDiv = tmpDiv.cloneNode(true);
errorInputDiv.innerHTML =
'正则表达式语法错误,特殊符号前考虑加上转义符:"∖"';
errorInputDiv.className = 'pink-text result-item';
elSearchResults.appendChild(errorInputDiv);
return;
}
// 在所有文章标题、内容中匹配搜索值
for (i = 0; i < itemLength; i++) {
var titleIndex = arrTitles[i].search(inputReg);
var contentIndex = arrContents[i].search(inputReg);
var resultIndex, resultArr;
if (titleIndex !== -1 || contentIndex !== -1) {
// 优先搜索标题
if (titleIndex !== -1) {
resultIndex = titleIndex;
resultArr = arrTitles;
} else {
resultIndex = contentIndex;
resultArr = arrContents;
}
// 保存匹配值的索引
indexItem.push(i);
var len = resultArr[i].match(inputReg)[0].length;
var step = 10;
// 将匹配到内容的地方进行黄色标记,并包括周围一定数量的文本
arrResults.push(
resultArr[i].slice(resultIndex - step, resultIndex) + '<mark>' +
resultArr[i].slice(resultIndex, resultIndex + len) + '</mark>' +
resultArr[i].slice(resultIndex + len, resultIndex + len + step));
}
}
// 输出总共匹配到的数目
var totalDiv = tmpDiv.cloneNode(true);
totalDiv.className = 'result-title';
totalDiv.innerHTML = '总匹配:<b>' + indexItem.length + '</b> 项';
elSearchResults.appendChild(totalDiv);
// 未匹配到内容的情况
if (indexItem.length == 0) {
var noneItemDiv = tmpDiv.cloneNode(true);
noneItemDiv.innerText = '未匹配到内容...';
noneItemDiv.className = 'teal-text text-darken-3 result-item';
elSearchResults.appendChild(noneItemDiv);
}
// 将所有匹配内容进行组合
for (i = 0; i < arrResults.length; i++) {
var itemDiv = tmpDiv.cloneNode(true);
var itemTitleDiv = tmpDiv.cloneNode(true);
var itemDetailDiv = tmpDiv.cloneNode(true);
var itemDetailDivAnchor = tmpAnchor.cloneNode(true);
itemDiv.className = 'card hoverable result-item';
itemTitleDiv.className = 'card-content result-item-title';
itemDetailDiv.className = 'card-action result-item-detail';
itemDetailDivAnchor.className = "blue-text";
itemTitleDiv.innerText = arrTitles[indexItem[i]];
itemDetailDivAnchor.innerHTML = arrResults[i];
itemDetailDivAnchor.href = arrLinks[indexItem[i]];
itemDiv.appendChild(itemTitleDiv);
itemDetailDiv.appendChild(itemDetailDivAnchor);
itemDiv.appendChild(itemDetailDiv);
elSearchResults.appendChild(itemDiv);
}
};
window.addEventListener('load', searchClear);
// 搜索快捷键
document.addEventListener('keydown', function(evt) {
if (isSearchFocused) return;
if (evt.key === '/') {
evt.preventDefault();
elSearchInput.focus();
window.isSearchFocused = true;
}
});
有改动, 我加了一些调试信息. (注释掉了)
feed.xml
一个生成 feed 文件(最后就从这里搜索内容)的 liquid 模板: (最后发现就是这里出了问题)
---
---
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>{{ site.title }}</title>
<link>{{ site.url }}</link>
<description>{{ site.description }}</description>
<lastBuildDate>{{ site.time | date_to_rfc822 }}</lastBuildDate>
{% for post in site.posts %}
<item>
<title>{{ post.title | escape }}</title>
<link>
{{ post.url | prepend: site.url }}
</link>
<description>
{{ post.content | escape }}
</description>
<pubDate>{{ post.date | date_to_rfc822 }}</pubDate>
<guid>
{{ post.url | prepend: site.url }}
</guid>
</item>
{% endfor %}
</channel>
</rss>
放在根目录/
css 样式(可选)
.search {
position: revert;
color: orange;
height: 350px;
text-align: right;
line-height: 30px;
padding-right: 10px;
}
.search .search-icon {
float: right;
height: 100%;
margin: 0 10px;
line-height: 30px;
cursor: pointer;
user-select: none;
}
.search .search-input {
float: right;
width: 30%;
height: 30px;
color: orange;
background-color: black;
line-height: 30px;
margin: 0;
border: 2px solid #ddd;
border-radius: 10px;
box-sizing: border-box;
}
.search .search-results {
display: block;
z-index: 1000;
position: absolute;
top: 30px;
right: 50px;
width: 60%;
max-height: 400px;
overflow: auto;
text-align: left;
border-radius: 5px;
/* background: #ccc;*/
box-shadow: 0 .3rem .5rem #333;
}
.card {
max-width: 60rem;
}
.search .search-results .result-item {
/* background: aqua;*/
color: #000;
margin: 5px;
padding: 3px;
color: white;
border-radius: 3px;
cursor: pointer;
}
为了配合我的主题做了一些修改.
放在 /assets/css/search.css
下.
图标
修改之前的 /_includes/head/favicon.html
文件, 添加图标源.
<!-- 搜索 -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
部署
主配置 _config.yaml
:
## => Search
##############################
search:
provider: custom # "default" (default), false, "google", "custom"
_includes
下新建一个目录, 然后新建search.html
文件.
.
├── article-footer.html
├── article-info.html
├── head
│ └── favicon.html
└── search-providers
└── custom
└── search.html
内容如下:
这里的
async
关键字很重要, 没有的话会直接加载 js 导致错误.
<link rel="stylesheet" type="text/css" href="/assets/css/search.css">
<script async src="/assets/js/search.js"></script>
<div class="search">
<i class="material-icons search-icon search-start">search</i>
<input type="text" class="search-input" placeholder="Searching..." />
<i class="material-icons search-icon search-clear">clear</i>
<div class="search-results z-depth-4"></div>
</div>
问题
下面来说说为什么一个 &
导致了搜索失败.
一开始我以为文本内容过多导致搜索失败, 后来发现其实不管有多少文本, 搜索其实都是很快的(静态的本地搜索, 借助客户端)
后来我以为是文本文件内的特殊字符, 那这个找起来可就费劲了. 300 多篇文章, 我用排除法一点一点找, 后来发现就是一个文件出了问题:
2022-05-10-Win&Mac下R语言安装xlsx包的完全解决方案.md
乍看下没有问题, 可是 xml 他不支持 &
啊!
保留这个文件, 查看 feed.xml, 发现 xml 格式不对…
博客文章的文件名不要加特殊字符!
博客文章的文件名不要加特殊字符!
博客文章的文件名不要加特殊字符!
血的教训…
于是, 问题解决了.
blog
附上我的新博客地址:
思考
-
遇到问题不要钻牛角尖(虽然对我来说还是有点难, 慢慢克服吧)
-
不要想当然(虽然有时候问题的解决也需要发散思考), 程序方面的问题大多还是要一步一步打 log 调试的, 一步一步追踪 肯定能找到问题
(当然, 玄学问题目前无解)
-
遇到问题不要放弃, 但是可以先记录下来, 一直想一个问题其实很浪费时间.