我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。。
本文作者:修能
以下内容充满个人观点。◡ ヽ(`Д´)ノ ┻━┻
基于分布表单的需求,在中后台管理中是一个非常常见的需求,通常具有如下布局:
其中,自定义需求度从高到低为,正文 > 按钮区 > 步骤条。
虽然布局类似,但是实现的方式却是天差地别,这里就探究一下究竟怎么样实现可以兼具代码的可维护性和可读性呢?
我们这里,以「指标-数据模型」的代码为例。
首先先来看看数据模型这里的代码是如何实现的?
export default () => {
...
return (
<>
<header>
<Steps current={current}>
{['tab1', 'tab2', 'tab3', 'tab4', 'tab5'].map(
(title, index) => (
<Step key={index} title={title} />
)
)}
</Steps>
</header>
<Spin>
{stepRender(current, {
childRef,
modelDetail,
globalStep: globalStep.current,
mode,
isModelTypeDisabled,
setModelDetail,
setDisabled,
onModelNameChange: handleModelNameChange,
})}
<Modal>...</Modal>
</Spin>
<footer>
{current === EnumModifyStep.tab1 ? (
<Button
onClick={() => router.push('/url')}
>
取消
</Button>
) : null}
...
</footer>
</>
)
}
这是数据模型编辑页面 Steps
所在的容器组件的 DOM 部分的代码。
可以看出来,设计者的思路是比较明确的,通过 header,content,和 footer 进行分层, 增加代码的可读性。
在 header 中,通过声明 title 数组的方式创建 Steps 的方式简洁又不失可读性。
在 content 中,有几个问题的存在:
main
或者section
,当然同时还需要考虑会不会造成过深的层级。stepRender
函数的实现把一大堆 params
传到子组件是否合适。getPopupContainer
的 Modal 来说,其会通过 createPortal
在 body 上创建,那么在这里不论是写在 content 还是 header,都不会影响它的渲染,所以我推荐把 Modal 写到最角落里,不影响可读性。current === 步骤
的方式去定义按钮,我认为这种方式会使代码显得较为冗余。我们这里以指标相关代码为例,以简见深,以小见大
export default (props) => {
...
const { cref, modelDetail, mode, onModelNameChange } = props;
useImperativeHandle(cref, () => {
return {
validate: () => {...},
getValue: () => {...},
}
});
useEffect(() => {
setFieldsValue({
a: modelDetail.a,
b: modelDetail.b,
c: modelDetail.c,
});
}, [modelDetail]);
return (
<Form>
<Row gutter={40}>
<Col span={12}>
...
</Col>
<Col span={12}>
...
</Col>
</Row>
<Row gutter={40}>
<Col span={12}>
...
</Col>
</Row>
</Form>
)
}
这里我想指出的第一个问题是,ref 的使用,由于 ref 无法在 props 中传递,需要通过 forwardRef 才能拿到。然而这里通过 cref 这种比较 hack 的方式进行一个操作。我认为这是一个不推荐的做法,如果需要拿 ref 我建议是老老实实通过 forwardRef 拿。
其次是 Row 和 Col 的使用,并不是说 Col 达到 24 之后就需要再写一个 Row,你可以继续写的呀,童鞋!
这里需要提出来的一个论点是,每一个子组件里去写 Form 的方式好(即上面的这种写法),还是总体写一个 Form 的方式更好?个人认为前者存在的问题如下:
除此之外,由于基础信息比较简单,所以不存在 props 层层往下传递的问题,但是复杂组件就会存在层层往下传递的情况,那么就涉及到是否需要 context 的问题了。当然,我推荐是需要 context 的。
这里再看一眼第二步关联表的设计
interface ITab2Props {
cref: IModifyRef;
modelDetail?: Partial<IModelDetail>;
mode: any;
globalStep: number;
updateModelDetail: Function;
setDisabled?: Function;
}
const RelationTableSelect = (props: ITab2Props) => {}
首先,这里需要支持的一个设计思路是,通常情况下,切忌直接把 dispatch 传递给子组件。
关联表这里的设计由于层级嵌套很深,子组件非常多,导致updateModelDetail
不断往下传递,你完全不知道哪层组件在什么情况下会去修改这个值!!! 这对于 SSOT 来说,是毁灭性的打击。
再加上 modelDetail
是一个很复杂的数据,对于可维护性来说,属于是力中暴力地打击了。
综上,我们设计分布表单的时候,需要规避以上的问题,遵循如下原则:
首先实现如下组件:
<StepsForm
current={current}
onChange={setCurrent}
titles={['tab1', 'tab2', 'tab3', 'tab4', 'tab5']}
/>
这一块代码比较简单,无非就是投传几个值到对应的组件中去。
接下来考虑底部按钮的可扩展性。
通过 submitter
属性支持定制按钮的交互属性。
<StepsForm
current={current}
onChange={setCurrent}
submitter={[
{
[StepsForm.PREV]: {
children: '取消',
},
},
null,
{
[PREVIEW]: {
danger: true,
children: '预览',
},
},
]}
/>
接下来要解决按钮的事件,这里有两种方案,一种是将事件挂载在 Container 上(即这里的 StepsForm 组件),通过诸如 onCancel,onSubmit,onPrev
等方式进行反馈。
我认为这种方式不够好,原因有如下几点
而目前我考虑通过事件订阅对按钮事件触发,通过 useEffect 监听事件,但是这种方式的缺点如下:
除了以上两种方式以外,其实还有一种方式,即通过实现 Children 组件,将 Children 组件作为 StepsForm 的子组件,从而使得将每一步相关的 title 和 onSubmit 等方式都挂载在 Children 组件上。即 ant-design-pro 中的 StepsForm
的实现方式。我认为这种方式的优点在于直观,不割裂。缺点在于如下:
useEffect
获取数据其中第二点我认为是无法忍受的,这和开发组件的思路完全相悖,故摒弃这种方式
暂时考虑不清楚是第一种好还是第二种好。
这里先考虑实现第二种方式后组件书写的效果:
export function () {
...
StepsForm.useFooterEffect(
({ prev }) => {
prev(() => {});
},
[StepsForm.PREV],
);
StepsForm.useFooterEffect(() => {
message.info('预览')
}, [PREVIEW]);
StepsForm.useFooterEffect(
({ next }) => {
next(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000);
});
});
},
[StepsForm.NEXT],
);
return (
...
)
}
hook 的实现方式也比较简单,基于事件订阅,结合每一个按钮都赋予一个唯一值。
实现按钮交互触发后,通过事件分发,触发当前渲染的组件中的监听 hook。
本文意在探索分步表单的最佳实践,防止不同的同学在开发该类型的需求会写出五花八门的代码,从而导致降低可维护性。
本文提到的解决方案也不认为是最佳实践,其中不同的方法经过分析都存在优点和缺点。在实际的开发过程中,仍然需要根据具体的需求进行调整。
但是基于分步表单的特性和使用场景,总结出适用大部分情况下的方法论是有必要的。
欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈UED团队持续为广大开发者分享技术成果,相继参与开源了欢迎star
本文介绍基于Python语言中的smogn包,读取.csv格式的Excel表格文件,实现SMOGN算法,对机器学习、深度学习回归中,训练数据集不平衡的情况加以解决的具体方法~