构建简单的Blog内容搜索方式

之前使用Ocean这款Hexo主题时,它的全文检索方式是使用hexo-generator-search这个库生成search.xmlxml文件会包含整站文章的标题和正文内容)。通过ocean.js在每个页面中异步加载xml文件,当用户进行搜索行为时,会触发search.js进行xml内容搜索和展示。

通过search.js进行内容搜索和展示通过search.js进行内容搜索和展示

但这种方式也存在一定弊端,一是资源会重复被加载(虽然目前市面上最新的浏览器已经实现第二次加载从硬盘读取缓存,但部分老旧版本浏览器仍会加载多次);二是这个xml文件体积极大,当网站有几百篇文章时这个体积可能会达到10M以上。

xml文件会被加载多次xml文件会被加载多次

当然我们也可以通过对hexo-generator-search进行配置来减少xml文件体积,例如将content设置为false就可以仅生成标题和元数据信息,大大减小xml文件体积

1
2
3
4
5
search:
path: search.xml
field: post
content: false #将content设置为false,生成的结果仅涵盖标题和其他元信息。
template: ./search.xml

那有没有更好的方案?譬如就不要xml文件了,还能不能实现内容搜索?这时我想到了——v2ex。在v2ex的搜索栏中进行搜索,会自动跳转到Google搜索引擎,并以“帖子URL前缀 + 搜索词”的形式搜索内容。这相当于在精确查找Google收录的页面内容。只要搜索引擎会及时收录内容,我们就可以借助该功能实现“站内搜索”啦!

效果截图效果截图

说干就干,先写出了第一个版本。回车搜索后就会前往Google进行搜索!但此时发现一个问题:手机上的回车键不生效啊

调试一番后发现移动端的e.keyCodeNumpadEnter,那给判断加上就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<input id="search" name="search" type="search">

<script>
/**
* 跳转搜索引擎进行关键词搜索
* @param {string} keyword
* @return {void}
*/
function searchKeyword (keyword) {
// 设置搜索框失焦,否则可能不会触发后续回车事件
document.getElementById('search').blur();

// 先打开Tab再跳转url,否则容易被拦截请求
const newWindow = window.open('_blank');
newWindow.location = 'https://www.google.com/search?q=' + keyword.split(' ').join('+') + '+site:https://www.youdiandongxi.com';
}

document.getElementById('search').addEventListener('keydown', function (e) {
// NumpadEnter是兼容手机端
if (e.code === 'NumpadEnter' || e.keyCode === 13) {
searchKeyword(e.target.value)
}
})
</script>

然后又想到google可能对大部分访客不友好,那就把bing、baidu都加上。三个拉出来一起跑,哪个速度快用哪个

判断思路也很简单,同时加载三个网站的favicon图标,哪个先出来用哪个!如果都失效了,就提示用户“搜索暂时不可用”!(逻辑主要在searchKeyword函数的Promise.any部分,以及testWebsiteConnectivity函数测试图片是否可访问)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 验证域名是否可访问
* @param {string} domain
* @return {Promise<string>}
*/
function testWebsiteConnectivity (domain) {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = function () {
resolve(domain)
}
img.onerror = function () {
reject(domain)
}
img.referrerPolicy = 'no-referrer'
img.src = domain + 'favicon.ico'

setTimeout(() => {
reject(domain)
}, 2000)
})
}

/**
* 跳转搜索引擎进行关键词搜索
* @param {string} keyword
* @return {void}
*/
function searchKeyword (keyword) {
const websites = ['https://www.google.com/', 'https://cn.bing.com/', 'https://www.baidu.com/']
const searchPrefix = ['search?q=', 'search?q=', 's?wd=']

const taskList = []
websites.forEach(website => {
taskList.push(testWebsiteConnectivity(website))
})

Promise.any(taskList).then(fastDomain => {
const index = websites.findIndex((domain) => domain === fastDomain);

// 设置搜索框失焦,否则可能不会触发后续回车事件
document.getElementById('search').blur();

// 先打开Tab再跳转url,否则容易被拦截请求
const newWindow = window.open('_blank');
newWindow.location = fastDomain + searchPrefix[index] + keyword.split(' ').join('+') + '+site:<%= config.url %>';
}).catch((e) => {
alert('搜索功能暂不可用,请稍后再试!')

// 设置搜索框失焦,否则可能不会触发后续回车事件
document.getElementById('search').blur();
})
}

document.getElementById('search').addEventListener('keydown', function (e) {
console.log(e)
if (e.code === 'NumpadEnter' || e.keyCode === 13) {
searchKeyword(e.target.value)
}
})

评论区