单文件个人资料展示页
把「一份人物资料文本 + 一批本地图片 + 一个参考模板」变成一个可离线打开、零外部 JS 依赖的 HTML 文件。
本技能沉淀自真实交付案例:依据百度百科词条 + 用户提供的 9 张本地图片,仿「个人资料展示响应式网页模板」的结构与动效,产出单文件资料展示页。完整案例见 references/case-momota-profile.md。
何时使用
- 用户给了人物/项目资料文本和图片,要求做成展示页、主页、作品集页。
- 用户给了一个「在线预览」模板页(模板王等),要求照着做一份。
- 明确要求"单文件"、"离线可用"、"不要外部库"的页面。
- 需要把模板的 WOW.js / animate.css / Bootstrap / prettyPhoto 动效搬过来但不想引依赖。
核心原则
- 单文件 + 零外部 JS。CSS 与 JS 全部内联,图片用相对路径。第三方动画库一律手写等价实现。
- 先破壳,再动手。模板王的「在线预览」页是 iframe 外壳,真模板在
{预览页名}_files/index.html。照着外壳做会做错。 - 动效触发用滚动位置判定,不用 IntersectionObserver。原因见下方「踩坑」。
- 图片先语义重命名再引用。原始文件名常含
&@空格 括号,直接进 HTML 属性必炸。 - 交付前必须跑渲染验证。不做验证的单文件页面,图片裂、横向溢出、整段空白这三类问题靠肉眼看不出来。
工作流
完整展开见 references/workflow.md。七个阶段:
| # | 阶段 | 产出 |
|---|---|---|
| 0 | 输入盘点 | 资料文本 / 图片清单 / 参考模板三者就位 |
| 1 | 破壳找真模板 | 定位 *_files/index.html |
| 2 | 提取设计语言 | 调色板、字体栈、栅格、组件风格 |
| 3 | 章节结构映射 | 从资料文本切出 8–10 个板块 |
| 4 | 图片语义归档 | images/ 语义命名 |
| 5 | 搭建页面 | 单文件 HTML,assets/page-skeleton.html 起步 |
| 6 | 渲染验证 | 三视口截图 + 三项断言 |
| 7 | 交付 | index.html + images/ |
阶段要点
1 破壳找真模板
在线预览 - {模板名} - 模板王.html ← 外壳,只有 header + iframe
在线预览 - {模板名} - 模板王_files/
├── index.html ← 真模板,读这个
├── style.css / *.min.css ← 设计语言在这里
└── *.下载 ← 被浏览器重命名的 js/css
读真模板的 index.html + style.css,抄它的结构顺序、配色、间距节奏、hover 表现;不要抄它的库引用。
2 提取设计语言
落成 :root 变量块,后续全部引用变量。参考配色见 references/design-tokens.md(含案例实测值)。
3 章节结构映射
人物资料页的通用骨架,按资料文本实际内容取舍:
banner(大图 + 身份 + 标签 + 双按钮)
sticky menu(吸顶导航 + 滚动高亮)
overview(2×2 卖点宫格)
profile(左大图 / 右长文 + 事实清单)
skill(左文字评价 + 右进度条)
resume(双段时间线,按时期分组)
testimonial(轮播评价)
portfolio(3×3 图集,hover 蒙版 + 灯箱)
contact(深色留言区 + 资料出处)
footer
4 图片语义归档
按内容语义重命名,例如 9 张图 → hero-collage / all-england / tokyo-2020 / olympic-walk / all-japan / ntt-east / team-event / off-court / action-seq。
批量脚本见 scripts/rename-images.sh。重命名后 grep 一遍确认无残留特殊字符。
5 搭建页面
从 assets/page-skeleton.html 复制起步:它已含变量块、重置样式、章节骨架、六个 JS 模块的函数桩。
各模块的完整可运行实现见 references/components.md,可直接粘贴:
- 滚动显现动画(rAF 节流 + 滚动位置判定 + 同级自动错峰)
- 进度条填充
- 吸顶导航 + 移动端折叠菜单 + 平滑滚动 + 滚动高亮
- 轮播(动画重放 + 定时器 + hover 暂停 + 圆点)
- 灯箱(键盘 Esc / ← / →,点遮罩关闭)
- 表单前端校验
6 渲染验证
node scripts/verify-render.mjs "path/to/index.html" --out ./_debug
脚本用无头 Chrome + CDP(Node 22 内置 WebSocket,不需要 puppeteer),在 1440 / 768 / 390 三个视口逐段滚动截图,并对每个视口断言:
| 断言 | 表达式 | 期望 |
|---|---|---|
| 图片真加载 | 有 src 且参与布局的 img.naturalWidth |
全部 > 0 |
| 无横向溢出 | documentElement.scrollWidth vs innerWidth |
差值 ≤ 1px |
| 显现动画触发 | .reveal 的 getComputedStyle().opacity |
滚过之后为 1 |
| 脚本已接管 | documentElement.classList.contains('js') |
true |
静态断言不够,必须再做交互探测。 脚本随后会实际驱动页面,逐项打勾:
| 探测 | 判据 |
|---|---|
| 图集点击被拦截 | 派发可取消 click 事件后 defaultPrevented === true(否则会真跳转) |
| 灯箱开/关 | .open 类增删、lbImg.naturalWidth > 0、图注非空、关闭后 body.style.overflow 复原 |
| 轮播 | 翻页后激活项变化、active 项恰好 1 个、圆点数 === 幻灯片数 |
| 表单 | 错误邮箱被拦、合法输入通过、提交后重置 |
| 移动端菜单 | 汉堡按钮可见时能展开/收起 |
> 只做上面三项静态断言,会漏掉"整块功能静默失效"这一类问题——页面看着完全正常,但灯箱点不开、轮播翻不动、表单没反应。本技能就是这么发现案例页面的脚本中断的,详见 references/case-momota-profile.md。
_debug/ 为非交付物,交付时说明清楚或直接移出。
7 交付
deliverable/
├── index.html ← 单文件,约 60–100 KB 量级
└── images/ ← 语义命名
踩坑(本技能最值钱的部分)
- 绝对不要把"浏览器另存为"的 HTML 当交付物或改动基线。另存会把
href="#anchor"改写成绝对 URL(file:///.../index.html#anchor),并把 JS 跑完后的 DOM 状态固化进文件(animated类、内联animation-delay、class=" js js")。后果是:页面脚本里document.querySelector(a.getAttribute('href'))会抛SyntaxError: ... is not a valid selector,脚本从那一行起全部中断——灯箱、轮播、表单静默失效,但滚动显现恰好还活着(它的 scroll 监听在中断点之前就注册好了),所以肉眼看不出问题。
两层防御:① 交付原始index.html,不要在另存文件上继续改;② 代码里凡是把href喂给querySelector的地方,先判charAt(0) === '#'再调用。 --disable-javascript在--headless=new下无效。想用"禁用 JS 对比截图"排查问题时,会拿到完全相同的两张图,白费功夫。- 单次 Shell 调用里用
&起的后台 Chrome 会在该调用结束时被回收。要用后台任务方式启动(run_in_background或nohup),否则截图脚本连不上调试端口。 - 不要
rm -rfChrome 的--user-data-dir,会被沙箱拦截。放到$TEMP下,用完不删即可。 - CDP 响应是三层嵌套
{result:{result:{value}}}。不判msg.error时取到的 value 永远是undefined,会误判成"页面没渲染"。 - 判页面就绪不能只看
document.readyState,也不能用Page.loadEventFired。Target.createTarget出来的about:blank天然就是complete状态,只看 readyState 会立刻返回,而导航还没提交,随后Runtime.evaluate报Inspected target navigated or closed。必须同时判location.href已不是about:、readyState === 'complete'、body里已有元素。 - 验证脚本里不要用
element.click()去点带href的<a>。页面脚本若没接上处理器,click()会真的跳转(还会连带把执行上下文干掉,报navigated or closed)。改为派发可取消的事件并读event.defaultPrevented,且这一步必须同步返回——表达式里只要有await,导航就会在 await 期间提交,连结论都拿不回来。 - 滚动显现不要用 IntersectionObserver。配合
#锚点直达 + 无头截图时,IO 的触发时序不可靠,出现过整段内容全空白。改成与 WOW.js 同机制的滚动位置判定后稳定。 - 本地缺字体文件。模板可能引用 font-awesome 图标字体但没带
.woff2,会显示成方块。直接改用内联 SVG 图标(viewBox="0 0 24 24"+stroke="currentColor"),顺带摆脱字体依赖。 - 图片文件名里的
&与@。微信传图下载名为_cgi-bin_...&MsgID=...&skey=@crypt_...&mmweb_appid=...jpg,放进src后浏览器会把&当实体起始。必须重命名。 prefers-reduced-motion要兜底。给.reveal { opacity: 1 !important }兜底,否则用户开了"减少动态效果"会看到空白页。- 验证脚本统计图片时要排除隐藏占位图。灯箱的
<img id="lbImg">初始没有src(或src=""),naturalWidth恒为 0,不过滤会误报"图片加载失败"。过滤条件:src非空 且getClientRects().length > 0。
资源清单
references/workflow.md— 七阶段详细流程与每步检查项references/design-tokens.md— 配色/字体/间距设计令牌与章节结构图references/components.md— 六个 JS 模块的完整可粘贴实现references/case-momota-profile.md— 完整案例复盘(输入、决策、验证结果、踩坑)assets/page-skeleton.html— 单文件页面骨架,直接复制起步,开箱可跑scripts/verify-render.mjs— 无头 Chrome CDP 多视口渲染验证 + 交互探测scripts/rename-images.sh— 图片批量语义重命名(默认 dry-run)scripts/map.example.txt— 重命名映射表示例