响应式可过滤展示页:用数据驱动取代DOM操作

发布时间:2026/9/15 0:00:27
响应式可过滤展示页:用数据驱动取代DOM操作 简介项目源码为使用 HTML、CSS 和 JS 构建的响应式可过滤游戏与工具展示页面面向前端学习者或有作品集展示需求的开发者也适合作为个人工具导航页的改造基底。页面采用卡片式布局集中展示 100 个小游戏和实用工具重点解决内容分类浏览、关键词筛选和不同设备下自适应展示的问题。zip 压缩包内仅 1 个 html 文件大小约 3KB直接打开即可查看完整效果页面借助 CSS3 媒体查询处理响应式断点利用 JavaScript 的 filter 方法实现分类筛选与动态 DOM 更新通过按钮或输入关键词即可触发过滤。代码结构简洁适合学习媒体查询、数组筛选、事件监听和 DOM 操作等基础交互也可参考其卡片布局和筛选逻辑快速搭建个人作品集页面。目前已有 711 人学习下载是快速上手前端综合小项目的不错选择。1. 响应式可过滤展示页面的核心是数据不是布局大多数人在动手做游戏工具展示页面时第一反应是拿起 CSS 框架开始搭栅格。真正把页面做到能上线、能维护的人会告诉你这个标题里最值得花时间的不是响应式那一层而是「可过滤」背后的数据结构和筛选逻辑。拿一个实际场景来说几十个游戏和工具的卡片在桌面端展示得很好一旦切到手机端就卡顿、错位、筛选失灵——问题的根源几乎都在于每次筛选都把整批 DOM 重建了一遍或者在 HTML 里写死了分类。本文用纯 HTML、CSS 和 JS不依赖框架做一张既能在手机上流畅滚动、又能按分类实时过滤的展示页。适合已经有基础前端能力、想摆脱框架惯性、把 DOM 操作和数据逻辑理清楚的开发者。2. 先搭数据模型再写布局骨架2.1 用 JSON 数组把游戏和工具建模成同一套结构展示页里同时有「游戏」和「工具」两类内容最常见的数据结构错误是把两类数据分开维护——游戏写一个数组工具再写一个数组然后在渲染时拼到两个容器里。这会导致两个问题一个是过滤逻辑要写两套条件判断另一个是当页面里出现「全部」「游戏」「工具」「休闲」「效率」这类复合筛选时条件组合会迅速膨胀到难以维护。我一般会做的是把它们合并成同一个数组每条记录用一个type字段区分种类用tags数组描述更细的归属const items [ { id: snake, title: 贪吃蛇, type: game, tags: [休闲, 经典], year: 2021, desc: 键盘方向键控制支持移动端点按, }, { id: md5, title: MD5 校验工具, type: tool, tags: [效率, 开发], year: 2023, desc: 本地计算文件哈希不上传文件内容, } ];这样做的收益是渲染逻辑只看一个数组筛选逻辑只对数组做过滤不需要知道每一项具体是游戏还是工具。type决定它出现在哪个分类下tags支持多重标签过滤。数据结构的统一性直接决定了过滤代码的复杂度这个成本应当在动手前就付掉而不是在写到一半时重构。字段命名上注意id在后续做筛选和事件绑定时要能被当作唯一标识。title和desc用于渲染卡片正文。year如果你要做「最新上架」之类的排序可以顺带用上没有排序需求就完全可以省略。2.2 用 CSS Grid 的 auto-fill 代替手写媒体查询栅格响应式布局不只是「屏幕窄了换一列」而是要尽量少写断点、让布局自己适应空间。CSS Grid 的grid-template-columns: repeat(auto-fill, minmax(280px, 1fr))是现阶段最省维护量的方案。.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; padding: 20px; max-width: 1400px; margin: 0 auto; }这段 CSS 的含义是每列最小 280px如果容器宽度能容纳两列就自动排两列能容纳五列就排五列不需要为每个断点写media。auto-fill和auto-fit的区别常常让人混淆——auto-fill在空间不够时会留下空轨道auto-fit会把空轨道压缩成 0让已有卡片拉伸填满。对展示页我会选auto-fill因为卡片过宽会变得空洞留出空间再用整体容器宽度控制视觉居中卡片本身的基本尺寸更稳定。配合这个网格每张卡片的容器样式可以这样写.card { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); transition: transform 0.2s ease, box-shadow 0.2s ease; }overflow: hidden是防止卡片里的图片圆角溢出transition是给后续的悬浮效果留出余地。整个布局不依赖任何 CSS 框架约 20 行样式覆盖了从 320px 到 1400px 宽度的自适应。2.3 一个可复制的展示页 HTML 骨架数据有了样式有了还差一个承载它们的 HTML 结构。下面是从零起步可以直接复用的骨架包含筛选按钮区和卡片挂载点header classpage-header h1游戏与工具集/h1 p一个页面两类内容按需筛选/p /header div classfilter-bar idfilter-bar button classfilter-btn is-active>const state { type: all, // all | game | tool tag: , keyword: , };所有 UI 交互都只修改state里的值然后重新执行一次过滤和渲染函数。页面里再多的按钮、输入框都只是「修改 state 的入口」而不是「直接改 DOM 的触发器」。这样逻辑上清晰排查问题也方便——只需要检验 state 里存的值是否符合预期。对应的过滤核心函数是function getFilteredItems() { return items.filter(item { // 主分类过滤 const matchType state.type all || item.type state.type; // 标签过滤 const matchTag !state.tag || item.tags.includes(state.tag); // 关键词搜索 const matchKeyword !state.keyword || item.title.toLowerCase().includes(state.keyword.toLowerCase()) || item.desc.toLowerCase().includes(state.keyword.toLowerCase()); return matchType matchTag matchKeyword; }); }三个条件是「且」的关系每一条都写成「不满足条件就放行的短路结构」。!state.tag表示标签为空时不参与过滤这样新增筛选维度时不需要改动已有代码结构只需要在state里多一个字段、在filter里多一行判断。JS 的Array.prototype.filter在这里是天然的工具它返回新数组而不修改原数组这就保证了每次渲染前拿到的items永远是完整的原始数据。3.2 渲染函数只做一件事把数据变成 DOM拿到过滤结果之后就是渲染。这里的关键是决定用什么方式更新页面常见的有三种innerHTML整段替换、createElement逐个创建、以及文档片段DocumentFragment批量挂载。对展示页面来说数据量通常在几十到几百条innerHTML的字符串模板方式效率足够代码也可读性最好。function renderCards() { const grid document.getElementById(card-grid); const filtered getFilteredItems(); if (filtered.length 0) { grid.innerHTML p classempty-tip没有符合条件的条目换个筛选条件试试。/p; return; } grid.innerHTML filtered.map(item article classcard>document.getElementById(filter-bar).addEventListener(click, (event) { const btn event.target.closest(.filter-btn); if (!btn) return; const filterValue btn.dataset.filter; const filterType btn.dataset.filterType || type; if (filterType type) { state.type filterValue; state.tag ; // 切换主分类时重置标签 } else if (filterType tag) { state.tag state.tag filterValue ? : filterValue; // 再点一次取消 } // 更新按钮激活样式 document.querySelectorAll(.filter-btn).forEach(b b.classList.remove(is-active)); btn.classList.add(is-active); renderCards(); });closest(.filter-btn)是为了处理按钮内部有span这类子元素的情况确保点到了子元素也能找到按钮本身。这里有一个容易忽略的细节切换主分类游戏/工具时如果之前选了「休闲」标签要把它自动重置为空否则可能出现「工具 休闲」的组合下什么都没有的情况。这是很多初版过滤页面常见的交互缺陷——用户会疑惑为什么列表全空了。4.2 搜索框与过滤条件的联动处理展示页里经常还带一个搜索框。搜索框的输入事件和按钮的点击事件不一样它触发频率高如果每次按键都重新渲染整个网格卡顿会很明显。这时候需要防抖debounce常见做法是设定 150 到 250 毫秒的延迟。let searchTimer; document.getElementById(search-input).addEventListener(input, (event) { clearTimeout(searchTimer); searchTimer setTimeout(() { state.keyword event.target.value.trim(); renderCards(); }, 200); });防抖的逻辑很简单每次输入先清掉上一次的定时器再重新计时只有用户停下来 200 毫秒后才真正执行渲染。在这个页面里搜索和筛选是并列的关系——搜索关键词在getFilteredItems里作为第三个过滤条件参与计算而不是独立的搜索函数。这样用户在搜索状态下切换分类时搜索词依然生效体验更连贯。有一点要注意event.target.value.trim()清掉了首尾空格避免用户输入一个空格导致筛选无结果。搜索时把keyword转成小写再比较是避免大小写导致漏掉结果的标准做法。4.3 空结果态和数量提示不能省过滤页面里最容易被忽视的是空状态。当筛选条件组合后没有任何结果时用户需要明确知道「现在没有匹配的内容」而不是看到一个空白区域。2.2 节里已经包含了一个空结果的渲染分支这里补充一个数量角标span classresult-count idresult-count/span// 在 renderCards 内统计并展示 document.getElementById(result-count).textContent ${filtered.length} 条内容;数量提示的价值不只是给用户看反馈更是给开发者自己调试用的——切换某个标签后数量是否正确能帮你快速判断筛选条件是不是写对了。textContent比innerHTML更安全这里只是在数字加字符串的拼接用前者避免无谓的 HTML 解析。5. 响应式进阶容器查询、动效与移动端细节5.1 用容器查询让卡片适应自己的容器而不是视口媒体查询只能根据整个视口宽度调整样式但在卡片网格内卡片实际宽度还受侧边栏、内边距影响。如果你希望卡片在变窄时自动调整内部排版CSS 容器查询Container Queries比媒体查询更精确。它已经被现代浏览器广泛支持是这两年响应式页面值得用起来的特性。.card-grid { container-type: inline-size; } container (max-width: 600px) { .card-body { flex-direction: column; } .card-type { margin-top: 8px; } }container-type: inline-size表示容器的内联尺寸变化会触发查询。这里的效果是当卡片网格的宽度小于 600px 时卡片内部布局自动切换为纵向排列。相比纯媒体查询这种写法让「组件自适应自己的空间」成为可能在嵌套布局和侧边栏场景里更好用。如果兼容性要求是最近两年内的浏览器可以放心用。5.2 鼠标移入事件和触屏设备的适配筛选按钮和卡片悬浮效果是页面里的常见细节。鼠标移入卡片时做一个轻微的抬起效果这是典型的增强体验。但要注意触屏设备上没有hover如果为 hover 绑定了展开或跳转逻辑移动端会出问题。.card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); } media (hover: none) { .card:hover { transform: none; } }第一种写法是常规的悬浮效果第二种写法把hover: none的触屏设备上的悬浮动效取消了——避免用户手指点上去时卡片「卡在半空」的感觉。这个细节很多人不写但移动端实际的体验差距就在这里桌面端的手感来自transform过渡移动端的不跟手则来自没有闭掉悬浮。5.3 图片加载策略和懒加载的取舍游戏和工具的卡片里通常会放缩略图。对于展示页图片加载策略直接决定首屏速度和滚动流畅度。原生loadinglazy是最简单的方案但要注意它的失效场景只有当图片进入视口附近时才开始加载如果图片在 CSS 中被隐藏掉浏览器仍然有可能加载它。用 JS 做懒加载的常见逻辑是监听scroll事件配合getBoundingClientRect判断图片是否在视口范围内document.addEventListener(scroll, () { document.querySelectorAll(img[data-src]).forEach(img { const rect img.getBoundingClientRect(); if (rect.top window.innerHeight rect.bottom 0) { img.src img.dataset.src; img.removeAttribute(data-src); } }); }, { passive: true });>function updateHash() { const params new URLSearchParams(); if (state.type ! all) params.set(type, state.type); if (state.tag) params.set(tag, state.tag); if (state.keyword) params.set(q, state.keyword); location.hash params.toString(); } function readHash() { const params new URLSearchParams(location.hash.slice(1)); if (params.has(type)) state.type params.get(type); if (params.has(tag)) state.tag params.get(tag); if (params.has(q)) state.keyword params.get(q); } // 页面初始化时 readHash(); renderCards(); // 每次筛选变化后 updateHash();URLSearchParams是浏览器内置 API用来解析和生成查询字符串比手写split()可靠。初始化时先读 hash再渲染这样用户从分享链接进来时页面直接展示对应的筛选结果。每次更新筛选状态后写入 hash也不会触发浏览器刷新页面——hash变化不会重载页面但会生成历史记录用户还能用浏览器的后退键回到上一个筛选状态。6.2 验证响应式效果的三步检查法页面做完之后不要急着说「好了」按下面三步验证。第一步浏览器开发者工具里的设备工具栏逐个宽度检查 320px、375px、768px、1024px、1440px 下的卡片列数变化和筛选栏换行情况。第二步把页面缩到最小宽度后刷新一次重点检查筛选按钮和搜索框有没有溢出到页面之外以及卡片里的文字有没有被截断。第三步用触屏模拟模式点一遍筛选按钮确认没有 hover 残留效果、没有「点击无反应」的按钮。关于响应式页面设计模板和 CSS 选择器的运用上面所有代码里用的都是最简单的类选择器和后代选择器如果你要用起来更顺手可以自行把.filter-btn、.card这些类名换成 BEM 风格。对没有 JS 基础的人这个页面里的 JS 只有三个核心能力点数组的filter/map、事件的监听与委托、以及DOM的innerHTML赋值搞清楚这三个整个页面的逻辑就能完全掌握。一个应用型的展示页真正值得深入的部分就是数据状态和视图的同步关系——把 filter 理解透后面无论接 React 还是 Vue 都会轻松不少。本文还有配套的精品资源点击获取