UI 工程的要素

参考:The Elements of UI Engineering - Dan Abramov

核心问题

本文关注 UI 工程中的问题,而不是特定的技术或库。这些是你在构建用户界面时会遇到的核心挑战。


核心挑战

1. 一致性(Consistency)

问题:当你点击”点赞”按钮时,文本更新:“你和 3 个朋友点赞了这条帖子。“再次点击,文本翻转回来。听起来很简单。但这个标签可能在屏幕上的几个地方存在。可能还有其他视觉指示(如按钮背景)需要改变。之前从服务器获取的”点赞者”列表现在应该包括你的名字。如果你导航到另一个屏幕然后返回,帖子不应该”忘记”它被点赞了。

挑战

  • 如何保持屏幕上不同部分的相同数据同步?
  • 如何以及何时使本地数据与服务器一致,反之亦然?

2. 响应性(Responsiveness)

问题:人们只能容忍对操作缺乏视觉反馈的有限时间。对于连续操作(如手势和滚动),这个限制很低(甚至跳过单个 16ms 帧都会感觉”卡顿”)。对于离散操作(如点击),有研究说用户感知任何 < 100ms 的延迟都同样快。

挑战

  • 如果操作需要更长时间,我们需要显示视觉指示器
  • 但导致页面布局”跳跃”或经历几个加载”阶段”的指示器可能使操作感觉更长
  • 同样,以丢弃动画帧为代价在 20ms 内处理交互可能感觉更慢,而不是在 30ms 内处理且不丢弃帧
  • 大脑不是基准测试

问题:我们如何保持应用对不同类型输入的响应性?


3. 延迟(Latency)

问题:计算和网络访问都需要时间。有时如果它不会伤害目标设备上的响应性,我们可以忽略计算成本(确保在低端设备上测试你的应用)。但处理网络延迟是不可避免的——可能需要几秒钟!

挑战

  • 我们的应用不能只是冻结等待数据或代码加载
  • 这意味着任何依赖于新数据、代码或资源的操作都是潜在的异步操作,需要处理”加载”情况
  • 但这可能发生在几乎每个屏幕上

问题

  • 我们如何优雅地处理延迟,而不显示”级联”的加载指示器或空的”洞”?
  • 我们如何避免”跳跃”的布局?
  • 我们如何在不每次”重新布线”代码的情况下更改异步依赖?

4. 导航(Navigation)

问题:我们期望 UI 在我们与之交互时保持”稳定”。东西不应该从我们鼻子底下消失。导航,无论是在应用内启动的(如点击链接)还是由于外部事件(如点击”返回”按钮),也应该尊重这个原则。

挑战

  • 例如,在个人资料屏幕上在 /profile/likes/profile/follows 标签之间切换不应该清除标签视图外的搜索输入
  • 即使导航到另一个屏幕也像走进一个房间。人们期望稍后返回并找到他们离开时的东西(可能有一些新项目)
  • 如果你在 feed 中间,点击个人资料,然后返回,失去你在 feed 中的位置或再次等待它加载是令人沮丧的

问题:我们如何构建应用来处理任意导航而不丢失重要上下文?


5. 陈旧性(Staleness)

问题:我们可以通过引入本地缓存使”返回”按钮导航即时。在该缓存中,我们可以”记住”一些数据以便快速访问,即使理论上我们可以重新获取它。但缓存带来了自己的问题。

挑战

  • 缓存可能变得陈旧
  • 如果我更改头像,它也应该在缓存中更新
  • 如果我发布新帖子,它需要立即出现在缓存中,或者缓存需要失效
  • 这可能变得困难且容易出错
  • 如果发布失败怎么办?
  • 缓存会在内存中保留多长时间?
  • 当我们重新获取 feed 时,我们是”拼接”新获取的 feed 与缓存的 feed,还是丢弃缓存?
  • 分页或排序如何在缓存中表示?

6. 熵(Entropy)

问题:热力学第二定律说类似”随着时间的推移,事物变成一团糟”(嗯,不完全是这样)。这也适用于用户界面。我们无法预测确切的用户交互及其顺序。在任何时间点,我们的应用可能处于令人难以置信的可能状态之一。

挑战

  • 我们尽力使结果可预测和受设计限制
  • 我们不希望看到 bug 截图并想知道”是怎么发生的”
  • 对于 N 个可能的状态,有 N×(N–1) 个可能的状态之间的转换
  • 例如,如果按钮可以处于 5 个不同状态之一(正常、活动、悬停、危险、禁用),更新按钮的代码必须对 5×4=20 个可能的转换正确——或禁止其中一些

问题:我们如何驯服可能状态的组合爆炸并使视觉输出可预测?


7. 优先级(Priority)

问题:有些事情比其他事情更重要。对话框可能需要物理上”高于”产生它的按钮,并”突破”其容器的裁剪边界。新调度的任务(如响应点击)可能比已经开始的长时间运行的任务(如渲染屏幕折叠下方的下一个帖子)更重要。

挑战

  • 随着应用的增长,由不同人员和团队编写的部分代码竞争有限的资源,如处理器、网络、屏幕空间和包大小预算
  • 有时你可以按”重要性”的共享规模对竞争者进行排名,如 CSS z-index 属性
  • 但这很少有好结果
  • 每个开发人员都倾向于认为他们的代码很重要
  • 如果一切都重要,那么什么都不重要!

问题:我们如何让独立的组件合作而不是争夺资源?


8. 可访问性(Accessibility)

问题:无法访问的网站不是一个小众问题。例如,在英国,残疾影响五分之一的人。

挑战

  • 我们需要使我们的应用对困难的人不那么糟糕
  • 好消息是有很多低挂的果实
  • 它从教育和工具开始
  • 但我们还需要让产品开发人员容易做正确的事情

问题:我们能做什么来使可访问性成为默认而不是事后考虑?


9. 国际化(Internationalization)

问题:我们的应用需要在世界各地工作。人们不仅说不同的语言,我们还需要以最少的努力支持从右到左的布局。

问题:我们如何支持不同语言而不牺牲延迟和响应性?


10. 交付(Delivery)

问题:我们需要将应用代码交付到用户的计算机。我们使用什么传输和格式?这听起来很简单,但这里有很多权衡。

挑战

  • 例如,原生应用倾向于提前加载所有代码,代价是巨大的应用大小
  • Web 应用倾向于较小的初始有效负载,代价是使用期间更多的延迟
  • 我们如何选择在哪个点引入延迟?
  • 我们如何根据使用模式优化交付?
  • 我们需要什么数据来获得最佳解决方案?

11. 弹性(Resilience)

问题:你可能喜欢 bug(如果你是昆虫学家),但你可能不喜欢在程序中看到它们。然而,你的一些 bug 将不可避免地进入生产。

挑战

  • 一些 bug 导致错误但定义明确的行为。例如,也许你的代码在某些条件下显示不正确的视觉输出
  • 但如果渲染代码崩溃了怎么办?那么我们就无法有意义地继续,因为视觉输出会不一致
  • 渲染单个帖子的崩溃不应该”拖垮”整个 feed 或使其进入半损坏状态,导致进一步崩溃

问题

  • 我们如何编写代码以隔离渲染和获取失败并保持应用的其余部分运行?
  • 用户界面的容错意味着什么?

12. 抽象(Abstraction)

问题:在一个小应用中,我们可以硬编码很多特殊情况来应对上述问题。但应用往往会增长。

挑战

  • 我们希望能够重用、分叉和连接我们代码的部分,并集体工作
  • 我们想要定义不同人熟悉的部分之间的清晰边界,并避免使经常变化的逻辑过于僵化

问题

  • 我们如何创建抽象来隐藏特定 UI 部分的实现细节?
  • 我们如何避免在应用增长时重新引入我们刚刚解决的问题?

学习建议

不要只看解决方案

阅读这些问题时,很容易想到特定的视图库或数据获取库作为解决方案。但我鼓励你假装这些库不存在,并从那个角度重新阅读。

你会如何解决这些问题? 在一个小应用上试试!

从小处开始

这些问题的有趣之处在于,它们中的大多数在任何规模上都会出现。你可以在小部件(如自动完成或工具提示)和大型应用(如 Twitter 和 Facebook)中看到它们。

思考:从你喜欢使用的应用中选择一个非平凡的 UI 元素,并浏览这个挑战列表。你能描述其开发人员选择的一些权衡吗?尝试从头开始重新创建类似的行为!


参考资源


关键要点:这些是 UI 工程中的核心挑战。理解这些问题有助于更好地设计和构建用户界面。尝试在小应用中实验这些问题,你会学到很多。