DSL@core:设计代码化
目录
UI 设计代码化,即将软件的 UI 设计与 UI 交互转换为特定的领域语言,并使用代码的方式来进行管理。它可以直接将需求转换为 UI 原型,让设计人员基于此进行设计;还负责将其转换对应的 UI 代码,方便开发人员进行编写。
将 UI 设计代码化,我们要做的这么几件事:
- 用户交互文档化。即所有的 UI 交互过程,以明确的格式记录下来,并与文档的形式存储。
- UI 工具无关。采用标准化的方式描述 UI 设计,让 UI 设计与 UI 设计工具脱离。
- 双向反馈。即我们的设计与 UI 原型、代码是相绑定的,当代码与设计不一致时,我们能即时得到反馈 —— 要么修改设计,要么修改代码。
- 连接需求与代码的胶水。从某种程度上来说,这个 DSL 还承担着作为需求与代码连接的胶水。即将需求转换为设计的描述,以便于这个描述转换为代码。
限制因素:
矢量可编程的 UI 设计。
UI 工具是整体过程中最令人头痛的问题。对于 UI 设计而言,如果它产出的内容不是矢量图形,那么它会限制我们的转换能力 —— 一个二进制文件不适合在代码库中存储。而,如果一个 UI 工具产生的格式是可直接编程操作的,那么就再好不过了,比如 SVG。但是呢,SVG 缺少一些引用等的相关设计。不过呢,Sketch 也是一个非常不错的工具,它的格式是易于进行编程操作的。
UI 元素可编程。
在进行 UI 设计的时候,我们会定义出一套 Design Sytem 或者 UI Guideline,上面充满了丰富的元素,如组件、字体等。对于这些元素来说,它应该是可以由代码生成,或者直接转换为设计 DSL。以用于核验代码中的元素是否真的与设计匹配。
对于交互的抽象。
对于交互的抽象是一个烦人的问题,但是呢,在我深入研究与探索之后,我发现这也不是一个复杂的问题。复杂度并不高,只是呢,我们要考虑如何与我们的设计、代码进行关联,形成统一的关系。
UI 设计代码化要素
综上所述,我们在对 UI 进行代码化时,要考虑这么一些要素。
要素 1:代码反馈设计
在云研发体系里,我们将所有一切代码化有两个原因:
- 流程代码化,并实现化转换自动化。
- 借助反馈进行自动优化。
对于 UI 设计代码化这一步来说,我们要:
- 寻找合适的 UI 设计工具及对应的解析库,以将解析 UI 设计,转换为特定的领域语言。
- 能解析修改过后的生成代码,将代码实现与 UI 设计进行对比。
- 自动绑定 UI 设计与代码,自动修改、提示不合理的地方。
要素 2:支持增量变更
设计与代码是相似的,在开发过程中,会伴随着需求的变化,影响到 UI 设计上的变化。因此,对于 UI 设计产物来说,它们应该:
- 可版本化。与代码库同在,能跟踪到设计的历史变化。
- 可编码。可以由需求生成设计,由代码反馈到设计。
在有了这两个条件的情况下,我们可以进行增量变更。
要素 3:抽象交互
尽管,我在本文中提出了一套交互相关的 DSL,但是它并不是那么完善。除此呢,在不同的公司里,人们也会自己的一些特定的 UI 设计模式等。所以呢,我们还需要设计一种抽象来描述系统对于用户的交互。
对于一个交互 DSL 来说,它需要做两件事:
- 描述用户交互。
- 能与需求进行对应。
- 能与代码进行对应。
接下来,让我们看看 Unflow DSL 的设计。
Unflow DSL
基于此呢,我们设计了 Unflow,它具备了如下的三个模式:
- 三段式交互设计:SEE-DO-REACT
- 拆分设计:原子设计
- 布局系统:AutoLayout 与 Flex 布局
除此呢,还有一个非常重要的部分:反馈式设计,我暂时还没有去验证。
模式 1 —— 三段式交互设计:SEE-DO-REACT
在日常的软件开发活动中,我们经常会看到不同的三段式表达:
- BDD 里的:Given - When - Then
- UI 设计的:显示 - 行动 - 响应
- HTTP 请求的:request - handle - response
- 代码的:输入参数 - 处理 - 输出结果
- 测试的:Arrange-Act-Assert
- 前端开发的:展示 - 事件 - 响应
对于 UI 设计来说,也存在类似的元素。我尝试着从一堆论文中寻找经验,初始时我尝试以 BDD 的三段式来总结。直到我看到了 Basecamp 的设计师 Ryan 在『A shorthand for designing UI flows』一文中看到几句话:
- What the user sees
- What them do
- What them see next / what them do next
基于此,添加了一个 React 的选项,即系统要对他们做出什么响应。于是,有了一个简单的 DSL 原型:
flow 登录 {
SEE 首页
DO 输入密码
DO [点击] "登录".Button
REACT 成功: 展示 "Login Success".Toast with ANIMATE(bounce)
REACT 失败: 展示 "Login Failure".Dialog
}
这里的 SEE 对应了用户的所见,DO 则是对应于用户所做,而 REACT 则是相应的可能结果。我们可以将它与需求代码化里的 Given-When-Then 进行一一应对。稍有区别的是,这里在 REACT 里进行了合并,方便后续与 UI 代码进行对应:
- 调用接口成功的场景下,则显示 Login Success,然后再往下进行操作。
- 调用接口失败的场景下,则显示 Login Failure 弹窗(Dialog),然后可以添加其它行为。
上述代码中的首页,可以对应到 UI 设计的场景、原型上,对应的按钮(Button) 则是组件使用上的声明。与此同时,基于上述的一系列关键描述,如 Login Success、Login Failure 还创建了对应的 UI 设计上的场景。
模式 2 —— 元素拆分:原子设计与元素定义
在设计人员与开发人员协作的过程中,Brad Frost 创建了原子设计的概念:原子设计是一个设计方法论,由五种不同的阶段组合,它们协同工作,以创建一个有层次、计划性的方式来界面系统。
于是,在 Unflow 中,我们依然采用了这个理念,与之对应的设计是:
- 原子 - library。描述基础、库组件的一些要素。
- 分子级 - component。描述组件
- 有机体 - component。描述组件
- 模板 - template。
- 页面 - page
这里的 library、component、template、page 都是 Unflow 中的定义。Unflow 的 DSL 只是提供定义,如下是一个对于颜色规范的定义:
library Color {
Primary {
label = "Primary"
value = "#E53935"
}
Secondary {
label = "Blue"
value = "#1E88E5"
}
Third {
label = "Third"
value = "#000000"
}
}
Unflow 定义的是这些要素,随后结合其它工具进行转换。在早期 ,我们结合 Node.js 里的 Sketch Constructor 进行了转换,它将转变为两部分:Sketch 里的颜色规范定义,以及前端代码库里的 SCSS 定义。
这种定义方式,对于 component
、page
也是类似的。
page HomePage {
LayoutGrid: 12x
LayoutId: HomePage
Router: "/home" # 由开发定义
}
稍有不同的是,我们在设计中加入了一个路由的概念,这个后期可以由开发人员来进行补充。
顺带一说,依旧的这只是 Unflow 的第一个版本,所以在设计上会比较粗糙。
模式 3 —— 布局系统:Flex
起先,如果只是站在早期的布局系统的维度之下,我怕是没有胆量去设计一个 DSL。而随着不同领域对于 Flex 布局的统一化程度:
- 移动端框架 Flutter 中的线性布局(Row、Column)
- 原生 UI 框架 Druid 采用的 Flex 布局
- 前端领域采用的 Flex 布局
- Android 端的 FlexboxLayout
- ……
那么,对于我们的布局系统来说,自然采用的是类似于 Flex 布局。如此一来,我们只需要考虑一下结合 Apple 的 Auto Layout,就能得到一个勉强可以用的 UI 系统。
而,我最早对于 Layout 体系的想法,语法来源是 autolayout.js。一个在前端实现了 AutoLayout 和 Visual Format Language 的布局系统,它的语法如下:
H:|[view1(==view2)]-10-[view2]|
V:|[view1,view2]|
虽是如此,我设计的第一个版本的布局系统有点不那么实用。关于这一点,我还在自我反思 ,为什么会设计出这么难写的语法:
Layout Navigation {
--------------------------------------
| "home" |"detail" | Button("Login") |
--------------------------------------
}
在设计布局的时候,想的是:
- 以 Flex 作为实现方式
- 以 Table 作为展示形式,方便开发人员维护
- 支持组件上的参数传递
在这种限制的交错之下,就有了现在这种奇怪的设计。