这天突然收到了 UI
修改设计稿的消息通知:"xxx 已修改 xxx 项目并 @ 了你,请及时查看变更内容
",一条、两条、三条 ......,修改消息铺天盖地而来,然后就什么都看不到了()!因为我选择开启消息免打扰
,但没多久产品就非常贴心的询问是否已经收到了对应的消息,自然免不了还要介绍一下本次修改的内容和原因(
)。消息免打扰失效
于是,我()打开了设计稿正打算好好欣赏欣赏,不曾想一道光芒一闪而过(心甘情愿的
),看到了这样的内容:是谁拉开了窗帘
而这个显示方式之前使用的是 Steps 步骤条 的方式展示的,类似于:
意味着要从一个 Steps 步骤条 改变成一个 progress-step 流程节点 的形式,这很难实现吗?不难!很容易实现吗?倒也未必!
【扩展】假设现在有个面试官就用这个 progress-step
组件作为场景题,想想你该如何描述对这个组件的设计思路!!!
千万先别急着撸起袖子开干,咱们先来聊聊组件设计的基本原则,往往一把梭哈的代码容易形成一个糅杂着各种逻辑的组件,因为这样的设计是脆弱的,很容易带来副作用和难以预知的结果,需求不变更还好,需求要是频繁变更,那么主要问题就会出现,可能因为一个原因去改变组件,就会破坏其他的职责(行为),于是你不得不在多处都进行修改。
怎么知道一个组件复不复杂,这就要看组件内部到底维护了多少和自身状态相关的数据,即非 props
传入的数据,对于现在的 Vue / React
而言都是可以通过 数据来驱动视图 的,用一个函数式表示即 UI = render(data)
或 UI = f(data)
,这意味着一个组件至少需要做到 数据(data) 和 视图(UI) 的解耦。
单一职责原则(SRP - single responsibility principle) 中的 职责 是什么,你可以理解为是组件的 行为,这个行为可以是渲染一个列表、展示一张图片、发起一个请求等等,当组件的这个行为发生修改,意味着这个组件本身发生了变化,此时该组件就是 单一职责 的。
大部分人经常会选择忽视一个组件的多个职责带来的缺陷,这里举个常见查询页面的例子:
一部分是和查询条件相关的表单部分
一部分是展示查询结果的表格部分
假设此时按照 撸起袖子一把梭 的原则去编写这个组件(页面),一定会将表单交互、表格交互、发起请求、处理响应等逻辑混在一个组件(页面)当中:
假设现在接口响应的数据结构发生了变化,在当前的组件(页面)中,你可能需要重新修改处理判断不同响应状态码下页面的表现,同时还得修改表格的展示方式,又或者需要根据响应的数据内容自动填充表单,但由于数据结构的变化还得去调整表单相关的部分,此时仅仅一个数据结构改变的原因就可能影响了三个部分的内容,因为它们耦合了。
多职责的缺陷:
经过不断的迭代会使得单个组件代码量不断增大
关于这一点,你完全可以从你平时在项目中使用到的 UI
组件库中去看看,比如 Element UI
中表单相关的组件就分为:
基于此还是将上述的例子改变成单一职责:
这样一来,即便后续响应的数据结构发生改变,也不需要再去修改表单和表格部分的内容,而是在当前的查询页面(组件)中处理好对应的数据格式即可,比如将接收到的数据处理成符合表格或表格组件需要的格式即可。
单一职责的优点:
在不断迭代中,只需要修改对应组件的内容,不会导致页面中的其他组件代码量增大
组件的核心是需要体现在业务中的,在项目中的大多数组件都属于业务组件,不具备通用性,因为具有通用性的组件不应该只满足于某个业务,因此组件的设计要考虑从业务中抽离。
数据(data) 和 视图(UI) 的解耦
保证组件的 单一职责(行为)
组件只提供 最基础的 DOM 和 交互逻辑
Element UI
中的 Table
组件就提供了 render-header(Table-column Attributes)、header(Table-column Scoped Slot)、append(Table Slot)
用于自定义渲染组件封装应该 隐藏内部细节和实现意义,通过 props
控制具体行为和输出
组件封装保证 可测试性
接下来,就该看看到底该如何分析和设计这个 progress-step 组件了,上面我们提到了最基本的要做到 数据(data) 和 视图(UI) 的解耦,那实际上也就意味着要从这两个大的方向去考虑。
拿到视图肯定要进行分析,UI 图展示的不一定全面,因此必须要确定所有可能的展示方式,在和产品沟通的过程中确认了这个视图的三种展示形态:
串行展示,类似于:
并行展示,类似于:
混合展示,类似于:
但你仔细查看 UI 稿的设计,其实完全可以将它归类为第二种展示方式,否则你还得为这个 UI 图单独实现另一种展示方式,并且在和产品的沟通中也得到了同意。
首先,肯定得去看看社区中是否有类似的方案可以直接使用,或者经过小的改动可以被使用的,奈何业务就是业务,果然没找到合适的,但在查找方案的时候也了解到了几种实现方案:
最后两种方案,我看到的大多是需要支持各种可比较复杂的拖拽、复制、连接的方式,并且其具体实现也是比较复杂,如果其其不是很熟悉的话,无论在实现还是在后续的各种调整上会花费大量时间,而且如上的一个需求无非是一些展示和简单的交互,并不需要涉及如此复杂的各种自定义操作。
再举个例子,比如前面提到的 Steps 步骤条 也是基于普通 DOM 元素实现的,并且也确实没有太多需要用户自定义的操作。因此,可以将 progress-step 组件当做 Steps 步骤条 的升级版,另外考虑开发时间的限制,选择方案一是最合适的。
上述展示方式虽然有三种,但实现时可以先实现最简单的 串行展示 方式, 而 并行展示 其实相当于三条串行展示的合并,分别是上边、中间、下边的串行方式,最后的 混合展示 其实只要你实现了前面两种方式,这种方式无非是相当于组件的递归渲染,只不过位置上需要做一些处理:
串行展示
::before、::after
并通过定位实现并行展示
混合展示
作为前端肯定要具备看到 UI 就能大致设想出其对应 data 的基本结构,而且上述经过视图分析之后,已经得到其对应的三种具体展示形式,在真正开始写代码前,请先把需要封装的组件涉及的核心数据结构给设计好,毕竟这个数据最终是需要从外部传入的:
串行展示
单个节点即对应数组的每一项元素信息
data = [
{status: 'completed' , name: '完成' },
{status: 'processing' , name: '当前处理节点' },
{status: 'pending' , name: '待处理' }
]
并行展示
可以看做是上下两个串行展示的合并,可将数据带有并行的节点也使用数组来表示
data = [
{status: 'completed' , name: '开始' },
[ // 1. 这个数组表示是并行节点
[ // 1.1 这个数组表示是并行节点中,上边 串行节点的数据
{status: 'completed' , name: '完成' },
{status: 'processing' , name: '当前处理节点' },
{status: 'pending' , name: '待处理' }
],
[ // 1.2 这个数组表示是并行节点中,下边 串行节点的数据
{status: 'completed' , name: '完成' },
{status: 'processing' , name: '当前处理节点' },
{status: 'pending' , name: '待处理' }
]
],
{status: 'pending' , name: '结束' },
]
混合展示
相当于并行节点中又包含并行节点,类似于:
data = [
{status: 'completed' , name: '开始' },
[ // 1. 这个数组表示是并行节点
[ // 1.1 这个数组表示是并行节点中,上边 串行节点的数据
{status: 'completed' , name: '完成' },
[// 1.1 这个数组表示是并行节点中的并行节点
[// 1.1.1 这个数组表示是并行节点中,上边 串行节点的数据
{status: 'processing' , name: '当前处理节点' }
],
[// 1.1.2 这个数组表示是并行节点中,下边 串行节点的数据
{status: 'processing' , name: '当前处理节点' }
],
],
{status: 'pending' , name: '待处理' }
],
[ // 1.2 这个数组表示是并行节点中,下边 串行节点的数据
{status: 'completed' , name: '完成' },
{status: 'processing' , name: '当前处理节点' },
{status: 'pending' , name: '待处理' }
]
],
{status: 'pending' , name: '结束' },
]
其大部分的实现思路已经在上面介绍过了,这里就不再额外介绍一些样式计算相关的内容,下面直接展示效果和源码。
space:节点之间的间距
const props = withDefaults(defineProps<PropsType>(), {
data: () => [],
colors: () => ["#d2d2d2", "#3a84fb", "#67d36f"],
status: () => ["pending", "processing", "completed"],
size: 25,
stepWidth: 80,
space: 20,
});
const data = [
{ status: "completed", title: "开始", description: "这是描述" },
{ status: "processing", title: "处理中", description: "这是描述" },
{ status: "pending", title: "待处理", description: "这是描述" },
{ status: "pending", title: "结束", description: "这是描述" },
]
const data = [
{ status: "completed", title: "开始", description: "这是描述" },
[
[
{ status: "completed", title: "已完成", description: "这是描述" },
{ status: "completed", title: "已完成", description: "这是描述" },
{ status: "pending", title: "待处理", description: "这是描述" },
],
[
{ status: "completed", title: "已完成", description: "这是描述" },
{ status: "pending", title: "待处理", description: "这是描述" },
],
],
{ status: "pending", title: "结束", description: "这是描述" },
]
const data = [
{ status: "completed", title: "开始", description: "这是描述" },
[
[
{ status: "completed", title: "已完成", description: "这是描述" },
{ status: "completed", title: "已完成", description: "这是描述" },
[
[
{ status: "completed", title: "已完成", description: "这是描述" },
{ status: "pending", title: "待处理", description: "这是描述" },
],
[
{ status: "completed", title: "已完成", description: "这是描述" },
{ status: "processing", title: "处理中", description: "这是描述" },
]
],
{ status: "pending", title: "待处理", description: "这是描述" },
],
[
{ status: "completed", title: "已完成", description: "这是描述" },
{ status: "completed", title: "已完成", description: "这是描述" },
[
[
{ status: "completed", title: "已完成", description: "这是描述" },
{ status: "pending", title: "待处理", description: "这是描述" },
],
[
{ status: "completed", title: "已完成", description: "这是描述" },
{ status: "processing", title: "处理中", description: "这是描述" },
]
],
{ status: "pending", title: "待处理", description: "这是描述" },
],
],
{ status: "pending", title: "结束", description: "这是描述" },
]
现存缺点比较明显:
需要查看代码的可通过此处查阅:源代码
在实际实现过程中涉及到动态计算的部分很容易理不清,包括动态计算矩形框高度、宽度以及节点位置等,以上仅仅算是一个实现思路(不要害怕写出不完美的代码
),期望各位大佬能够在评论区给出更优质的方案!!!