这个 API 允许网页安全地访问用户本地文件系统,前提是用户必须明确授权。
下面我将为你提供一个完整的、可直接使用的解决方案,包括详细的代码解释和使用方法。
最终效果预览
你会得到一个包含搜索框和搜索结果列表的页面,当你在搜索框中输入关键词时,它会自动扫描同目录下的所有 .html 文件,并在文件名和内容中匹配关键词,最后将结果展示出来。
完整代码 (单个 HTML 文件)
将以下所有代码复制到一个新的文本文件中,将其命名为 search.html(或其他你喜欢的名字),然后将其和你想要搜索的本地网页文件(page1.html, page2.html 等)放在同一个文件夹里。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">本地网页搜索</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f4f7f6;
color: #333;
margin: 0;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
}
#search-container {
margin-bottom: 20px;
}
#search-input {
width: 100%;
padding: 12px 15px;
font-size: 16px;
border: 2px solid #ddd;
border-radius: 25px;
outline: none;
transition: border-color 0.3s;
}
#search-input:focus {
border-color: #3498db;
}
#results-container {
list-style-type: none;
padding: 0;
}
.result-item {
padding: 15px;
border-bottom: 1px solid #eee;
border-radius: 5px;
margin-bottom: 10px;
background-color: #fdfdfd;
transition: background-color 0.2s;
}
.result-item:hover {
background-color: #f0f8ff;
}
.result-item a {
text-decoration: none;
color: #3498db;
font-weight: bold;
font-size: 18px;
display: block;
}
.result-item a:hover {
text-decoration: underline;
}
.result-snippet {
color: #666;
font-size: 14px;
margin-top: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.no-results {
text-align: center;
color: #999;
font-style: italic;
padding: 20px;
}
#status {
text-align: center;
color: #888;
font-size: 14px;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔍 本地网页搜索</h1>
<div id="search-container">
<input type="text" id="search-input" placeholder="输入关键词搜索本地网页..." autofocus>
</div>
<ul id="results-container">
<!-- 搜索结果将在这里动态显示 -->
</ul>
<div id="status">请输入关键词开始搜索...</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const searchInput = document.getElementById('search-input');
const resultsContainer = document.getElementById('results-container');
const statusDiv = document.getElementById('status');
// 存储所有已加载的网页文件内容
let indexedFiles = new Map();
// 初始化:请求访问文件系统并索引文件
async function init() {
try {
// 1. 请求用户选择一个目录(即存放网页的文件夹)
const dirHandle = await window.showDirectoryPicker();
// 2. 遍历目录,找到所有 .html 文件
for await (const entry of dirHandle.values()) {
if (entry.kind === 'file' && entry.name.endsWith('.html')) {
const file = await entry.getFile();
const contents = await file.text();
// 将文件名和内容存储起来,用于后续搜索
indexedFiles.set(entry.name, contents);
}
}
statusDiv.textContent = `已加载 ${indexedFiles.size} 个网页文件,请开始搜索,`;
} catch (error) {
// 用户可能取消了选择,或者浏览器不支持该API
if (error.name === 'AbortError') {
statusDiv.textContent = '操作已取消,请刷新页面并选择包含网页的文件夹。';
} else {
statusDiv.textContent = `初始化失败: ${error.message}`;
console.error('初始化失败:', error);
}
}
}
// 搜索函数
function performSearch(query) {
if (!query) {
resultsContainer.innerHTML = '';
statusDiv.textContent = '请输入关键词开始搜索...';
return;
}
const results = [];
const lowerCaseQuery = query.toLowerCase();
// 遍历已索引的文件
for (const [fileName, fileContent] of indexedFiles) {
// 1. 在文件名中搜索
if (fileName.toLowerCase().includes(lowerCaseQuery)) {
results.push({ fileName, fileContent, matchType: 'filename' });
continue; // 如果文件名匹配,就不需要再匹配内容了,提高效率
}
// 2. 在文件内容中搜索
const contentMatch = findSnippet(fileContent, lowerCaseQuery);
if (contentMatch) {
results.push({ fileName, fileContent, matchType: 'content', snippet: contentMatch });
}
}
// 显示结果
displayResults(results, query);
}
// 从内容中提取匹配片段
function findSnippet(content, query, snippetLength = 150) {
const lowerCaseContent = content.toLowerCase();
const index = lowerCaseContent.indexOf(query);
if (index === -1) {
return null;
}
const start = Math.max(0, index - snippetLength / 2);
const end = Math.min(content.length, index + query.length + snippetLength / 2);
const snippet = content.substring(start, end);
// 高亮显示匹配的词
const regex = new RegExp(`(${query})`, 'gi');
const highlightedSnippet = snippet.replace(regex, '<mark>$1</mark>');
return {
text: highlightedSnippet,
context: `...${snippet}...`
};
}
// 显示搜索结果到页面上
function displayResults(results, query) {
statusDiv.textContent = `找到 ${results.length} 个与 "${query}" 相关的结果,`;
if (results.length === 0) {
resultsContainer.innerHTML = '<li class="no-results">未找到相关结果。</li>';
return;
}
resultsContainer.innerHTML = '';
results.forEach(result => {
const li = document.createElement('li');
li.className = 'result-item';
const link = document.createElement('a');
link.href = `./${result.fileName}`; // 假设文件在同一目录
link.textContent = result.fileName;
link.target = '_blank'; // 在新标签页打开
const snippetDiv = document.createElement('div');
snippetDiv.className = 'result-snippet';
if (result.matchType === 'filename') {
snippetDiv.textContent = '匹配于文件名';
} else if (result.snippet) {
// 使用 innerHTML 来显示高亮效果
snippetDiv.innerHTML = `...${result.snippet.text}...`;
}
li.appendChild(link);
li.appendChild(snippetDiv);
resultsContainer.appendChild(li);
});
}
// 监听搜索框的输入事件
searchInput.addEventListener('input', (e) => {
performSearch(e.target.value);
});
// 页面加载时,首先初始化文件系统访问
init();
});
</script>
</body>
</html>
如何使用
- 创建文件:将上面的代码保存为
search.html文件。 - 准备网页:将你想要搜索的所有本地网页(
about.html,products.html,contact.html)和search.html放在同一个文件夹里。 - 打开浏览器:用 Chrome 或 Edge 等现代浏览器打开
search.html文件。 - 授权访问:
- 页面加载后,会弹出一个文件选择对话框,提示你“选择一个文件夹”。
- 请选择你刚刚创建的那个包含
search.html和其他网页的文件夹。 - 点击“选择”按钮,浏览器会请求你的授权,允许此网页访问你选择的文件夹,请点击“允许”或“授权”。
- 开始搜索:
- 授权成功后,页面下方会显示“已加载 X 个网页文件...”。
- 你可以在顶部的搜索框中输入任何关键词,它会实时搜索文件名和文件内容,并显示结果。
- 点击搜索结果中的链接,可以在新标签页中打开对应的网页。
代码核心逻辑解析
-
init()函数 - 初始化与索引window.showDirectoryPicker(): 这是 File System Access API 的核心,它会弹出一个系统级的文件夹选择对话框,让用户选择一个目录。await dirHandle.values(): 这是一个异步迭代器,可以遍历所选目录中的所有文件和子文件夹。entry.kind === 'file' && entry.name.endsWith('.html'): 我们只关心.html类型的文件。await entry.getFile()和await file.text(): 获取文件对象,然后读取其全部文本内容。indexedFiles.set(entry.name, contents): 将文件名和其内容存储在一个Map对象中,以便后续快速搜索。
-
performSearch(query)函数 - 执行搜索- 它接收用户输入的查询字符串。
- 遍历
indexedFiles这个Map。 - 首先在
fileName中进行不区分大小写的搜索。 - 如果文件名不匹配,再调用
findSnippet()函数在fileContent中搜索。 - 将所有匹配到的结果(文件名和内容片段)收集到一个
results数组中。
-
findSnippet()函数 - 提取并高亮显示片段- 这个函数负责从长文本中截取包含关键词的一小段内容,方便用户预览。
- 它还会使用
<mark>标签将匹配到的关键词包裹起来,实现高亮显示效果。
-
displayResults()函数 - 渲染结果- 它负责将搜索结果数组转换成 HTML 元素,并添加到页面的结果列表中。
- 为每个结果创建一个
<li>元素,里面包含一个指向文件的<a>链接和一个显示内容片段的<div>。
注意事项和局限性
- 浏览器兼容性:File System Access API 是一个较新的 Web API,目前主要在 基于 Chromium 的浏览器(Chrome, Edge, Opera) 中得到良好支持。Firefox 和 Safari 支持有限或不支持,在旧版浏览器中,此功能将无法工作。
- 安全性:由于涉及访问本地文件,此功能必须通过用户明确授权才能启动,这是浏览器为了保护用户隐私而设计的强制性安全措施。
- 首次加载:用户首次打开
search.html时,必须手动选择一次文件夹,之后每次刷新页面,只要文件夹内容不变,索引都会被保留(除非用户清除了浏览器数据)。 - 性能:对于包含大量网页(例如几百个)的文件夹,首次索引和搜索可能会有轻微的延迟,但对于个人项目或小型网站来说,性能是完全足够的。
