最近在开发 Sketch 插件和设计工程时,频繁接触了设计工具对象层。结合之前对 DSL 的理解,有了一些浅见。

设计工程化实践

C2D探索

C2D (Code To Design)比较知名的实践有 react-sketchapphtml-sketchapp
通过在前端写 react-native 式的代码,然后在 sketch 端就可以渲染输出成设计稿。理论上这是一种非常美好的设想:唯一的 source of truth 就是代码,通过代码生成设计稿,那么类似组件库这样的文件就可以变得工程可控。但是在生产端落地的过程中,遇到一个很大的问题,前端使用的代码和 react-sketchapp 编写的组件代码是两套代码。一旦经历版本升级,就很有可能导致版本不一致。额外维护一套代码的成本已经高过收益,因此这种方式最后被我们放弃了。

html-sketchapp 的思路不一样,后者可以直接识别 html 节点,并将其转换为设计稿。
基于这样的能力,我实现了表格自动生成和二次编辑的功能,封装到插件 🍱Microwave 中。(演示如下)
1.gif
由于 html-sketchapp 这个包都是由 JS 写成,在使用过程中提示非常不完善。再加上要拓展这个库必须要懂它是如何实现的,所以我用 ts 将这个库进行了重构升级。
这个库的实现逻辑其实非常简单:将需要转换的 html 节点找到之后,计算其位置、宽度、文本、样式等数据,将其转换为符合 Sketch 的数据结构。最后导入到 sketch 中。
比如下图所示的一个矩形框,我们可以用 getComputedStyle 方法得到这个矩形的所有样式信息。(在浏览器的 Computed 里即可查看到)从下图我们可以看到,这个 div 对象的背景颜色、边框颜色、圆角宽度等等信息都可以获取到。
image.png
然后接下来就是将提取这些信息,转换为相应的 Sketch 对象。由于 Sketch 的文件数据结构已经开源出来,所以这一步转换变成了可能。
比如上述这个矩形转换为 Sketch 对象,生成出来的数据结构如下(170 行代码就不完整展开了):

  1. {
  2. "booleanOperation": -1,
  3. "edited": false,
  4. "fixedRadius": 0,
  5. "hasClickThrough": false,
  6. "hasConvertedToNewRoundCorners": true,
  7. "isClosed": true,
  8. "isFixedToViewport": false,
  9. "needsConvertionToNewRoundCorners": false,
  10. "numberOfPoints": 0,
  11. "points": [
  12. {
  13. "_class": "curvePoint",
  14. "cornerRadius": 0,
  15. "curveFrom": "{0, 0}",
  16. "curveMode": 1,
  17. "curveTo": "{0, 0}",
  18. "hasCurveFrom": false,
  19. "hasCurveTo": false,
  20. "point": "{0, 0}"
  21. },
  22. {
  23. "_class": "curvePoint",
  24. "cornerRadius": 0,
  25. "curveFrom": "{1, 0}",
  26. "curveMode": 1,
  27. "curveTo": "{1, 0}",
  28. "hasCurveFrom": false,
  29. "hasCurveTo": false,
  30. "point": "{1, 0}"
  31. },
  32. {
  33. "_class": "curvePoint",
  34. "cornerRadius": 0,
  35. "curveFrom": "{1, 1}",
  36. "curveMode": 1,
  37. "curveTo": "{1, 1}",
  38. "hasCurveFrom": false,
  39. "hasCurveTo": false,
  40. "point": "{1, 1}"
  41. },
  42. {
  43. "_class": "curvePoint",
  44. "cornerRadius": 0,
  45. "curveFrom": "{0, 1}",
  46. "curveMode": 1,
  47. "curveTo": "{0, 1}",
  48. "hasCurveFrom": false,
  49. "hasCurveTo": false,
  50. "point": "{0, 1}"
  51. }
  52. ],
  53. "sharedStyleID": "",
  54. "_class": "rectangle",
  55. "do_objectID": "19727987-E66D-4752-BE76-DDCA5AFBBC35",
  56. "exportOptions": {
  57. "_class": "exportOptions",
  58. "exportFormats": [],
  59. "includedLayerIds": [],
  60. "layerOptions": 0,
  61. "shouldTrim": false
  62. },
  63. "isFlippedHorizontal": false,
  64. "isFlippedVertical": false,
  65. "isLocked": false,
  66. "isVisible": true,
  67. "layerListExpandedType": 0,
  68. "name": "rect",
  69. "nameIsFixed": false,
  70. "resizingConstraint": 63,
  71. "resizingType": 0,
  72. "rotation": 0,
  73. "shouldBreakMaskChain": false,
  74. "layers": [],
  75. "clippingMaskMode": 0,
  76. "hasClippingMask": false,
  77. "style": {
  78. "borderOptions": {
  79. "_class": "borderOptions",
  80. "lineCapStyle": 0,
  81. "lineJoinStyle": 0,
  82. "dashPattern": [
  83. 2,
  84. 2
  85. ],
  86. "isEnabled": true
  87. },
  88. "colorControls": {
  89. "_class": "colorControls",
  90. "brightness": 0,
  91. "contrast": 1,
  92. "hue": 0,
  93. "isEnabled": false,
  94. "saturation": 1
  95. },
  96. "do_objectID": "",
  97. "endMarkerType": 0,
  98. "startMarkerType": 0,
  99. "windingRule": 1,
  100. "_class": "style",
  101. "fills": [
  102. {
  103. "_class": "fill",
  104. "isEnabled": true,
  105. "color": {
  106. "_class": "color",
  107. "red": 0,
  108. "green": 0.5450980392156862,
  109. "blue": 0.5450980392156862,
  110. "alpha": 1
  111. },
  112. "fillType": 0,
  113. "noiseIndex": 0,
  114. "noiseIntensity": 0,
  115. "patternFillType": 1,
  116. "patternTileScale": 1,
  117. "contextSettings": {
  118. "_class": "graphicsContextSettings",
  119. "blendMode": 0,
  120. "opacity": 1
  121. },
  122. "gradient": {
  123. "_class": "gradient",
  124. "elipseLength": 0,
  125. "from": "0.5 0",
  126. "to": "0.5 0",
  127. "stops": [],
  128. "gradientType": 0
  129. }
  130. }
  131. ],
  132. "borders": [
  133. {
  134. "_class": "border",
  135. "isEnabled": true,
  136. "color": {
  137. "_class": "color",
  138. "red": 0.7215686274509804,
  139. "green": 0.5254901960784314,
  140. "blue": 0.043137254901960784,
  141. "alpha": 1
  142. },
  143. "fillType": 0,
  144. "position": 1,
  145. "thickness": 2,
  146. "contextSettings": {
  147. "_class": "graphicsContextSettings",
  148. "blendMode": 0,
  149. "opacity": 1
  150. },
  151. "gradient": {
  152. "_class": "gradient",
  153. "elipseLength": 0,
  154. "from": "0.5 0",
  155. "to": "0.5 0",
  156. "stops": [],
  157. "gradientType": 0
  158. }
  159. }
  160. ],
  161. "shadows": [],
  162. "innerShadows": [],
  163. "miterLimit": 10,
  164. "contextSettings": {
  165. "_class": "graphicsContextSettings",
  166. "blendMode": 0,
  167. "opacity": 1
  168. }
  169. },
  170. "frame": {
  171. "_class": "rect",
  172. "constrainProportions": false,
  173. "height": 36,
  174. "width": 2000,
  175. "x": 0,
  176. "y": 0
  177. },
  178. "pointRadiusBehaviour": 1
  179. }

其中最关键的一步,也是非常 dirty 的一步,就是分析各种样式,然后转换成符合 sketch 文件格式的代码。
但是由于 sketch 只有绝对坐标,没有布局引擎的概念,所以要知道一个对象的位置,必须去计算出它在浏览器中的位置。
比如当文本在 html 中左中右对齐时,其 x 坐标的计算可能就要如下所示:

  1. // 如果是左对齐
  2. if (textAlign === 'left') {
  3. // 确认下 padding 的距离
  4. const pl = parseFloat(paddingLeft);
  5. x = x + pl;
  6. }
  7. // 如果是居中对齐
  8. if (textAlign === 'center') {
  9. x = x + nodeBCR.width / 2 - textWidth / 2;
  10. }
  11. // 如果是右对齐
  12. if (textAlign === 'right') {
  13. // 确认下 padding 的距离
  14. const pl = parseFloat(paddingRight);
  15. x = nodeBCR.right - textWidth;
  16. x = x - pl;
  17. }

所以理论上来说,有了这样的一个HTML解析器之后,我们就能够将这份 HTML 文件一模一样地解析成任意的 sketch 对象(如全局样式、Symbol等等),达到代码转设计的目的。由于这个解析器只对最终的 HTML 渲染样式负责,所以这份 HTML 可以由任意的前端框架来实现。无论是 Vue、React 都可以达到一样的效果。

SlateJS 开发经历

实现完这个 HTML 解析器,不由得让我想起不久前我使用 Slate 编辑器 (就是语雀编辑器的前身)的经历。Slate 的官网其中一个 demo 让我印象非常深刻。
这个 demo 实现的功能其实很简单——复制 HTML 文本时保留格式。其 demo 的实现方式是:解析复制过来的 html 节点类型,然后转换为 JSON 对象插入到JSON文档对象中,再将 JSON 文档对象按照编辑器已有的渲染规则将其展示出来。

共通点

在某种意义上这个 demo 的实现思想和 html-sketchapp 是一致的:将已有的某种结构的对象解析为通用的数据结构体,然后再通过相应的规则实现目标对象的转换。
如果把这两个问题放在一起看,做一层抽象,其实它的本质是一回事,即:在通用/抽象层面我们该如何去描述相应的问题域?

DSL与设计工程化

DSL介绍

所以到这里,DSL 的出现变得自然而然。DSL(Domain Specific Language)的中文文翻译为【领域特定语言】(下简称 DSL)。有对比才能更好去理解这个概念,所以可以简单说一下 GPL(General Purpose Language),即【通用编程语言】,也就是我们非常熟悉的 JavaScript、Python 以及 Java 语言等等。而我们常见的 DSL 包括 HTML、Regex(正则表达式)、Markdown 等,甚至React 也可以作为一种 JS 的内部 DSL 来看待。
DSL 往往有下述几个特征:

  • 没有计算和执行的概念;
  • 其本身并不需要直接表示计算;
  • 使用时只需要声明规则、事实以及某些元素之间的层级和关系

世界级软件工程师 Martin Fowler 对 DSL 的看法我认为非常有穿透力:DSL 通过在表达能力上做的妥协换取在某一领域内的高效
例如 markdown 仅仅是规定了几种非常简单的语法规则,就极大地提升了文档的书写能力,同时也给样式的自定义留了巨大的空间。(关于这部分的讨论,有兴趣可以查看我另外一篇文章:基于 Markdown 的文本工作流与写作哲学
DSL 的本质在某种意义上和我们去解决通用问题的思路一样——通过限定问题域边界,牺牲域外的一部分(乃至全部)灵活性,带来域内效率的提升
更多关于 DSL 的介绍,推荐几篇不错的文章:

  • 前端 DSL 实践指南(上)—— 内部 DSL
  • 如何写一个类似 LESS 的编译工具?

    DSL 在设计工具的应用

    DSL可以帮助我们更加快速的去描述问题域,好的DSL会具有极好的描述力和扩展性(例如React是一个很好的前端视图层的DSL、markdown是一个很好的书写域的DSL)。一个不好的DSL在描述这个领域时就会显得捉襟见肘。
    如果我们从 DSL 的角度出发来看待设计工具,Sketch 从几年前开始在交互、视觉设计领域逐渐流行的原因可见一斑。
    在界面设计(UI)的这个问题域中,我们基本上可以抽象出常用的绘图对象是矩形、圆角矩形、椭圆(正圆)、直线、多边形(星形)、自由路径和自由形状这几种。而图形的样式基本只需要描边、填色、内外阴影、模糊这几种。
    所以在全世界交互设计发展的初期阶段, Sketch 准确构建了界面设计问题域中的关键要素,通过牺牲了绘图的灵活性(比如无法绘制特别复杂的花纹等),带来了UI领域的高效。就我个人而言,Sketch 其实就是一个阉割版的 AI(Adobe Illustrator),Sketch 能实现的效果 AI 基本上都能实现。但是 Sketch 通过限定问题域,有效降低了工具上手的复杂度,在界面设计领域成功上位。
    但是发展至今,已经有越来越多的声音在说 Sketch 要不行了,未来是属于 Figma 的等等的声音。
    原因是目前的界面设计的发展已经进入了深水区,我们已经不再仅仅满足绘图层面的基础能力,我们希望能够获得更加全面,更加完善的交互设计效能的提升,比如团队协作、复杂组件智能生成、自动布局、设计转换为代码等等更加工程化的需求被提上台面。从 DSL 的角度来看,Sketch并没有在上述的一些环节中有所发力,所以大概是成也萧何败萧何吧~
    比如在布局这一层,Sketch 就缺少了对布局的抽象描述能力。例如下图这种场景的业务场景,如果我要多增加两个元素,我们很难按照已有布局的方式快速添加。如果 Sketch 的布局中某个对象尺寸发生变化,导致间距不一致时,我们往往需要手动去处理间距问题。在用户侧来看,这是 Sketch 的功能缺失。但是本质上,其实是 Sketch 在元素布局这部分缺少 DSL 层的梳理,使得sketch 缺少了描述常见布局的能力。
    image.png

可以与之明显对比的就是figma。figma中有一个功能叫 auto layout,可以实现类似浏览器中的Flex布局模型的效果,可以快速做到对齐、新元素自动布局等等特性。
1.gif

这同样导致的一个问题是目前现在基于sketch(还可以包含上ps xd)的D2C很难推进。因为Sketch缺少布局方式的描述信息,使得sketch生成的设计稿数丢失了设计师在布局层的「设计意图」(sketch 存储的数据基本只有绝对定位,缺少有效的相对布局信息)。
而通过上述演示的这个对比,我们可以发现如果利用 figma 产出的数据信息进行d2c,可能生成的代码可以更好的还原「设计意图」。

所以我可以大胆地做一个预言:一旦有良好的描述设计领域的DSL时,设计稿的描述能力将会有极大的提升,我们甚至可能围绕这样的设计稿延展出全新的设计开发流程。

例如未来我们有了一个全新的设计工具,这个设计工具基于一套有效的 DSL 进行开发和构建。利用这个设计工具制作的设计稿就能够完整地包含设计师的「设计意图」,当需要转换时,我们只需要按照相应的规范(例如 sketch 文件格式、前端代码语法规范)转译成相应平台的代码/文件,就可以在相应平台下进行使用。这将大大降低设计与开发之间的摩擦,达到极致提效的目标。设想一下:当你完成了设计稿,相应的前端代码就已经工工整整地生成完毕,前端工程师只需要完成功能逻辑的开发,不需要做走查,大家都能更早下班,这样的画面,想想不就觉得非常激动人心吗?
画板.png

DSL、D2C与人工智能

阿里经济体的 imgcook 前端团队写过不少D2C 相关的文章,例如 如何使用深度学习识别 UI 界面组件?前端代码是怎样智能生成的-布局算法篇 等。
在人工智能识别组件、生成代码的过程中, DSL作为其中的桥梁是非常核心的一环。我个人认为只要拥有一个良好的 DSL 能够完整高效地记录下设计稿中的设计意图,这样就能为 D2C 带来极大的可能性。
举个不成熟的小例子:
如果我们对交互的业务侧做一些分类和整理,我们可以大致用这样的分类方式来整理原子业务交互:

  • 视图
    • 实体
      • 表单
      • 详情
    • 实体集合
      • 列表
      • 表格
      • 饼图
      • 柱状图
      • 折线图
    • 增强的实体集合
        • 组织架构
      • 分组
        • 看板
  • 字段

    • 简单数据

      • 布尔

        • Switch
        • Checkbox
        • RadioGroup
        • Select
      • 整数

      • 浮点数
      • 字符串/长文本
      • 日期
      • 枚举
      • 金额
      • ……
    • 关联关系

      • 一对一

        • 嵌入式表单
      • 多对一

        • Select
        • RadioGroup
      • 一对多

        • 带创建的 List
        • 带创建的表格
      • 多对多

        • 穿梭框
        • 多选下拉框
        • 带选取的表格

来自徐飞老师 《交互的本源 —— 对渐进式交互优化路径的初步探索》https://www.yuque.com/xufei-coder/code/pxt4zr

引用徐飞老师的观点:基于以上原则划分的交互形态,基本上都是可以同类互换的,将一种形态切换为另外一种,并不会影响业务实质。比如说,业务上想要表达布尔类型,可以在 Switch、Checkbox、RadioGroup、Select 中任意选取。
像这样的业务属性,如果能够在 DSL 中有所描述,就能够给人工智能生成前端代码带来新的可能性——即业务层逻辑组件的智能生成。
凡是可被定义,就有可能AI化。

DSL与设计

在往大一层去看,DSL 对设计有什么帮助?
我在知乎上看过一个问题的回答:

实现不了的需求太多了,我就说一个看上去特别简单的吧: 能不能给我设计一个DSL+runtime,让我可以轻松的描述出一个模型,从而得到一张表单,并且支持扩展? 如果这个DSL能做到易学易用(你别给我搞一个几百页的文档+培训才能用的东西),中后台管理页面的开发效率,可以轻松提升5到10倍。 大家可能不知道,你们用的企业内部系统里面,那些看起来无甚技术含量的增删改查页面,至今开发效率还低到让人难以忍受——一个月薪上万前端,每天只能调通一到两个表格+详情模块(哪怕他用了市面上的apaas),遇到字段较多,联动较复杂的模块,甚至两天才能调通一个。 这简直是在浪费生命。 ps:说一下我对这个问题的理解。 大家在评论里面推荐了一些库和低代码平台,这确实是前端热门领域之一,各家也都有做了几年的产品,开始陆陆续续借着aPaaS的风往外show。 但这个问题的核心,并不是低代码,也不是配置化,而是: 第一,我们到底应该如何为表单建模,然后设计一套语言去方便的描述它。 第二,这套工具是如何与平台集成在一起的,它吃什么,吐什么,如何扩展。 第二点先不说,关于第一点,我们能看到很多反例。有些产品做的很漂亮,拖拽编辑器,内置物料,内置数据流,看起来已经很完善了。但是你一用,总感觉它很孱弱,要么你挠掉头发也配不出你要的功能,要么是不是的就让你写js代码。 原因在于,这些产品没有从源头上解决问题——先把表单的数学模型搞清楚,再设计一套用户友好的DSL(不是配置文件,也不是schema)去描述模型,最后才是UI搭建部分。 如果你上来就从UI搭建开始设计,对于前两个问题不去深究,你设计出来的,就是“唯像”的产品,经不起折腾。 作者:欲三更 链接:https://www.zhihu.com/question/403244729/answer/1301175728 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

DSL 是描述问题域的语言,最核心的其实是对问题域的抽象和建模。所以在这里提一个自己造的新词:DSM(Domain Specific Model)  ,中文我自个儿叫【问题域模型】。
如引用的内容所述,其实很多设计上的问题,本质上来看,就是 DSM 的抽象和设计问题。
就我自己个人对交互设计的理解,交互的实质就是数据状态的变化,用公式表示就是:

  1. newState = interacte(oldState)

再说个极端一点的观点辅助理解:交互的实质就是数据的增删改查
因为交互无非是基于下述了三个问题进行展开:
1、针对什么数据;
2、做什么操作(增删改查)
3、怎么操作(即操作状态和操作形式,例如独占态、临时态、精灵态)
而这三个问题本质上都是围绕 DSM 进行展开的。
JCD 方法论中有提到实体分析,其实某种程度上就是对 DSM 进行分析。只不过每个实体相对较小,而 DSM 则是包含了所有实体和实体之间关系的模型。
所以说,一个产品的体验设计得好,非常易用,有很大概率是 DSM 设计好了。而某个产品体验不好, 很多流程存在断点,大概率是 DSM 的设计存在缺陷,没有补齐。
一个抽象好的 DSM,无论是设计还是开发,都会带来很大的收益。
说的这么玄乎,那么 DSM 到底怎么去建模或者去抽象呢?说实话目前我也没有体系化的方式,大部分情况下也还是自底向上的方式瞎摸黑在做。如果有提炼某些经验的话,大致有两点:

  1. 对象模型层用 JSON 结构表达;
  2. 视图层用 JSX(React) 表达。

但是这一层有点太偏向开发,就不做进一步展开了。

总结

以前我都不太 care 交互设计的五层模型,尤其是战略层和信息架构层,觉得这个太泛太虚。但是如果从 DSL 和 DSM 的角度来看,信息架构层就变得非常重要和充实。我相信在 DSL 和 DSM 进行深入挖掘,一定能够发现一片新的天地。