cd ..

纯 UI 设计

11月30日12min

今年是我从事前端开发的第 5 年,而今天也是我拜读 Guillermo Rauchpure-ui 的第 3 年。 这篇文章到今年也发布了第 10 个年头,我觉得非常有意思,因为我已经逐步走上了这条纯 UI 设计的道路。

当这种种事件刻度重叠在一起的时候,让我不禁回望 UI 开发的来时路,也思索它的去向。

我们学会了"看",却还没学会"懂"

过去十年,前端团队在界面协作上取得了惊人的进步。我们终于学会了系统性地思考:同一个界面,在不同用户、数据、设备、语境下,该如何呈现。 我们学会了"看"界面。 然而,面对界面在无数种用户交互下可能呈现的行为,我们依然力不从心。绝大多数界面,总会在你以某种"不正常"的方式操作时露出破绽。 即便是开发者自己,也无法完全预见它的行为。我们还能做得更好。 让界面行为变得可理解,并非天方夜谭。我们可以借鉴解决"视觉状态"问题的思路,来改善团队对"交互行为"的认知。


在 "Pure UI" 中看到状态

Pure UI 这篇堪称经典的文章里, Guillermo Rauch 描绘了 React 如何重塑他与设计师的协作。 核心转变在于:让"视觉状态"成为显式的第一公民。设计师在画板上并排列出组件的所有状态,开发者的代码也按这些状态分支渲染。 设计稿与 render 函数之间形成精准的镜像关系。更重要的是, 当开发者发现一个新边界情况时,它天然地就该出现在设计画板上,设计与开发只需在同一维度上追加一颗思考的棋子。

这种工作方式如今如此自然,以至于我们忘了它并非生来如此。要体会其中的革命性,不妨回溯一下十年前我刚入行时的协作图景。

那时,设计交付物是一个 Photoshop 文件,通常只呈现单一终态。 即便最细心的设计师,也只会把其他状态藏在图层组里切换。更多状态要么被忽略,要么只在口头描述为"对原状态的小改动"。 这些次生状态从未获得视觉肉身,于是成为误解的重灾区。

对应的代码由模板和一系列函数组成,这些函数描述如何根据用户输入或数据变化来修改 DOM

设计稿与代码在这两点上惊人地相似:

  • 所有状态必须逐个手动操作才能查看。

  • 没有一份显式的"状态清单"。

这让团队思考与沟通陷入困境:

  • 无法一览全貌,界面在脑海中始终是碎片化的。

  • 没有状态清单,仅凭默认状态看起来"完工"就误判进度。

  • 开发者发现新状态时,它在设计文档中没有容身之所,于是常常干脆不被设计。

  • 每新增一个设计元素都愈发艰难,因为所有状态切换逻辑都必须考虑:该显示它?隐藏它?还是修改它?

我们其实可以发现,这些本质上不是技术问题,也不是流程问题,而是思维问题。 设计与代码都将组件描述为"默认视觉 + 可应用的变更"。 这种表达方式无法帮助前端团队对齐认知、识别未决问题,也无法让工作本身变得更简单。

Pure UI 之所以有效,是因为它改变了我们描述 "视觉状态" 的方式。现在,我们将组件描述为一组状态,每个状态拥有独立的视觉表达。 这为设计画板与开发者样式指南之间建立了自然映射。两者都允许我们并置所有状态,从而整体审视、发现不一致、提出新问题。 修改也变成原子化和局部化的:改动一个状态,只影响该状态。

由此我们可以提炼一个通用策略,每当问题复杂到超出团队协作认知边界时,它都适用:

  • 寻找当前表达中缺失但至关重要的概念。

  • 创造一种新表达,让那个概念显式化,帮助我们梳理已知与未知。

  • 最后,才考虑用何种技术与流程将这个表达落地。

带着这个策略,让我们审视当下大多数 UI 团队仍在挣扎的问题:如何理解应用程序所有可能的交互行为。

意外的行为

从我写第一行界面代码起,无论使用何种技术、何种测试策略,生产环境总有一些意料之外的 BUG。 都不是什么显性 BUG ,更不会有产品经理为它写用户故事。

这些 Bug 总藏在"意料之外的操作"里——那些让你忍不住问"谁会这么干?"的操作路径。但只要可能,终将发生。 几乎所有这类 Bug 的共同点是:它们依赖业务的顺序,尤其是那些"紊乱"的顺序。

举些例子,你大概率在项目中遇到过类似的麻烦:

  • 校验组件在你输了一半表单、切到另一屏折腾一番再回来时,突然罢工。

  • 正在编辑对象 A ,点击按钮加载对象 B ,如果响应时机恰好,你的编辑会保存到 B 上,或永远丢失。

  • ...

我见过一些团队喜欢将这些问题归咎于代码写得烂、 redux 太糟,或别的技术背锅。 但问题根源既非异步工具,也非开发者懒惰。 的确,一旦理解了 Bug 如何发生,代码错误显而易见。 但写代码时,一个 reasonable 的开发者会主动考虑这种情况吗? 设计师会在脑中推演这个可能性并给出应对策略吗?

通常不会。

所以这不是技术问题,也不是态度问题,而是思维问题。 我们需要一种表达,能帮助我们思考界面的行为逻辑。

计算器的陷阱

来看一个简单的计算器应用。它只有一个视觉状态,但行为深度令人惊讶。 人人都知道它该做什么,但除非你坐下来认真拆解,否则很难说清楚它如何做到。这玩意儿比看起来棘手得多。

你可能不信,觉得五分钟就能撸出来。我劝你亲手试试。(欢迎贴出代码链接和耗时。)

calculator

看看 UI 团队用什么工具思考这个界面:

  • 设计师交付一张简洁的 Mockup 后可能就收工了。再用心点,会写几段文字描述几个输入序列的期望行为。

  • 开发者迅速把显示和事件绑定。假设他们用 Redux 或 Next 架构,需要定义一个函数:接收当前状态和动作,返回新状态。

他们先实现数字键:简单,把现有值乘 10 再加新数字。

接着实现小数点:这不会立即改变数值,但要显示出来。

于是加一个 decimalPressed 标志位,按下小数点时设为 true ,让显示带小数点。 之后输入 2 0 . 正确显示为 20.

但问题来了——当 decimalPressedtrue 时,之前数字键的处理逻辑就错了。 于是任何改变数字的动作都得检查这个标志。

哦对,任何意味着"开始输入新数字"的动作还得记得把它重置为 false …… 除非刚按完小数点,那得保持 true ……

实现完整逻辑有优有劣,我不想展开。最 naive 的策略是慢慢堆砌标志位和其他不可见的"状态幽灵"来记录历史。 这能跑通,但引发新问题:

  • 新增动作越来越难,因为它可能要响应任意历史动作,或影响任意未来动作。你搞不清楚该考虑哪些组合,也说不清何时才算完。

  • 代码里嵌套的条件分支与其他任何表达都对不上,于是代码本身成了正确性的最终定义。设计师或其他任何人,都无法对"这种情况下该发生什么"发表意见。

  • 无法确信是否穷尽所有情况。单元测试能覆盖你想到的场景,但事件序列的可能性无限,测试能覆盖一切吗?

这显然不是技术问题,而是思维问题。将行为描述为发散或 reducer 代码,无助于我们思考问题或验证理解。 正如 Pure UI 在视觉状态上的困境,我们需要一个新的组织概念,一种新的表达来辅助思考。

交互的状态

我认为我们缺失的概念是交互。前面我们将视觉状态枚举为"所有意义不同的呈现方式",而交互,则是从"响应用户输入"的视角,枚举界面所有可能的状态。 有时这两层状态对齐,比如 loading 时既显示 spinner 又屏蔽所有输入;更多时候,视觉状态不变,交互态却在流转。

以计算器为例。当显示屏写着 20 时,它可能身处两个不同的交互态:

  • "正在输入操作数" 状态中,再按数字会追加到当前数字末尾。

  • "显示结果" 状态中(刚刚计算完答案),按数字意味着开启新的操作数,并切换到 "正在输入操作数" 态,从而改变后续动作的解读方式。

(计算器应用的高层状态图,来自 Ian Horrocks《Constructing the User Interface with Statecharts》第 215 页, Addison Wessley 出版)

有了"交互"这个概念,我们可以像之前一样,想象输入序列并追问界面该如何响应。 但这一次,我们能将思考结晶为一份明显的交互清单,以及每个状态下应该如何响应用户动作。

用交互的状态来思考界面行为,还能产出更易懂的沟通物。非程序员也能轻松读懂状态转移图。 即便对程序员而言,图表的视觉结构也可能带来醍醐灌顶的 "啊哈!" ——原来界面真正允许的是这些。

交互的状态也能自然映射到代码。不该再为每个按钮定义一个充满历史判断的处理器,而应为(当前交互状态 + 动作)的特定组合定义转移逻辑。

设计交互本身仍需深度思考,但它提供了组织框架,让我们的表达更自然。 结果,过去需要绞尽脑汁的问题,如今可简化为直观的视觉感知。 开发者曾苦于向设计师解释的边缘情况,现在只需手指一点。

现在我们的团队终于可以一起思考界面的行为了。

尾声

过去几年, Web 团队为设计工作引入了以状态为中心的新表达。 这让思考"界面在不同数据与语境下该是什么样子"变得容易。

尽管有此进步,大多数界面在面对所有可能的用户交互排列时,依然会暴露意外行为。 团队无法呈现这些可能性,也无法沟通应用该如何响应不同的输入序列。

要解决此问题,团队可以采纳交互态的概念——它描述了从响应能力视角出发,界面所有可能的状态。 通过显式设计交互态的转移图谱,他们能得出可理解、有限的、应用必须展现的行为集合。 有了精心设计的交互态空间,团队便能更轻松地共同推理所创造之物。

Back to Top
CC BY-NC-SA 4.02022-PRESENT © Elone Hoo