如何高效地写 Form
工作少不了写“增删改查”,“增删改查”中的“增”和“改”都与 Form 有关,可以说:提升了 Form 的开发效率,就提升了整体的开发效率。
本文通过总结 Form 的写法,形成开发规范,用以提升团队开发效率。
1.布局
不同人开发的表单,细看会发现:表单项的上下间距、左右间距有差别。如果 UE 同学足够细心,挑出了这些毛病,开发同学也是各改各的,用独立的 css 控制各自的表单样式。未来 UE 同学要调整产品风格,开发需要改所有表单样式,代价极高。
解决这个问题的办法是:统一布局方式:Form + Space + Row & Col。
以下图表单为例,进行说明。

const App = () => {
const [form] = Form.useForm();
return (
<Form
form={form}
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
requiredMark={false}
onFinish={console.log}
>
<Form.Item name="name" label="名称" rules={[Required]}>
<Input />
</Form.Item>
<Form.Item label="源IP" style={{ marginBottom: 0 }}>
<Address namePathRoot="src" />
</Form.Item>
<Form.Item label="目的IP" style={{ marginBottom: 0 }}>
<Address namePathRoot="dst" />
</Form.Item>
<Form.Item label=" " colon={false}>
<Space>
<Button type="primary" htmlType="submit">
确定
</Button>
<Button>取消</Button>
</Space>
</Form.Item>
</Form>
);
};
antd 采用的是 24 栅格系统,即把宽度 24 等分。以下代码设置了:标签占 4 个栅格,内容占 20 个栅格。
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
...
</Form>

确定、取消按钮中间的间隔,通过 Space 组件来实现,不写样式。
<Space>
<button>确定</button>
<button>取消</button>
</Space>
按钮和上方的输入框左对齐,靠的是:设置 Form.Item 的 label 为一个空格,并且不显示冒号。
<Form.Item label=" " colon="{false}">
<Space>
<button>确定</button>
<button>取消</button>
</Space>
</Form.Item>
还有一种做法是用栅格系统的 offset,让 offset 值等于 Form labelCol 的 span。这种做法形成了依赖关系,以后调整 Form labelCol 的 span,还需要调整 offset,因此不建议这样使用。
<Form.Item wrapperCol={{ offset: 4 }}>...</Form.Item>
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
...
</Form>
再来看 Address 组件。

Address 组件被用在两个地方:
<>
<Form.Item label="源IP" style={{ marginBottom: 0 }}>
<Address namePathRoot="src" />
</Form.Item>
<Form.Item label="目的IP" style={{ marginBottom: 0 }}>
<Address namePathRoot="dst" />
</Form.Item>
</>
const Address = ({ namePathRoot }) => {
return (
<Row gutter={[8, 8]}>
<Col span={24}>
<Form.Item name={[namePathRoot, "type"]} initialValue="ip" noStyle>
<Select>
<Select.Option value="ip">IP地址</Select.Option>
<Select.Option value="iprange">IP地址段</Select.Option>
</Select>
</Form.Item>
</Col>
<Col flex={1}>
<Form.Item name={[namePathRoot, "version"]} initialValue="v4">
<Select>
<Select.Option value="v4">IPV4</Select.Option>
<Select.Option value="v6">IPV6</Select.Option>
</Select>
</Form.Item>
</Col>
<Col flex={2}>
<Form.Item
dependencies={[
[namePathRoot, "type"],
[namePathRoot, "version"],
]}
noStyle
>
{({ getFieldValue }) => {
const type = getFieldValue([namePathRoot, "type"]);
const version = getFieldValue([namePathRoot, "version"]);
if (type === "ip") {
return (
<Form.Item
name={[namePathRoot, "ip"]}
dependencies={[
[namePathRoot, "type"],
[namePathRoot, "version"],
]}
validateFirst
rules={[Required, version === "v4" ? IPv4 : IPv6]}
>
<Input placeholder="请输入IP地址" />
</Form.Item>
);
} else {
return (
<Row gutter={8} style={{ lineHeight: "32px" }}>
<Col flex={1}>
<Form.Item
name={[namePathRoot, "iprange", "start"]}
dependencies={[
[namePathRoot, "type"],
[namePathRoot, "version"],
]}
validateFirst
rules={[Required, version === "v4" ? IPv4 : IPv6]}
>
<Input placeholder="请输入起始IP" />
</Form.Item>
</Col>
-<Col flex={1}>
<Form.Item
name={[namePathRoot, "iprange", "end"]}
dependencies={[
[namePathRoot, "type"],
[namePathRoot, "version"],
[namePathRoot, "iprange", "start"],
]}
validateFirst
rules={[
Required,
version === "v4" ? IPv4 : IPv6,
buildMultiFieldsRule(
[
[namePathRoot, "iprange", "start"],
[namePathRoot, "iprange", "end"],
],
(start, end) => ipToInt(end) > ipToInt(start),
"结束IP需要大于起始IP"
),
]}
>
<Input placeholder="请输入结束IP" />
</Form.Item>
</Col>
</Row>
);
}
}}
</Form.Item>
</Col>
</Row>
);
};
注意 Address 组件中第一个 Form.Item 有属性 noStyle,noStyle 让 Form.Item 没有样式,这样 Form.Item 就不会有 margin 了,Form.Item 之间就会更紧凑了。
对比一下有和无 noStyle 的区别:
有 noStyle:

无 noStyle:

下面来看如何用 Row & Col 实现两行的布局。
第一行包含一个下拉框;第二行分为两部分:左侧部份是下拉框,右侧部份根据第一行下拉框的选中条件渲染。

<Row gutter={[8, 8]}>
<Col span={24}>第一行</Col>
<Col flex={1}>第二行左侧部分</Col>
<Col flex={2}>第二行右侧部分</Col>
</Row>
gutter={[8, 8]} 指定 Col 之间的水平间隔和垂直间隔。
<Col span={24}>第一行</Col>,antd 采用 24 栅格系统,因此该 Col 占满整行。Row 默认自动换行 wrap={true},所以后面的 Col 会换行。
<Col flex={1}>第二行左侧部分</Col>
<Col flex={2}>第二行右侧部分</Col>
第二行的实现有个细节,两个 Col 的宽度用的不是 span,而是 flex。如果用 span={8} 和 span={16},那么这两个 Col 的宽度会固定为 1:2。
这里的设计是:第二行左侧部分【下拉框】的宽度是变化的,当第二行右侧部分展示两个输入框时候,第二行左侧部分宽度变小。

Col 使用 flex 指定宽度可以实现这个效果,对应的 css 样式是如下:
| Col:第二行左侧部分 | Col:第二行右侧部分 |
|---|---|
flex={1} |
flex={2} |
flex-grow:1;flex-shrink: 1;flex-basis: auto; |
flex-grow:2;flex-shrink: 2;flex-basis: auto; |
这样的效果是:
- 如果
组件默认宽度总和小于行宽,剩余的宽度根据flex-grow的比例来分配; - 如果
组件默认宽度总和大于行宽,超出的宽度根据flex-shrink的比例来缩小。
我们的目标是在项目中统一布局方式,不要把“不写样式”作为规则规范,那会让我们束手束脚。
实际上这个表单也写了两处样式。
源 IP、目的 IP 的 Form.Item 设置了 marginBottom: 0。
<Form.Item label="源IP" style={{ marginBottom: 0 }}>
<Address namePathRoot="src" />
</Form.Item>
这是因为输入框的错误要显示在输入框的正下方,这样 Address 组件内的输入框就不能写 noStyle。

如果设置 noStyle, 它的错误会向上传递:

但不写 noStyle,它就会有 marginBottom,因此需去除包裹 Address 的 Form.Item 的 marginBottom。
<Form.Item label="源IP" style={{ marginBottom: 0 }}>
<Address namePathRoot="src" />
</Form.Item>
起始、结束 IP 中间的横杠,为了垂直居中,在 Row 上设置了 line-height。

<Row style={{ lineHeight: "32px" }}>...</Row>
2.name 重名
<>
<Form.Item label="源IP">
<Address namePathRoot="src" />
</Form.Item>
<Form.Item label="目的IP">
<Address namePathRoot="dst" />
</Form.Item>
</>

上图的 Address 组件在表单中出现两次,如何保证 Form.Item 的 name 不重名?
有的同学把所有 Form.Item 的 name 作为 props 传入组件。这种方法固然可行,但比较费事,更好的做法是利用 NamePath。
<Form.Item name={["a", "b", "c"]}>
<Input />
</Form.Item>
Form.Item 的 name 不仅可以是字符串,也可以是字符串数组,即 NamePath。这样表单项生成的 value 会是嵌套结构:
{
a: {
b: {
c: "xxxx";
}
}
}
我们只需要让两个 Address 实例 NamePath 的根不同,就可以做到区分,就像指定了不同的命名空间。
<>
<Form.Item label="源IP">
<Address namePathRoot="src" />
</Form.Item>
<Form.Item label="目的IP">
<Address namePathRoot="dst" />
</Form.Item>
</>
const Address = ({ namePathRoot }) => {
return (
<Row gutter={[8, 8]}>
<Col span={24}>
<Form.Item name={[namePathRoot, "type"]}>...</Form.Item>
</Col>
...
</Row>
);
};
有的同学问:实际项目中,后台数据是扁平结构的怎么办?
我的建议是:前台在 action 层做数据转换。
3.条件渲染

下拉框选择不同,后面的表单项也会不同。遇到这种需求,有的同学使用 state 来实现:
const Address = () => {
const [option, setOption] = useState("ip");
return (
<>
<Form.Item name="type" onChange={setOption}>
<Select>
<Select.Option value="ip">IP地址</Select.Option>
<Select.Option value="iprange">IP地址段</Select.Option>
</Select>
</Form.Item>
{option === ip ? "IP地址表单项" : "IP地址段表单项"}
</>
);
};
实现条件渲染,这种做法需要在 3 处写代码:声明 state、设置 state、根据 state 条件渲染,逻辑是割裂的,会给阅读和维护代码造成麻烦。更好的方式是采用 renderProp。
Form.Item 的 children 传一个函数:
<Form.Item>
{form => {
const type = form.getFieldValue("type");
if (type === "ip") {
return "ip地址表单项";
} else {
return "ip地址段表单项";
}
}}
</Form.Item>
除此以外,还需要在 Form.Item 上说明,在什么情况下,需要执行 children 函数。
<Form.Item shouldUpdate>
{(form) => {
...
}}
</Form.Item>
以上代码相当于设置 shouldUpdate={true},即每次 render,都重新渲染 children,显然这样性能不好。
<Form.Item shouldUpdate={(preValue, curValue) => preValue.type !== curValue.type}>
{(form) => {
...
}}
</Form.Item>
当表单值发生变化时,检查 type 值是否改变,改变了才重新渲染 children。这种做法消除了性能问题,但还不是最好的做法。
<Form.Item dependencies={["type"]}>
{(form) => {
...
}}
</Form.Item>
上述 dependencies 表示:该表单项依赖 type 字段,当 type 发生改变时,需要重新渲染 children。这种声明式的写法更清晰高效。
4.校验
从经验来看,能在各个项目中复用的校验逻辑是 isXyz:
declare function isXyz(str: string): boolean;
如:
- isIPv4
- isIPv4NetMaskIP
- isIPv4NetMaskInt
- isIPv6
- ...
这些原子的校验函数库做好规范后,我们利用函数式的写法,通过 and、or、 not来组合出更强大的校验函数。如一个输入框可以输入 IPv4 也可以输入 IPv6,那校验函数就是:
or(isIPv4, isIPv6);
在校验函数之上,我们再提供 buildRule 方法,将校验函数转成 antd 的 Rule。
const buildRule = (validate, errorMsg) => ({
validator: (_, value) =>
validate(value) ? Promise.resolve() : Promise.reject(errorMsg),
});
还有一种比较复杂的情况,是多个表单项的关联校验,如起始 IP 和结束 IP,结束 IP 的要大于起始 IP。
这个需求核心的校验逻辑是判断 IP 的大小:
(start, end) => ipToInt(end) > ipToInt(start);
这个函数能正常执行的前提是:起始 IP 和结束 IP 输入框都输入了合法的 IP。
<>
<Form.Item name="start" validateFirst rules={[Required, IPv4]}>
<Input placeholder="请输入起始IP" />
</Form.Item>
<Form.Item
name="end"
dependencies={["start"]}
validateFirst
rules={[
Required,
IPv4,
buildMultiFieldsRule(
["start", "end"],
(start, end) => ipToInt(end) > ipToInt(start),
"结束IP需要大于起始IP"
),
]}
>
<Input placeholder="请输入结束IP" />
</Form.Item>
</>
我们让 Rule 有层层递进的关系:
[
Required,
IPv4,
buildMultiFieldsRule(
["start", "end"],
(start, end) => ipToInt(end) > ipToInt(start),
"结束IP需要大于起始IP"
),
];
先校验填了,再校验是 IPv4,最后校验大小合适。
同时,我们设置了 Form.Item 的 validateFirst,顺序执行 Rule,有一个出错了,后续的就不执行了。
在 buildMultiFieldsRule 方法中,封装判断各个 field 都填写正常的逻辑:
const buildMultiFieldsRule =
(fields, validate, errorMsg) =>
({ getFieldValue, isFieldTouched, getFieldError }) => ({
validator: () => {
if (fields.some(f => !isFieldTouched(f) || getFieldError(f).length > 0)) {
return Promise.resolve();
} else {
return validate(...fields.map(getFieldValue))
? Promise.resolve()
: Promise.reject(errorMsg);
}
},
});
5.总结
以上总结了项目中开发 Form 的好的实践。这类总结经验的文章,需要是活的,能随着项目经验积累不断进化,而不是一写下来就死了。
如何高效地写 Form的更多相关文章
- Django中三种方式写form表单
除了在html中自己手写form表单外,django还可以通过 继承django.forms.Form 或django.forms.ModelForm两个类来自动生成form表单,下面依次利用三种方式 ...
- 如何优雅高效的写博客(Sublime + Markdown + Evernote)
如何优雅高效的写博客(Sublime + Markdown + Evernote) 本文主要是参照了几位大神的博客加上自己捣鼓了半天,比较适合新手流畅阅读 非常感谢下面两位大神: @dc_726: h ...
- django中写form表单时csrf_token的作用
之前在学习django的时候,在template中写form时,出现错误.百度,google后要加{% csrf_token %}才可以,之前一直也没研究,只是知道要加个这个东西,具体是什么也不明白. ...
- 如何高效地写CSS--等以后有空多加总结一下
CSS写的并不多,如果从零开始的项目,自己一定想搬砖来得容易点.CSS编写一定有其工程化的方法,来时编写更加有效率. 考虑将CSS的预处理LESS.Sass或Stylus引入,或者将CSS的后处理Po ...
- 如何高效的写出markdown笔记
重置用户名和密码 安利一个小工具donet-cnblog可以同步图片到cnblog中,同时生成对应的Markdown笔记.写博客的时候我们可以本地写,用这个工具同步到cnblog上能够大大节省我们的时 ...
- Form表单中的action路径问题,form表单action路径《jsp--->Servlet路劲问题》这个和上一个《jsp--->Servlet》文章有关
Form表单中的action路径问题,form表单action路径 热度5 评论 50 www.BkJia.Com 网友分享于: 2014-08-14 08:08:01 浏览数44525次 ...
- 表单form的属性,单行文本框、密码框、单选多选按钮
基础表单结构: <body> <h1> <hr /> <form action="" name="myFrom" en ...
- jquery.form.js实现将form提交转为ajax方式提交的使用方法
本文实例讲述了jquery.form.js实现将form提交转为ajax方式提交的方法.分享给大家供大家参考.具体分析如下: 这个框架集合form提交.验证.上传的功能. 这个框架必须和jquery完 ...
- 浅谈MVC Form认证
简单的谈一下MVC的Form认证. 在做MVC项目时,用户登录认证需要选用Form认证时,我们该怎么做呢?下面我们来简单给大家说一下. 首先说一下步骤 1.用户登录时,如果校验用户名密码通过后,需要调 ...
随机推荐
- a.equals(b) 判断对象相等
一.值是null的情况: 1.a.equals(b), a 是null, 抛出NullPointException异常. 2.a.equals(b), a不是null, b是null, 返回fals ...
- Rank & Sort Loss for Object Detection and Instance Segmentation 论文解读(含核心源码详解)
第一印象 Rank & Sort Loss for Object Detection and Instance Segmentation 这篇文章算是我读的 detection 文章里面比较难 ...
- 一文搞懂Google Navigation Component
一文搞懂Google Navigation Component 应用中的页面跳转是一个常规任务, Google官方提供的解决方案是Android Jetpack的Navigation componen ...
- 对比显示每条线路的价格和该类型线路的平均价格,分别使用子查询和 exists 获取线路数量
查看本章节 查看作业目录 需求说明: 对比显示每条线路的价格和该类型线路的平均价格 分别使用子查询和 exists 获取线路数量超过"出境游"线路数的线路类型信息,要求按照线路数升 ...
- 2.spring系列之404异常的捕获
回顾 我在之前发布了一篇spring统一返回的文章,最后提到是无法捕获404异常的,这里我们先来测试一下 @RestController public class TestController { @ ...
- C# .net 环境下使用rabbitmq消息队列
消息队列的地位越来越重要,几乎是面试的必问问题了,不会使用几种消息队列都显得尴尬,正好本文使用C#来带你认识rabbitmq消息队列 首先,我们要安装rabbitmq,当然,如果有现成的,也可以使用, ...
- CSS基础 CSS的三大特性以及选择器优先级计算方法
1.子元素默认会继承父元素的样式,但不是所有的元素都有继承 常见的继承父元素特点的元素有: 1.color 2.font-sytle.font-weight.font-size.font-family ...
- JMeter_请求参数
在做接口测试时,发送请求的参数有两种格式,一种是Parameters,一种是JSON 一.Jmeter传参 JMeter 传Parameters格式的参数 JMeter 传JSON格式的参数 二.区分 ...
- float类型数据精度问题:12.0f-11.9f=0.10000038,"减不尽"为什么?
现在我们就详细剖析一下浮点型运算为什么会造成精度丢失? 1.小数的二进制表示问题 首先我们要搞清楚下面两个问题: (1) 十进制整数如何转化为二进制数 算法很简单.举个例子,11表示成二进制数: 1 ...
- Word2010制作课程表
原文链接:https://www.toutiao.com/i6487759634751816205/ 插入表格: 选择"插入"选项卡,"表格"功能组," ...