欧雷的周报:2021 年第 8 周
相对来说,除夕及刚过完年那几天略微「放纵」,没写一行代码。出去看了两场电影,分别是《唐人街探案 3》和《刺杀小说家》。
《唐人街探案 3》说实话看着不过瘾,里面还穿插着烂梗,如「东京热」,看得我一脸尴尬……结尾处几个「神秘人」好像是在英国(貌似看到了大本钟)开「圆桌会议」,还露脸了,神秘感荡然无存了好吗!反倒片尾曲给了我小惊喜,是南征北战NZBZ的《酷你吉娃》,因为从除夕前我就开始被他们的《骄傲的少年》所洗脑……
《刺杀小说家》比《唐人街探案 3》吸引我,头一次看到用这种虚实两个世界交替手法的中国电影,让我感到眼前一亮。并且动画部分做得质量也不错,从当前国产动画的主流采用 3D 形式来看,这方面的技能和积累不用担心。片中的那个「铠甲」让我想起了《 寄生兽 》中的「小右」……

这几天开始渐渐进入工作状态——上午八、九点起床,写代码到下午一、两点,玩游戏到晚饭前。
Q1 OKR
这周进展还可以,在 Petals 中加了 20 个以上的 UI 组件 interface。
看到这个数字,肯定会有人觉得:「才写这么几个就觉得还不错了?!而且还是 interface 而不是成品!」
我不是照搬 Ant Design、Element 或 Vuetify 等,每往 Petals 里加一个 UI 组件,我都要好好想下它的存在意义以及 API 如何设计;在进行 API 设计时又会纠结该有哪些,并查资料佐证我的想法——往 Petals 里加 UI 组件的过程,就是我重新理解交互并定义 UI 组件的过程,因此会花费更多的时间。
具体加了什么 UI 组件就不说了,主要分享下这过程中的一点思考——
受控结构生成
什么是「受控结构」?在这里是指「可被使用者自定义的受 UI 组件本身控制的内部结构」。它有别于子组件,会被渲染到 UI 组件内部预先设置的「指定坑位」——Vue 中具名插槽的内容就是。
具名插槽是个很是便利的机制,即便如此,作为一个跨环境的 UI 组件体系,要么在每个环境中都实现具名插槽机制,要么在 Vue 中舍弃具名插槽而另辟蹊径——显然后者是更明智的选择。
不考虑具名插槽,生成受控结构主要有两个手段:一是通过 UI 组件的属性传入数据结构或返回数据结构的函数,如所谓的「render prop」,常用于比较简单的结构;二是设计专门的 UI 组件,如 Ant Design 中的 TabPane
之于 Tabs
,主要在结构较为复杂时使用。
我之前说过,在设计业务无关的 UI 组件时应该注意——
UI 组件的属性值尽可能是简单数据类型,也就是数字、字符串等。
在 Ant Design 中,UI 组件的属性大量使用虚拟 DOM 节点,这在我的体系里是个反模式。
假如有个卡片(Card)组件,它的内部从上到下被划分为 header、body 和 footer 三个部分:header 中是标题、图标和一些操作;子组件被放在 body 内;footer 中则可以是操作或其他附加信息。默认情况下,header 和 footer 中是没内容的,所以连它们本身都不生成。
我会设计一个字符串类型的 title
属性用来设置标题,在使用者给这个属性传值后会生成 header。当使用者想在标题前显示图标,或在卡片组件的右上角显示一些操作时,该怎么办?
Ant Design 的做法是 title
属性接收虚拟 DOM 节点,这就可以传入一个视图结构了;同理,又弄了个接收虚拟 DOM 节点的 extra
属性去控制卡片组件右上角的结构。
在这里,我的做法是——设计一个专门的 UI 组件,姑且叫做 CardHeader
,作用就是让使用者能够完全自定义 header 中的内容;当检测到 CardHeader
时就忽略 title
属性。
为什么不是给卡片组件添加 icon
属性去控制图标的显示,设计 actions
属性传入对象数组去生成右上角的操作呢?
首先,如果把 icon
和 actions
这两个属性加到卡片组件上,没看过使用手册的使用者在看到它们时,第一反应会想到它们实际是影响 header 部分吗?
若是加到卡片组件上,它们既可以被理解成在 header 里,又可以被认为是在 body 或 footer 里,因为这两个词不具备唯一性,不像 title
;反而加到 CardHeader
上比较合适。
其次,要是非要直接通过卡片组件来控制,就得加上限定词让人更容易理解,如: headerIcon
、 headerActions
。但这样一来,结构控制都集中到了卡片组件上,它的属性将越来越多,这就又违背了另外一个原则——
UI 组件是什么?可以认为它是一个返回视图结构的函数,而 UI 组件的属性(prop)和事件(event)就是这个「函数」的参数。属性是 UI 组件的外部与其内部进行主动通信的数据,事件则是进行被动通信的回调函数。
一个封装得好的函数,它的参数应尽可能少,要想明白每个参数的语义,且必须确实有其存在的意义——UI 组件的属性和事件的设计也该如此。
在设计 UI 组件的属性时,先思考下要加的这个属性是不是属于这个 UI 组件本身的特性?若不是,那要加的属性的值所对应的 UI 组件的特性是什么?如果这两个问题都没有得到答案,那么这个属性可以不用加了。
再者,如上文所说,用复杂数据类型做属性值是要尽量避免的,即用数字、字符串等简单数据类型做属性值。虽然如此,但有时传对象或数组却可能会更合适些。
比如,有些 UI 组件会包含一些可自定义的操作,这种情况下再让使用者手动拼个视图结构的话,未免太不贴心了;这时若能通过传入一组代表操作配置的数据结构去生成一堆按钮,应该会更好些。需要注意的是,这个配置应当是(一定范围内)规范化的对操作(动作)的抽象。
综上所述,受控结构的生成优先考虑设计一个专门的 UI 组件,让属性列表尽可能简洁与整洁,把视图结构放回到它该待的地方;「不得已」时可以采用一个接收具有配置语义的数据结构的属性,如可能泛化的操作配置。
基础组件拆分
假设有道面试题:「什么样的 UI 组件算是基础组件?」你会如何作答?是「没有业务逻辑的 UI 组件」吗?那么,什么是「业务逻辑」?一个专门用来设置 cron 定时任务的 UI 组件是不是基础组件?
需要知道并承认的是,很多概念是模糊不清的,没有明确的边界的,无法被精确定义的——你能描述下什么是「桌子」吗?
影响一个 UI 组件是不是「基础组件」的因素之一,就是它所处的组件体系对「原子性」的定义。
相较而言,Vuetify 比 Ant Design 和 Element 更加原子化,因而更加灵活。但更为原子化就代表更好吗?这不一定。
在我的体系中,原子性主要体现在 UI 组件语义的唯一性上——不存在像 Input
这种,根据 type
属性的值改变了它实际语义的万能 UI 组件;也不存在两个语义十分相近,以至于让使用者产生很大疑惑的 UI 组件。
不是说没有相对泛化、抽象的 UI 组件,它们一般作为被继承的 interface、被组合的函数存在,而不是真正有视图结构的 UI 组件。
结语
终于又重操「主机游戏玩家」这个「旧业」了,还是有蛮多感慨的,并且在社交软件方面又有了几个「小」决定;由于本次周报在 Q1 OKR 部分占用了较多篇幅,就日后再说吧。
如果让我选择一个对他人、对社会没啥价值和贡献,在别人看来是「堕落」的活法,我就天天看动漫、打游戏。