UI 工程的要素
December 30, 2018
在我上一篇文章中,我谈到了承认自己的知识盲区。你可能会因此认为我是在建议大家安于平庸。但其实并不是!这是一个非常广阔的领域。
我坚信你可以“从任何地方开始”,不必按照特定的顺序学习技术。但我同样非常重视积累专业知识。就我个人而言,我一直对构建用户界面最感兴趣。
我一直在思考,自己究竟掌握了哪些知识,并认为哪些是有价值的。 当然,我对一些技术(比如 JavaScript 和 React)很熟悉。但从经验中获得的更重要的教训却很难捉摸。我从未尝试用语言将它们表达出来。这是我第一次尝试对其中的一些进行梳理和描述。
关于技术和库,有很多“学习路线图”。2019 年会流行哪个库?2020 年又会怎样?你应该学 Vue 还是 React?还是 Angular?Redux 或 Rx 呢?需要学 Apollo 吗?REST 还是 GraphQL?这些问题很容易让人迷失。如果作者的建议错了怎么办?
我最大的学习突破并不是关于某个特定技术。 相反,每当我努力解决某个具体的 UI 问题时,才学到最多。有时,我后来会发现有些库或模式能帮上忙。也有时候,我会自己想出解决方案(无论好坏)。
正是这种对问题的理解、对解决方案的尝试,以及不同策略的应用,带来了我人生中最有价值的学习体验。这篇文章只关注这些问题本身。
如果你做过用户界面开发,肯定遇到过其中一些挑战——无论是直接面对,还是通过某个库来处理。无论哪种情况,我都鼓励你尝试用不依赖任何库的小应用,亲自复现并解决这些问题。这些问题没有唯一正确的答案。学习的过程,就是探索问题空间、尝试不同权衡的过程。
-
一致性。 你点击“点赞”按钮,文本更新为:“你和另外 3 位朋友喜欢这条动态。”你再点一次,文本又切回去了。听起来很简单。但也许类似的标签在屏幕上有多个地方。也许还有其他视觉提示(比如按钮背景)需要变化。之前从服务器获取并在悬停时显示的“点赞者”列表,现在应该包含你的名字。如果你切换到其他页面再回来,这条动态不能“忘记”已经被点赞。即便只是本地一致性,也会带来一系列挑战。而且,其他用户也可能会修改我们展示的数据(比如他们也点赞了我们正在看的动态)。我们如何保证屏幕不同部分的数据同步?我们又该如何以及何时让本地数据与服务器保持一致,反之亦然?
-
响应性。 用户对操作缺乏视觉反馈的容忍度是有限的。对于连续操作,比如手势和滚动,这个容忍度很低。(哪怕丢掉一个 16ms 的帧都会让人感觉“卡顿”。)对于离散操作,比如点击,有研究显示用户对小于 100ms 的延迟感知为一样快。如果操作耗时更长,我们就需要显示视觉指示。但这里面有一些反直觉的挑战。比如,指示器导致页面布局“跳动”或经历多个加载“阶段”,会让操作感觉更慢。同样,如果为了在 20ms 内处理交互而丢帧,反而会感觉比在 30ms 内处理且不丢帧更慢。人脑并不是基准测试工具。我们如何让应用对不同类型的输入都保持响应?
-
延迟。 计算和网络访问都需要时间。有时,如果计算开销不会影响目标设备上的响应性(记得在低端设备上测试应用),我们可以忽略它。但网络延迟是无法避免的——可能需要几秒钟!我们的应用不能在等待数据或代码加载时卡死。这意味着任何依赖新数据、代码或资源的操作都可能是异步的,都需要处理“加载中”的情况。但几乎每个页面都可能遇到这种情况。我们如何优雅地处理延迟,而不是让页面出现一连串的加载动画或空洞?如何避免布局“跳动”?又如何在更换异步依赖时,不用每次都“重接线”代码?
-
导航。 我们期望界面在交互过程中保持“稳定”。元素不应该突然消失。导航操作,无论是应用内发起(如点击链接),还是外部事件触发(如点击“返回”按钮),都应遵循这一原则。例如,在个人资料页的
/profile/likes
和/profile/follows
标签页之间切换时,不应清空标签页外的搜索输入。即使导航到另一个页面,也像是走进另一个房间。用户期望稍后返回时,能看到离开时的状态(也许会有新内容)。如果你在信息流中间,点击进入某个个人资料页,再返回时,失去原来的位置或需要重新加载信息流会让人很沮丧。我们如何架构应用,支持任意导航而不丢失重要上下文? -
陈旧性。 我们可以通过引入本地缓存,让“返回”按钮的导航瞬时完成。在缓存中,我们可以“记住”一些数据,即使理论上可以重新获取。但缓存带来了自己的问题。缓存可能会变陈旧。如果我更换头像,缓存也应该更新。如果我发了新动态,缓存里应该立刻出现,或者缓存需要失效。这会变得复杂且容易出错。如果发动态失败怎么办?缓存在内存中保留多久?重新获取信息流时,是把新获取的信息和缓存拼接,还是直接丢弃缓存?分页或排序又如何在缓存中表示?
-
熵。 热力学第二定律大致说“随着时间推移,事物会变得混乱”(当然并不完全是这样)。这同样适用于用户界面。我们无法预测用户的具体操作及其顺序。任何时刻,应用都可能处于数量惊人的各种状态之一。我们尽力让结果可预测,并通过设计加以限制。我们不希望看到 bug 截图时还要问“这怎么发生的”。对于N种可能状态,存在*N×(N–1)*种可能的状态转移。例如,一个按钮有 5 种状态(正常、激活、悬停、危险、禁用),那么更新按钮的代码必须正确处理 5×4=20 种状态切换——或者禁止其中一些。我们如何驾驭状态组合爆炸,让视觉输出可预测?
-
优先级。 有些事情比其他更重要。比如一个对话框需要物理上“浮在”触发它的按钮之上,并“突破”其容器的裁剪边界。一个新调度的任务(如响应点击)可能比已经开始的长任务(如渲染屏幕下方的下一批内容)更重要。随着应用规模增长,由不同人和团队编写的代码会争夺有限的资源,比如处理器、网络、屏幕空间和包体积预算。有时你可以用类似 CSS 的
z-index
属性在同一优先级尺度上排序。但这很少有好结果。 每个开发者都觉得自己的代码最重要。如果一切都重要,那就没有什么重要了!我们如何让独立的组件协作,而不是争抢资源? -
可访问性。 网站不可访问绝不是小众问题。例如,在英国,每五个人中就有一人受残障影响。(这里有一张很好的信息图。) 我自己也有切身体会。虽然我才 26 岁,但我已经很难阅读那些字体细、对比度低的网站。我尽量少用触控板,也很担心以后不得不用键盘来操作那些实现糟糕的网站。我们需要让应用对有障碍的人也不那么糟糕——好消息是,这里面有很多“低垂的果实”。这需要从教育和工具做起。但我们还需要让产品开发者更容易做对。我们能做些什么,让可访问性成为默认而不是事后补救?
-
国际化。 我们的应用需要在全球范围内可用。人们不仅说不同的语言,还需要支持从右到左的布局,而且要让产品工程师付出最少的努力。我们如何在不牺牲延迟和响应性的前提下支持多语言?
-
交付。 我们需要把应用代码传递到用户的电脑上。用什么传输方式和格式?这听起来很简单,但其实有很多权衡。例如,原生应用通常会提前加载所有代码,代价是应用体积很大。Web 应用则倾向于初始包更小,但使用过程中会有更多延迟。我们如何选择在哪个环节引入延迟?如何根据使用模式优化交付?要实现最优方案,需要哪些数据?
-
弹性。 如果你是昆虫学家,可能会喜欢 bug,但你大概不会喜欢程序里的 bug。然而,一些 bug 总会不可避免地进入生产环境。那时会发生什么?有些 bug 会导致错误但可预期的行为。例如,某些条件下代码显示了错误的视觉输出。但如果渲染代码崩溃了呢?那我们就无法继续,因为视觉输出会变得不一致。渲染单条动态时崩溃,不应该“拖垮”整个信息流,或让其进入半崩溃状态,导致更多崩溃。我们如何编写代码,将渲染和数据获取的失败隔离开,让应用其他部分继续运行?对于用户界面,容错到底意味着什么?
-
抽象。 在一个小应用里,我们可以硬编码很多特殊情况来应对上述问题。但应用总会变大。我们希望能够复用、分叉、合并代码的各个部分,并协作开发。我们希望在不同人熟悉的模块之间定义清晰的边界,避免让经常变化的逻辑变得过于僵化。我们如何创建抽象,隐藏某个 UI 部分的实现细节?又如何避免随着应用增长,把刚解决过的问题又重新引入?
当然,还有很多问题我没有提到。这份清单远非详尽!比如,我没有谈到设计师与工程师的协作,也没有涉及调试和测试。也许以后会聊。
在阅读这些问题时,很容易联想到某个视图库或数据获取库作为解决方案。但我鼓励你假装这些库不存在,再读一遍,换个角度思考。你会如何解决这些问题?试着在一个小应用里动手实践!(如果你在 GitHub 上有相关实验,欢迎在推特上 @我。)
这些问题有趣之处在于,它们几乎在任何规模下都会出现。无论是像 typeahead 或 tooltip 这样的小部件,还是像 Twitter、Facebook 这样的大型应用,你都能看到它们的影子。
想一想你喜欢用的某个非平凡 UI 元素,对照这份清单,能否描述出开发者做出的一些权衡?试着从零实现类似的行为吧!
我通过在小应用中不依赖库地尝试解决这些问题,学到了很多 UI 工程的知识。对于想更深入理解 UI 工程权衡的人,我也推荐你这样做。
Pay what you like