[WebKit]管好页面缓存

“最快的网络请求就是不发请求。”


无论何时有人说到这句话,我都会心一笑。这的确是真理,HTTP Cache对网页速度至关重要。

现在缓存的使用看似正渐入佳境,但还不够。下面HTTP Archive的数据显示在过去一年里(2011),  缓存的资源增加了10%,而同时页面资源却增加了12%,页面数据量增加了24%(LINK)



推动缓存的应用涉及很多方面,无法加以明确具体的进度。唯一所能明确的是应用缓存是我们必然的选择。

下面是收集到的一些比较明显的缓存问题,可以帮助我们了解下一步要做些什么:

  • 55%的资源没有指定Max-Age
  • 没有指定Max-Age的资源中有46%的资源在2周内并没有变化过。
  • 许多常用的页面资源仅允许缓存一两个小时。
  • 40-60%的页面访问者并没有对应的缓存。
  • 30%的页面用户有页面的完整缓存可用。
  • 用户平均需要花4小时的浏览时间才能拥有页面的完整缓存。


Max-Age的分布

在以前就提到缓存的一些空间限制的问题,缺少足够HTTP请求头的Bug, 以及过期淘汰算法的问题。可是一些页面并未指定缓存的headers,比如在响应中指定Max-Age。下面的例子指定资源可以缓存一年:

   Cache-Control: max-age=31536000

你既然读了这篇博客,那你很可能已经开始运用max-age了。不过下面这分HTTP Archive的图表显示55% 的资源并未指定Max-Age,也就是平均页面资源数是81个,其中有45个资源会被反复请求。



没有使用缓存并非'有意为之'

为什么55%的资源没有指定缓存信息?很可能是页面开发者缺少使用缓存的意识,认为自己的太简单,不需要使用缓存。另一种情况是很多资源是动态的(JSON, ads, beacons, …)所以无法缓存。HTTP Archieve同样相应的统计。


HTTP Achieve会在每月的1日和15日分析世界近Top 50K个页面,并记录每个资源的HTTP headers。我们可以使用这些数据来观察缓存的变化情况。上面说到有55%无法缓存的资源是出自2012年2月15日的数据,下图则显示这些资源在近两周(2月1日)及一个月(1月15日)中的变化情况:(HTTP Archive没有保存数据体,所以这些数据的唯一性是通过相同的URL, Last-Modified, ETag及Content-Length这些字段来标识的。)



没有指定max-age的资源中有46%超过2周没有变化过,也就是说平均每页本来可以有21个子资源是不需要的发起网络请求。另外38%的资源超过一个月没有变过,相当于平均每个页面17个这样的子资源。

可以看到这里的确有许多不必要的HTTP数据请求。个人认为这绝不是故意的,而是缺少使用缓存的意识。下面这个数据可以证明这一点,它显示出所有资源(包含所有可缓存的和不可缓存的资源)在2012年2月15日前近一年内未变化的情况。


可以看到无论是全部资源,或是未支持缓存的资源中,其中在2周及1月内未变化的比例十分相近(分别是44% vs. 46%, 及35% vs. 38%)。

*译注:下面是www.sina.com.cn的最近一年(2014/2/1)支持缓存资源的占比。:


以及缓存生命周期的分布:


对比相应的网络流量的变化情况:


可以看到缓存支持最高(81%)时,网络请求数及流量处于最低值(710kB)。


第三方内容

如果一个页面没有很好地支持缓存,只会影响他们的页面和用户。但如果一个第三方的内容开发者未能提供良好的缓存支持,就会影响到所有使用它的网页。反之,应用了较好的缓存支持,就可以大幅提升性能。

下面是HTTP Archive统计出的最常使用的30个第三方资源,以及它们的max-age值。

其中有些有趣的发现.

  • 简单的URLs有着较短的缓存时间 – 一些资源时间特别短,它们常常是在粘贴到页面中的一个片段。短的缓存时间有利于解决一些紧急的问题。
  • 较长的URLs有着同样较长的缓存时间 – 许多第三方bootstrap脚本会动态地加载其它资源,他们所生成的URLs一般都比较长,里面含有一些唯一标识符。如果出现了紧急问题,负责加载的脚本会被修改支加载不同URL的资源,这样就不需要对应更新那些资源了。
  • Facebook中点赞按钮去哪了? – Facebook的like.php和likebox.php是经常被使用的,但没有出现在上面的列表里。原因是不同的页面下会有一个唯一标识,从而降低了总体的占比。相比于那些负责加载的脚本,它们有着更为激进的过期策略,如no-cache, no-store, must-revalidate。
  • 拥有较短缓存时间的资源通常是异步的 – 为那些bootstrap脚本指定较短的缓存时间有利于应对紧急状况,但会因为发送条件更新(Conditional GET)请求而影响性能。幸运地是一些第三方资源开发者已经意识到这个问题,并提供了异步的方式来加载这些脚本,以减少较短的缓存时间带来的影响。

常用的第三方资源通常有不错的表现,但当我们考察更多的资源时,就可以发现这里相当的提升空间。


缓存容量不足 

根据2007年的一项实验(experiment) 40%-60%的用户没有所浏览页面的缓存,其中20%的浏览页面没有本地缓存。


缺少足够缓存的原因很多,我认为其中最重要的原因是过小的缓存容量。下面是我在我自己的设备测试的数据(一些浏览器的缓存大小会基于可用磁盘空间计算,而我的磁盘是250GB,其中54GB可用。):

  • Chrome:320MB
  • Internet Explorer 9:250 MB
  • Firefox 11: 830 MB (about:cache)
  • Opera 11: 20 MB (Preferences | Advanced | History)
  • iPhone 4, iOS 5.1: 30-35 MB (基于测试)
  • Galaxy Nexus: 18 MB (基于测试)

除了Firefox 11比较接近我期望的大小,其它的都太小了。一部钢铁侠2也有1.82GB了,根本不够用。(让浏览器中缓存多媒体文件!!!!!)。


现实的缓存状况 

为了提供让浏览增加缓存容量的依据,必须要获取真实用户的统计数据。为此参与Velocity Summit的各家浏览器团队一起完成了一项课题 。特别推荐Will Chan的提交的这篇Chromium cache metrics,里面列出了一些非常有价值的缓存数据。

主要数据有:

  • 近30%用户的缓存完整(320 MB)
  • 对于缓存满的用户,他们平均要花4小的浏览时间(约20小时)才会拥有完整的缓存
  • 7%的用户至少一周清一次缓存
  • 19%用户会一周经历一次"缓存损坏"的错误,然后清掉他们的缓存

最后一项关于缓存损坏的统计非常有趣,我非常欣赏这样的坦率。IE9团队有着相似的数据(something similar). IE 7&8 将缓存容量限制在50MB, 因为基于测试当时发现超过50MB并没有提升缓存命中率。而IE9中,他们终于知道更大的缓存容量的确可以改善缓存命中率:

In IE9, we took a much closer look at our cache behaviors to better understand our surprising finding that larger caches were rarely improving our hit rate. We found a number of functional problems related to what IE treats as cacheable and how the cache cleanup algorithm works. After fixing these issues, we found larger cache sizes were again resulting in better hit rates, and as a result, we’ve changed our default cache size algorithm to provide a larger default cache.


Will也认识到Chrome的320MB缓存限制需要更新。对于拥有完整缓存的用户而言,30%的比例有些偏低。不过用户也许只对少量的页面比较活跃,比如是只看看Gmail和Facebook。建议加一下针对活跃度的统计。很可能一个用户常常浏览大量的网页,虽然拥有满满的缓存,却体验着低效的页面加载时间。


下一步 

浏览器开发者对于改进缓存责无旁贷。1)提升缓存容量就是改进之一,特别是对于移动设备。2)需要提供和缓存容量及用户行为相关的数据。3)更加有效的淘汰算法(比如IE 9′s prioritization based on mime type )。 4)更加关注个性化,当用户访问常用页面可以得到更快的体验。


转载请注明出处: http://blog.youkuaiyun.com/horkychen  

原文地址: Cache them if you can

参考: <<HTTP缓存>>


<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>CAD属性块分类树状视图</title> <style> /* 禁用整个页面的滚动条 */ html, body { margin: 0; padding: 0; height: 100%; width: 100%; overflow: hidden; font-family: "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; white-space: nowrap; } /* 主容器 - 固定宽度避免闪烁 */ .container { display: flex; flex-direction: column; height: 100vh; width: 100vw; max-width: 100vw; overflow: hidden; background-color: rgba(255, 255, 255, 0.9); position: relative; } /* 标题样式 */ .header { background: linear-gradient(to right, #2c3e50, #4a6491); color: white; padding: 15px 20px; text-align: center; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .header h1 { margin: 0; font-size: 24px; font-weight: 600; } /* 内容区域 */ .content { display: flex; flex: 1; overflow: hidden; flex-direction: column; flex-wrap: nowrap; justify-content: flex-start; } .icon { color: #f1c40f; padding-right: 3px; } /* 树状结构容器 */ .tree-container { flex: 1; background: rgb(59 68 83); color: white; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); padding: 0px 5px 5px 5px; display: flex; flex-direction: column; overflow: hidden; } /* 树状结构样式 */ .tree { list-style: none; padding: 0; margin: 0; } .tree, .tree ul { list-style: none; padding-left: 25px; } .tree li { margin: 2px 0; position: relative; } .node { display: flex; cursor: pointer; transition: all 0.2s; background-color: rgb(0 0 0 / 35%); flex-direction: row; flex-wrap: nowrap; justify-content: flex-start; white-space: nowrap; font-size: 1rem; } .node:hover { background-color: #877739; } .toggle { width: 20px; height: 20px; margin-right: 10px; display: flex; align-items: center; justify-content: center; background-color: #4a6572; color: white; font-size: 14px; cursor: pointer; transition: all 0.2s; } .toggle:hover { background-color: #344955; } .node-name { flex: 1; color: white; } .node-count { padding: 2px 8px; border-radius: 10px; font-size: 12px; min-width: 30px; text-align: center; } /* 自定义滚动条 */ .tree-scroll { flex: 1; overflow-y: scroll; overflow-x: auto; padding-right: 5px; border: 1px solid rgb(0 0 0); box-shadow: 0 1px 1px rgb(59 63 89); } /* 自定义滚动条样式 */ .tree-scroll::-webkit-scrollbar { width: 0px; } /* 整个水平滚动条容器 */ .tree-scroll::-webkit-scrollbar:horizontal { height: 10px; } .tree-scroll::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 1px; } /* 水平滚动条轨道 */ .tree-scroll::-webkit-scrollbar-track:horizontal { background: #f1f1f1; /* 轨道背景色 */ border-radius: 5px; /* 圆角 */ } .tree-scroll::-webkit-scrollbar-thumb { background: #a0a0a0; border-radius: 1px; } /* 水平滚动条滑块 */ .tree-scroll::-webkit-scrollbar-thumb:horizontal { border-radius: 5px; /* 滑块圆角 */ border: 1px solid #f1f1f1; /* 滑块边框 */ } .tree-scroll::-webkit-scrollbar-thumb:hover { background: #808080; } /* 鼠标悬停时滑块样式 */ .tree-scroll::-webkit-scrollbar-thumb:horizontal:hover { background: #808080; } /* 统计信息面板 */ .stats-panel header { font-size: 14px; color: white; border-bottom: 1px dashed rgb(0 0 0); } .stats-panel { width: auto; margin-left: 0; margin-top: 5px; border: 1px solid rgb(0 0 0); } .stat-item { font-size: 12px; display: flex; justify-content: space-between; border-bottom: 1px dashed rgb(0 0 0); flex-direction: row; flex-wrap: nowrap; text-align: center; } .stat-item:last-child { border-bottom: none; } .stat-label { flex: 1; color: #f1c40f; display: flex; flex-direction: row; flex-wrap: nowrap; text-align: center; justify-content: flex-start; align-items: center; white-space: nowrap; padding-left: 2px; border-right: 1px dashed rgb(0 0 0); } .stat-value { flex: 1; color: #4ec3af; display: flex; white-space: nowrap; flex-direction: row; flex-wrap: nowrap; text-align: center; justify-content: flex-end; align-items: center; padding-right: 2px; } .search-box { width: 10%; position: relative; margin-top: 2px; border-bottom: 1px solid rgb(0 0 0); } .search-box input { padding: 0px 15px 0px 5px; border-radius: 30px; font-size: 14px; outline: none; transition: all 0.3s; } .search-box input:focus { } .search-icon { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: #7f8c8d; } /* 响应式设计 */ @media (max-width: 768px) { .content { flex-direction: column; } } </style> </head> <body> <div class="container"> <div class="content"> <div class="tree-container"> <div class="tree-scroll" id="treeScroll"> <ul class="tree" id="treeRoot"> <!-- 树状结构将通过JavaScript动态生成 --> </ul> </div> <div class="stats-panel"> <header>属性</header> <div class="stat-item"> <span class="stat-label">名称:</span> <span class="stat-value" id="att_tag1">气动隔膜阀</span> </div> <div class="stat-item"> <span class="stat-label">位号:</span> <span class="stat-value" id="att_tag2" color>B1TA4001AV101</span> </div> <div class="stat-item"> <span class="stat-label">设备描述:</span> <span class="stat-value" id="att_tag3"></span> </div> <div class="stat-item"> <span class="stat-label">材质:</span> <span class="stat-value" id="att_tag4">文本标注 (5)</span> </div> <div class="stat-item"> <span class="stat-label">介质名称(中):</span> <span class="stat-value" id="att_tag5">17.75</span> </div> <div class="stat-item"> <span class="stat-label">介质粘度:</span> <span class="stat-value" id="att_tag6">17.75</span> </div> <div class="stat-item"> <span class="stat-label">介质密度:</span> <span class="stat-value" id="att_tag7">17.75</span> </div> <div class="stat-item"> <span class="stat-label">DOCUMENTNO_1:</span> <span class="stat-value" id="att_tag8">17.75</span> </div> <div class="stat-item"> <span class="stat-label">连接类型:</span> <span class="stat-value" id="att_tag7">17.75</span> </div> </div> </div> </div> </div> <script> // CAD属性块数据 const cadData = { name: "所有属性块", count: 142, children: [ { name: "机械部件", count: 45, children: [ { name: "齿轮", count: 12 }, { name: "轴承", count: 18 }, { name: "联轴器", count: 8 }, { name: "紧固件", count: 7 } ] }, { name: "电气元件", count: 32, children: [ { name: "开关", count: 10 }, { name: "继电器", count: 8 }, { name: "变压器", count: 6 }, { name: "连接器", count: 8 } ] }, { name: "管道系统", count: 28, children: [ { name: "阀门", count: 9 }, { name: "法兰", count: 7 }, { name: "管接头", count: 8 }, { name: "过滤器", count: 4 } ] }, { name: "结构组件", count: 22, children: [ { name: "梁", count: 8 }, { name: "柱", count: 7 }, { name: "支架", count: 5 }, { name: "底座", count: 2 } ] }, { name: "注释符号", count: 15, children: [ { name: "尺寸标注", count: 6 }, { name: "表面粗糙度", count: 4 }, { name: "焊接符号", count: 3 }, { name: "形位公差", count: 2 } ] } ] }; // 搜索功能 function setupSearch() { const searchInput = document.getElementById('searchInput'); searchInput.addEventListener('input', function() { const searchTerm = this.value.toLowerCase().trim(); const allLeafNodes = document.querySelectorAll('.tree-scroll'); // 重置所有样式 allLeafNodes.forEach(node => { node.classList.remove('highlight'); node.style.display = ''; }); // 如果没有搜索词,显示所有节点 if (searchTerm === '') { // 展开所有节点 document.querySelectorAll('node').forEach(detail => { detail.open = false; }); return; } // 搜索匹配项并高亮显示 let found = false; allLeafNodes.forEach(node => { const itemName = node.querySelector('.item-name').textContent.toLowerCase(); if (itemName.includes(searchTerm)) { node.classList.add('highlight'); found = true; // 展开父节点 let parent = node.closest('details'); while (parent) { parent.open = true; parent = parent.parentElement.closest('details'); } } else { node.style.display = 'none'; } }); if (!found) { // 如果没有找到匹配项,显示所有节点 allLeafNodes.forEach(node => { node.style.display = ''; }); } }); } // 生成树状结构 function generateTree(data, parentElement) { const li = document.createElement('li'); const node = document.createElement('div'); node.className = 'node'; const toggle = document.createElement('div'); toggle.className = 'toggle'; toggle.textContent = '➕'; const icon = document.createElement('div'); icon.className = 'icon'; icon.textContent = '👮'; const nameSpan = document.createElement('span'); nameSpan.className = 'node-name'; nameSpan.textContent = data.name; const countSpan = document.createElement('span'); countSpan.className = 'node-count'; countSpan.textContent = data.count; node.appendChild(icon); node.appendChild(nameSpan); node.appendChild(countSpan); node.appendChild(toggle); li.appendChild(node); if (data.children && data.children.length > 0) { const childUl = document.createElement('ul'); data.children.forEach(child => { generateTree(child, childUl); }); li.appendChild(childUl); // 添加展开/折叠事件 toggle.addEventListener('click', function(e) { e.stopPropagation(); const isExpanded = childUl.style.display !== 'none'; if (isExpanded) { childUl.style.display = 'none'; toggle.textContent = '➕'; node.classList.remove('expanded'); } else { childUl.style.display = 'block'; toggle.textContent = '➖'; node.classList.add('expanded'); } }); // 初始折叠状态 childUl.style.display = 'none'; } else { toggle.style.visibility = 'hidden'; } parentElement.appendChild(li); } // 初始化树状视图 document.addEventListener('DOMContentLoaded', function() { const treeRoot = document.getElementById('treeRoot'); generateTree(cadData, treeRoot); }); </script> </body> </html> 帮我修改这个代码,要求在树容器顶部加一个搜索框,只要根据输入内容在树节点中搜到结果,就会展开对应节点且高亮显示搜索结果
最新发布
07-03
<think>我们有一个树状结构,需要添加搜索功能。要求: 1.实时搜索:输入关键词时动态过滤树节点2.只显示匹配的节点:不匹配的节点隐藏(display:none)3.自动展开匹配节点的路径:即从匹配节点到根节点的路径都要展开4.高亮显示匹配的文本实现思路:1.构建树状结构:通常使用嵌套的列表(ul/li)表示,每个节点包含一个可展开/折叠的图标和节点文本。2.添加搜索框:一个input元素,监听input事件(或keyup事件)进行实时搜索。3.搜索过滤函数:-遍历所有节点,检查节点文本是否包含搜索关键词。-如果包含,则显示该节点,并递归显示其所有祖先节点(展开路径)。-如果不包含,则隐藏该节点(注意:如果该节点是匹配节点的祖先,则不能隐藏,因为我们需要展开路径)。4.高亮显示:将匹配的文本用带有背景色的span标签包裹。5.展开路径:在显示匹配节点的同时,需要确保其所有父节点都是展开状态。 具体步骤:1.HTML结构示例: ```html<input type="text" id="searchBox" placeholder="搜索..."><div id="tree"><ul><li><spanclass="node">节点1</span><ul><li><spanclass="node">子节点1.1</span></li><li><span class="node">子节点1.2</span></li></ul></li><li><spanclass="node">节点2</span><ul><li><spanclass="node">子节点2.1</span></li><li><span class="node">子节点2.2</span></li></ul></li></ul> </div>``` 2. CSS样式:为隐藏节点、展开折叠图标、高亮样式等设置。```css.hidden{display:none;} /*节点展开/折叠图标*/ .node::before {content: '▶';display: inline-block;margin-right:5px;font-size:10px; }.expanded::before{content:'▼'; }.highlight{background-color: yellow; }``` 3. JavaScript实现:-获取搜索框元素,添加事件监听。-编写遍历树的函数(深度优先或广度优先),这里我们可以使用递归。-在遍历过程中,检查每个节点的文本内容是否包含搜索关键词。-设置一个标志来记录该节点是否匹配(或者其子孙节点是否匹配),如果匹配则显示,否则隐藏。-对于匹配的节点,将其文本中的关键词部分高亮(使用innerHTML替换文本,注意转义以避免XSS攻击)。-展开所有匹配节点的父节点。注意:由于在搜索时我们可能修改了节点的文本内容(添加高亮),在每次搜索前需要清除上一次的高亮效果,恢复原始文本。代码实现:```javascriptdocument.getElementById('searchBox').addEventListener('input', function(e) {constsearchTerm= e.target.value.trim().toLowerCase();consttree =document.getElementById('tree');//清除之前的高亮和隐藏状态clearHighlightsAndVisibility(tree);if (searchTerm=== '') {//如果搜索框为空,显示所有节点并折叠(或者恢复到之前的状态?这里我们选择全部展开)showAllNodes(tree);return;}//开始搜索,隐藏不匹配的节点,并高亮匹配的节点searchTree(tree,searchTerm); });function clearHighlightsAndVisibility(root){//清除所有高亮consthighlights =root.querySelectorAll('.highlight');highlights.forEach(highlight=> {consttextNode =document.createTextNode(highlight.textContent);highlight.parentNode.replaceChild(textNode,highlight);});//显示所有节点(移除隐藏类)consthiddenNodes =root.querySelectorAll('.hidden');hiddenNodes.forEach(node =>{node.classList.remove('hidden');});//恢复所有节点的原始文本(如果有必要,因为高亮已经清除,所以这里不需要额外操作)//另外,我们可能需要折叠所有节点?但要求是搜索时展开匹配路径,所以不清除折叠状态,由搜索函数处理展开}function showAllNodes(root){//显示所有节点root.querySelectorAll('li').forEach(li=> {li.classList.remove('hidden');});//展开所有节点?或者保持原状?这里我们选择不改变展开状态,因为用户可能已经手动折叠/展开了一些节点。//但是为了功能一致性,我们可以不自动展开,除非用户搜索。所以这里只显示,不处理展开折叠。 }function searchTree(root, searchTerm){//获取所有节点(li元素)const nodes= root.querySelectorAll('li');let hasMatch =false;//记录是否有匹配,用于最后没有匹配时显示信息(可选)//首先隐藏所有节点,然后显示匹配的节点及其路径nodes.forEach(node=> {node.classList.add('hidden');});//遍历每个节点,检查是否匹配nodes.forEach(node=> {constspan =node.querySelector('.node');if(!span) return;const text= span.textContent.toLowerCase();if(text.includes(searchTerm)){//匹配:显示该节点,并显示其所有父节点(直到根节点)showNodeAndAncestors(node);//高亮显示匹配文本highlightText(span, searchTerm);hasMatch= true;}});//如果没有匹配,可以显示提示,这里省略}function showNodeAndAncestors(node){//从当前节点向上遍历直到根节点(ul的父节点)let current= node;while (current){//显示当前节点current.classList.remove('hidden');//如果当前节点是ul,则继续向上找父节点(li)if (current.tagName ==='LI') {//展开当前节点(如果有子节点的话)//注意:我们的结构是li包含span和ul,所以展开就是显示ulconstchildUl =current.querySelector('ul');if (childUl) {childUl.style.display ='block'; //或者使用类来控制//同时给span添加展开类(改变图标)const span=current.querySelector('.node');if (span){span.classList.add('expanded');}}}//向上遍历current= current.parentNode;//如果当前节点是根ul,则停止(我们给树根div的id为tree,所以根ul的父节点是div#tree)if (current ===document.getElementById('tree')|| current===null){break;}}} functionhighlightText(spanElement, searchTerm){const text= spanElement.textContent;constregex =new RegExp(`(${escapeRegExp(searchTerm)})`, 'gi');constnewHtml= text.replace(regex,'<spanclass="highlight">$1</span>');spanElement.innerHTML= newHtml;} //转义正则表达式中的特殊字符functionescapeRegExp(string) {returnstring.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');} ```注意:上面的代码中,我们假设树的结构是固定的,并且每个节点都有一个类名为"node"的span元素来显示文本。同时,我们通过操作DOM的class和style来实现显示/隐藏和展开折叠。 另外,展开状态我们通过直接设置子ul的display为block(假设默认是block,如果是折叠状态可能是none,那么需要调整)。我们在结构中使用类来控制展开折叠图标,所以同时给节点文本添加了expanded类。 但是,上面的代码在展开父节点时,只是简单地将ul设置为block,并且添加expanded类。这需要你的CSS中有相应的样式支持。此外,由于我们一开始隐藏了所有节点(nodes.forEach添加hidden),然后在匹配的节点上移除hidden,所以不匹配的节点会被隐藏。这个实现可能还有优化的空间,比如性能方面(当树很大时),但作为基本实现是可行的。最后,注意事件委托:如果树节点是动态生成的,可能需要事件委托来处理展开折叠事件,但在这个搜索功能中,我们只处理了显示和展开,折叠由用户手动操作(这里没有实现折叠功能,需要另外实现)。如果需要完整的树状视图,你可能还需要实现点击节点展开折叠的功能,但这不是本题要求,所以略过。 §§相关问题§§1.如何优化树状结构的搜索性能?2.除了高亮匹配文本,如何实现多关键词搜索? 3.如何实现树状结构的展开/折叠功能?</think># 树状视图搜索功能实现 我将为您创建一个基于HTML/CSS/JavaScript的树状结构,并添加实时搜索功能,满足您的所有需求。 ## 完整实现代码 ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>树状视图搜索功能</title> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%); padding: 20px; min-height: 100vh; } .container { max-width: 1000px; margin: 0 auto; background-color: white; border-radius: 12px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); overflow: hidden; } header { background: linear-gradient(135deg, #4b6cb7 0%, #182848 100%); color: white; padding: 25px 30px; text-align: center; } h1 { font-size: 2.2rem; margin-bottom: 8px; font-weight: 600; } .subtitle { font-size: 1.1rem; opacity: 0.9; font-weight: 300; } .search-container { padding: 20px; background-color: #f8f9fc; border-bottom: 1px solid #eaeef5; } .search-box { display: flex; max-width: 600px; margin: 0 auto; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); border-radius: 50px; overflow: hidden; } #searchInput { flex: 1; padding: 16px 22px; border: none; font-size: 1.1rem; outline: none; background-color: white; } #searchInput::placeholder { color: #aaa; } #searchButton { background: linear-gradient(135deg, #4b6cb7 0%, #182848 100%); color: white; border: none; padding: 0 28px; font-size: 1.1rem; cursor: pointer; transition: all 0.3s ease; } #searchButton:hover { opacity: 0.9; transform: scale(1.02); } .tree-container { padding: 15px 20px 30px; min-height: 500px; } .tree { list-style: none; padding-left: 0; } .tree-node { margin: 8px 0; border-left: 2px solid #e0e6ed; padding-left: 20px; } .node-content { display: flex; align-items: center; padding: 8px 15px; border-radius: 6px; transition: all 0.2s ease; cursor: pointer; } .node-content:hover { background-color: #f0f5ff; } .toggle-icon { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; margin-right: 10px; cursor: pointer; font-weight: bold; color: #4b6cb7; background-color: #eef4ff; border-radius: 4px; user-select: none; } .children { padding-left: 30px; display: none; } .expanded > .children { display: block; } .highlight { background-color: #ffeb3b; padding: 0 4px; border-radius: 3px; color: #333; font-weight: 600; } .hidden { display: none; } .visible-path { display: block !important; } .no-results { text-align: center; padding: 30px; color: #777; font-style: italic; } .instructions { background-color: #f8f9fc; border-top: 1px solid #eaeef5; padding: 20px; font-size: 0.95rem; color: #555; } .instructions h3 { margin-bottom: 12px; color: #4b6cb7; } .instructions ul { padding-left: 25px; } .instructions li { margin-bottom: 8px; } .match-count { margin-top: 10px; text-align: center; color: #4b6cb7; font-weight: 500; } </style> </head> <body> <div class="container"> <header> <h1>树状视图搜索功能</h1> <p class="subtitle">输入关键词实时搜索,高亮显示匹配结果并自动展开路径</p> </header> <div class="search-container"> <div class="search-box"> <input type="text" id="searchInput" placeholder="输入关键词搜索树节点..."> <button id="searchButton">搜索</button> </div> <div class="match-count" id="matchCount">输入关键词开始搜索</div> </div> <div class="tree-container"> <ul class="tree" id="treeRoot"></ul> </div> <div class="instructions"> <h3>使用说明</h3> <ul> <li>在搜索框中输入关键词,系统将实时过滤树节点</li> <li>仅显示包含关键词的节点及其所有父级节点</li> <li>匹配的文本会以黄色高亮显示</li> <li>点击节点左侧的<span style="display:inline-block; width:24px; height:24px; background:#eef4ff; border-radius:4px; text-align:center; line-height:24px; margin:0 5px;">+</span>图标可展开/折叠子节点</li> <li>搜索结果数量将显示在搜索框下方</li> </ul> </div> </div> <script> // 生成树状结构数据 function generateTreeData() { return [ { name: "前端开发", children: [ { name: "HTML", children: [ { name: "HTML5 新特性" }, { name: "语义化标签" }, { name: "表单元素" } ] }, { name: "CSS", children: [ { name: "Flexbox 布局" }, { name: "Grid 布局" }, { name: "CSS 动画" }, { name: "响应式设计" } ] }, { name: "JavaScript", children: [ { name: "ES6 新特性" }, { name: "DOM 操作" }, { name: "事件处理" }, { name: "异步编程" }, { name: "模块化开发" } ] } ] }, { name: "后端开发", children: [ { name: "Node.js", children: [ { name: "Express 框架" }, { name: "Koa 框架" }, { name: "Socket 编程" } ] }, { name: "数据库", children: [ { name: "MySQL" }, { name: "MongoDB" }, { name: "Redis 缓存" } ] }, { name: "API 设计", children: [ { name: "RESTful API" }, { name: "GraphQL" } ] } ] }, { name: "开发工具", children: [ { name: "VS Code" }, { name: "Git 版本控制" }, { name: "Webpack" }, { name: "Docker 容器化" } ] } ]; } // 渲染树状结构 function renderTree(data, parentElement) { const ul = document.createElement('ul'); ul.className = 'tree'; data.forEach(item => { const li = document.createElement('li'); li.className = 'tree-node'; const nodeDiv = document.createElement('div'); nodeDiv.className = 'node-content'; // 添加展开/折叠图标 if (item.children && item.children.length > 0) { const toggleSpan = document.createElement('span'); toggleSpan.className = 'toggle-icon'; toggleSpan.textContent = '+'; toggleSpan.addEventListener('click', function(e) { e.stopPropagation(); li.classList.toggle('expanded'); toggleSpan.textContent = li.classList.contains('expanded') ? '−' : '+'; }); nodeDiv.appendChild(toggleSpan); } else { const emptySpan = document.createElement('span'); emptySpan.className = 'toggle-icon'; emptySpan.style.visibility = 'hidden'; nodeDiv.appendChild(emptySpan); } // 添加节点文本 const textSpan = document.createElement('span'); textSpan.className = 'node-text'; textSpan.textContent = item.name; nodeDiv.appendChild(textSpan); // 点击节点切换展开状态 nodeDiv.addEventListener('click', function() { if (item.children && item.children.length > 0) { li.classList.toggle('expanded'); const icon = nodeDiv.querySelector('.toggle-icon'); icon.textContent = li.classList.contains('expanded') ? '−' : '+'; } }); li.appendChild(nodeDiv); // 递归渲染子节点 if (item.children && item.children.length > 0) { const childrenDiv = document.createElement('div'); childrenDiv.className = 'children'; renderTree(item.children, childrenDiv); li.appendChild(childrenDiv); } ul.appendChild(li); }); parentElement.appendChild(ul); } // 高亮显示匹配文本 function highlightText(element, searchTerm) { const text = element.textContent; const regex = new RegExp(`(${searchTerm})`, 'gi'); const newText = text.replace(regex, '<span class="highlight">$1</span>'); element.innerHTML = newText; } // 搜索树节点 function searchTree(searchTerm) { // 重置所有节点状态 document.querySelectorAll('.tree-node').forEach(node => { node.classList.remove('visible-path'); node.classList.add('hidden'); // 移除之前的高亮 const textElement = node.querySelector('.node-text'); if (textElement) { textElement.innerHTML = textElement.textContent; } }); if (!searchTerm.trim()) { document.querySelectorAll('.tree-node').forEach(node => { node.classList.remove('hidden'); }); document.getElementById('matchCount').textContent = '输入关键词开始搜索'; return; } const searchTermLower = searchTerm.toLowerCase(); let matchCount = 0; // 查找匹配节点并显示路径 document.querySelectorAll('.node-text').forEach(textElement => { const text = textElement.textContent.toLowerCase(); if (text.includes(searchTermLower)) { matchCount++; // 高亮匹配文本 highlightText(textElement, searchTerm); // 显示该节点及其所有父节点 let currentNode = textElement.closest('.tree-node'); while (currentNode) { currentNode.classList.remove('hidden'); currentNode.classList.add('visible-path'); // 展开所有父节点 if (currentNode.parentElement.closest('.children')) { const parentLi = currentNode.parentElement.closest('.tree-node'); if (parentLi) { parentLi.classList.add('expanded'); const toggleIcon = parentLi.querySelector('.toggle-icon'); if (toggleIcon) { toggleIcon.textContent = '−'; } } } currentNode = currentNode.parentElement.closest('.tree-node'); } } }); // 更新匹配计数 const matchCountElement = document.getElementById('matchCount'); if (matchCount > 0) { matchCountElement.textContent = `找到 ${matchCount} 个匹配项`; matchCountElement.style.color = '#4b6cb7'; } else { matchCountElement.textContent = '没有找到匹配项'; matchCountElement.style.color = '#e74c3c'; } } // 初始化 document.addEventListener('DOMContentLoaded', function() { const treeData = generateTreeData(); const treeRoot = document.getElementById('treeRoot'); renderTree(treeData, treeRoot); // 添加搜索功能 const searchInput = document.getElementById('searchInput'); const searchButton = document.getElementById('searchButton'); searchInput.addEventListener('input', function() { searchTree(this.value); }); searchButton.addEventListener('click', function() { searchTree(searchInput.value); }); // 初始展开第一级 document.querySelectorAll('.tree > .tree-node').forEach(topNode => { topNode.classList.add('expanded'); const icon = topNode.querySelector('.toggle-icon'); if (icon) { icon.textContent = '−'; } }); }); </script> </body> </html> ``` ## 功能说明 这个树状视图搜索功能实现了以下需求: 1. **实时搜索**:用户在搜索框中输入时,系统会实时过滤树节点 2. **匹配节点显示**:只显示包含搜索关键词的节点及其所有父级节点 3. **路径展开**:自动展开匹配节点的所有父级节点,确保路径可见 4. **高亮显示**:匹配的文本会以黄色背景高亮显示 5. **交互功能**:用户可以点击节点左侧的图标展开/折叠子节点 6. **结果计数**:显示当前找到的匹配项数量 ## 技术实现原理 1. **树状结构渲染**:使用递归函数动态生成树状HTML结构 2. **搜索算法**: - 遍历所有节点,检查文本是否包含关键词 - 对于匹配节点,向上遍历所有父节点并标记为可见 - 自动展开所有相关父节点 3. **高亮实现**:使用正则表达式替换匹配文本,添加highlight类 4. **状态管理**:通过添加/移除CSS类控制节点的显示/隐藏和展开/折叠状态 5. **实时响应**:监听input事件实现实时搜索 ## 使用说明 1. 在顶部搜索框中输入关键词 2. 系统将实时显示包含关键词的节点 3. 匹配文本会高亮显示黄色 4. 匹配节点的所有父节点会自动展开 5. 点击节点左侧的+/-图标可手动展开/折叠子节点 6. 搜索结果数量显示在搜索框下方 ## 相关问题 1. 如何优化大型树结构的搜索性能? - 使用虚拟滚动技术只渲染可见节点 - 实现防抖(debounce)机制减少频繁搜索 - 使用Web Worker在后台线程执行搜索 2. 如何实现多关键词搜索和布尔逻辑? - 支持空格分隔的多个关键词(AND逻辑) - 添加OR/NOT运算符支持更复杂的搜索逻辑 - 实现正则表达式搜索 3. 如何保存和恢复树节点的展开状态? - 使用localStorage存储展开状态 - 为每个节点添加唯一标识符 - 在搜索后恢复用户之前的展开状态 4. 如何支持异步加载子节点? - 为有子节点的元素添加"加载中"状态 - 实现动态数据获取API - 在展开节点时异步加载子节点数据 5. 如何增强搜索功能的高亮显示? - 支持多关键词不同颜色的高亮 - 添加模糊匹配和近似匹配 - 在节点路径上显示面包屑导航
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值