5分钟搭建MC·JC网页版入口:纯静态三件套实现导航页
1. 先把“MC·JC 网页版入口”这件事想明白
1.1 这个入口页到底解决什么问题
MC·JC 是一套由若干子功能拼起来的工具组合,它的问题特别典型:功能不是没有,而是入口太散。主控面板在一个地址,作业中心在另一个地址,说明文档放在共享盘,常用的小脚本又在本地文件夹里,谁接手都得先在聊天记录里翻十几分钟链接。所谓“网页版入口”,本质上就是修一个门面,把这堆散落的地址收拢到一个静态页面上,打开浏览器就是一排卡片,点哪去哪,搜索框一搜就能定位。
它不装服务、不连数据库、不涉及任何后台逻辑,就是三个文件:index.html、style.css、app.js,外加一份描述入口的sites.json。正因为这么轻,5 分钟这个承诺才站得住脚——你不需要配环境、装依赖、跑构建,新建文件夹写完就能双击预览(严格说要用本地服务预览,后面会讲原因)。
这套东西适合谁?三类人最合适。第一类是刚接触前端、想找一个“有真实用途又不至于劝退”的练手项目的人,入口页麻雀虽小,HTML 语义化、CSS 响应式、JS 数据渲染和本地存储全都能摸一遍。第二类是需要给团队或自己做一个导航页的人,改几个字段就能上线,维护成本几乎为零。第三类是想给内部工具做统一门面的人,把链接、说明、状态提示集中到一个页面,新人上手时不用再靠口头传授。它不解决业务逻辑,它解决的是“找不到入口”这种每天都在发生、却没人愿意专门立项的小麻烦。
1.2 为什么选纯静态三件套而不是上框架
做入口页最容易犯的错,是把它当成一个正经项目来做。有人一上来就搭脚手架、上组件库、配路由、写状态管理,结果页面本身只有一张卡片列表,构建产物却有几兆,改一个链接还得重新打包部署。我在几个不同规模的项目里都见过这种过度设计,最后维护的人换了两拨,没人愿意碰那个“就为了显示十个链接”的工程。
选纯静态的理由很实在。体积上,三件套压缩后通常在 10KB 到 30KB 之间,首次加载几乎无感;框架方案动辄几百 KB 的运行时,对一张卡片列表来说是纯粹的浪费。部署上,静态文件扔到任何能托管静态资源的地方就能跑,不需要运行时环境,也不会有版本兼容问题。维护上,新人打开文件夹就能看懂,改一行 JSON 就完成一次内容更新,不需要知道什么是依赖注入。
| 方案 | 首次加载体积 | 上手门槛 | 内容更新方式 | 适用场景 |
|---|---|---|---|---|
| 纯静态三件套 | 10–30KB | 会写标签即可 | 改 JSON 或 HTML | 入口页、导航页、说明页 |
| 前端框架 | 150KB 起 | 需要懂构建与组件 | 改代码后重新构建 | 有复杂交互与状态 |
| 后端渲染 | 取决于服务 | 需要服务与模板 | 改模板或数据库 | 需要权限与动态数据 |
反过来讲,什么时候该换方案?当入口页开始需要登录态、需要按角色显示不同入口、需要记录点击行为做统计时,纯静态就该让位了。判断标准很简单:如果页面上出现“不同人看到不同内容”,纯静态就不够了。在那之前,别提前给自己加戏。
1.3 目录结构与文件职责划分
目录我习惯这样铺,扁平、一眼看完,不搞多层嵌套:
mc-jc-entry/ ├── index.html ├── style.css ├── app.js ├── sites.json └── assets/ └── favicon.svgindex.html只负责骨架,也就是页头、搜索框、卡片容器、页脚这几块固定结构,绝不把十几个入口卡片硬编码进去。style.css负责全部视觉,变量统一放在:root里,改主题色只改一行。app.js负责三件事:拉取数据、渲染卡片、处理搜索与主题切换。sites.json是内容层,所有入口的名称、描述、地址、标签都放这儿。
把数据抽出来这一步,是整篇里最值得强调的设计决定。很多人图省事直接在 HTML 里复制粘贴十段卡片结构,当时确实快,但第一次要调整卡片样式或者新增五个入口时,你就得在几百行重复标签里翻找。抽成 JSON 之后,视图和内容彻底分开:样式改一次全站生效,内容改一处即时更新,而且这份 JSON 后续还能被别的页面复用,比如做一个只显示“文档类”入口的子页面。
命名上我建议全部小写加连字符,别用中文文件名,也别用空格。看着是小事,等到部署到某些对大小写敏感的静态托管环境上,Style.css和style.css就是两个文件,白屏排查半小时起步。
2. 页面四层骨架拆解:结构、样式、交互、数据
2.1 结构层:语义化标签怎么选才不留坑
入口页的结构其实就四块:顶部栏、标题区、卡片区、页脚。听起来简单,但我见过大量用一堆div堆出来的版本,读屏软件念出来是一串没有意义的“分组”,键盘 Tab 键跳转顺序也乱。语义化标签不是为了好看,它直接决定了页面在无障碍场景下能不能用。
顶部栏用header,里面放品牌标识、搜索输入框、主题切换按钮。标题区用section包一个h1和一句副标题,h1全页只能有一个,这是硬规矩。卡片区用section加aria-live="polite",这样搜索过滤后结果数量变化时,读屏软件会播报,不用用户自己去数。每张卡片本身用a标签直接当容器,而不是div套a——前者天然可聚焦、天然支持回车打开、天然有链接语义,后者你得手动补tabindex和键盘事件,纯属自找麻烦。
页脚用footer,放版本号和一句说明就够。整个页面不需要nav,因为卡片区不是导航菜单,用nav反而语义错位。搜索框必须带aria-label,因为它的提示文字在placeholder里,占位符在输入后消失,读屏用户拿不到标签信息。
<header class="topbar"> <div class="brand"> <span class="brand-mark">MC</span> <span class="brand-text">JC 入口</span> </div> <div class="search"> <input id="search" type="search" placeholder="搜索入口,按 / 聚焦" aria-label="搜索入口"> </div> <button id="theme" class="theme-btn" aria-label="切换深浅主题">切换主题</button> </header> <main> <section class="hero"> <h1>MC·JC 网页版入口</h1> <p>把散落的入口收进一个页面,打开就能点。</p> </section> <section id="grid" class="grid" aria-live="polite"></section> <p id="empty" class="empty" hidden>没有匹配的入口,换个关键词试试。</p> </main> <footer class="foot"> <span>MC·JC 入口页 · 静态版</span> </footer>注意#empty那个提示段落,它默认带hidden。搜索无结果时才由脚本去掉这个属性。这个细节很多人漏掉,结果用户搜了个不存在的词,页面一片空白,以为页面挂了。
2.2 样式层:CSS 变量加 Grid 一行搞定响应式
样式这块,最省事的做法是把所有颜色、圆角、间距抽成 CSS 变量放在:root,深色模式只需要覆盖同名变量,不用写两套选择器。变量命名的原则是“按用途不按颜色”,写--brand而不是--blue,哪天品牌色从蓝换成绿,变量名不用动。
响应式布局别写一堆媒体查询。卡片区用grid-template-columns: repeat(auto-fit, minmax(260px, 1fr))一行解决:浏览器自己算当前宽度能塞几列,每列最窄 260px,剩余空间均分。这个 260px 不是拍脑袋定的,它来自卡片内最小内容的宽度——标题两到四个字加上一行描述,再留出左右各 20px 内边距,260px 是保证描述不被压成两三个字一行的下限。如果你的入口名称普遍较长,把它调到 280px 或 300px 更合适。
:root { --bg: #f5f6f8; --panel: #ffffff; --text: #1f2329; --muted: #6b7280; --line: #e5e7eb; --brand: #2f6fed; --radius: 14px; --gap: 16px; --shadow: 0 1px 2px rgba(16,24,40,.06), 0 8px 24px rgba(16,24,40,.06); } [data-theme="dark"] { --bg: #0f1216; --panel: #171c22; --text: #e6e9ee; --muted: #9aa4b2; --line: #242c36; --brand: #5b8cff; --shadow: 0 1px 2px rgba(0,0,0,.45); } * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--text); font: 16px/1.65 system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; transition: background .2s ease, color .2s ease; } .topbar { display: flex; align-items: center; gap: 12px; padding: 12px 20px; background: var(--panel); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 9; } .search { flex: 1; } .search input { width: 100%; padding: 9px 14px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg); color: var(--text); font-size: 14px; outline: none; } .search input:focus { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(47,111,237,.15); } main { max-width: 1120px; margin: 0 auto; padding: 28px 20px 48px; } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--gap); } .card { display: block; padding: 18px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); text-decoration: none; color: inherit; transition: transform .18s ease, border-color .18s ease; } .card:hover { transform: translateY(-2px); border-color: var(--brand); } .card:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; } .card h2 { margin: 0 0 6px; font-size: 16px; } .card p { margin: 0; color: var(--muted); font-size: 13px; }prefers-color-scheme这个媒体查询值得加一段:用户系统是深色模式时,首次进来直接给深色,别让人一睁眼被白板闪一下。实现方式是脚本读取matchMedia('(prefers-color-scheme: dark)')的匹配结果作为初始值,再叠加本地存储里用户手动选择过的偏好,后者优先级更高。
2.3 交互层:搜索过滤与主题记忆
交互只有两件事,但每件都有讲究。搜索这块,直接在input事件里逐字过滤就够了,入口数量在几十条的量级,性能完全不是问题。真要防抖,用 120ms 到 200ms 之间比较合适,太短起不到作用,太长用户会觉得输入卡顿。
过滤逻辑是拿关键词去匹配名称、描述、标签三个字段,全部转小写再比对,避免大小写带来的漏匹配。这里有个坑:中文没有大小写概念,但标签里常混着英文缩写,不转小写的话搜 “doc” 匹配不到 “Doc”。转小写是成本最低的兼容处理。
主题切换按钮点一下,把document.documentElement.dataset.theme在dark和light之间来回切,同时写进localStorage。之所以写在documentElement上而不是body上,是为了让:root级别的变量覆盖生效——变量定义在:root,覆盖也必须发生在同一层级或更高层级。这个细节我第一次写的时候踩过,写在body上结果变量根本没被覆盖,排查了半天。
键盘快捷键/聚焦搜索框,是入口页这类页面里体验提升最明显的一个小功能。做法是在document上监听keydown,判断event.key === '/'且当前焦点不在输入框里(用document.activeElement.tagName判断,排除INPUT和TEXTAREA),然后preventDefault并聚焦。不排除输入框的话,用户想在搜索框里输入斜杠打不出来,反而添乱。
注意:
preventDefault一定要加,否则浏览器会把这个按键当成“快速查找”的触发键,部分环境下会弹出浏览器自带搜索条,体验很割裂。
2.4 数据层:入口配置抽成 JSON 的结构设计
sites.json的结构直接决定后续好不好维护。我的字段设计是这样的:name是卡片标题,desc是一句话说明,url是目标地址,tag是分类标签,icon是可选图标或首字母。tag字段除了显示分类,还能参与搜索匹配,一举两得。
[ { "name": "主控面板", "desc": "查看整体运行状态与任务概览", "url": "./console.html", "tag": "核心" }, { "name": "作业中心", "desc": "提交任务、查看执行记录", "url": "./jobs.html", "tag": "核心" }, { "name": "使用说明", "desc": "从零开始的完整操作文档", "url": "./docs.html", "tag": "文档" }, { "name": "常见问题", "desc": "高频问题与排查思路汇总", "url": "./faq.html", "tag": "文档" } ]url用相对路径是个有意的选择,比如./console.html,这样整个目录搬到任何子路径下都不会失效。如果写成/console.html,一旦部署在/entry/这样的子路径里,请求就会跑到根目录去,404 是必然的。
数据加载失败必须有降级方案。fetch出错时不要让页面空着,给一个明确提示,同时把所有可能的原因大致列一下。这个提示看起来是小事,但它是用户和你之间唯一的沟通渠道,没有它,用户只能看到白屏然后来问你“页面坏了”。
3. 五分钟实操:从零到能打开
3.1 第一步:建目录并把四个空文件铺好
打开终端,两行命令解决问题。这里不用任何包管理器,所以也没什么初始化流程要走。
mkdir mc-jc-entry && cd mc-jc-entry mkdir assets touch index.html style.css app.js sites.json顺带把favicon.svg也放进去。图标这事别嫌麻烦,浏览器标签页上默认那个空白文档图标,在一堆标签里根本分不清,做一个纯色底加两个字母的 SVG,十几行就够。SVG 图标的好处是不用管分辨率,也不会因为尺寸不对发虚。
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> <rect width="64" height="64" rx="14" fill="#2f6fed"/> <text x="32" y="42" font-size="26" font-family="sans-serif" fill="#fff" text-anchor="middle">M</text> </svg>index.html的<head>里记得加上favicon引用、viewport元信息、description描述。viewport漏掉的话,手机上整个页面会按 980px 宽度渲染再缩小,字小到看不清,这是移动端适配最常见的第一个坑。
3.2 第二步:把卡片渲染逻辑写进 app.js
渲染函数的核心思路是:拿数据、清空容器、循环建节点、塞进去。用document.createElement而不是直接拼innerHTML,是为了避免数据里的特殊字符破坏结构,同时也省去手动转义的麻烦。虽然入口数据是自己维护的、可信度很高,但养成这个习惯没坏处——哪天数据源变成别人提供的,你就不用回头返工。
const grid = document.getElementById('grid'); const emptyTip = document.getElementById('empty'); const searchInput = document.getElementById('search'); const themeBtn = document.getElementById('theme'); let allSites = []; function buildCard(item) { const card = document.createElement('a'); card.className = 'card'; card.href = item.url; card.target = '_blank'; card.rel = 'noopener noreferrer'; const title = document.createElement('h2'); title.textContent = item.name; const desc = document.createElement('p'); desc.textContent = item.desc; card.append(title, desc); return card; } function render(list) { grid.innerHTML = ''; list.forEach(item => grid.appendChild(buildCard(item))); emptyTip.hidden = list.length > 0; } function filter(keyword) { const key = keyword.trim().toLowerCase(); if (!key) return render(allSites); const result = allSites.filter(item => [item.name, item.desc, item.tag].join(' ').toLowerCase().includes(key) ); render(result); } searchInput.addEventListener('input', e => filter(e.target.value)); document.addEventListener('keydown', e => { const inField = ['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName); if (e.key === '/' && !inField) { e.preventDefault(); searchInput.focus(); } }); fetch('./sites.json') .then(res => { if (!res.ok) throw new Error('HTTP ' + res.status); return res.json(); }) .then(data => { allSites = data; render(allSites); }) .catch(err => { grid.innerHTML = ''; emptyTip.hidden = false; emptyTip.textContent = '入口数据加载失败:' + err.message + ',请检查文件路径与访问方式。'; });target="_blank"配rel="noopener noreferrer"是必须成对出现的。少了noopener,新打开的页面能通过window.opener反过来操作当前页,是个已知的安全隐患;noreferrer则顺带把来源信息也拦住。
3.3 第三步:主题切换与偏好持久化
主题这块单拎出来说,因为它有一个“闪烁”问题需要处理。如果等脚本执行完再切换到深色,用户会先看到一帧白屏再变黑,观感很差。解决办法是在head里内联一小段脚本,在页面渲染前就把>(function () { const saved = localStorage.getItem('mcjc-theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const theme = saved || (prefersDark ? 'dark' : 'light'); document.documentElement.dataset.theme = theme; })();
切换逻辑放在app.js里,读取当前值取反,写回dataset和localStorage。用try...catch包一下localStorage的读写,因为在部分隐私模式下访问它会直接抛异常,不包的话整个脚本就停在那儿,后面的渲染逻辑全部不执行,页面照样白屏。
themeBtn.addEventListener('click', () => { const current = document.documentElement.dataset.theme; const next = current === 'dark' ? 'light' : 'dark'; document.documentElement.dataset.theme = next; try { localStorage.setItem('mcjc-theme', next); } catch (e) { /* 存储不可用时静默降级,仅本次会话生效 */ } });3.4 第四步:本地起服务预览,别直接双击文件
这一步是新手最容易翻车的地方。双击index.html用file://协议打开,样式和脚本能跑,但fetch('./sites.json')会失败,控制台报一串跨域相关的错误。原因不复杂:file://下浏览器把本地文件视为不同源,fetch请求被安全策略拦截。这不是你代码写错了,是协议限制。
绕过方式很简单,起一个本地静态服务即可。装过 Python 的话,一行命令:
python3 -m http.server 8080然后在浏览器访问http://localhost:8080。没装 Python 也行,Node 环境用npx serve一样的效果,或者干脆用编辑器自带的预览插件。核心就是让页面走http://而不是file://。
注意:本地服务起来的页面,改动文件后刷新即可看到效果。但如果浏览器缓存比较激进,可能刷不出新样式,这时候用强制刷新(多数浏览器是 Ctrl+F5 或 Cmd+Shift+R),比清缓存省事。
3.5 第五步:托管上线与缓存参数怎么设
静态文件上线,本质就是把这四个文件传到一个能对外提供静态资源的地方。这里不讲具体平台,讲原理和参数,因为无论放哪儿,规矩是一样的。
第一,路径全部用相对路径。index.html引用./style.css,app.js请求./sites.json,卡片地址也写成相对路径。这样整个目录可以挂在根路径,也可以挂在/tools/mc-jc/这样的子路径,不用改任何一行代码。
第二,HTML 不缓存,静态资源长缓存。index.html设Cache-Control: no-cache或者很短的max-age,保证用户每次拿到最新的页面结构;style.css、app.js、图标这类带文件指纹的可以设很长的max-age。如果没有构建流程加指纹,那就折中:把max-age设成几小时,别设成年。
第三,sites.json的缓存要短。它是内容层,改得最频繁,设成no-cache或者max-age=60比较合适。要是设成长缓存,你改完链接自己能看到,同事打开还是旧的,然后跑来问你为什么不生效,这种沟通成本比省下的那点带宽贵得多。
第四,确认 MIME 类型。.json必须是application/json,.css是text/css,.js是text/javascript。多数托管环境会自动处理,但自己搭服务的时候容易漏。类型不对时浏览器会拒绝执行,表现为“文件明明存在但功能不生效”。
4. 常见问题排查实录
4.1 打开页面一片白,什么都没有
白屏是最高频的问题,排查顺序按“先看控制台,再看网络,最后看代码”来,别一上来就怀疑自己逻辑写错。
第一步,打开浏览器开发者工具的控制台。如果有一条红色的Uncaught SyntaxError,那就是 JS 语法写错了,通常是少了个括号或逗号,错误信息里会直接给出行号。第二步看网络面板,找sites.json那一条,状态码是 404 就是路径写错,是 200 但内容为空就是文件真没内容。第三步才轮到看渲染逻辑,比如getElementById拿到的元素是不是null——脚本写在body里元素之前,就是这个问题,把script标签挪到body末尾或者加defer属性都能解决。
还有一种白屏是“有内容但看不见”,多半是颜色问题。深色模式下文字和背景撞色,或者变量没被正确覆盖,看起来就是空白。用开发者工具的元素检查面板手动改一下背景色,能立刻判断是不是这个原因。
4.2 样式完全没生效,页面变成纯文本
样式全丢,第一反应应该是去看style.css的请求。如果网络面板里这条请求是 404,那问题在路径;如果是 200 但样式没应用,问题在 CSS 本身。
路径问题里最常见的是大小写。本地环境某些文件系统不区分大小写,Style.css和style.css访问都一样,部署到区分大小写的环境上立刻 404。所以从第一天起就统一小写,别给自己埋雷。
CSS 本身的问题,按这几种查:选择器写错(#grid写成.grid)、属性值缺单位(padding: 18而不是18px)、变量拼错(定义了--brand却引用--brand-color,引用不存在的变量不会报错,只会静默失效)。最后一种最难查,因为控制台干干净净,什么都不提示。
4.3 fetch 报错或数据加载不出来
报错信息大致分两类,判断方法看协议。地址栏是file://开头的,那就是前面说的本地文件跨域限制,起个本地服务即可。地址栏是http://还报错的,看具体信息:404是路径问题,检查sites.json到底在不在那个位置;Unexpected token开头的是 JSON 格式错误,多半是最后一个对象多了个逗号,或者引号用了中文全角。
JSON 格式错误有个很实用的排查技巧:把文件内容整个粘到浏览器的控制台里,用JSON.parse()跑一遍,报错信息会指出具体位置。比肉眼逐行扫快得多。
还有一种是“数据加载了但没渲染”。这时候在.then里加一句console.log(data),看看拿到的是数组还是对象。如果sites.json最外层写成了{"list": [...]}这样的对象,而代码按数组处理,forEach就会报not a function。
4.4 手机上卡片挤成一列或者横向溢出
移动端的问题基本集中在三处。宽溢出最常见的原因是某个元素设了固定宽度,比如卡片写死width: 420px,窄屏下直接撑破视口,页面出现横向滚动条。用max-width: 100%或者干脆改成 Grid 的弹性布局能解决。
卡片在手机上只剩一列是正常的,minmax(260px, 1fr)在 375px 宽的屏幕上确实只够放一列。想在小屏上更紧凑,可以在媒体查询里把最小值降到 220px 左右,同时把内边距从 18px 收到 14px。但别低于 200px,否则描述文字会被压成每行两三个字,阅读体验反而更差。
还有一种是点击区域太小。手机上卡片之间如果挨得太近,容易点错。把gap保持在 12px 到 16px 之间,卡片高度不低于 72px,基本就不会误触了。
4.5 常见问题速查表
| 现象 | 最可能原因 | 排查动作 | 解决方式 |
|---|---|---|---|
| 整页白屏 | JS 语法错误 | 看控制台第一条红色报错 | 按行号修语法 |
| 有 HTML 无样式 | CSS 路径 404 或大小写不符 | 网络面板查 style.css | 统一小写、改相对路径 |
| 卡片不出现 | JSON 加载失败 | 看有没有 Uncaught 报错 | 起本地服务、修 JSON 格式 |
| 搜索无反应 | 事件未绑定或元素 id 不符 | 控制台打印元素引用 | 核对 id 与脚本位置 |
| 主题切换无效 | 变量覆盖层级不对 | 检查 dataset 写在哪个元素 | 写到 documentElement |
| 手机横向滚动 | 元素固定宽度超出视口 | 逐个隐藏元素定位 | 改弹性宽度 |
| 上线后 404 | 用了根路径引用 | 看请求地址是否缺前缀 | 全改相对路径 |
| 改了内容不生效 | 缓存策略过长 | 强刷后是否正常 | 缩短 json 缓存时间 |
5. 几个踩过坑之后才明白的经验
5.1 数据与视图分离这一步,早做早省事
我一开始做入口页也是硬编码,十个卡片复制十遍结构,当时觉得快。三个月后要加一个“分类分组”的功能,我得把十段重复标签逐个改,改到第六个就开始怀疑人生。抽成 JSON 之后,同样的改动只是给渲染函数加一层分组逻辑,数据一行不动。
这件事的普适价值在于:只要一份内容会出现两次以上,它就该被抽出来。入口页里每条链接的名称、描述、地址,在搜索、渲染、排序里都要用到,散在 HTML 里就意味着三处维护。抽出来之后只有一处,剩下两处都是读。这个判断标准放到任何项目里都成立。
配套的一个小习惯是给 JSON 加字段时保持向后兼容。比如后面想加一个hot: true标记热门入口,渲染时用item.hot判断,没这个字段的条目照样正常显示,不用回头把旧数据全补一遍。
5.2 入口页的安全边界要提前划清楚
入口页本身没有后端,攻击面很小,但有两点必须守住。第一,只放可信的链接。这个页面很容易变成“什么链接都往里塞”的杂物间,一旦混进来源不明的地址,点进去的风险就落在使用者身上。我的做法是每条入口在提交前都过一次人工确认,宁可少放,不放不确定的。
第二,新窗口打开必须带rel="noopener noreferrer"。这个属性组合前面提过,这里再说一次是因为它真的容易被忘。少了它,被打开的页面可以通过window.opener.location把当前页重定向到别处,用户看到的是“我点了个链接,回到入口页却变成了另一个网站”,很难解释清楚。
还有一点是别在页面里放任何敏感信息。入口页是公开可访问的静态文件,写在里面的东西等于公开。地址、名称、说明这些是设计上就要公开的,除此之外的东西一概不放,包括任何看起来“只有内部人看得懂”的缩写。
5.3 这个页面后续还能怎么长
入口页做出来之后,最容易想到的扩展是按标签分组显示。sites.json里已经有tag字段,渲染时先按tag归并,每组前面加个小标题,页面结构立刻清晰不少。再进一步,把标签做成可点击的筛选条,点“文档”就只剩文档类入口,和搜索框互补。
第二个方向是加一个“最近访问”区。用localStorage记录点击过的入口,取最近五条显示在顶部。这个功能对高频使用者很实用,实现也就是在卡片的click事件里写一条记录,成本很低。
第三个方向是状态提示。如果某个入口暂时不可用,卡片上可以显示一个灰态标记。但这需要数据层支持,得在sites.json里加status字段并手动维护,或者接一个轻量的可用性检查。前者简单但容易过期,后者要写逻辑但更准,按实际需要选。
我个人在实际操作中的体会是,入口页这类小工具的价值不在于技术多复杂,而在于它是否真的被用起来。判断标准很朴素:如果两周后还有人主动打开它找东西,说明入口收拢得对;如果没人用,多半是入口不全或者分类不清,回去补数据比加功能更管用。