单文件 HTML 交付物的无头自检
做完 HTML 交付物(尤其是带交互逻辑的单文件工具)后,不要直接交。 本环境有 Chrome,可以在一秒内完成「逻辑断言 + 视觉检查」,比让用户截图反馈快得多。
0. 环境要点
- Chrome 路径:
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome - 必须加
--no-sandbox,否则在本沙箱里起不来 - 加
--virtual-time-budget=4000让内联脚本跑完再输出 - 用
file://绝对路径;若页面靠<script src="...">加载同目录文件, 临时副本必须放在原目录内,否则相对路径 404(放/tmp会静默变成空数据)
1. 逻辑断言:--dump-dom + 正则
cd <项目目录> && "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--headless=new --no-sandbox --disable-gpu --virtual-time-budget=5000 \
--dump-dom "file://$PWD/page.html" 2>/dev/null > /tmp/dom.html
然后用 Python 逐项断言关键标记(用标记判断,不要靠肉眼读整份 DOM):
import re
h = open('/tmp/dom.html', encoding='utf-8').read()
for pat, label in [(r'tr class="row', '表格行'),
(r'class="board"', '看板'),
(r'已载入 44 条', '数据载入')]:
print(('✅' if re.search(pat, h) else '❌'), label)
# 统计数字、空状态、错误关键字
print('NaN:', h.count('NaN'), '| undefined:', h.count('undefined'))
⚠️ 坑:
--dump-dom会把<script>源码也一起输出。 所以脚本里出现的字符串(模板片段、错误文案、typeof x !== 'undefined') 都会被统计到 —— 计数 ≥1 不代表渲染出错,要结合上下文或改用 DOM 结构标记判断。
2. 视觉检查:--screenshot + PIL 局部放大
"$CH" --headless=new --no-sandbox --disable-gpu --hide-scrollbars \
--window-size=1440,1150 --virtual-time-budget=4000 \
--screenshot=/tmp/shot.png "file://$PWD/page.html"
整页截图看宏观版式;具体某个组件要裁切放大(不然细节看不清、会误判为 bug):
from PIL import Image
im = Image.open('/tmp/shot.png')
im.crop((20, 295, 1480, 355)).resize((1460, 180), Image.LANCZOS).save('/tmp/crop.png')
裁切坐标怎么定位:先看整图,量出组件在图中的大致 y 区间;
注意 Read 工具展示时可能被缩放(图宽 ≠ --window-size 宽度),
用「展示坐标 ÷ 展示宽 × 实际宽」换算回真实像素再裁。
3. 验隐藏状态:临时副本注入 UI 状态
展开面板、弹窗、看板、错误态 —— 默认截图都看不到。 复制一份到同目录、注入一行强制状态、截图、删掉:
python3 - <<'PY'
s = open('page.html', encoding='utf-8').read()
anchor = "var dirty = false;" # 找一个注入锚点
s = s.replace(anchor, anchor + "\nsetTimeout(function(){ ui.open['app001']=true; renderBody(); }, 50);")
open('_tmp_probe.html','w',encoding='utf-8').write(s)
PY
"$CH" --headless=new --no-sandbox --disable-gpu --hide-scrollbars \
--window-size=1440,1150 --virtual-time-budget=4000 \
--screenshot=/tmp/shot_state.png "file://$PWD/_tmp_probe.html"
rm -f _tmp_probe.html
也可以给页面内置
#hash视图路由(如#table/#board), 这样直接file://.../page.html#board就能截图,且链接可分享 —— 比每次注入干净。
4. 静态部署后的联通性验证
for p in "/" "/data.js" "/download.xlsx"; do
echo "$p -> $(curl -s -o /dev/null -w '%{http_code} %{size_download}' --max-time 25 "$URL$p")"
done
三个都要 200 且 size 非 0。部署目录里避免中文文件名(URL 编码容易出问题), 需要中文名就本地保留、部署副本改 ASCII。
5. 交付前 checklist
-
--dump-dom关键标记全过,无NaN/ 无空数据静默降级 - 数据加载失败时有显式提示,不是白屏或空表
- 截图确认:无横向溢出、无被裁切的列、sticky 表头生效
- 每个隐藏态(展开/看板/空状态)都截过图
- 手机宽度过一遍(注意下面的 500px 限制)
- 在线链接的每个静态资源都 200
⚠️ 两个会骗到你的坑
(1)无头 Chrome 的视口宽度最小约 500px。
--window-size=390,844 会被钳到 innerWidth=500,--force-device-scale-factor=2
也不会改变 CSS 视口。所以 390px 真机宽度测不了 —— 别拿 414 的截图当 390 的结论
(看起来「被裁切」的区域,往往只是截图比实际视口窄)。
窄屏只能靠:① 加防御性 media query;② 用下面这段量溢出代替肉眼判断:
var vw = innerWidth, bad = [];
document.querySelectorAll('body *').forEach(function(el){
var r = el.getBoundingClientRect();
if(r.right > vw + 1 && r.width > 0 && !el.closest('.tblwrap')) // 排除有意内部滚动的容器
bad.push(el.tagName + '.' + (el.className||'-') + '@' + Math.round(r.right));
});
document.title = 'VW=' + vw + ' SW=' + document.documentElement.scrollWidth + ' || ' + (bad.slice(0,10).join(' | ')||'clean');
SW === VW 且 bad 为空(或只剩预期滚动容器)= 没有真实溢出。
(2)动态插入 DOM 后,closest() 的选择器可能匹配不到。
「展开详情」这类额外插进去的行,若只给主行加了 data-id,
el.closest('tr.row') 在详情行里会返回 null —— 表现是该区域所有编辑静默失效,
不报错、不提示。稳妥做法:所有可编辑容器统一带 data-id,统一用 closest('[data-id]')。
6. 配套:单文件工具 + Excel 双形态
单文件 HTML 工具的 localStorage 是按浏览器隔离的,不是云同步。
交付时务必同时给一份 xlsx(openpyxl):
DataValidation 做下拉、FormulaRule 做条件格式(逾期红/临近黄/Offer 绿)、
freeze_panes + auto_filter、第二个 sheet 用 COUNTIF/COUNTIFS/SUMPRODUCT 做统计。
生成器脚本要写成幂等可重跑的,并把「本地中文名 + 部署用 ASCII 名」两份都输出,
这样页面里的下载链接本地也能点开。
7. 换设备/改数据后的同步纪律
deploy-xxx/ 目录永远是快照。改完源文件必须:
cp 源文件 → deploy-xxx/ → 重新部署 → curl 复验。
忘了同步 = 线上还是旧版,而且不会有任何报错。