搞定~下面把“站内搜索”组件拆成一个最小可运行示例(三个文件),并逐行中文注释。功能:在输入框键入“标题/分类/正文”关键词,实时筛选并按命中得分排序,高亮命中词;无结果时给出提示。界面与截图一致(标题“站内搜索”+ 占位文案)。
search.html(逐行注释)<!DOCTYPE html> <!-- 声明 HTML5 文档类型 -->
<html lang="zh-CN"> <!-- 页面主语言为中文 -->
<head> <!-- 头部:元信息与样式 -->
<meta charset="UTF-8"> <!-- 防止中文乱码 -->
<meta name="viewport" content="width=device-width, initial-scale=1"> <!-- 移动端友好 -->
<title>站内搜索</title> <!-- 标签栏标题 -->
<link rel="stylesheet" href="search.css"> <!-- 引入样式表 -->
</head>
<body> <!-- 页面主体开始 -->
<section class="card"> <!-- 外层卡片容器,控制留白与边框 -->
<h2 class="title">站内搜索</h2> <!-- 模块标题(与截图一致) -->
<!-- 搜索输入框:占位文案与截图一致 -->
<input
id="q" <!-- DOM 选择用的 id -->
class="search-input" <!-- 输入框的样式类 -->
type="search" <!-- 搜索类型(移动端键盘更友好) -->
placeholder="输入标题、分类或正文关键字..." <!-- 占位提示 -->
autocomplete="off" <!-- 关闭浏览器自动补全 -->
aria-label="站内搜索输入框" <!-- 无障碍可读标签 -->
>
<!-- 结果统计信息:通过 aria-live 实时向读屏播报 -->
<div id="meta" class="meta" aria-live="polite">请输入关键字开始搜索</div>
<!-- 结果列表:JS 渲染 <li>;无 JS 时会预置完整列表作为兜底 -->
<ul id="results" class="result-list">
<!-- 无 JS 兜底:初始塞入若干条(与数据一致);JS 启动后会重渲染覆盖 -->
<li class="result-item">
<a href="#post-flexbox" class="result-title">Flexbox 布局与盒模型</a>
<div class="result-meta">分类:CSS / 布局</div>
<p class="result-excerpt">掌握主轴/交叉轴、justify-content、align-items 的组合与调试。</p>
</li>
<li class="result-item">
<a href="#post-bom" class="result-title">BOM 操作示例</a>
<div class="result-meta">分类:JavaScript / 浏览器</div>
<p class="result-excerpt">使用 window.location 与 window.scrollTo 实现一键返回顶部与跳转。</p>
</li>
</ul>
</section>
<script src="search.js"></script> <!-- 引入交互脚本 -->
</body>
</html>
search.css(逐行注释)/* ===== 页面基调与重置 ===== */
* { box-sizing: border-box; } /* 统一盒模型(padding/border 计入尺寸) */
body {
margin: 0; /* 去默认外边距 */
background: #fff; /* 白色背景 */
color: #212529; /* 正文字色 */
font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Noto Sans SC",Arial,sans-serif;
line-height: 1.6; /* 提升可读性 */
padding: 24px; /* 页面留白 */
}
/* ===== 卡片与标题 ===== */
.card {
max-width: 1100px; /* 限宽便于阅读 */
margin: 0 auto; /* 居中 */
padding: 20px; /* 内边距 */
border: 1px solid #dee2e6; /* 细描边 */
border-radius: 12px; /* 圆角 */
background: #fff; /* 白底 */
}
.title { margin: 0 0 12px; font-size: 28px; font-weight: 700; } /* 标题样式 */
/* ===== 搜索输入框(与截图风格一致) ===== */
.search-input {
width: 100%; /* 占满一行 */
height: 48px; /* 高度 */
padding: 0 16px; /* 左右内边距 */
border: 1px solid #ced4da; /* 边框 */
border-radius: 10px; /* 圆角 */
font-size: 16px; /* 字号 */
outline: none; /* 去默认外描边 */
}
.search-input:focus {
border-color: #0d6efd; /* 聚焦高亮边 */
box-shadow: 0 0 0 2px #0d6efd22; /* 微弱的外发光 */
}
/* ===== 统计信息与结果列表 ===== */
.meta { margin: 10px 2px 6px; color: #6c757d; } /* 灰色提示文本 */
.result-list {
list-style: none; /* 去圆点 */
padding: 0; margin: 8px 0 0; /* 顶部留白 */
display: grid; gap: 12px; /* 网格布局+行距 */
}
.result-item {
padding: 12px; /* 内边距 */
border: 1px solid #e9ecef; /* 边框 */
border-radius: 8px; /* 圆角 */
background: #f8f9fa; /* 浅灰底 */
}
.result-title { color: #0d6efd; text-decoration: none; font-weight: 700; } /* 标题链接 */
.result-meta { color: #6c757d; font-size: 14px; margin: 4px 0 6px; } /* 分类信息 */
.result-excerpt { margin: 0; } /* 摘要段落 */
mark { background: #ffe69c; } /* 高亮命中词的底色 */
search.js(逐行注释)// ===== 0. 示例数据:可替换为真实文章列表(标题/分类/正文/链接) =====
const POSTS = [ // 用数组存储每篇文章对象
{ title: 'Flexbox 布局与盒模型', category: 'CSS/布局', link: '#post-flexbox',
content: '掌握主轴 cross 交叉轴、justify-content、align-items 的组合与 DevTools 调试。' },
{ title: 'BOM 操作示例', category: 'JavaScript/浏览器', link: '#post-bom',
content: '使用 window.location 与 window.scrollTo 实现跳转与返回顶部,理解浏览器对象模型。' },
{ title: 'HTTP 状态码演示', category: '网络/调试', link: '#post-http',
content: '通过 fetch 访问 httpstat.us 观察 200/302/404/500,并在 Network 面板查看请求响应头。' },
{ title: 'XSS 风险演示', category: '安全/前端', link: '#post-xss',
content: '对比 innerHTML 注入与 textContent 安全渲染,理解事件型 payload 与输出编码的重要性。' },
{ title: '评论与本地存储', category: 'DOM/交互', link: '#post-comments',
content: '在 localStorage 保存评论,表单校验、渲染列表与 Application 面板调试。' }
];
// ===== 1. DOM 获取 =====
const input = document.getElementById('q'); // 搜索输入框
const meta = document.getElementById('meta'); // 统计信息区域
const list = document.getElementById('results'); // 结果列表容器
// ===== 2. 工具函数:转义 HTML,避免把结果当成标签解析 =====
function escapeHTML(str) { // 接收原始字符串
return str.replace(/[&<>"']/g, s => ({ // 使用映射表替换特殊字符
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
}[s]));
}
// ===== 3. 简单的“高亮”函数:把命中词用 <mark> 包起来(已对原文做转义) =====
function highlight(text, tokens) { // text: 已转义的纯文本;tokens: 关键字数组
let html = text; // 初始化为转义后的文本
tokens.forEach(t => { // 遍历每个关键字
if (!t) return; // 空词跳过
const re = new RegExp(`(${t.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')})`, 'ig'); // 构造忽略大小写的安全正则
html = html.replace(re, '<mark>$1</mark>'); // 命中包裹 <mark>
});
return html; // 返回带 <mark> 的 HTML 字符串
}
// ===== 4. 评分函数:标题>分类>正文,命中越多分越高 =====
function scorePost(post, tokens) { // 为每篇文章计算得分
let score = 0; // 初始化得分
const hay = (post.title + ' ' + post.category + ' ' + post.content).toLowerCase(); // 合并成一段小写文本
tokens.forEach(t => { // 遍历关键字
if (!t) return; // 空词跳过
const k = t.toLowerCase(); // 统一小写
// 命中标题 +3,分类 +2,正文 +1(只要包含即可加分,简单直观)
if (post.title.toLowerCase().includes(k)) score += 3;
if (post.category.toLowerCase().includes(k)) score += 2;
if (post.content.toLowerCase().includes(k)) score += 1;
// 额外:若全文包含多次,再加一点(体现频次)
const count = hay.split(k).length - 1;
score += Math.max(0, count - 1) * 0.2; // 从第二次命中开始每次 +0.2
});
return score; // 返回最终得分
}
// ===== 5. 渲染函数:根据查询词过滤 + 排序 + 生成列表 =====
function render(query) { // 接收查询字符串
const q = query.trim(); // 去除首尾空格
const tokens = q.split(/\\s+/).filter(Boolean); // 以空白分词并清掉空项
let data = POSTS.slice(); // 拷贝数据,避免修改原数组
if (tokens.length) { // 有关键词才过滤
data = data
.map(p => ({ p, s: scorePost(p, tokens) })) // 先计算每篇文章的得分
.filter(x => x.s > 0) // 只保留有分数的(至少有一个命中)
.sort((a, b) => b.s - a.s) // 按得分从高到低排序
.map(x => x.p); // 取回排好序的原文章对象
}
// 统计信息:数量 + 查询词回显
meta.textContent = tokens.length
? `共 ${data.length} 条结果:${q}`
: '请输入关键字开始搜索';
// 清空旧列表
list.innerHTML = '';
// 无结果时提示
if (!data.length) {
const li = document.createElement('li'); // 新建一条列表项
li.className = 'result-item'; // 设置样式
li.textContent = '没有匹配的内容,请尝试更换关键字。'; // 只输出文本(安全)
list.appendChild(li); // 插入列表
return; // 结束渲染
}
// 渲染每一条命中结果
data.forEach(post => {
const li = document.createElement('li'); // 创建列表项
li.className = 'result-item'; // 设置样式类
// 生成标题链接(对标题做转义与高亮)
const a = document.createElement('a'); // 创建 <a>
a.className = 'result-title'; // 标题样式
a.href = post.link; // 指向文章链接
a.innerHTML = highlight(escapeHTML(post.title), tokens); // 高亮后的标题 HTML
li.appendChild(a); // 加入列表项
// 分类信息(纯文本 + 高亮)
const meta = document.createElement('div'); // 分类容器
meta.className = 'result-meta'; // 样式
meta.innerHTML = '分类:' + highlight(escapeHTML(post.category), tokens); // 高亮分类
li.appendChild(meta); // 插入
// 摘要(正文片段,高亮关键字)
const p = document.createElement('p'); // 摘要段落
p.className = 'result-excerpt'; // 样式
p.innerHTML = highlight(escapeHTML(post.content), tokens); // 高亮摘要
li.appendChild(p); // 插入
list.appendChild(li); // 整条结果加入列表
});
}
// ===== 6. 输入事件:节流/防抖,提升体验 =====
let timer = null; // 用于防抖的定时器句柄
input.addEventListener('input', () => { // 监听用户输入
clearTimeout(timer); // 清除上次的等待
timer = setTimeout(() => render(input.value), 200); // 200ms 防抖后执行渲染
});
// ===== 7. 键盘增强:Esc 清空输入并重置结果 =====
input.addEventListener('keydown', e => { // 监听键盘事件
if (e.key === 'Escape') { // 如果按下 ESC
input.value = ''; // 清空输入框
render(''); // 渲染为空查询
}
});
// ===== 8. 页面首次加载:展示“请输入关键字开始搜索”提示 =====
render(''); // 初始化渲染(空查询)
search.html 即可运行。POSTS 数组改成你博客的文章数据(标题 title、分类 category、正文或摘要 content、链接 link)。core.css,或直接复用现成按钮/卡片样式类。要不要我再给你一版\\“从 markdown/front-matter 自动生成 POSTS 数据”\*\*的小脚本,方便从你的文章源里批量导入?