• 首页
  • 博客
  • 关于
  • 归档
  • 分类

舞乐漪的小站

一个折腾服务器、Live2D 看板娘和 Typecho 的个人站点
工具箱

别再「看起来对」:用真浏览器验证前端

2026-10-18 浏览量 0 评论数 1

站点是 Typecho 1.3 加 sagiri 主题,跑在一台 2 核 2G 的云服务器(Ubuntu 22.04)上,同一台机器还兼着 QQ 机器人。站里 18 篇文章、6 个分类、26 个标签。前端这一摊基本都是我一个人折腾,没有测试同学、没有设计走查,所以“谁来判断它对不对”这件事,最后只能落在我和一张截图上。

下面记的,就是我被打脸之后改出来的验证方式。

一、我以前的验证方式:肉眼 + 一张截图

以前我“验证前端”的完整流程是这样的:改完 CSS,浏览器里打开页面,往下拉两屏,看着顺眼,截一张图存进笔记,收工。要是有人问“你怎么确定修好了”,我就把图发过去。

这套流程的问题不在于“看图”本身,而在于它验证的是我的眼睛,不是页面。眼睛只覆盖了我恰好滚到的那一屏、恰好触发的那几个状态、恰好没走缓存的那一次加载。剩下的部分——首屏之外的、hover 才出现的、手机上才有的、第二次访问才发生的——全在证据之外。

更要命的是它不好失败。截图永远是“生成成功”的,脚本也永远是“退出码 0”,只要我不盯着看,它什么都不会说。下面这几个坑,全都是“我的截图说好,别人的屏幕说不好”。

二、截图骗过我一次:滚动显现的动画

主题给列表页的文章卡片加了滚动显现:进入视口时 opacity 从 0 过渡到 1。我在无头浏览器里截了一张列表页,整块内容区是空白的。

第一反应是样式没生效,翻 CSS 翻了半天。后来在 page.evaluate 里读了一下才知道:

await page.evaluate(() => getComputedStyle(document.querySelector('#posts article')).opacity);
// "0"

页面从加载到截图,一次都没有滚动过。显现动画的 IntersectionObserver 压根没被触发,卡片老老实实停在 opacity: 0。截图没有骗我,是我用一个不会滚动的采样,去验证一个依赖滚动的效果。

修法很土:截图前先滚到底,再滚回顶,等过渡结束。

await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(600);
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(600);
await page.screenshot({ path: 'list.png' });

教训是:截图只能证明“这一屏、这一刻、这个滚动位置,有这些东西”。它证明不了“元素可见”,也证明不了“效果生效”。拿它当可见性判据,测的是采样时机。

三、page.mouse 不是手指

看板娘手机端有个“点击换人”的交互。我写完以后用 Playwright 测:移动视口配好、isMobile: true、hasTouch: true,然后鼠标在 canvas 上点一下,换人成功,控制台里 [live2d] 就绪 也打出来了。我认为它过了,写了句“移动端已验证”。

真机上点了没反应。

根因很直白:page.mouse.click() 派发的是鼠标事件流(mousedown / mouseup / click),移动端浏览器在真实触摸下合成的是 touch 事件流。我那套换人逻辑绑在 touchstart 上,page.mouse 这条路径上它一句都没跑到——但脚本“点了”,于是“通过了”。

正确的写法是把触屏能力真的打开,用触屏 API 去点:

const ctx = await browser.newContext({
  viewport: { width: 390, height: 844 },
  isMobile: true, hasTouch: true, deviceScaleFactor: 2
});
const page = await ctx.newPage();
const box = await page.locator('.l2d-box canvas').boundingBox();
await page.touchscreen.tap(box.x + box.width / 2, box.y + box.height / 2);

顺带一条:坐标别拍脑袋。看板娘是绝对定位加 canvas 叠层,坐标拿错就是从别的元素身上点过去,同样不报错。稳妥的做法是先 elementFromPoint 确认这个点上最上层是谁,再派发事件(这套方法后面还会用到)。

四、CDP 也有物理极限

真触屏也不是万能的。有个交互要求“320ms 内连点两下算双击”,我一开始这么写:

await page.touchscreen.tap(x, y);
await page.waitForTimeout(150);
await page.touchscreen.tap(x, y);

结果永远不算双击。原因不在页面上:page.touchscreen.tap 走的是 CDP 的 Input.dispatchTouchEvent,一次 touch 序列的往返,在这台 2 核 2G 的机器上实测 300–600ms。也就是说我 waitForTimeout(150) 之后,两次点击的真实间隔早就过 320ms 了,双击窗口根本进不去。这种测试不只是测不出东西,还可能给出反的结论——“用户点两下没打开”,未必是代码错,可能只是往返太慢。

结论:UI 层的触屏 API 适合验证“点了有没有反应”;要验证“时间窗内的反应”,就得把事件派发下沉到页内,自己构造真实 TouchEvent:

await page.evaluate(() => {
  const el = document.querySelector('.l2d-box canvas');
  const r = el.getBoundingClientRect();
  const x = r.x + r.width / 2, y = r.y + r.height / 2;
  const mk = (type) => {
    const t = new Touch({ identifier: 1, target: el, clientX: x, clientY: y, pageX: x, pageY: y });
    const touches = type === 'touchend' ? [] : [t];
    return new TouchEvent(type, {
      bubbles: true, cancelable: true,
      touches, targetTouches: touches, changedTouches: [t]
    });
  };
  el.dispatchEvent(mk('touchstart'));
  el.dispatchEvent(mk('touchend'));
  el.dispatchEvent(mk('touchstart'));
  el.dispatchEvent(mk('touchend'));
});

这样两次点击的间隔由页内时钟决定,是毫秒级,320ms 的窗口才是真的。当然它验证的是我的监听逻辑,不是浏览器的手势语义——合成事件不触发默认行为,这一点自己心里要有数。

五、position: sticky 在 fullPage 截图里整块消失

侧栏 280px,用了 position: sticky。截图里侧栏整块不见了,一像素都没有。

不是 CSS 坏了,是截图方式和 position 语义在打架。fullPage: true 的做法是把视口撑到整页高度再拍一张;页面高度一口气变成几千像素的那一刻,sticky 元素按它的包含块重新定位,那块本该“贴在视口里”的内容就哪儿都不在了。

替代做法是先量页高,再撑视口,最后做一张普通截图:

const h = await page.evaluate(() => document.documentElement.scrollHeight);
await page.setViewportSize({ width: 1240, height: h });
await page.screenshot({ path: 'full.png' });   // 注意:不要传 fullPage

视口够高、位置仍然是“视口内”,sticky 元素就会正常画进图里。这两种截图的差别,光看图片文件名是看不出来的,我第一次踩就是踩在这——两张图长得不一样,我还以为是主题的锅。

六、移动端那次“看起来对”:侧栏空、哨兵卡住、分号吞脚本

移动端改造我动了四处,每一处都通过了“看起来对”。

侧栏是空的。 isPc() 是按普通 User-Agent 正则判断的,手机上连侧栏 HTML 都不输出——这本身没错。但我改完以后,手机上的侧栏是空的:模板默认激活的是「文章目录」,而主题 JS 在移动端根本不构建 TOC。DOM 在、盒子在、内容没有,桌面上一切正常。

无限滚动的哨兵卡住了。 IntersectionObserver 的哨兵如果加载完还留在视口里,就不会再次触发。桌面大屏第一页短,哨兵一直可见,于是“滑到底自动加载”永远只加载一次。

注入的脚本被前一句吞掉。 我用 (function(){ ... })() 往页尾塞代码,前面没加分号,紧贴在上一段表达式后面,浏览器把它当成了调用,报 setInterval(...) is not a function。加了前导分号才活过来。

新插入的图片没人管。 后插入的卡片里的图片,主题的懒加载不接管——扫描只在初始时跑过一次,新图永远停在 lazy-src 上不加载。所以取数据时还得手动补一句 img.src = img.getAttribute('lazy-src')。

这四个坑的共同点是:DOM 结构对、控制台不报错、截图也看不出问题。它们只在“真的滚到底、真的等一次异步加载、真的数一次请求、真的在手机断点上看一眼”的时候才暴露。

七、我现在的取证四件套

不再拿“看起来对”当结论了,落四个硬信号:

1. computed style。 getComputedStyle(el).opacity / display / position —— 断言的是浏览器算完之后的值,不是我以为写在文件里的那行文本。样式被后面的规则覆盖、被媒体查询改掉、被动画停在中间,都在这里现形。

2. 几何矩形。 el.getBoundingClientRect() 给出位置、尺寸、有没有被挤出视口、有没有压在别人身上。看板娘那一笔账就是这么算出来的:原来桌面 300px,实测压住了 150×79 的数据卡和 150×152 的词云区;改成 240px 并锚定内容区右边缘,手机 200px。这种“压住”只有矩形相交能证明,截图里它俩都只是“存在”。

3. elementFromPoint(x, y)。 直接问浏览器“这个坐标上最上层是谁”。所有点击类问题,先用它确认命中对象,再派发事件。第三节、第四节的坐标都是它兜住的。

4. 控制台日志。 page.on('console') 和 page.on('pageerror') 全量收下来,别只看有没有红字。我给关键路径打了点([live2d] 就绪),这些字符串就是断言锚点——“等到了这行日志”和“等到了 1.5 秒”是两回事。

布局那轮我也是量着改的:容器 1110px → 1240px,内容区 772px → 937px,卡片三列每列 299px、内边距 40px 收到 20px,侧栏 280px 加 sticky;词云 22 个词,字号 12px → 26px 按文章数线性映射、8 色循环,分类 6 个、标签 16 个胶囊。这些数字不是审美,是断言的基线——改完再量一遍,对不上就是错。

八、缓存也是验证的一部分

静态资源上了 gzip 加 Cache-Control: max-age=2592000(30 天),首屏传输量从 703KB 掉到 48.8KB,代价是“我修好了,你说没变”变成了日常。浏览器拿着 30 天前的 CSS 给你看,你看到的确实是我上周的代码。

所以我给自己立了三条:

  • 引用带版本。 style.css?v=20261018,改一次换一次。靠人记着刷新是记不住的。
  • 留自测钩子。 ?testswitch=1 强制走测试分支,?l2dfast=1 跳过空闲等待。646KB 的 l2d-widget.min.js 改成页面 load 之后空闲注入,正常路径要等 idle,验证时等不起——钩子让“等条件”变成“改条件”,而不是把超时时间调大。
  • 验证用干净上下文。 每次 browser.newContext(),冷缓存跑。不拿“我这边刚清过缓存”当证据。

自测钩子还有一个副作用挺值钱:它把“我心里知道要等什么”变成了代码里的一个开关,三个月后的我也能复现同一套验证。

九、能证伪的验证才算验证

回头看,我所有“看起来对”的误判都是同一个形状:我验证了一个更弱的命题。

截图证明“这一屏此刻有像素”,不证明“元素可见”;page.mouse 证明“坐标上发生了鼠标点击”,不证明“手指点了”;fullPage 证明“页面总高度有这么长”,不证明“整页都画出来了”。每一次我都在用弱命题冒充强命题,还都觉得挺踏实。

好的验证应该能失败。写下一条断言之前,先问自己它在什么情况下会变红:如果无论代码对错它都是绿的,那它不是验证,是装饰。

现在的顺序固定了:先想清楚“什么信号能证伪这次改动”(computed style / 矩形 / 命中对象 / 控制台日志 / 请求数),再开真浏览器跑,最后才截图。截图留着交付,不当证据。

相关阅读

  • 给博客加看板娘:从选型到交互,我把 Live2D 的坑走了一遍
  • 把站点从「能看」推到「能扛」:一次安全、性能与可发现性的系统加固
故障排查工具
一屏能塞多少信息:三列卡片、侧栏重做与移动端
没有了

评论/ 取消评论

  1. pcwl pcwl
    Quark Android

    你好

    2026-10-29 17:27 回复
  • 文章目录
  • 站点概览
    舞乐漪

    舞乐漪

    个人站长

    Funny创造世界

    河北省廊坊市广阳区 2026-10-17
    14 文章 6 分类
    10 标签

    分类与标签

    踩坑复盘运维建站故障排查前端工具TypechoLive2DAI安全加固

    分类

    默认分类2运维实录1建站笔记5前端折腾2工具箱3杂谈1

    标签

    踩坑复盘4运维4建站4故障排查3前端3工具3Typecho3Live2D1AI1安全加固1

    浏览量 : 0

    冀ICP备2026047696号-1

    © 2026 舞乐漪 · Powered by Typecho · Theme Sagiri

    This is just a placeholder img.