React 中的抽象以及我们如何构建表单
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
一般抽象
抽象是概括上下文、组织和隐藏内部复杂性的过程。整个计算机科学都基于这一理念。如果你是一名前端开发人员,那么你编写的代码下已经存在多层抽象。抽象是一个非常强大的概念,如果运用得当,可以极大地加快开发速度。
抽象概念在我们生活中随处可见,不仅仅局限于软件开发领域。例如,汽车的自动变速器有两个挡位:倒挡(R)和前进挡(D)。这些挡位抽象化了使汽车前进或后退所需的操作,从而使用户能够专注于驾驶。例如,如果用户想要让汽车后退,他们只需要考虑两个操作:将挡位拨到倒挡(R)并踩下油门。
编程也是如此,我们不断地运用抽象。抽象从非常底层的层面开始,例如将电流的电荷转换成0和1,一直延伸到你正在开发的应用程序的理念层面。在更高的层面上,抽象可以体现为例如规范特定流程的函数,或者创建数据结构的类。
在 React 中,抽象是通过组合实现的。高层组件将标准化的底层组件组合在一起,构成用户界面的一部分。例如,一个按钮可以是反馈表单的一部分,而反馈表单又是联系页面的一部分。每一层组件都将相关的逻辑隐藏在内部,并将必要的外部组件暴露出来。
例如,如果我们有一个负责手风琴效果的组件,那么当我们想在屏幕上添加手风琴效果时,就可以复用这个组件,而无需重新编写代码。我们可能需要不同的设计或略微不同的功能,但只要屏幕上的手风琴效果仍然像手风琴一样,我们就可以复用其基本功能。
成功构建项目的关键在于为项目组件找到合适的抽象层。抽象层过多或过少都会导致代码冗余,并降低开发速度。抽象层过大意味着每个组件中都会重复使用一些较小的通用代码。同时,抽象层过小会导致组件的使用过度重复,而过多的代码层也会拖慢开发初期。
在应用程序的主要部分准备就绪之前,很难准确评估合适的抽象层级,而错误的抽象层级通常是后期重构的必要原因。在开发之前定义组件的职责有助于减少所需的重构量,因为它迫使开发者为决策提供合理的依据。此外,我建议创建比较少的抽象层级略多一些,因为层级之间的合并更容易、成本更低。
在我们的手风琴示例中,我们首先决定将展开和折叠功能以及颜色主题暴露在外,这意味着手风琴组件不再负责这些功能。这也意味着我们希望这两个属性在屏幕上起到显著的区分作用。分析并确定组件的职责有助于了解如何构建组件,使其能够与应用程序兼容。对我而言,这一点在我最近参与的一个项目中变得尤为明显。
案例:企业应用程序前端的表单
大约一年前,我们开始开发一个应用程序,旨在加速公司的一项流程。与所有这类商业应用程序一样,该软件会处理用户输入,填充必要数据,然后将其转化为产品。我将以这个项目为例,展示抽象化是如何运作的。我将重点介绍我们如何构建表单,因为表单是该软件的关键,最终也成为了我所做过的抽象化工作的最佳范例。
启动一个项目
让我们从起点入手,了解促成我们做出这项决定的因素。项目启动之初,流程的最终状态是未知的,这在敏捷开发中很常见。然而,这让我们在定义抽象概念时能够更好地应对诸多不确定性,从而在定义组件之前进行更加细致的分析。
在表单方面,基本需求是能够创建多个具有不同输入内容的表单。对我而言,这意味着我们应该让表单组件尽可能地适应各种场景,同时保持核心功能的标准化。
我们如何抽象形式
在开始构建抽象层之前,我们需要了解表单的用途。在我们的案例中,表单是用户创建新数据或修改现有数据流程的一部分。虽然大多数数据点彼此独立,但我们仍然希望确保能够处理表单字段之间或表单字段与服务器返回值之间的依赖关系。
字段的作用之一是限制给定的值集。数据类型是限制输入的一般依据。例如,当要求输入数字时,我们应该限制用户输入其他值。我们还应该能够通过限制输入或验证输入来将输入限制在特定的值列表中。
这个过程表明我们应该有两个抽象概念:表单和表单字段。此外,我们还注意到,如果我们想以不同的方式限制输入,则可能需要不同类型的字段。
形式
根据之前的流程描述,我们决定在本例中,表单将负责处理表单数据的状态和验证。此外,表单还应允许用户提供初始值并触发提交操作。表单本身无需关心初始值的来源或提交后的具体操作,这意味着这两个参数应该对外公开。
const Form = ({ initialValues, onSubmit, children }) => {
return children({ ... })
}
场地
对于字段,我们定义了需要对用户输入内容设置不同类型的限制。如果只有几个选项,那么将逻辑包含在抽象层内是合理的。但对我们来说,从一开始就很明显,我们会有很多不同类型的数据,因此应该将逻辑暴露出来。这不仅包括逻辑,还包括每个限制对应的用户界面部分。例如,如果我们希望用户只能从列表中选择,则应该为此创建一个用户界面(例如下拉菜单)。
所有字段元素都具有一些共同特征,例如输入框顶部或侧边的标签,以及输入框下方可能出现的错误或信息提示。由于我们预期这些特征会是所有表单字段的组成部分,因此决定将它们包含在抽象层中。
这两个决定最终产生了两种不同的抽象概念:一个字段负责输入的数据和上下文,另一个输入类型负责显示输入字段。每种不同的输入类型(例如 TextInput)都是它们的组件,这些组件都履行着相同的职责,但实现方式不同。
const Field = ({ name, label, inputComponent: Input, inputProps }) => {
const value = undefined /* Presents the value */
const onChange = undefined /* Changes the value */
return (
<React.Fragment>
{label}
<Input
name={name}
value={value}
onChange={onChange}
{...inputProps}
/>
</React.Fragment>
)
}
// Text input in here is an example
// The props would be the same for all inputTypes
const TextInput = ({ name, value, ...props}) => (...)
const App = () => (
<Form>
<Field
label='Test input'
name='TestElement'
inputComponent={TextInput}
/>
</Form>
)
执行抽象
在准备好抽象概念及其需求之后,我们意识到我们的方案具有通用性,因此应该已经有人解决了这个问题。使用现成的软件包可以简化我们的工作,因为我们无需从头开始构建所有内容。经过一番探索,我们最终决定在抽象概念中使用Formik 。
我想指出的是,我们并没有将 Formik 完全暴露给我们的应用程序,而只是在表单和字段级别上进行了暴露。Formik 只是实现了抽象层的功能,而不是为我们创建了这些功能。这使得我们将来如果需要不同的功能,可以选择替换该包,并且我们还可以扩展我们的抽象层,使其超出 Formik 提供的功能范围。这种做法的缺点是,我们需要编写额外的集成测试,以确保 Formik 能够与我们的组件协同工作。
创建输入类型
表单设置的最后一部分是输入类型。由于我们在字段级别已经暴露了输入,因此我们需要一个单独的组件来承担这项功能。
在创建这些输入类型的过程中,我们很快发现,除了数据类型(例如文本、数字、日期)之外,输入类型组件还取决于我们希望如何限制用户的选择。例如,文本输入框和单选按钮组虽然用途相同,但限制选择的方式却截然不同。最终,我们的应用程序中大约有 20 种不同的输入类型。之所以需要这么多组件,是因为我们希望将每种输入类型单独抽象出来。例如,文本输入框和数字输入框看起来几乎一样,但它们的行为却截然不同。对于开发人员来说,如果输入类型是不同的组件,也更容易区分它们。
由于输入组件由更小的组件构成,因此我们无需重复编写大量代码。我非常喜欢原子设计拆分组件的方式,因为它能够很好地描述抽象层,并有助于保持组件的可组合性。
对于输入,我们创建了两个抽象层:
- 原子——单一功能组件,例如输入字段的设计、工具提示弹出窗口的功能。
- 分子——由原子组成,用于构建更高级别的项目,例如我们例子中的输入类型组件。
例如,在我们的案例中,由于输入组件非常通用,因此一半的输入组件都重复使用了该组件。日期选择器可能是我们案例中可组合原子组件的最佳示例。
日期选择器示例
起初,我们使用浏览器自带的方式处理日期,但由于我们希望所有浏览器中的日期字段显示效果一致,因此决定自行实现。在研究了各种可用的包之后,我们最终选择了优秀的@datepicker-react/hooks hooks,并在此基础上构建了我们自己的设计。
由于我们已经开发了很多原子组件,所以我们只需要创建日历设计,从开始到结束(包括测试)大约花了1.5天时间。在我看来,这充分展现了精心选择的抽象层的强大功能,它们有助于将小型组件概括为可组合的原子组件。
结论
通用的抽象和可组合组件能够加快开发速度,因为每个新功能都会生成可重用的组件。当我们开始开发日期选择器时,这一点就显而易见了。除了日历本身,我们已经有了所有其他组件。
明确抽象组件的职责有助于选择组件内部的显式逻辑和隐藏逻辑。这使得团队内部的讨论更具建设性,因为我们最终讨论的是架构而非实现细节。例如,我们一开始就明确规定将输入组件暴露在字段组件之外。这样做的最主要原因是,我们最终可能会用到大量不同类型的字段,而我们不想限制字段内部的使用。
通过一些规则来构建抽象层结构,有助于明确各抽象层之间的职责和联系。我们以原子设计为基础制定了这些规则。它定义了五个抽象层,并赋予它们高层级的职责。这有助于在初期创建具有相同抽象级别的组件。
感谢阅读。如果您也有过类似的经历,或者有任何评论或疑问,欢迎与我分享。
文章来源:https://dev.to/akirautio/abstractions-in-react-and-how-we-build-forms-50d8





