字段联动
- github: github.com/easy-page/e…
- 文档站官网:easy-page.github.io/easy-page-d…
- 文档站 github: github.com/easy-page/e…
在前面的章节中,我们或多或少提到了一些联动,我们这里,就来详细的介绍一下。 在表单开发中,常见的有如下几种联动场景:
- 当一个字段变化了,改变另外一个字段组件的
Props。 - 当一个字段变化了,改变另外一个字段的值。
在正常的开发中,我们往往会如下做:
- 额外存储字段的值或配置,再基于
useEffect进行监听变化,执行相关的副作用。
在使用 useEffect 的时候,有两个概念:
- 依赖项:如下方第二个参数,表明受谁的变化。
- 副作用函数,如下方第一个参数,表明收到变化后需要执行的副作用处理。
useEffect(() => {...}, [name])
但在上述使用过程中,我们会遇到一些繁琐的点:
- 如果你想监听
name值的变化,那就需要使用:useState额外存储一下name的值。 - 对于同一个组件来说,同一个副作用多次触发时,那可能就会遇到:时序问题,因为异步的原因,后触发的副作用可能先执行完毕,导致不符合预期的效果。
- 上述副作用中,我们即希望其组件挂载时执行,也希望
name变化时执行,则可能需要拆分成 2 个副作用。 - 如果拆分表单后,想做副作用,可能还涉及到更多的
Props透传,但若不拆分,则会导致整个表单过大。
联动组件的值
当一个字段变化了,改变另外一个字段的值。这种场景我们经常遇到的场景是:
- 当一个下拉框选择了 A 选项后,另一个下拉框默认选项自动调整默认值。
import { SelectState, UI_COMPONENTS, nodeUtil } from '@easy-page/antd-ui';
export const opt2 = nodeUtil.createField<
SelectState<string>,
{
opt1: SelectState<string>;
}
>(
'opt2',
'选项字段2',
{
value: { choosed: '3' },
required: true,
mode: 'single',
actions: [
{
effectedKeys: ['opt1'],
action: ({ effectedData, value }) => {
console.log('effectedData:', effectedData);
if (effectedData.opt1.choosed === '1') {
return { fieldValue: { ...value, choosed: '3' } };
} else if (effectedData.opt1.choosed === '2') {
return { fieldValue: { ...value, choosed: '4' } };
}
return {};
},
},
],
},
{
ui: UI_COMPONENTS.SELECT,
select: {
options: [
{ label: '选项3', value: '3' },
{ label: '选项4', value: '4' },
],
},
}
);
如上定义的:actions 中,我们基于 opt1 的值的变化,返回了对应opt2 的状态值:fieldValue: { ...value, choosed: '3' },其中 fieldValue 即为要改变的值,如果什么都不返回,则不会有任何变化,如果返回了:fieldValue,则会更新opt2 的值。
我们看看效果:
见:easy-page.github.io/easy-page-d…
联动组件的 Props
在 EasyPage 中,我们完美的解决了上述痛点,下面将介绍一下 EasyPage 里的副作用是如何实现来解决上述问题的。首先基于上述分析,我们理解到副作用的两个
核心内容:
- 依赖项
- 副作用函数
我们以之前的一个例子来看看咱们该如何定义:
- 年龄字段的
placeholder随着name字段的值变化而变化
import { Empty, InputEffectedType, nodeUtil } from '@easy-page/antd-ui';
export const age = nodeUtil.createField<
string,
{ name: string },
Empty,
InputEffectedType
>('age', '年龄', {
value: '',
required: true,
/** 字段副作用 */
actions: [
{
effectedKeys: ['name'],
/** 加载时,立即执行 */
initRun: true,
action: ({ effectedData, value, initRun }) => {
return {
effectResult: {
inputProps: {
placeholder: `${effectedData.name || '-'} 的年龄`,
},
},
};
},
},
],
});
在上述定义中,我们的 actions 是一个数组,表明可能有多个副作用,每个副作用都有一个:effectedKeys 配置,即依赖项配置。
其类型定义在:nodeUtil.createField<string, {name: string}> 第二个范型上。和 effectedKeys 同级有一个 initRun 配置,
表明是否在组件挂载的时候执行此副作用;和 effectedKeys 同级有一个 action 函数,表明要执行的副作用,其中,比较常用的几个参数含义:
effectedData表示受影响的值,其类型就是上述所说的第二个范型。value表明是当前字段最新的值initRun表明是否是组件挂载时执行
我们返回了: effectResult,其中包含了输入框的 Props: placeholder 的变化,那原理是啥呢?
- 其实除了:
fieldValue外我们可能改变组件的任意东西,因此,就出了字段值以外的变化,我们都可以返回:effectResult effectResult到底会影响什么,类型是什么,取决于基础组件的定义,如上述定义中:nodeUtil.createField里的第四个范型:InputEffectedType, 即框架默认的输入框组件所能解析的副作用值
我们可以提前看一下,Input 组件里的处理逻辑:
<AntdInput
{...baseProps}
{...(effectedResult?.inputProps || {})}
onBlur={(e) => {
onChange({ target: { value: fieldValue } });
onBlur?.(e);
}}
onChange={(val) => {
setFieldValue(val.target.value);
}}
value={fieldValue}
/>
由上可见,组件定义的:effectResult 类型,接收到对应 Props 后就会传递给组件,即可实现上述联动。
在扩展基础组件 章节里,会重点讲解:
effectResult的使用。
我们演示下效果:
见:easy-page.github.io/easy-page-d…
副作用触发时序问题
在 基础-搜索下拉框 的例子中,有这么一段副作用:
actions: [
{
effectedKeys: ['goods'],
/** 初始化执行 */
initRun: true,
/** 初始化查询选中数据 */
action: async ({ value, initRun }) => {
/** 只有初始化时,或者存在关键词时才做查询 **/
if (initRun || value.keyword !== undefined) {
/** 查询选项 */
const options = (await searchByKeywords(value.keyword)) || [];
const hasChoosed = options.find((x) => x.value === value.choosed);
if (value.choosed && !hasChoosed) {
/** 选择的选项不在查询的列表中,单独搜索选中选项 */
const choosed = await searchById(value.choosed);
return {
fieldValue: {
...value,
options: [choosed, ...options],
keyword: undefined,
},
};
}
return { fieldValue: { ...value, options, keyword: undefined } };
}
return {};
},
},
]
按照上述定义,我们解读一下这个副作用:
- 当初始化加载的时候,执行选项查询逻辑
- 当存在关键词的时候,执行选项查询逻辑
那这样就会遇到一个问题:如果初始化加载时,查询网络较慢,要:3s 钟,而在这个 3s 内立马输入搜索关键词,假设搜索关键词后正好网络较好,1s 中返回了。 此时,搜索的结果先回来,更新了选项;初始化加载的选项后回来更新了选项,此时的选项就会是:初始化加载的选项,导致不符合预期。
上述讲解的是同一个副作用被多次触发,其实多个副作用先后被触发也会遇到这个问题,在 EasyPage 中,针对上述场景,我们封装了 Promise 的执行,如果之前存在副作用执行中,现在新来了一个副作用,则前者副作用会被抛弃。
小结
在上述的介绍中:
- 我们通过
action中的effectedData解决了副作用状态额外存储和传递的问题。 - 我们通过副作用时序控制,解决了时序问题。
- 我们通过
initRun,解决了需要多次定义副作用的问题。 - 上述设计整体上符合:“高内聚、低耦合”,从而不需要和外界耦合和
Props透传。