这个博客上线那天,我在封面上写了一句:《三日间的幸福》里那句「如果把余生的日子,标上价格出售」。
我想要的只是一个安静的开场——一屏全幅的海报,楷体,落款,雪在背后落。
接下来一天半,我几乎没再写内容,全在改「这一屏该怎么退场」。
代码绝大部分不是我敲的,是让一个 agent 照着github上面开源博客Firefly的文档敲的。CSS 我几乎没动过手,JS 只零星改过几行,剩下的全是我说要什么、它来写、我看一眼、说不对、再说一遍。
这篇记的就是这个过程:它怎么越来越偏,我怎么被它带着走,最后又怎么靠读源码爬回来。
后来我才想明白一件事:正因为我不写这些代码,我才必须会另一件事——验收。这篇从头到尾都在讲这个。
一、地基:能跑,但处处差一口气
我一开始就把技术栈压到最低,理由是「以后自己还看得懂」:
- Hugo v0.168 extended(
C:\Hugo\bin\hugo.exe),没有前端框架、没有打包器 - 一份 CSS,
assets/css/main.css,现在 2150 行——它生成的,我只改过几个数字 - 一个 IIFE 包住的原生 JS,
assets/js/main.js,855 行,按横幅切成八块——同样是它写的 - 四个模板:
baseof.html、index.html、list.html、single.html,外加五个功能页 layout - 布局偷了 Firefly(Astro 主题,Fuwari 那一支):固定毛玻璃导航条、左资料栏、右主列,容器 1120px
它能跑,但每一处都差一口气。我把这些「差一口气」一条条丢给 agent,它改得很快,快到我有种错觉:这个站我其实已经懂了。
二、四个小毛病(和一次被我忽略的伏笔)
1. 一篇文章只算 106 个字
文章页写着「約 106 字」,可那篇两千多字。
agent 的判断很准:Hugo 默认按空格分词,中文没空格,一段只算一个词。
# hugo.toml 顶层
hasCJKLanguage = true
一行解决,106 变 1735。顺带修好了站点总字数和阅读时长。
2. 音符点了没反应
右上角那个音符从建站起就没绑过点击事件,只做了「播放中变红脉动」的样子货。
补静音开关:切 audio.muted,图标上叠一条 rotate(45deg) 的斜线,状态存 hb_muted。第一版agnet把角度写反了——rotate(45deg) 画出来是 /,我要的是 \,得写 -45deg。我盯着那条斜线看了半天才确认不是自己眼花。
3. 卡片上印着 0001.01.01
五个功能页(关于 / 友链 / 留言 / 链接 / 我的)front matter 里没有 date,.Date 拿到零值时间。
补上建站日期就干净了。但顺手牵出一个更大的问题:首页列表用 .Site.RegularPages,把它们也当文章列出来了。
4. 文章数把「关于」「友链」也数进去了
口径收窄成只算 posts 分区:
{{ len (where .Site.RegularPages "Section" "posts") }}
站点统计卡、资料卡、关于页——我让它改了前两处。
这一处漏掉的代价,是后面一整晚。而它当时说的是「三处统计都已同步」,我信了,因为前面六件事它都做对了。
这是第一个伏笔:它会说「已修复」,但那是一次声明,不是一次验证。
三、封面:九个版本,全是被逼出来的
我对首页只有一个要求:打开像拆一封信。剩下所有版本,都是为这句话付的代价。
v0|固定层 + margin 补偿(它的第一个方案)
.site-cover { position: fixed; inset: 0; }
.layout-below-cover { margin-top: 100vh; }
封面用 fixed 钉在视口上,正文用 margin-top: 100vh 往下推一屏,滚动时 JS 给封面加 .site-cover-hidden 让它淡出。
问题是它根本没「滑走」,是「淡掉」——封面一动不动,正文自己在下面往上爬,中间必然留一段什么都没发生的空白带。因为背景和封面同图,看着不像空,但你一滚就知道:那段什么都没在发生。
我调了半天 margin、加了吸附、试了各种 px,全治不好。
v1|把海报放回文档流(我否掉了它那套)
.site-cover {
position: relative;
width: 100%;
height: 100vh;
}
它变成文档流里一个普通的一屏高元素,跟着页面 1:1 滚走,滚完正文自然接上。没有补偿、没有吸附、没有淡出补丁。
这一版让我确认一件事:结构错了,参数怎么调都是错的。
v2|底部羽化(它做得很漂亮,我否掉了)
我又嫌封面和正文之间那条硬边生硬,让它做底部羽化——两层背景各挂一条 mask-image,让海报底边化进后面的壁纸。
它做得很好,连 inset: -3% 导致基准高度多 6% 这种细节都算进去了,两层百分比分开写。
但我看十秒就难受:首页背景是动态下雪。羽化出来的透明带会一直透出流动的雪,跟静止的信笺拼在一起,割裂得厉害。
这是第一条真正属于我的判断:局部羽化只适合静态背景。背景会动时,透明区域等于给动态开了一扇窗,它和静止内容的落差比硬边更刺眼。
v3|滚动时间轴(它最坚持、也最不该用的一版)
我真正想要的是「滑下去,字一点点长出来」。它给了 CSS 原生方案:
.hero {
animation: heroRise linear both;
animation-timeline: view();
animation-range: entry 20% entry 60%;
}
opacity、translateY、blur 一起动,观感确实像一句话慢慢凝聚成形。为了给它跑道,它还在封面和正文之间塞了一个 40vh 的空 div。
它坚持这套的理由很漂亮:原生、零 JS 开销、跑在合成器上、性能最好。
这套东西有两个病:
- 整个元素共用一个进度,标题和引言只能一起浮,分不出层次
- 进度跑在合成器上,
getComputedStyle永远只读到起始值
第二条对我是致命的。我能看见它在动,但没法证明它动到了哪,也没法量出「停在半透明」到底是我的错觉还是真的。
后来我把这句话写进了自己的规矩里:一个测不出来的效果,等于一个交付不了的效果。
但当时我还不这么想。当时我觉得「看着挺好」,只是偶尔觉得哪里不对。
它在这里犯的错不是写错代码,是选了一个更漂亮、但不可验收的方案。漂亮是它的偏好,可验收是我的需求。
v4|把进度算在 JS 里
同一件事,我让它算在 JS 里:
var rect = hero.getBoundingClientRect();
var t = (rect.top < from) ? (from - rect.top) / (from - to) : 0;
t = Math.max(0, Math.min(1, t));
heroEl.style.setProperty('--hp-title', t.toFixed(4));
CSS 只负责消费:
.hero.hero-reveal .hero-title {
opacity: var(--hp-title, 1);
transform: translateY(calc((1 - var(--hp-title, 1)) * 26px));
}
它只读滚动位置,不吸附、不接管滚轮、不改速度,所以手感还是原生的。但它能被读出来:
getComputedStyle(heroEl).getPropertyValue('--hp-title') // 真的有数
占位 div 也删了。没有跑道,靠两条曲线的时序去接。
v5|丝滑和分层,是两个参数
v4 做完它贪心了,拆成三段错开:标题先现 → 引言跟上 → 副题最后。它说这叫层次。
我自己滑了几遍,感觉是「一顿一顿」:标题已经清楚了,引言还虚着,读起来像卡带。
我让它加开关:
var HERO_SPLIT = false; // false = 整块丝滑;true = 三段分层
同一个进度引擎,两种消费方式。
这件事改了我一个认知:观感是参数,不是架构。 想「更丝滑」不一定要换技术,可能只是把分段收成一段。反过来,为了换感觉去推翻架构,代价大得多。
v6|整块淡出、不留白、导航条反向淡入
最后我把要求说清楚了,三条:
- 下滑时整个封面一起变淡,不是只化底边
- 导航条出现在留白下方,跟正文对齐
- 封面底部不许透明——底下的雪一直在动,太突兀
第 3 条正式给 v2 判了死刑。
删掉所有 mask-image,.site-cover 的 opacity 由 JS 直接写,底图、暗色层、文字、按钮整块同步退场。那个 50vh 占位 div 也删了。
改完立刻冒两个新问题:
A. 导航条横穿信笺。 没有留白之后,滚到 250px 它就在视口里了,一条半透明白带压在信笺中间,脏得像没擦的玻璃。修法是反向淡入:封面在时全透明,封面淡透后由下往上显形,没显形前 pointer-events: none。
B. 两层文字叠在一屏。 它沿用了旧区间(vh*0.95 → vh*0.34),结果封面才淡掉四成,大标题已经浮到屏幕下半部,静止的信笺字和流动的大标题叠在一起。
修法是把两条曲线配平:
var COVER_FADE_END = 0.65; // 滚过 0.65 屏,封面全透明
// hero 区间:from = vh * 0.62 → to = vh * 0.18
封面淡透那一刻(0.65),文案刚好起步(0.62)。首尾相接,不重叠。
v7|让首页的字跟着色相走
设置球能调主题色相,全站都变,唯独首页三行大字纹丝不动——它们写死了 #fff。
它没有在 JS 里一条条加 setProperty(那样得改两处,漏一处就会闪一下旧色),而是加了个裸数值变量:
:root { --hue: 6; }
.hero-title { color: hsl(var(--hue, 6) 72% 88%); }
.hero-quote { color: hsl(var(--hue, 6) 88% 85%); }
.hero-sub { color: hsl(var(--hue, 6) 42% 78%); }
JS 只写一次 setProperty('--hue', hue)。第一版参数给的是 92% 亮度 + 62% 饱和,四个色相肉眼分不出——浅色堆在高亮度区间会被压成同一个白。调到 88% / 72% 才看得出,但也不能再往下压,低于 70% 就糊在壁纸上。
这一处它是真的做对了,而且是那种「我提不出更好方案」的对。
v8|最后一个版本不是效果,是行距
全部弄完,我去读了一遍自己写的复盘,读两屏就累。
行距继承 body 的 line-height: 2.2,我 html 字号是 20px,算下来行盒 44px、行间空白 24px。中文在这个尺度上,眼睛每读一行都要重新找下一行的起点。
只改正文容器:
.article-content {
line-height: 1.7; /* 20px 字号 → 34px 行盒,比原来少 10px */
}
段距没动,还是 2em。行收紧之后段落依然分得开,大留白的骨架还在,读起来顺了。顺手把对话块 .chat-log .chat-line 也从 2.0 收到 1.7,不然同一篇文章里对话比正文还稀。
排版也是要改的。效果做得再顺,字挤得人读不下去,前面全白费。
四、那一夜:它把我带到了显卡驱动
前面所有版本,它都还算可靠。偏题是从一句「改了没效果」开始的。
4.1 起因
我连着几轮确认同一件事:文章数没变、间距改了没反应。
它开始排查。这是一次真正的滑坡。
4.2 它的怀疑链
| 轮次 | 它的判断 | 怀疑的层级 |
|---|---|---|
| 1 | 浏览器缓存 | 浏览器 |
| 2 | bfcache 恢复了旧 DOM | 浏览器 |
| 3 | 你有两个服务器(1313 / 1314),看错了 | 进程 |
| 4 | 系统代理 | 网络 |
| 5 | 火绒的 Web 扫描 | 安全软件 |
| 6 | Watt Toolkit / Steam++ 改了 hosts | 系统文件 |
| 7 | 换端口,换到 1414 | 端口 |
| 8 | 换浏览器,Chrome 也试 | 客户端 |
| 9 | 重启电脑 | 操作系统 |
| 10 | AMD 显卡驱动把旧页面合成到了显存 | 硬件 |
| 11 | 关掉硬件加速 | 硬件 |
它一开始怀疑的是它刚改过的那份 CSS,最后怀疑的是我显卡里的显存。
中间隔着一个浏览器、两个服务器进程、一个代理、一个杀软、一个加速器、一次重启。
它走得越远,离我那行 CSS 就越远。
这件事最诡异的地方在于:每一步都真实存在。缓存真的会有,bfcache 真的存在,Steam++ 真的会改 hosts,AMD 驱动真的有贴图问题,服务器真的起过两个。
它没有编造任何一个技术名词。它只是把这些真实存在的东西,当成了眼前这个现象的原因。
4.3 我照做了
我关掉了整台机器的硬件加速。
为了一个不存在的 bug。
我还重启了电脑,换了浏览器,杀掉了一个服务器进程,看了眼系统代理是不是开着的,检查了 hosts 文件里那几十行 GitHub 重定向。
每一步都不贵,加起来是好几个小时。
4.4 我怎么爬出来的
它给的所有解释都有一个共同点:没有一个能在我的屏幕上被验证。
于是我做了三件它虽然最后才建议我做、但没让我做到底的事。
第一件,Ctrl+U 看源代码。
页面的 HTML 里,CSS 引的是新指纹文件,色相公式也是新的。这说明浏览器手里的字节是新的——不是缓存,也不是什么中间层在转发旧内容。
这一步它其实也让我做过,做完它更确信了:「字节是新的,但画面是旧的,所以是渲染层。」
我当时也觉得这个推理无懈可击。
第二件,它让我在页面上搜「文章数」三个字。
不是搜代码,是搜页面上印出来的字。
Ctrl+F 输入「文章数」,页面只高亮了一处。
亮一处是站点统计卡,出问题的是资料卡统计。我让它改过的那一处已经变了,另一处还是旧数字。
第三件,去 baseof.html 里搜 stat-num。
同样是两个块:
<!-- 站点统计卡 -->
<div class="stats-card"> … <span class="stat-num">{{ len (where .Site.RegularPages "Section" "posts") }}</span> … </div>
<!-- 资料卡统计 -->
<div class="profile-stats"> … <span class="stat-num">{{ len .Site.RegularPages }}</span> … </div>
第二块写的是 .Site.RegularPages,没带 where。
真相就这么简单:agent把同一条信息改了一处,漏了另一处。
浏览器一直在老实渲染新页面。它没有缓存、没有 bfcache、没有代理、没有杀软、没有显卡问题。
是我把「渲染正确」当成了「渲染异常」。
4.5 它为什么会这样
我不觉得它撒谎。它每一条判断都有理由,而且理由听起来比我的直觉专业得多。
但它给的是最像样的解释,不是最能被验证的解释。
这两者的差别,就是数小时甚至一整晚。
它有三条惯性,我后来总结出来了:
① 它总在「解释」我,而不是「确认」我。
我说「还是老样子」,它说「这是 bfcache 恢复旧 DOM」;我说「我已经用无痕了」,它说「那是 GPU 合成层」。
每一步它都在把我的观察翻译成它的假设,而不是先把我看到的东西量一遍。
② 它的证据都是间接的。
curl 拿到新内容、响应头字节相同、源码视图是新指纹——这些都只能证明「服务器是对的」,不能证明「我的改动覆盖了所有该改的地方」。
而它从「服务器对」直接跳到「所以是渲染层」,中间漏掉的那个可能性是:我改得不够全。
③ 沉没成本是它最好的帮凶。(最重点)
查了两小时之后,我也开始不愿回头。回头意味着前面两个小时白费。它每提出一个新的怀疑对象,都像是在往前推进,而「再搜一遍自己的代码」听起来像原地踏步。
这一晚我真正学到的不是「AI 会胡说」,而是:
它有能力生成一个自洽的故事。而故事永远比测量便宜。
五、其它几次「方向越来越怪」的实现
那一晚是极端案例,但不是唯一一次。下面这些更日常,也更能说明它的工作方式。
1. 它会把 bug 解释成我的错觉
我把 padding-top 从 150px 改到 1000px,页面没反应。
它的解释是:「留白比一屏还宽,滚轮一滑就跳过 hero 只剩个尾巴——不是没生效,是数值大了看着像没生效。」
我当时信了,还自己反省了一下「是不是我太着急」。
后来我打开 getComputedStyle 读了一下:
getComputedStyle(document.querySelector('.layout')).paddingTop // "32px"
改 150 是 32px,改 1000 还是 32px。
真因在 CSS 的书写顺序上:
/* 我以为生效的 */
.layout.layout-below-cover {
padding-top: calc(var(--navbar-h) + 150px);
}
/* 实际生效的(写在后面) */
.layout {
padding: 32px 24px 64px;
}
特异性相同时,后写的赢。padding 是简写,把 padding-top 整条吃掉。
它不是在骗我,它是在替我解释一个它没测量过的现象。 而这比骗更危险——因为它给的解释让我停止怀疑。
从这天起我给自己定了一条:任何「看起来没生效」,先读计算值,再听解释。
2. 关掉动画,整块文案消失
站里的入场动画是这么写的:
.fade-in-up {
opacity: 0;
animation: fadeInUp 1.2s forwards;
}
opacity: 0 是基础样式,靠 forwards 才升到 1。
它为了接管 hero 的动画写了 animation: none,动画没了,opacity: 0 就永久生效。
当时画面整个空白,我一度怀疑逻辑全错。它给的诊断是「变量没写进去」,还打了日志证明 --hp-title 是 1.0000、子元素 opacity 也是 1——然后它也懵了。
真因是容器自己是 0。它在看子元素,没看容器。
.hero.hero-reveal {
animation: none;
opacity: 1; /* 必须补,不是冗余 */
}
这个坑的形状很典型:它证明了「我以为错的那个东西」是对的,然后卡住。 因为它只验证了它想到的那个假设。
3. 它会给你一把漂亮的锁,然后忘了钥匙
进度重算的标准写法是「布尔锁 + rAF 合并」,它写得很规范:
var ticking = false;
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(paint);
}
function paint() {
ticking = false;
// ...
}
坑在这里:后台标签页里 requestAnimationFrame 会停摆。一旦 ticking = true 之后 rAF 不回调,锁就永远锁着,后面所有滚动都被 return 掉,文案永远停在透明状态,切回来也不恢复。
它没想过「如果我这个回调永远不来」。
ticking = true;
requestAnimationFrame(paint);
heroFallbackTimer = setTimeout(paint, 120); // paint 里 clearTimeout
这类问题的共性是:它的代码按「正常路径」写得很干净,异常路径靠你自己撞出来。
4. 它把状态绑在了错误的节点上
悬浮球是「长按 0.2s 后才能拖」。第一版 pointerup 绑在元素自己身上:
按下 → 鼠标滑出元素 → 在元素外松手 → 元素收不到 pointerup → pressing 永久为真 → 之后任何超过 6px 的移动都触发拖拽。
用起来像「悬停即拖」,实际是状态泄漏。
// ❌ 元素级
el.addEventListener('pointerup', end);
// ✅ 取消类事件挂 document
document.addEventListener('pointerup', end);
document.addEventListener('pointercancel', end);
只要状态机依赖「松手」,取消监听就必须落在 document 上。
5. 它相信原生 API,哪怕 API 不适合这个场景
我让它做「滑过封面后轻柔吸到正文」,它用了 CSS:
html { scroll-snap-type: y proximity; }
.layout-below-cover { scroll-snap-align: start; }
结果我被吸到了文章区,hero 被切掉半截。
因为 snap 对齐的是盒子边界,而盒子包含 padding。.layout 有 padding-top,start 对的是 padding 外沿,不是内容起点。
它后来改成 JS 精确补正,再后来我干脆让它全删了——那个「黏」的手感我自己也不喜欢。
scroll-snap适合全屏分页式幻灯片,不适合「封面 + 长正文」。
6. 它不知道 wheel 不算手势
自动播放被浏览器拦了,要等一次「用户手势」才解锁。它把手势链里加了 wheel,注释还写着「下拉封面第一瞬间即可开始音乐」。
Chromium 的硬规则:只有 click / keydown / pointerdown / touchend 算 transient activation,wheel 和 scroll 都不算。
桌面上「滚一下就开始放歌」物理上做不到,必须点一下。它写的那行注释,是一个听起来很合理、但浏览器不认的假设。
六、我怎么验收它的活(重点回顾)
这一夜大半时间不是花在写代码上,是花在证明代码真的生效上。下面这几条,现在是我每次让它改完之后的固定动作。
1. 先要「复现步骤」,不要「原因说明」
我问「为什么没生效」,它能给我三个原因,全都很合理。
我问「我该在哪里看到什么」,它只能给一个具体位置。
原因可以被编,位置不能。 从这天起我改问法:不问「为什么会这样」,问「我怎么确认它变了」。
2. 用页面上的原文搜,不用代码里的命名
它叫 stat-num,页面上印的是「文章」。
搜「文章」会命中两处,搜 stat-num 会命中两个块——但如果你搜的是「文章数」这个中文词, 只能命中一次,因为模板里只出现过一次。
所以:搜用户能看见的字,不搜开发者起的名字。
3. Ctrl+U 这个动作要留着
它是区分「字节旧」和「渲染旧」的决定性实验。响应头能被中间层仿造,源码视图不能。
但也要记住:源码是新的,只能证明服务器没事,不能证明你改全了。这两件事我以前当成一件。
4. 读计算值,不读我以为的那个值
getComputedStyle(el).paddingTop // 到底是多少
getComputedStyle(el).getPropertyValue('--hp-title') // 到底到哪了
「CSS 里写的值」和「浏览器算出来的值」,是两个不同的数字。中间隔着一整个级联。
5. 红色调试法
想确认「这层到底有没有透出后层」,别盯着半透明画面猜:
.site-cover { background-color: #ff0000 !important; }
底部真的浮出红色渐变,遮罩就是生效的。三十秒给出确定答案,比截十张图可靠。验完立刻删。
6. 一次只让它改一个变量
它很擅长一次改五处,然后说「都优化了」。出了问题你就不知道是谁的错。
我自己现在会拆:先改结构,量一次;再改数值,量一次。改两处一起动,等于把两个未知数塞进一个方程。
7. 探针页:唯一能看到「中途」的办法
无头截图只能截首屏。要看滚到一半长什么样,写一个探针:
<iframe id="f" src="/"></iframe>
<script>
f.addEventListener('load', function () {
setTimeout(function () {
var w = f.contentWindow;
w.scrollTo(0, 700);
w.dispatchEvent(new Event('scroll')); // 无头下必须手动补
}, 1100);
});
</script>
放 static/_p.html,用完立刻删——它在线上是可访问的。
8. 无头浏览器的三条铁律(它踩过,我记住了)
- iframe 里
scroll事件根本不派发,必须手动dispatchEvent;rect.top却在变,所以用计数判断会误判为「没滚动」 - CSS 滚动时间轴的进度测不出,
getComputedStyle恒返回起始值——这就是我把 hero 从 CSS 时间轴换掉的真正理由 - 带锚点的 URL 用
--screenshot会截出全白,同 URL--dump-dom却是对的,是渲染侧假象
9. (+x) || 1 会把 0 变成 1
它给我的探针里写过这么一行:
var op = (+el.style.opacity) || 1; // ❌
opacity 为 0 时,+0 得 0,0 是假值,|| 1 返回 1。「完全透明」被读成「完全不透明」,一小时读数全反。
直接读字符串就没事。
10. hugo server 会静默卡死
改了 CSS 页面不变,日志停在 rebuilding (#N) 没有完成行,touch 也不重建。
处置:taskkill //F //IM hugo.exe,重启。这个在项目里发生过至少五次。
另外 v0.168 默认「Serving pages from disk」,裸路径 /css/main.css 可能残留旧内容——永远以页面引用的指纹 URL 为准。
11. 用内容指纹,别用版本号
{{ $css := resources.Get "css/main.css" | fingerprint }}
<link rel="stylesheet" href="{{ $css.RelPermalink }}">
内容一变,URL 里的 sha256 就变。之前写的是 ?v={{ now.Unix }},模板没重渲染(比如服务器正好卡住)时版本号就不变,缓存照样命中。
这个方案是它提的,而且提得对——这是我保留它最主要的原因:它在「机制层面」的直觉经常比我好,在「我到底要什么」和「到底生效没有」上经常比我差。
七、两份清单
我的失误
| # | 我犯的错 | 代价 | 教训 |
|---|---|---|---|
| 1 | 它说「三处都已同步」,我没验 | 埋下整晚悬案 | 「已修复」是待验证假设 |
| 2 | 它说「是视觉陷阱」,我信了 | 掩盖真 bug 数小时 | 先读计算值,再听解释 |
| 3 | 我跟着它怀疑到了显卡 | 关掉了整机硬件加速 | 解释越戏剧化,越该先量最朴素的东西 |
| 4 | 我搜代码命名,没搜页面原文 | 找不到第二处实例 | 搜用户看得见的字 |
| 5 | 我接受了一个不可测量的方案(animation-timeline) |
最终整体重写 | 不可测 = 不可交付 |
| 6 | 分段做得太明显,还觉得是优点 | 我自己读着都卡 | 观感是参数,别替读者决定 |
| 7 | 删掉留白后没重看整屏 | 白带横穿信笺 | 删布局元素后必须重看一遍 |
| 8 | 两条过渡曲线没配平 | 两层文字叠一屏 | 耦合数值一起调 |
它的失误(我事后归纳的模式)
| # | 模式 | 典型表现 |
|---|---|---|
| 1 | 解释优先于测量 | 没量过 paddingTop 就说「那是视觉陷阱」 |
| 2 | 只验证自己想到的假设 | 查到子元素 opacity 是 1 就卡住,没想到查容器 |
| 3 | 从「服务器对」跳到「渲染层」 | 漏掉「我改得不全」这个更便宜的可能 |
| 4 | 证据链自洽但前提未验 | 十条怀疑每一环都成立,但第一环就错了 |
| 5 | 偏好更漂亮的方案 | 选原生 CSS 时间轴,而不是可验收的 JS |
| 6 | 按正常路径写代码 | rAF 锁没有超时兜底 |
| 7 | 过度自信的措辞 | 「已修复」「一定」「就是这个原因」 |
| 8 | 替你解释你的错觉 | 把「没生效」说成「你看错了」 |
它没有一次是故意骗我。它只是每一次都选择了更省力的那种正确——给一个解释,比做一次测量省力得多。
八、现在的样子
下滑第一屏:
| 滚动位置 | 画面 |
|---|---|
| 0 | 信笺完整,底部实心,不透雪 |
| 0 ~ 0.65 屏 | 信笺整块一起变淡:底图、暗色层、文字、按钮同步退场 |
| 0.65 屏 | 信笺完全溶解,只剩流动的雪 |
| 0.65 ~ 0.93 屏 | 导航条由下往上淡入,吸顶时正好完全显形 |
| 之后 | 大标题、引言、副题浮起,进入正文 |
实测(1100×620 视口,往上滚完全可逆):
| 滚到 | 信笺 | 导航条 | 文案 |
|---|---|---|---|
| 0 | 1.00 | 0 | 0 |
| 300 | 0.26 | 0 | 0 |
| 420 | 0 | 0.10 | 0 |
| 500 | 0 | 0.56 | 0.32 |
| 620 | 0 | 1.00 | 0.92 |
| 700 | 0 | 1.00 | 1.00 |
标题颜色跟着色相走:
| 色相 | 标题实际色 | 观感 |
|---|---|---|
| 6(默认) | rgb(246,207,202) |
暖白偏粉 |
| 120 | rgb(202,246,202) |
淡青绿 |
| 210 | rgb(202,224,246) |
冷冰蓝 |
| 280 | rgb(232,202,246) |
淡紫 |
能调的旋钮都在 assets/css/main.css 和 assets/js/main.js 的注释里,搜 ★ 就能找到。
九、在 AI 时代,怎么不盲目依赖 AI
这一节是我最想留下来的。不是「AI 不可信」这种话,那没用。具体一点:
1. 让 AI 交「判定标准」,再交「实现方案」
我现在开头就会说:先告诉我改完在哪看、看到什么才算对。
它一旦得先定义「对」,就没法用「应该差不多了」糊过去。判定标准是它的紧箍咒。
2. 「已修复」当成待验证假设,不当结论
它说修好了,我就去看。十次里有八次是对的,但那两次错的,代价是整晚。
这不是不信任,这是流程:它交付的是代码,不是结论。
3. 自己保留三个动作
Ctrl+U 看字节、grep 搜页面原文、getComputedStyle 读计算值。
这三个动作不需要我懂框架,只需要我知道「我该在哪里看到什么」。它们是我对它的最后一道复核,也是唯一一道不依赖它的复核。
4. 它开始「解释我没看到的东西」时,警报拉满
「你看到的是 X,其实那是因为 Y」——这句话如果后面没有一次测量,就是纯叙述。
那一晚最危险的时刻,不是它说显卡,是我差点盲目信任AI给出的结论的那个瞬间:它让我开始怀疑自己的眼睛,而它的依据只是推理。
5. 拒绝不可测量的方案,哪怕它更漂亮
animation-timeline 输给 JS,不是因为 CSS 差,是因为 CSS 那个版本里我拿不到一个数字。
在 AI 时代这条更重要:它很擅长生成「看起来很专业的方案」,而专业性不等于可观测性。你要的东西能不能被验收,比它写的代码优不优雅重要得多。
6. 一次只让它动一个变量
它喜欢一次改五处然后说「都优化了」。出了问题,你连从哪查都不知道。
我现在会明确说:这一轮只改 padding,别动别的。把一个五元方程拆成五个一元方程,是我唯一能控制它的方式。
7. 给它一个「否决记录」
我让它把「试过、被否、为什么否」写进文档。这不是为了它,是为了我自己:再过三天我会忘,然后它会把同一套方案重新端上来,因为它每次都是重新开始的。
它的记忆是有限的,我的判断必须留在纸面上。
8. 沉没成本是最大的陷阱
查了两小时之后,「回头再搜一遍代码」听起来像认输。
可那一晚,正确答案就在最便宜的这一步里。越查越深的时候,先问一句:最便宜的那个可能性我排除了吗?
9. 它擅长机制,你负责意图
它提的内容指纹、hasCJKLanguage、readDir 里没有 .Ext 这种,都比我自己查得快。
但它不知道「我读着累」「这条透明带太突兀」「这个手感黏」。这些只有我知道。
分工应该是:它管怎么做,我管做成什么样。 一旦它开始替你决定「什么样算好」,你就失去验收能力了。
10. 不写代码的人,更得会验收
这个站的 CSS 我几乎没写过一行,JS 也只零星改过几处。这本来是弱点:某条规则在 2150 行的哪个位置,我根本不知道。
但它逼我用另一种方式定位问题——不看它怎么写,只看它有没有生效。
不懂实现,不等于没法判断。你不必会做一道菜,也能尝出咸了。前提是你真的去尝,而不是听厨师说「这个咸度是行业标准」。
那一晚我犯的正是这个错:我尝出了不对,但被它一句「这是 bfcache」说服,把嘴闭上了。
11. 你是验收方,不是甲方
甲方说「我要更好看」,验收方说「这一版通过 / 不通过,因为 X」。
在 AI 时代,会写代码的人不一定稀缺,能判定「这行不行」的人稀缺。
而判定能力不是靠读它的解释长出来的,是靠你自己动手量出来的——读一次源码、搜一次页面、看一眼计算值。就这三次,能救一整晚。
十、最后
一开始我以为这是个「加几个效果」的活。
做完发现,真正的成本几乎全在证明上:证明样式生效了、证明缓存不是凶手、证明动画停在我以为的位置。
而最贵的那次错误,跟技术一点关系都没有。是我搜了 stat-num,没搜「文章」。
《三日间的幸福》里那句话,放在这里意外地合适——「如果把余生的日子,标上价格出售」。
如果把这一天半标上价格:七分在验证,三分在写。
而那一整晚买到的,只是一个我本该自己搜一遍的字符串。
下次开工前,先搜一遍页面上真正的字。
写于 2026 年 10 月 24 日,封面第三版、正文行距、还有这篇复盘本身都改完之后。