聊聊中后台前端应用:业务中的组件体系
欧雷 发表于
在我写的其他系列的文章中有提到——
在软件工程中,「组件(component)」一般是指软件的可复用块,好比制造业所使用的「构件」。这是一个比较宽泛的概念,它可以是软件包,可以是 web 服务,也可以是模块等。
但在前端眼里,「组件」通常是指页面上的视图单元,即「UI 组件」。可以说,「UI 组件」是「组件」的子集。你可能还总会听到「控件(control)」这个词。放轻松,别抓头,它只是「UI 组件」的一个别名而已。
普通的组件通用性很差,也就是说,它基本只能用于某个特定的系统且不能被替换。有一种组件,它是基于标准化的接口规范开发出来的,能用在任何对接了该接口的系统,也能被任何符合该接口规范的组件替换——它就是「可交换组件」,就像制造业所使用的「标准件」。
可交换的 UI 组件是前端 GUI 开发从手工作坊到自动装配的关键所在。
本文就来简单说说在中后台前端应用开发中,一个可复用性尽可能高的组件体系大概是什么样的。
前端开发是 GUI 开发,会同时涉及到视觉交互和数据逻辑,因此在看待前端时要能从视觉和数据这两个角度出发——
视觉视角
从视觉角度看,就是在拿到设计稿后在脑中把各个界面分解成若干具有层级关系且结构化的「区块(block)」:
将这些区块按照职责、粒度等可大致划分为「控件(control)」、「部件(widget)」和「页面(page)」这三类。其中,「页面」粒度最大,「控件」粒度最小,再小就是 HTML 元素了,这不在探讨范围内。区块的粒度越大,可复用性就越低。
控件
正如本文开头所说,在常规语境中「控件」是「UI 组件」的别名;但在我所阐述的组件体系中,它是指那些业务无关的原子组件,也就是「基础组件」。
较真的人看到上面描述中的「业务无关」和「原子组件」,也许会要问「怎样算业务无关」及「如何判断一个 UI 组件是不是原子组件」。
说实话,「业务无关」和「原子组件」都不是具有清晰边界的概念,我无法精确地去定义——就像「桌子」一样。要明确「业务」和「原子」的边界,需要结合所处行业、企业、项目等环境的特点以及自己和团队的理解。
UI 组件是什么?可以认为它是一个返回视图结构的函数,而 UI 组件的属性(prop)和事件(event)就是这个「函数」的参数。属性是 UI 组件的外部与其内部进行主动通信的数据,事件则是进行被动通信的回调函数。
一个封装得好的函数,它的参数应尽可能少,要想明白每个参数的语义,且必须确实有其存在的意义——UI 组件的属性和事件的设计也该如此。
在设计 UI 组件的属性时,先思考下要加的这个属性是不是属于这个 UI 组件本身的特性?若不是,那要加的属性的值所对应的 UI 组件的特性是什么?如果这两个问题都没有得到答案,那么这个属性可以不用加了。
UI 组件的属性只应与其本身的特性有关,与业务意义无关——自身特性是自然特性,业务意义是附加特性。
这段引文一方面说了 UI 组件的本质是「返回视图结构的函数」,UI 组件的属性和事件都是这个「函数」的参数;另一方面强调 UI 组件或者说控件的属性和事件的设计只应与其本身的自然特性有关,并且要尽可能少——控件的内部与外部主要是靠属性和事件进行通信。
作为体系中最小粒度的视图单元,控件提供了单纯且纯净的结构(包括视觉结构和内容结构)、表现(主题风格)与交互的复用。
关于控件的研究我已经/将要在文章系列「聊聊前端 UI 组件」中进行,这里就不多做赘述了。
部件
在做业务开发时总会碰到这种情形——
几个表格页或者表单页看起来很相似,有很多重复代码,觉得可以封装成一个「业务组件」,于是就那么去做了;当看到重复代码减少了一大半,很有成就感,心里美滋滋~
但随着这类看似相近的页面越来越多,自己封装的那个「业务组件」的属性也跟着多了起来,并且那些属性中使用率高的也没几个,这时不禁开始怀疑自己:「我是不是被眼前的表象给忽悠了?!」
这种在业务开发中封装的比较强依赖于特定场景的「业务组件」,在我所阐述的组件体系中叫做「部件」。
过往的经验让自己意识到在相对大粒度的部件中像相对小粒度的控件一样主要靠属性和事件进行通信不够理想。那么,在部件中的主要通信手段该是什么呢?
还记不记得,有个「八股文」前端面试题大概是这样的:「请说说组件间如何进行通信?」
把「八股文」背得滚瓜烂熟的人会立马脱口而出:「父子间用属性和事件,兄弟间就以父组件为中介通过属性和事件;跨层级的话,在 Vue 中用 provide
/ inject
,在 React 中用 Context
;再就是 Vuex 之类全局的状态管理。」——如此用心的准备,值得面试官呱唧呱唧几下。
有的面试官可能还会进一步问:「跨层级通信的上下文和状态管理是什么?为什么会有它们?」听到这个问题,面试者心里一颤:「这……这有点超纲了吧?!我背的八股文里没有这个啊……」
在我的理解中,虽然细节上有点区别,但「上下文」和「状态管理」都是更广义一点的「上下文」——
在编程语言中,「上下文」一般是指让程序能够正常执行的一组环境变量,如执行上下文;而在应用开发中,通常衍生为用来维护作用于一定范围的状态的对象。
构造并传入或注入「上下文」是一种比较好的让 UI 组件变「瘦」的实践——
在 UI 组件树中从某一层往下的几层所包含的 UI 组件是一个相对独立的子系统,它们要协作完成同一个任务,与这个任务相关的状态和操作无需分散在各个 UI 组件中,经由「上下文」集中管理可让状态更好维护,状态变化更容易追踪。
在相对大粒度的部件中,如果主要依赖属性和事件进行通信的话,它们的数量很容易变得失控,并且内部的结构和逻辑也会被改得面目全非,维护起来十分困难和难受——这就变成了一坨翔💩!
所以,与 UI 组件本身的自然特性无关的东西不应作为其属性或事件存在,而是「上下文」。
先来拿人做类比,帮助理解下——
人有头、躯干、四肢、脑、五脏六腑等组成部分,经过脑活动可以进行交流、创造等——这些是人的自然特性。人的职业、角色、身份等是自然特性吗?当然不是!这些是人脑的运作机制在特定的环境、上下文中对接收到的信息处理后所形成的结果。
由此可见,人的自然特性是有限的,而由自然特性所衍生出来职业、角色、身份等则是无限的。鉴于此,倘若把 UI 组件的非自然特性设计为属性,其数量将多如牛毛。那些具有业务、配置语义的东西,理应作为环境、上下文被 UI 组件内部起到人脑作用的程序所「理解」,并做出相应的「反应」或「动作」。
另外,集成了像「数据上下文」(后续文章中会讲)这种业务应用开发框架所提供的机制的 UI 组件也叫「部件」。这类部件大、小粒度的都有,比如下文将要说到的「字段」,就很可能是小粒度的。
综上所述,「上下文」是业务的状态和逻辑复用的一种方式,是「部件」的主要通信手段;而「部件」则是连接了「上下文」与「控件」的「适配器」。
页面
一般意义上的「页面」就是指网页整体,但在这个组件体系中,它是具备网页整体布局功能,即包含了页头、页脚、侧边栏、主体区域等槽位的 UI 组件;在此之上,除了主体区域之外的其他槽位都已被填充的 UI 组件,也可被称作「页面」:
主体区域与其他部分可以看作是两个相互隔离的环境——主体区域是显示表格、表单、图表等以领域/业务为中心的内容;而其他部分则显示导航菜单、面包屑等以(一般意义层面的)页面为中心的内容。
一般来说,主体区域与其他部分之间不会有通信,若有,可以利用应用级的上下文。
对其他部分影响最大的是路由配置,因为无论是导航菜单、面包屑还是页面标题都能够基于它和 URL 计算出来;所以最好不要直接用 Vue Router 或 React Router 的方式去配置路由,而是在自定义的结构的基础上经过一定的处理之后生成它们所需要的结构。
数据视角
从数据的视角来看,那些承载着数据输入、输出相关职责的区块,称为「视图」和「字段」——列表和对象结构的数据对应的是「视图」,对象结构数据的属性/键是「字段」。
根据列表与对象、对象与属性/键、属性/键与值之间的内在关系,它们也形成了层级结构——
更多描述请看《聊聊前端 UI 组件:核心概念》的「数据及其形态」部分。
「视图」的具体呈现就是列表、表格、表单等,而「字段」则是输入框、下拉列表、文本框等。
理论上来说,与数据输入、输出无关的区块不是「视图」,但为了架构、工程等方面的统一,把不包含网页整体布局部分的大粒度区块(主要是指在页面或对话框主体区域中的)都叫做「视图」——这就产生了狭义与广义之分。
从数据角度看前端是后续文章中要讲的「数据上下文」的重要前提。
总结
一个人如果什么都干,那他很可能什么都会,但什么都不精。这对做做自己「能用就行」的「玩具」来说也许没什么问题,但在与他人协作去想要打造一个「精品」时,往往是不行的——每个人都需要分清职责,尽量分工没有重叠,并尽力做好自己负责的那摊子事儿。
我所阐述的组件体系也是这样,始终贯彻关注点分离和单一职责原则,细化分工并有能力有机结合成一个庞大复杂的前端应用——
从视觉视角将界面分解为若干具有层级结构的「区块」,按照职责、粒度等把它们大致分为「控件」、「部件」和「页面」。
其中,「控件」是业务无关的原子组件,由「风格组件」(基本对应所谓的「Design Tokens」)、「视觉组件」、「无头组件」和「结构组件」所构成(详见《聊聊前端 UI 组件:组件体系》),提供了结构、表现与交互的复用能力;「部件」是连接「上下文」与「控件」的「适配器」;「页面」则是包含了页头、页脚、侧边栏、主体区域等槽位的具备网页整体布局功能的 UI 组件。
以数据视角划分的「视图」和「字段」,催生出后续文章要讲的「数据上下文」,为业务的状态和逻辑复用提供了一种方式。
这个组件体系的目标之一就是「轻」UI——将非交互、展示类的业务逻辑从 UI 组件中剥离,令 UI 组件变薄变轻巧;让 UI 组件变得不被重视,减轻它的负担并降低它的地位。
理想情况下,最终会发现——除了业务逻辑,好像其余部分几乎都是接口(interface)——具体实现可以任意移除,随意替换!