找回密码
 会员注册
查看: 26|回复: 0

需求迭代中的组件设计

[复制链接]

3

主题

0

回帖

10

积分

新手上路

积分
10
发表于 2024-10-10 23:47:06 | 显示全部楼层 |阅读模式
需求迭代中的组件设计 hhhhhh KooFE前端团队 KooFE前端团队 关注前端最新动态 89篇内容 2024年09月09日 06:40 北京 我们对于组件的设计并不是一蹴而就的,通常是随着需求在不断地迭代。我们希望,组件能够更好地扩展和复用,纠结于未来可能潜在的场景。本文主要是通过需求的不断迭代,来讨论组件的设计和实现。一个简单的表单首先我们来实现一个表单,只有 name 一个表单项,为了简化实现这里省略了表单校验等逻辑。用户填写表单后,点击 Submit 按钮将数据提交给后端。我们使用 antd 组件库来实现这个表单,代码如下:jsximport { Form, Button, Input } from 'antd';const BasicForm = () => { const onFinish = (values) => { submitToServer(values) }; return ( Submit )}增加随机生成的 code 字段随着业务的迭代,新的需求来了,表单要新加一个编码字段:这个字段被命名为 code,由前端来生成与 name 一起提交给后端,它的格式要求是custom\_${随机 6 位字符串}。接下来,需要思考一下如何实现它。在已有的表单基础上,我们能想到两种实现方式:第一种是对数据做处理,也就是在提交请求的数据上加 code 字段;第二种是在表单上添加字段,方法是将 code 字段作为一个隐藏的表单项。方案1: 在提交时添加 code 字段这种方式的具体实现是,在表单提交的时候,将 code 字段加到了要提交的数据中。代码如下:diffconst BasicForm = () => { const onFinish = (values) => {- submitToServer(values)+ const data = {+ ...values,+ code: `custom_${uid().slice(0, 6)}`+ };+ submitToServer(data) }; return ( // ... ... )}方案2: 在表单中添加 code 字段表单添加的 code 字段要被隐藏了,因为它是自动生成的并不需要用户填写。代码如下:diffconst BasicForm = () => { // ... ... return ( // ... ...+ + + // ... ... )}这两种实现方案的实现思路差别很大:前者相当于在表单提交时做了一次数据加工处理;后者在表单中统一处理数据。它们在成本方面差别不大,这时我们很难评判哪个方案更优秀,可以说都是当前可接受的方案。可编辑的 code 字段业务还在发展,那么在接下来的需求中,需要支持用户编辑 code 字段,但是只能编辑 custom_ 后面的部分。这时我们会发现,如果之前的表单是采用方案一的话,需要将其变更为方案二的方式来实现。接下来再来讨论一下,如何将 custom_ 和用户输入的值拼接在一起。这里也有两种实现方案,而且与上面的两种方案比较类似,一种是在提交表单时处理数据拼接,一种是在表单中完成拼接。方案 1: 在提交时处理 code 字段在这个方案里,表单中的 code 字段保存的是用户输入的值,在数据提交给后端之前要做好相应处理,将 custom_ 拼接到一起:diffconst BasicForm = () => { const onFinish = (values) => { const data = { ...values,- code: `custom_${uid().slice(0, 6)}`+ code: `custom_${values.code}` }; submitToServer(data) }; return ( // ... ... - + // ... ... )}方案 2: 在表单中处理 code 字段由于整个表单使用的是 antd 组件实现的,为了能够处理用户输入的数据,需要抽象出一个自定义的 Input 组件,我们将这个组件命名为 InputWithPrefixCustom,在它的内部有一个 Input 组件,通过触发 onChange 事件将用户输入的值和 custom_ 前缀拼接起来:jsxconst InputWithPrefixCustom = (props) => { const { onChange, value } = props; const prefix = 'custom'; const handleChange = (event) => { const value = event.target.value; onChange & onChange(`${prefix}_${value}`); }; const reg = new RegExp(`^${prefix}_`, 'g'); const valueWithoutPrefix = value.replace(reg, ''); return ( );};在表单中使用刚刚实现的 InputWithPrefixCustom 组件:diffconst BasicForm = () => { const onFinish = (values) => { submitToServer(values) }; return ( // ... ... - + // ... ... )}对比两种实现方案,从成本上来看,方案一相对比较简单,易于实现;方案二涉及到了组件的抽象和设计,实现起来会更复杂一些。这两种方案孰优孰劣,还是很难评判。支持复制 code 字段一段时间之后,需求又一次迭代,要在 code 表单项的后面加一个图标,点击图标要能复制当前 code 的值。更通用的组件设计考虑到将来的扩展与复用,在这里做了更深层次的抽象:实现 Text + Input + Icon 这种布局的组件,也就是在 Input 的基础上添加了文字前缀和图标后缀。在这个组件中,文字前缀与 Input 的 value 值拼接在一起构成了最终的 value,当 Input 中的 value 值发生变化时,通过 onChange 可以获取到整个 value 的变化。图标后缀可以支持开发者使用任意的图标,以及根据需要来定义图标的 onClick 事件,并不限于对 value 进行拷贝操作。不难发现这个组件更加基础,我们把它命名为 InputWithPrefixTextAndSuffixIcon:jsxconst InputWithPrefixTextAndSuffixIcon = (props) => { const { prefixText, value, suffixIcon, onChange } = props; const handleChange = (event) => { const value = event.target.value; onChange & onChange(`${prefixText}_${value}`); }; const reg = new RegExp(`^${prefixText}_`, "g"); const valueWithoutPrefix = value.replace(reg, ""); return ( {suffixIcon} );};在上面实现的 InputWithPrefixTextAndSuffixIcon 组件中,给到 Input 的 value 值要删除掉 prefixText,而在 Input 的 onChange 中要拼接上 prefixText。suffixIcon 则是一个组件元素,比如要实现点击图标进行拷贝,可以将赋值给 suffixIcon。下面是具体的表单实现:diffconst BasicForm = () => {+ const [form] = useForm(); const onFinish = (values) => { submitToServer(values) };+ const handleCopy = () => {+ const code = form.getFieldValue("code");+ navigator.clipboard.writeText(code);+ }; const prefixText = 'custom'; return ( // ... ... + + }+ /> // ... ... )}尽管 InputWithPrefixTextAndSuffixIcon 看起来比较通用,但是回归到具体需求,我们要实现的是表单字段的复制,如果我们还有其他的位置的表单也需要实现完全一致的功能,那么 handleCopy 需要重新写一遍,而且它依赖于 Form 表单实例,如果 antd 的版本低于 v5 版本的话,通过 Form 表单实例来取数据并不是那么优雅。另外,我们也可以继续将 prefixText 的拼接放在 onFinish 和 handleCopy 中去实现,这需要维护两处拼接,对于代码的维护并不友好,可能会出现误改或漏改的情况。因此,在这里不再考虑和介绍这种方案。正如上面提到的那样,我们把 InputWithPrefixTextAndSuffixIcon 当作一个更底层的组件来实现的,我们期望它能够更通用,能够在更多的场景中进行复用,这就导致了在具体的业务场景中需要补充一些代码逻辑,为了解决这个问题需要在它的基础上封装一个业务组件 InputWithPrefixTextAndSuffixCopyIcon,专门来处理复制功能:jsxconst InputWithPrefixTextAndSuffixCopyIcon = (props) => { const { value, prefixText, onChange } = props; const handleCopy = () => { navigator.clipboard.writeText(value); }; return ( } /> );};在表单中,我们直接使用 InputWithPrefixTextAndSuffixCopyIcon 即可:diffconst BasicForm = () => { const onFinish = (values) => { submitToServer(values) }; const prefixText = 'custom'; return ( // ... ... + // ... ... )}不难发现,这是一个自底向上的设计过程,首先设计和实现一些基本的、通用的组件,然后逐步组合这些组件组合起来实现更高层次的功能。但是在组合的过程中,我们不得不做下面的思考:如果是出于可复用性实现了基础组件 InputWithPrefixTextAndSuffixIcon,那么它将来被复用的可能性有多大?或者换个角度,即便有可复用的场景,那么它一定会以我们抽象出来的形式出现嘛?很显然,未来充满了诸多不确定性,将来也可能不会有被复用的场景。那倒不如等到未来必要的时候,再将其作为基础组件来实现。从开发和维护的视角来看,InputWithPrefixTextAndSuffixIcon 为我们带来了什么?首先是更深的组件层级,这意味着一些 props 的传递得更深;其次更多的组合带来的是更高的复杂性,因此需要更多的理解成本。基于这些问题,我们在组件的设计上要尽量满足当下最优,且不要为未来的成本付费。按需实现的组件设计接下来,我们不再考虑考虑通用性的问题,只是封装一个为需求服务的组件,这个组件命名为 InputWithPrefixTextAndSuffixCopy:jsxconst InputWithPrefixTextAndSuffixCopy = (props) => { const { prefixText, value, onChange } = props; const handleChange = (event) => { const value = event.target.value; onChange & onChange(`${prefixText}_${value}`); }; const handleCopy = () => { navigator.clipboard.writeText(value); }; const reg = new RegExp(`^${prefixText}_`, "g"); const valueWithoutPrefix = value.replace(reg, ""); return ( );}在表单中使用该组件:diffconst BasicForm = () => { const onFinish = (values) => { submitToServer(values) }; const prefixText = 'custom'; return ( // ... ... + // ... ... )}总结不难发现,示例中的表单实现方案是随着需求动态变化的,我们在最初的需求中是很难预测将来会迭代成什么样子。因此,我们要尽量避免在方案中对未来做出假设,除非有十足的把握将来一定会是那样。每一次的方案都不是唯一的,我认为最好的方案应该是当前的最优解。如果不是最优那么应该做一些重构工作,通过重构让实现方案更加合理,维护起来更加高效。没有永恒的设计,只有不断地重构。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-27 14:22 , Processed in 0.614972 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表