全文搜索
要在静态站点博客中实现全文搜索功能,你可以使用一些基于前端的搜索库,如Elasticlunr、FlexSearch或Fuse.js
使用 Fuse.js实现逻辑
-
1、准备数据:将你的Markdown文档转换为适合Fuse.js搜索的数据格式。你可以将Markdown文档的标题、内容和其他相关信息组织为一个数组
使用工具,把项目上所以文档,都转换为适合Fuse.js搜索的数据格式
-
2、创建Search组件:用于接收用户的搜索查询并使用Fuse.js进行搜索
转换搜索数据格式
要将项目中的所有文档转换为适合Fuse.js搜索的数据格式,可编写脚本
1、获取项目中的所有文档:
- 确定你想要包含在搜索中的文档类型,例如Markdown文件、HTML文件等。
- 确定文档存储的位置,可以是本地文件系统、数据库或其他存储介质。
2、解析文档内容:
- 根据文档类型,使用相应的解析器将文档内容提取为文本形式。
- 对于Markdown文件,可以使用第三方库(如
markdown-it)将Markdown解析为纯文本。
3、组织文档数据:
- 创建一个数组,用于存储每个文档的数据。
- 对于每个文档,至少需要指定一个唯一的标识符(例如ID)和一个表示文档内容的字段。
4、遍历项目中的文档:
- 使用合适的方法遍历项目中的所有文档。
- 对于每个文档,提取标题和内容等必要字段,并将其添加到搜索数据数组中。
5、存储搜索数据:
- 将搜索数据保存为一个单独的文件,以便在应用中使用。
- 可以选择将搜索数据存储为JSON文件,或将其存储在数据库中。
例子
在代码中,我们使用了Node.js的fs模块来读取和处理文件,markdown-it库来解析Markdown文件,以及Fuse.js库来进行搜索。你需要使用适合的模块来解析和处理文件内容,以及根据实际需求编写自定义方法来提取标题和内容。
请将 path/to/docs替换为实际的文档目录路径,并根据实际需求在自定义方法中编写适当的逻辑来提取标题和内容。
此脚本的执行步骤为:
- 遍历指定目录中的所有Markdown文件。
- 解析每个Markdown文件的内容,并提取标题和内容。
- 将提取到的标题、内容和文件路径组织为适合Fuse.js搜索的数据格式。
- 创建Fuse对象并进行搜索。
genSearchData.js 生成搜索数据
// 使用脚本来处理转换Fuse.js搜索的数据格式
const fs = require('fs');
const path = require('path');
const markdownIt = require('markdown-it');
const Fuse = require('fuse.js');
// 配置Fuse.js
const options = {
keys: ['title', 'content'],
};
// 存储转换后的数据
// const searchData = [];
// 解析Markdown文件并添加到搜索数据
const parseMarkdownFile = (filePath,searchData) => {
const markdownContent = fs.readFileSync(filePath, 'utf-8');
const md = markdownIt();
const htmlContent = md.render(markdownContent);
// 提取标题和内容
const titles = extractAllTitles(markdownContent); // 自定义提取标题的方法
const content = extractContent(htmlContent); // 自定义提取内容的方法
// // 添加到搜索数据
// searchData.push({
// id: filePath,
// title,
// content,
// });
// 添加到搜索数据
titles.forEach((title) => {
searchData.push({
id: filePath,
title,
content,
});
});
};
// 遍历目录中的Markdown文件
const traverseDirectory = (dirPath,searchData) => {
const files = fs.readdirSync(dirPath);
console.log('dirPath',dirPath)
files.forEach((file) => {
const filePath = path.join(dirPath, file);
console.log('file',file)
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
traverseDirectory(filePath,searchData);
} else if (path.extname(filePath) === '.md') {
parseMarkdownFile(filePath,searchData);
}
});
};
// 自定义方法:提取标题
function extractTitle(markdownContent) {
const titleRegex = /^#\s+(.*)$/m; // 匹配以#开头的行
const match = markdownContent.match(titleRegex);
if (match && match[1]) {
return match[1].trim(); // 返回匹配到的标题并去除首尾空格
}
return ''; // 如果未找到标题,则返回空字符串或默认标题
}
// 自定义方法:提取所有标题
function extractAllTitles(markdownContent) {
const titleRegex = /^#+\s+(.*)$/gm; // 匹配以#开头的行
const matches = [];
let match;
while ((match = titleRegex.exec(markdownContent))) {
const title = match[1].trim();
matches.push(title);
}
return matches;
}
// 自定义方法:提取内容
function extractContent(htmlContent) {
// 在这里编写逻辑来提取内容
// 返回提取到的内容字符串
// 这里可以根据需要进行自定义处理,例如:
// - 去除HTML标签
// - 去除特殊字符和换行符
// - 截取内容的摘要部分等
// 示例:去除HTML标签并返回纯文本内容
const sanitizedContent = htmlContent.replace(/<[^>]+>/g, '');
return sanitizedContent.trim(); // 去除首尾空格并返回内容
}
// 将搜索数据生成的searchData数据写入到一个JSON文件中
const saveSearchData = (searchData,filePath) => {
const jsonData = JSON.stringify(searchData, null, 2);
fs.writeFileSync(filePath, jsonData, 'utf-8');
};
// 生成路由path
const generateRoutePaths = (data) => {
const updatedData = [];
for (const item of data) {
// 处理标题字段生成 path 值
const path = item.title
.toLowerCase()
.replace(/ /g, '-') // 替换空格为短横线
.replace(/[^\w-]/g, ''); // 移除非字母、数字、短横线的字符
// 在数据项中添加 path 字段
const updatedItem = { ...item, path };
updatedData.push(updatedItem);
}
return updatedData;
};
// 需要转换的md文档
const md1 = path.resolve(__dirname,'../docs')
const md2 = path.resolve(__dirname,'../blog')
// 搜索数据集合
const searchDataJson = {
docsData:[],
blogData:[]
}
// 执行遍历和转换
traverseDirectory(md1,searchDataJson.docsData);
traverseDirectory(md2,searchDataJson.blogData);
// 保存json文件路径
const saveDataPath1 = path.join(__dirname,'../static/json/docsSearchData.json')
const saveDataPath2 = path.join(__dirname,'../static/json/blogSearchData.json')
// 保存搜索数据到文件
saveSearchData(searchDataJson.docsData,saveDataPath1);
saveSearchData(searchDataJson.blogData,saveDataPath2);
// 生成带有 path 字段的新 searchData 数据
// const updatedSearchData = generateRoutePaths(searchData);
// console.log('updatedSearchData',updatedSearchData)
// 例子
// 创建Fuse对象
// const fuse = new Fuse(searchData, options);
// // 传入搜索关键词,返回搜索结果
// const pattern = "test"
// const results = fuse.search(pattern)
// console.log(results,'result');
搜索组件
import React, { useState } from 'react';
import { useHistory } from '@docusaurus/router';
import { AudioOutlined } from '@ant-design/icons';
import { Input, Space } from 'antd';
import Fuse from 'fuse.js';
import searchData from '../../../static/json/searchData.json'; // 导入搜索数据
const SearchWrapper = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const history = useHistory();
const fuseOptions = {
keys: ['title', 'content'],
};
const fuse = new Fuse(searchData, fuseOptions);
const handleSearch = (value) => {
setQuery(value);
if (value) {
const searchResults = fuse.search(value);
console.log('searchResults', searchResults);
setResults(searchResults.map((result) => result.item));
} else {
setResults([]);
}
};
const onTitle = (id) => {
console.log('id', id);
const regex = /docs\\(.+)\.md$/;
const match = id.match(regex);
console.log('match', match);
// history.push('docs/intro');
};
const onChange = (e) => {
console.log('e', e);
handleSearch(e.target.value);
};
return (
<div>
<Input.Search
placeholder="搜索"
onSearch={handleSearch}
style={{ width: 200 }}
onChange={onChange}
// value={query}
/>
{results.length > 0 && (
<ul>
{results.map((result, index) => (
<li key={result.id + index} onClick={() => onTitle(result.id)}>
{result.title}
</li>
))}
</ul>
)}
</div>
);
};
export default SearchWrapper;