封面裁剪看起来是接一个裁剪组件就完事,第一版两天做完。上线后四类反馈,每一类都是用户能直接看见的错。
一是裁出来的和看到的不一样。创作者在裁剪框里框住了人脸,出图之后人脸偏到了角落。原因是我把裁剪框的坐标(相对于页面上显示的图片)直接当成了裁剪参数传给服务端,而那张图在页面上是被容器缩放显示的——显示宽度 600px,实际是 1920px。坐标差了三倍多,裁出来的区域完全不对。这是最典型的坐标系错误。
二是发布时才被告知封面不合规。分发位对封面有比例和最小尺寸要求。创作者裁完、填完标题、点发布,才被告知「封面尺寸不足」——他得回去重新裁,而且不知道要裁多大。这个校验放错了位置。
三是前端出图在不同设备上不一致。我最初用 Canvas 在前端裁好再上传。结果不同浏览器的图片解码和缩放算法有差异,同一张图在不同设备上裁出来的清晰度和色彩略有不同;更麻烦的是出了问题无法复现——用户说图糊了,我在自己机器上裁一遍是清楚的。
四是没有候选帧,创作者只能自己上传。大部分创作者懒得单独做封面图,他希望从视频里挑一帧。而转码时其实已经抽了帧,只是没暴露给前端。
所以四个改动:坐标按自然像素换算、比例与尺寸校验前置到裁剪阶段、前端只传裁剪参数、由服务端出图、暴露转码抽帧的候选让创作者直接选。
这个模块最想说的一句话是:涉及图像的交互,前端负责「让用户表达意图」,服务端负责「按意图产出结果」。前端出图看起来少一次往返,但它把产出质量绑定到了用户的设备和浏览器上,而这是你完全不可控也无法复现的变量。想清楚这个分工,后面的问题就少一半。
naturalWidth 在图片加载完成前是 0,此时算出的 scale 是无穷或 0。必须在 load 事件之后再初始化裁剪器。服务端出图的代价是多一次往返、预览有延迟。前端 Canvas 出图可以立刻预览。但它把产出质量绑定到了用户的设备和浏览器上——这是完全不可控且无法复现的变量。用户说「裁出来的图糊了」,我在自己机器上裁一遍是清楚的,这种问题排查不了。服务端出图的结果是确定的、可复现的、而且裁剪参数留了下来(以后新增规格可以重新出图不打扰用户)。判据是「产出物的质量要不要可控」——要,就必须放服务端。
比例约束选「锁定裁剪框」而不是「提交时校验」。锁定的代价是用户不能自由裁(想裁个方形也不行)。但自由裁加提交校验的体验更差:他裁完了才被告知比例不对,还得猜怎么裁才对。把约束变成交互的一部分,用户压根做不出不合规的选择——这比事后拒绝好得多。这个原则在表单设计里也一样:能用控件限制的就不要靠校验提示。
抽帧候选数量控制在几张,不做「任意时间点抽帧」。让用户拖进度条任意选帧体验最好,但那需要前端解码视频(性能和兼容性都是问题)或者服务端按需抽帧(每次拖动一次请求)。预抽几张候选是成本和体验的折中,覆盖了大部分需求。
踩过的坑一:按显示坐标传裁剪参数,裁出来整体偏移。这个 bug 上线后立刻有人反馈。更值得说的是我当时的第一反应是「服务端裁错了」,让后端同学查了半天。后来才发现是我传的参数就是错的——坐标系搞混了。教训是:凡是涉及坐标、单位、时区的参数传递,都要明确「这是哪个坐标系/哪个单位/哪个时区的值」,最好在字段名或注释里写清楚。我们后来把字段名改成了 cropXNatural 这种带坐标系标识的名字。
踩过的坑二:在图片 load 之前初始化裁剪器,scale 算成了无穷。naturalWidth 此时是 0,除法结果是 Infinity,裁剪框一动就报错。修法是等 load 事件。这类「依赖的东西此刻还不存在」的时序问题在前端很常见,判据是「我读的这个属性,此刻真的有值了吗」。
踩过的坑三:窗口 resize 后没重算 scale,用户调整窗口再裁就偏了。这个坑很隐蔽——正常测试不会去改窗口大小,但用户会(尤其是笔记本外接显示器切换时)。修法是监听容器尺寸变化重算。这也提醒我:「一次算好就不再变」的假设,在响应式布局里通常不成立。
没做的部分:没做封面的 A/B 测试(同一视频用两个封面看点击率)。产品提过,但它需要分发侧支持按用户分流展示不同封面,不是前端能独立完成的。前端这边留了扩展点:因为封面存的是「原图 + 多组裁剪参数」,支持多个封面版本在数据结构上是自然的。
坐标换算的正确性用构造用例验证,这是最该测的一条:准备几张不同分辨率的图(比如 800px 宽和 4000px 宽),在同样的显示尺寸下裁同样的区域,断言换算出的自然像素参数与预期一致。关键是要用「显示尺寸相同但自然尺寸不同」的图——如果测试图恰好没被缩放(自然尺寸等于显示尺寸),这个 bug 永远测不出来,而我最初就是这样。
裁剪结果的一致性验证:同一组裁剪参数,断言服务端出的图与预览显示的区域一致。做法是拿出图后的图和原图对比,检查裁剪区域的四个角是否落在预期位置。这个可以半自动化(对比像素)。
「发布时才被告知不合规」不再出现:断言型——尝试裁一个小于最小尺寸的区域,断言在裁剪阶段就被限制(框无法继续缩小并有提示),而不是提交时被拒。
resize 场景:手工验证——裁好后改变窗口大小、再次调整裁剪框,断言参数仍然正确。这一条必须手工测,自动化测试不容易模拟窗口变化。
上传校验的提示质量:不是数字指标,报「提示是否包含当前值与要求值」这个事实(「需不小于 800 像素,当前 640」)。可定位的提示是这类功能的关键。
不要报什么:不要报「裁剪速度」——出图在服务端,前端的耗时就是一次网络往返。该报的是「不同分辨率下坐标换算正确」「裁剪结果与预览一致」「约束在交互阶段生效」这三件可验证的事。
视频发布之后不是立刻可见的:要转码、要机审、可能还要人工审核。创作者会盯着列表等,所以列表必须能反映进展。第一版的做法是最直觉的:定时器每几秒拉一次整个列表。四个问题。
一是请求量失控。创作者习惯把这个页面挂在一个标签页里一整天。每几秒一次,一天就是上万次请求,而其中绝大多数返回的数据完全没变——他的视频早就都发布完了,没有任何中间态。这些请求纯属浪费,而且乘上创作者数量之后对服务端是实打实的压力。
二是切到后台还在轮询。浏览器切到别的标签页,定时器还在跑(虽然会被节流但仍在发请求),手机端更明显:切到后台继续轮询会耗电。
三是整列表重渲染导致操作被打断。轮询回来直接替换整个列表数据,用户正在某个条目上悬停操作菜单,列表一刷新菜单就关了;如果他正在输入搜索框,滚动位置也可能跳。这个体验问题被反馈了好几次。
四是永久轮询。有个视频转码失败了但状态没更新(服务端的 bug),它永远停在「转码中」。前端就永远轮询下去,一天几万次请求都在问同一个视频。没有终止条件是个隐患。
所以四个改动:只对中间态条目轮询(没有中间态就不轮询)、间隔退避(越等越久,因为越等越可能是慢任务)、页面不可见时暂停、恢复时立即补拉、有明确终止条件(超过最长时长转为提示手动刷新)。
这个模块最想说的一句话是:轮询的设计重点不是「多久拉一次」,是「什么时候不拉」。只对中间态拉、不可见时不拉、超时后不拉——三个「不拉」加起来把请求量降了一个量级,而功能上用户完全感觉不到差别。这个视角我觉得比调间隔参数有价值得多。
用轮询而不是长连接或服务端推送。推送能做到即时且请求量最小,但它需要服务端支持长连接、要处理连接管理与重连、而这个页面的实时性要求其实不高(转码要几十秒到几分钟,晚几秒知道无所谓)。判据是「实时性要求」和「引入长连接的边际成本」——这个场景下优化好的轮询已经够用,而长连接是一整套基础设施。如果平台本来就有长连接通道(比如做私信用的),那复用它是更好的选择。
退避的上限设得不太大,接受慢任务的反馈延迟。上限设很大能进一步省请求,但用户等了十分钟终于转码完成,却要再等一分钟才看到状态变化,体感很差。所以上限控制在可接受的延迟内,并配合「恢复可见时立即补拉」来兜住主要场景——用户真正关心的时刻通常是他刚切回页面的时候。
最长等待时长设置的依据是「正常处理时长的上界再乘一个余量」。不能拍脑袋——设短了正常的慢任务会被误判为卡住,设长了起不到防护作用。做法是先统计真实的处理时长分布,取一个覆盖绝大多数正常情况的值再放宽。这个值也要可配置,因为处理时长会随业务变化。
踩过的坑一:整列表替换导致用户操作被打断。创作者反馈「我点开菜单它自己就关了」。排查出来是轮询回来替换了整个列表数据,Vue 重新渲染了所有条目。修法是局部更新加稳定 key。这件事让我理解了「数据更新的粒度」和「渲染的粒度」是两件事——数据全量返回没问题,但应用到视图时要 diff 到字段级。
踩过的坑二:一个视频永远停在「转码中」,前端一天问了几万次。是服务端的状态没更新(转码失败但没写回)。但这暴露的是前端的问题:没有终止条件。教训是任何等待外部状态的轮询都必须有超时退出——不能假设外部状态一定会到达终态。这和后端「异步任务必须有卡死兜底扫描」是同一个道理,只是换到了前端。
踩过的坑三:用数组索引做列表 key,删除一个视频后剩下的条目状态错乱。某个条目的展开状态跑到了另一个条目上。这是 Vue/React 列表渲染的经典坑,根因是索引不稳定(删除会让后面所有元素的索引变化)。修法是用视频 ID。
没做的部分:没做「转码进度百分比」。产品想要一个进度条,但转码的进度需要转码服务上报,而它只有开始和结束两个事件,中间的百分比是假的(用时间估算)。假进度条会带来新的问题(卡在 99% 不动比没有进度条更让人焦虑),所以我们选择只显示状态文字加预估剩余时间范围。
请求量下降:报「单个页面挂一小时产生的请求次数」,分两种场景:列表全是终态(改造后应为 0 或极少)、列表有中间态(按退避序列可以精确算出)。分场景报比报一个平均值有说服力得多,因为节省主要来自第一种场景。
退避序列可以直接算不用测:给出起始间隔、退避系数、上限,就能算出「N 分钟内发多少次请求」。把这个序列写出来比给一个模糊的「减少了很多」清楚。
可见性暂停的验证:切到别的标签页,观察网络面板确认没有新请求;切回来确认立即有一次请求。这是手工可验证的,而且必须验证——因为浏览器的节流行为容易让人误以为已经停了。
终止条件的验证:构造一个永远停在中间态的条目(mock 接口固定返回中间态),断言超过最长时长后轮询停止且出现手动刷新提示。这一条必须测,它防的是最严重的隐患。
局部更新的验证:展开某条目的操作菜单,等一轮轮询回来,断言菜单仍然是展开的。这是个可以自动化的断言,而且它直接对应用户反馈的那个问题。
不要报什么:不要报「状态同步延迟降低」——退避实际上让慢任务的反馈延迟变大了,硬说降低是不诚实的。该报的是「请求量按场景的下降」「有终止条件」「操作不被打断」这三件事,并诚实说明退避带来的延迟以及用可见性补拉来缓解。
这个功能的坑集中在两件事上:时间和状态语义。四个具体问题。
一是跨时区的定时发布错了。创作者在海外,他设定「晚上八点发布」。我把日期时间选择器的值直接格式化成字符串提交(2026-08-26 20:00:00),服务端按自己的时区解析,于是在服务端时区的晚八点发布了——对创作者来说可能是凌晨。他的粉丝在他的时区,这个错误直接影响了内容的曝光效果。这是最严重的问题。
二是客户端时钟不准导致校验不一致。前端校验「必须是未来时间」用的是浏览器的当前时间。有的用户系统时钟慢了几分钟,他选了一个自己看来是未来的时间,前端通过了,服务端却拒绝了——提示「不能设置过去的时间」,而他明明选的是未来。这类问题用户完全无法理解。
三是草稿和定时被混成了一种状态。我最初都归为「未发布」,列表上显示一样。但它们的语义完全不同:草稿是「还没写完,随时可以继续改」;定时是「已经写完了,等时间到自动发」。混在一起的后果是创作者搞不清哪些会自动发、哪些不会,有人以为定时的还能继续改,结果它自己发出去了。
四是离开页面丢内容。编辑了一半点了别的菜单,内容没了。而这个页面的内容不像评论那样短,创作者可能写了很长的描述和标签。
所以四个改动:时间以带时区的绝对时刻提交并在界面明示时区、「是否未来」的校验以服务端时间为准、草稿与定时分成两种独立状态、离开前拦截未保存改动。
这个模块最想说的一句话是:只要涉及时间,就必须回答「这是哪个时区的时间」和「以谁的时钟为准」这两个问题。我最初两个都没回答——提交了一个没有时区信息的字符串、用客户端时钟做校验,于是两个问题各自产生了一类 bug。这条经验在做任何跨端、跨地域的时间处理时都适用。
时间提交格式选「带偏移的 ISO 串」而不是「时间戳」。时间戳最没有歧义,但它丢失了「用户当时看到的是哪个时区的几点」这个信息。而这个信息在展示和排查时都有用——列表要按创作者的时区展示、出问题时要能还原他当时的选择。带偏移的 ISO 串同时保留了绝对时刻和时区信息,是更好的选择。如果后端更习惯时间戳,那就时间戳加一个独立的时区字段,不要只传时间戳。
服务端时间只在页面加载时取一次算偏移,不是每次校验都请求。每次都请求最准,但增加了一次网络往返和失败可能。一次取偏移的误差来源是「页面开着期间时钟漂移」,量级极小,对「是否未来时间」这种精度要求不高的判断完全够。判据是「所需精度」——如果是秒杀这种需要精确对齐的场景,就要更频繁地校准。
草稿采用「手动为主、自动为辅」而不是纯自动保存。纯自动保存(像在线文档那样)体验更好,但它和「定时发布」这个状态容易混淆——用户改了一个已定时的内容,自动保存了,那这次改动算不算生效?手动按钮让用户的意图明确,自动保存只作为防丢失的兜底。这个取舍是为了状态语义的清晰。
踩过的坑一:提交裸时间字符串,海外创作者的定时发布差了好几个小时。他设晚八点,实际在服务端时区的晚八点发了。他的粉丝在他的时区,这直接影响了内容的曝光。这件事让我记住了「时间字符串没有时区就没有意义」——它只是一串数字,谁解析谁按自己的时区来。
踩过的坑二:用客户端时钟校验未来时间,用户系统时钟慢导致前端通过服务端拒绝。用户完全无法理解「我明明选的是未来」。修法是取服务端时间算偏移。教训是「凡是和当前时间比较的判断,都要问一句:以谁的时钟为准」——客户端时钟是用户可以随意修改的,也可能就是不准。
踩过的坑三:草稿和定时混成一种状态,有创作者的内容自己发出去了。他以为那是草稿还在改,实际上它是定时状态。这不是技术 bug,是状态语义没设计清楚——两个语义不同的东西被塞进了同一个状态。教训是:状态设计要按「行为差异」划分,而不是按「都是未发布」这种表面共性划分。
没做的部分:没做「按粉丝活跃时段推荐发布时间」。产品提过,需要数据侧支持(分析粉丝的活跃分布)。前端这边留了扩展点:定时选择器可以接受一组「推荐时刻」并高亮显示,数据能力具备时接上即可。
时区正确性用构造用例,而且必须换时区测:把系统时区改成几个不同的时区(东八、西五、跨日期线的),各选同一个墙上时间提交,断言提交的绝对时刻符合预期。关键是「换时区」这一步——如果只在本地时区测,时区 bug 永远测不出来,这和坐标换算那个 bug 是同一类盲区(系数恰好为 1 时错误被掩盖)。
服务端时间校准的验证:把系统时钟故意调快或调慢几分钟,断言校验结果与服务端一致(本地看起来是未来但实际是过去的时间应该被拒绝,反之应该通过)。这一条也必须手工改时钟来测。
状态语义的验证:断言草稿不会自动发布、定时会自动发布、定时可撤销后变回草稿。这些是行为断言,写成用例进回归。
离开拦截的验证:有改动时离开断言有提示、已保存后离开断言无提示。后一条容易漏但很重要——每次都拦截会让用户麻木。
自动保存的反馈:不是数字指标,报「保存后界面是否显示时间戳」这个事实。因为没有反馈的自动保存等于没有。
不要报什么:不要报「定时发布准时率」——那取决于服务端的调度,不是前端的功劳。该报的是「跨时区提交的绝对时刻正确」「时钟不准时校验与服务端一致」「草稿与定时的行为符合各自语义」这三件可验证的事。
没有匹配的内容,换个关键词试试。
项目拆解 · 创作者中心常用页(Vue · 实习级)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据