我这次处理的是一个看起来很小的页面问题:从列表里点开一条内容,地址栏里应该留下一个可以分享、可以刷新、也可以用前进后退恢复的链接。最初看它,似乎只要把当前对象的编号塞进地址就够了。对象能打开,页面就像有了深链。
但实际走了一遍手机端流程,问题很快暴露出来。查询之后缺少回到概览的入口,照片被按视频的方式打开,审看器没有接上前后切换,刷新后筛选条件也不能稳定保留。每一处都可以被单独修补,可单独修补的结果仍然不可靠:首次点击看起来有效,下一次刷新、返回或换一个媒体类型,状态就散掉了。
这件事让我重新意识到,深链的价值不是让某个对象“能被打开”,而是让页面能够从地址重新长回当时的工作状态。
首次打开正常,不等于深链成立
深链最容易被误判的地方,就是把“地址里有参数”和“页面可以恢复”当成同一件事。前者只是保存了一个线索,后者要求页面知道这个线索属于哪个列表、应该用什么方式加载、加载失败时回到哪里。
这次的审看页面里,用户先查询一组结果,再从结果中进入某个对象。对象本身不是完整上下文。列表里可能有筛选条件,当前对象可能是照片,也可能是视频,还可能需要知道它在结果中的前一个和后一个是谁。如果地址只保留对象编号,刷新时页面就只能重新猜:筛选条件用默认值,媒体类型按某个固定组件处理,返回动作也没有明确目标。
如果不处理这个问题,会导致一种很隐蔽的错误:用户第一次打开链接时觉得功能已经完成,过几分钟刷新,看到的却是默认列表或空白审看器;按后退回去,原来的查询不见了;点前进回来,当前对象又变成了另一个。它不是“偶尔体验不好”,而是同一个地址在不同时间代表了不同页面。
这件事表面上是链接,实际上是状态模型
我后来没有继续给地址栏增加零散参数,而是先把页面状态拆开看。至少有几类信息必须同时存在:列表上下文、当前对象、媒体类型、加载状态,以及返回动作。列表上下文负责说明用户从哪组结果进入;当前对象负责说明正在看什么;媒体类型决定使用照片还是视频的审看方式;加载状态决定是显示内容、空态、错误还是重试;返回动作则决定用户能不能回到刚才的工作位置。
这里的关键不是参数越多越好,而是每个状态都要有明确的归属。筛选条件属于列表上下文,不应该藏在某个临时组件里;照片或视频属于当前对象的类型,不应该让审看器根据文件名猜;失败状态属于当前加载过程,也不应该在刷新后被当成默认空态。
一个可恢复的深链,至少要能重建“从哪来、正在看什么、怎么打开、失败后去哪”这4个问题。
我选择把这些状态放进同一套页面状态模型,是因为它比在多个按钮事件里分别记忆更稳。地址负责保存可重建的上下文,页面状态负责承接加载和交互,组件只消费已经判断好的对象与类型。这样做的理由很实际:以后增加前后切换、键盘操作或错误重试时,不需要再去猜某个按钮之前发生了什么。
照片和视频不能共用一个含糊的打开动作
这次最直观的错误是照片曾经被当成视频打开。它看起来像一个媒体组件的小问题,实际上说明“当前对象是什么”没有成为状态模型的一部分。只要打开动作接收到一个地址,就直接把它交给视频容器,代码很短,行为却取决于输入恰好是什么。
错误捷径就是把所有媒体都归到一个“可播放内容”里,再靠容器自行处理。最容易误判的是,开发时手头的测试对象刚好是视频,所以路径能走通;换成真实照片,播放器既不能给出正确预览,也不能告诉用户应该如何恢复。
我判断照片和视频应该分别审看,是因为它们的加载能力、失败表现和用户动作都不同。照片需要图片预览,视频需要播放容器;照片加载失败时应保留来源入口,视频超时则需要重试或取消。把类型先判定清楚,审看器才知道自己要承担什么责任。
这也解释了为什么不能只把对象编号写进地址。地址至少需要携带,或能通过稳定数据重新推导出媒体类型。否则页面刷新后即使找回了对象,也可能用错展示方式。媒体类型不是展示层的附加信息,而是深链恢复时必须验证的状态。
返回、刷新和前进后退要走同一条路径
页面的返回行为是另一个容易被遗漏的地方。用户从查询结果进入审看器时,返回不是简单地关闭一个弹窗,而是要回到刚才的概览,并保留筛选条件和结果上下文。如果返回按钮只把当前组件隐藏,页面可能停在旧滚动位置;如果直接回到默认列表,用户就得重新查询。
我更愿意把“返回概览”看成状态转换,而不是视觉动作。进入详情时,记录当前列表上下文;返回时恢复它;刷新时从地址重建它;浏览器前进后退时,也使用同一套转换。这样,按钮、地址栏和浏览器历史不会各自维护一份互相矛盾的记忆。
这里的验收不能只测一次点击。我先走正常路径,确认查询、进入详情和返回概览连起来;再刷新详情页,确认筛选条件仍能恢复;然后用前进后退检查历史是否对应真实页面;最后测试无效链接,确认页面不会卡在半加载状态,而是给出可理解的恢复入口。
如果这些路径各自有一套实现,问题通常不会在顺利时出现,而会在用户离开页面以后出现。一个流程成熟的标志,不是它能从列表打开详情,而是刷新、后退和无效链接都能回到一个可解释的状态。
失败恢复也是深链的一部分
我一开始容易把失败当成加载组件自己的事情:请求超时了就显示错误,用户再想办法。但深链页面的错误不能只停在“加载失败”,还要说明下一步是否可以重试、取消、回到概览,原来的上下文是否仍然有效。
这次验收专门覆盖了超时重试和取消查询。重试应该针对当前对象重新发起加载,而不是把用户送回默认页面;取消应该让页面回到可操作状态,而不是留下一个永远转圈的遮罩;无效链接则应该清理无法识别的当前对象,同时尽可能保留列表上下文。这样,失败不会把页面状态机打断。
键盘操作也属于同一个问题。前后切换需要有明确的焦点和边界,Esc 退出审看后,焦点要回到触发入口。否则页面虽然能响应鼠标点击,却让键盘用户失去位置感。它提醒我,状态恢复不只发生在地址栏和数据层,也发生在用户当前能操作什么的位置上。
真实照片查询时还发现一个边界:返回的真实照片没有登记可用的预览映射,因此只能确认页面展示了来源入口,不能把真实照片加载说成已经验收通过。这种结论看起来保守,却比把测试数据的成功当成真实数据的成功更有价值。未知能力要被标出来,后续验收才知道缺口在哪里。
一套可以复用的深链验收流程
以后遇到类似页面,我会先画出最小状态机,而不是先改地址字符串。第一步列出列表上下文、当前对象、媒体类型、加载状态和返回目标;第二步说明每个状态怎样进入地址、怎样从地址恢复;第三步确定无效对象和加载失败分别落到什么安全状态。
接着按路径验收。先测真实查询到详情的正常路径,再测刷新,确认页面不依赖内存中的临时变量;再测返回概览,确认筛选条件、滚动位置和焦点没有被无故丢掉;再测前进后退,确认历史记录和页面内容一一对应;最后测无效链接、超时、重试和取消,确认失败仍然能回到可操作页面。
我会把媒体类型单独拿出来测,而不是把它埋在“详情能打开”这个结果里。照片要验证图片容器,视频要验证播放容器,真实来源还要验证是否有可用映射。布局则至少用手机端和较宽的页面各测一次,因为移动端缺少返回入口,常常会把状态问题放大成“找不到路”。
这个方法的核心规则可以压缩成一句话:先问地址能否重建状态,再问组件能否展示内容;先验证失败如何恢复,再把正常路径当成完成。
下次我会怎么做
下次再接到“加深链”这样的需求,我会先追问页面状态,而不是先找路由参数。用户从哪个列表来?筛选条件是否需要保留?当前对象的类型是什么?刷新、后退和无效链接分别应该回到哪里?这些问题没有答案时,深链其实还没有被定义。
实现时,我会让地址、页面状态和审看器共享同一份恢复逻辑。对象加载成功只是其中一项结果,类型判断、返回目标、失败状态和焦点恢复也要一起验收。遇到真实数据能力不足,就明确记录为待源媒体验收,不用测试数据替它背书。
这件事让我重新意识到,表面上我是在处理一个地址,实际上我是在处理用户离开页面以后,系统还能不能记得自己刚才在做什么。深链真正留下的不是一个编号,而是一条可重新走通的判断链。只要这条链在刷新、返回和失败时仍然成立,页面才算真正可恢复。