JS前端组件设计以业务为导向实践思考
作者:Kiera 发布时间:2024-06-21 04:13:33
引言
本文重点探讨前端组件设计。我相信好的组件设计不仅需要考虑技术实现,同时也需要考虑用户体验、可扩展性和易用性等多个方面。因此,我会重点强调这些方面的实现方法,从而更好地为产品的实际需求服务。我不打算在这里重复一些大家都耳熟能详的前端组件设计的原则和技巧。设计永远是为需求服务的。
下面,让我们进入正题。
一个组件设计的例子
考虑一个业务场景。一个表单组件,用来采集用户的基本信息,每个甲方爸爸都有自己的定制需求。比如样式、比如要展示的控件种类。分析表单控件类型,假设分别可能有文本输入框、单选框、多选框、下拉框等等。
我们很容易就能想到用动态表单来实现,支持自定义控件类型,支持自定义表单元素的样式和行为。api设计为了易于扩展,泛型显然更具可重用性。
一个基本的表单类型像这样:
type FieldProps<T> = {
label: string;
name: string;
value: T;
onChange: (name: string, value: T) => void;
};
创建一个接口,表示 Field 组件可以渲染的不同类型的表单元素:
interface FieldRenderer<T> {
(props: FieldProps<T>): JSX.Element;
}
接着,我们可以创建一个泛型的 Field 组件,根据传入的泛型类型 T,来确定 Field 组件要渲染的表单元素类型以及 props 的类型。
function Field<T>({ label, name, value, onChange, render }: FieldProps<T> & { render: FieldRenderer<T> }) {
return (
<div className="field">
<label htmlFor={name}>{label}</label>
{render({ label, name, value, onChange })}
</div>
);
}
创建一些不同的 Field 组件渲染器,比如 TextInput、SelectInput、CheckboxInput。
const TextInput: FieldRenderer<string> = ({ label, name, value, onChange }) => (
<input type="text" id={name} name={name} value={value} onChange={(e) => onChange(name, e.target.value)} />
);
const SelectInput: FieldRenderer<string> = ({ label, name, value, options, onChange }) => (
<select id={name} name={name} value={value} onChange={(e) => onChange(name, e.target.value)}>
{options.map(option=><option value={option.value}>{option.label}</option>)}
</select>
);
现在,我们可以在使用 Field 组件的时候,传入不同的泛型类型,来渲染不同的表单元素了。
const DynamicForm = ({ fields, onSubmit }: { fields: FormField[], onSubmit: ()=>void }) => {
//这里做一些映射之类的。。
return (
<form onSubmit={onSubmit}>
{fields.map(field=><Field {...field} render={field.customComponent} />)}
</form>
)
}
以上,我们基本完成一个高度抽象化的组件设计。
现在,让我们的目光从这个细节上挪开,切换到一个宏观的视角上,重新审视项目整体架构的组件设计。一个前端架构通常会有其系统规范,比如统一的命名规范、代码风格,合理的文件组织结构,前端开发的基础设施,性能优化方案,依赖管理等。那么,我们如何在这个规范的框架下设计组件呢?
模块化设计原则
从整体角度规划方案,我们可以对项目进行分层和模块化的设计,实现不同模块之间的解耦合。
分层设计是指将整个系统分成分层模块,每一层模块都有自己的职责和功能。前端的分层设计主要涉及以下几层:展示层、控制层、逻辑层、服务层。模块化设计是指将整个系统分成小的模块,每个模块都有自己的功能,不同模块之间通过明确的接口进行通信和数据交换。在前端项目中,往往可以将不同的功能分配到不同的模块中,甚至可以将某些通用的功能写成独立的模块进行引入。
在实际业务中,我们通常需要将分层设计与模块化设计相结合。在不同的层次上实现代码结构的划分和内部逻辑的编写,不同的功能分配到不同的模块,通用的功能写成独立的模块引入,将代码封装成可复用的单元。
业务组件的设计因素
业务组件是在实现业务过程中抽象出来的组件,作用是在应用中复用业务逻辑。我们应该进行怎样的抽象?简单的功能如果抽象成组件,是否是一种过度设计?我们尝试从以下几个角度来思考这些问题。
1. 状态与接口
在设计接口时,我们都知道,接口应该简单清晰、易于扩展。比如一个loading flag, 我们通常会用布尔值来切换loading状态,分别展示不同的UI界面。
比如一个发送验证码的按钮,我们可能用isEnd
就能满足展示不同按钮文字的需求。但如果,我们分别需要在点击按钮前、点击后的倒计时阶段、倒计时进入到指定的时刻、倒计时结束后执行不同的逻辑。那么,我们就需要考虑将这个接口设计成字符串,以便于扩展。
同理,一些使用loading布尔值的场景,是否可以考虑设计为字符串,以便满足更加个性化的需求?站在用户的角度,你是否已经厌倦了在等待一份大体积的数据时看着一个动画圈圈在转动?
现在,我们来考虑组件的使用场景。
这个组件是否与业务逻辑绑定?比如一个登录功能,可以是弹窗、也可以是单独的页面,它往往带有以下功能:用户名和密码的前端校验规则、对后端响应的处理、完成登录后的逻辑处理。像这样的组件,就不需要抽象,因为它难以通过修改参数就直接在其它系统中使用。
这个组件的功能有可能被重用吗?如果是,我们如何做预先的接口设计?比如一个表格组件,通常包含以下状态:要展示的数据、对数据的排序规则、数据过滤规则、用户选择器。我们初期可能据此做了4个接口:数据、排序、过滤、选择器。随着业务的扩展,我们可以预见后期的数据量开始加大,我们可能考虑增加一个分页接口。但是分页接口是否真的需要被集成在这个表格组件中?这是一个开放性的问题,相信不同的CRUD专家会有不同的解决方案。
2. 调用方式
组件的调用方式有多种,比如在模版文件中引入组件标签直接调用,或通过函数调用(常见的message/loading类组件),或者通过接口调用(Ant Design的DatePicker)等。并没有一套通用的标准来指定某种类型的组件的调用方式,总体还是取决于项目的需求和场景。
3. 测试
假设一个组件需要访问api,这很常见。我们应该如何设计以便于在测试组件时隔离组件的功能?
import APIService from './APIService';
function MyComponent({ apiService }) {
const fetchData = () => {
const data = apiService.get('/data');
// 处理数据并返回结果
}
return <>{/* 渲染组件的内容 */}</>
}
// 渲染组件时,可以将 APIService 实例作为 props 传递
const apiService = new APIService();
ReactDOM.render(<MyComponent apiService={apiService} />, document.getElementById('root'));
在组件内部,我们定义了一个 fetchData
函数,它可以在需要的时候调用 apiService.get()
来获取数据。此时,我们可以轻松地模拟 apiService
,以进行单元测试,而不会对 MyComponent
的实现产生任何影响。
大多数时候,一个单一职责的组件的功能测试,是要比复合组件更容易的。使用标准和通用的API和数据格式,并将组件的功能和状态限制在组件内部,确保组件可以独立地进行测试。此外,我们还要考虑边界测试,比如组件接收到无效的或非预期的参数该如何处理?
当然,组件不是颗粒度越细越好。是否遵循单一职责原则,不应该以功能点的数量,而是以功能和目标来衡量。
4. 文档
编写一份前端组件设计文档,明确记录项目的设计标准和项目迭代过程中的变更,创建标准的组件库以便于多人协作时的组件复用。尤其在一个多人协作的项目里,文档能够提供统一的开发规范,使得不同的开发人员在编写不同的代码时能保持一致的开发习惯。
分析业务逻辑
我们已经有了设计的原则和需要考虑的因素,下面我们开始分析业务逻辑。
通常我们会有原型图展示各个业务操作的流程和业务环节的逻辑关系,在设计组件时我们需要考虑如何支持这些流程和操作。比如根据业务逻辑,将整个项目分解为多个独立的组件,划分组件的接口和参数,指定其数据类型等。
此外,我们还要分析各种数据的处理和存储方式。这里我们只讨论前端的管理方式。比如一个给定的系统,我们通常会有用户数据、产品数据、订单数据等。我们可能会考虑使用状态管理工具、Context API、组件间的通信、本地存储等方式来管理这些数据。不同的场景需要采用不同的数据管理方法。
最后,我们要分析用户交互的影响,考虑控制用户的交互范围。比如表单校验、提高反馈信息和错误处理。
来源:https://juejin.cn/post/7209110611858849851
猜你喜欢
- 在函数参数中乱用表达式作为默认值Python允许给一个函数的某个参数设置默认值以使该参数成为一个可选参数。尽管这是这门语言很棒的一个功能,但
- 一、项目视图分析通过上图,我们可以看到,一个完整的项目,基本包括三个部分:用户视图层、接口层、数据处理层,其中,用户视图层是用来接收用户的数
- 1.安装pip我的个人桌面系统用的linuxmint,系统默认没有安装pip,考虑到后面安装requests模块使用pip,所以我这里第一步
- 背景工作中,当我们需要对字符串按照某个字符串切分成字符串数组数时,常用到strings.Split()最近在使用过程中踩到了个坑,后对踩坑原
- 在学习pygame模块过程中,我们可以通过使用 pygame模块实现很多功能性的东西,但是很多人应该没有利用pygame实现过雪花飘落的效果
- 除了3天就会失效的临时素材外,开发者有时需要永久保存一些素材,届时就可以通过本接口新增永久素材。最近更新,永久图片素材新增后,将带有URL返
- 一个继承nn.module的model它包含一个叫做children()的函数,这个函数可以用来提取出model每一层的网络结构,在此基础上
- 1.场景问题说明mysql中一般的update写法支持的方式是,update 表 set 字段名=修改后的字段值 where 条件1 and
- 项目地址:https://github.com/chen0495/pythonCrawlerForJSU环境python 3.5即以上req
- 1. vue打包后font-awesome字体失效解决方案:2. 打包后图片资源失效解决方案:针对html中引用的图片针对css添加的背景中
- 今天发现有一个程序插入的时间不对,而该字段是配置的默认值 CURRENT_TIMESTAMP,初步判断是数据库的时区设置问题。查看时区登录数
- 前言一般从数据库或者是从日志文件读出的数据均带有时间序列,做时序数据处理或者实时分析都需要对其时间序列进行归类归档。而Pandas是处理这些
- 本文实例为大家分享了Python KNN分类算法的具体代码,供大家参考,具体内容如下KNN分类算法应该算得上是机器学习中最简单的分类算法了,
- Python 数据库编程,ODBC方式实现通讯录,供大家参考,具体内容如下#-*-coding:utf-8-*-import pyodbci
- 加载1个或多个要素<template> <div id="map" style="
- 1、PyInstaller简介PyInstaller是一个跨平台的Python应用打包工具,支持 Windows/Linux/MacOS三大
- 原文地址:http://ilovetypography.com/2007/10/22/so-you-want-to-create-a-fon
- 之前在使用Pandas处理csv文件时,发现如果文件名为中文,则会报错:OSError: Initializing from file fa
- 本文实例讲述了Python2.7简单连接与操作MySQL的方法。分享给大家供大家参考,具体如下:Python号称简单优雅,其实新手摆弄一些东
- 一、MySQL数据库的实例管理器概述:1、MySQL数据库的实例管理器(IM)是通过TCP/IP端口运行的后台程序,用来监视和管理MySQL